├── .gitignore ├── .travis.yml ├── .versions ├── LICENSE ├── README.md ├── cfs-autoform-hooks.js ├── cfs-autoform-util.js ├── cfs-autoform.css ├── cfs-autoform.html ├── cfs-autoform.js └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build* 3 | .npm* 4 | .meteor* 5 | smart.lock 6 | /packages/ 7 | .idea* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - "curl -L http://git.io/ejPSng | /bin/sh" -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | aldeed:autoform@4.0.0 2 | aldeed:simple-schema@1.1.0 3 | base64@1.0.3 4 | binary-heap@1.0.3 5 | blaze@2.1.2 6 | blaze-tools@1.0.3 7 | boilerplate-generator@1.0.3 8 | callback-hook@1.0.3 9 | cfs:access-point@0.1.46 10 | cfs:autoform@2.2.1 11 | cfs:base-package@0.0.29 12 | cfs:collection@0.5.5 13 | cfs:collection-filters@0.2.4 14 | cfs:data-man@0.0.6 15 | cfs:file@0.1.17 16 | cfs:http-methods@0.0.27 17 | cfs:http-publish@0.0.13 18 | cfs:power-queue@0.9.11 19 | cfs:reactive-list@0.0.9 20 | cfs:reactive-property@0.0.4 21 | cfs:standard-packages@0.5.7 22 | cfs:storage-adapter@0.2.1 23 | cfs:tempstore@0.1.4 24 | cfs:upload-http@0.0.20 25 | cfs:worker@0.1.4 26 | check@1.0.5 27 | ddp@1.1.0 28 | deps@1.0.7 29 | ejson@1.0.6 30 | geojson-utils@1.0.3 31 | html-tools@1.0.4 32 | htmljs@1.0.4 33 | http@1.1.0 34 | id-map@1.0.3 35 | jquery@1.11.3_2 36 | json@1.0.3 37 | livedata@1.0.13 38 | logging@1.0.7 39 | meteor@1.1.6 40 | minifiers@1.1.5 41 | minimongo@1.0.8 42 | mongo@1.1.0 43 | mongo-livedata@1.0.8 44 | mrt:moment@2.6.0 45 | observe-sequence@1.0.6 46 | ordered-dict@1.0.3 47 | raix:eventemitter@0.1.1 48 | raix:ui-dropped-event@0.0.7 49 | random@1.0.3 50 | reactive-var@1.0.5 51 | retry@1.0.3 52 | routepolicy@1.0.5 53 | spacebars@1.0.6 54 | spacebars-compiler@1.0.6 55 | templating@1.1.1 56 | tracker@1.0.7 57 | ui@1.0.6 58 | underscore@1.0.3 59 | url@1.0.4 60 | webapp@1.2.0 61 | webapp-hashing@1.0.3 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Eric Dobbertin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cfs:autoform 2 | ========================= 3 | 4 | A smart package for Meteor that provides a file UI component for use within an autoform. The UI component supports clicking to select files or dropping them. Once the full form is valid, the files are uploaded using CollectionFS. The form document is inserted only if the uploads are all successful. If the form document fails to insert on the server, the uploaded files are removed. 5 | 6 | ## Installation 7 | 8 | In a Meteor app directory, enter: 9 | 10 | ``` 11 | $ meteor add cfs:autoform 12 | ``` 13 | 14 | ## Prerequisites 15 | 16 | Requires Meteor 0.9.3+ 17 | 18 | Add `aldeed:collection2`, `aldeed:autoform`, and `cfs:standard-packages` packages to your app. Also add any other CFS packages you need, particularly a storage adapter package such as `cfs:gridfs`. 19 | 20 | ## Example 21 | 22 | *Assumption: autopublish, insecure, and cfs:gridfs packages are in use* 23 | 24 | *common.js* 25 | 26 | ```js 27 | Docs = new Mongo.Collection("docs"); 28 | Docs.attachSchema(new SimpleSchema({ 29 | name: { 30 | type: String 31 | }, 32 | fileId: { 33 | type: String 34 | } 35 | })); 36 | 37 | Files = new FS.Collection("files", { 38 | stores: [new FS.Store.GridFS("filesStore")] 39 | }); 40 | 41 | Files.allow({ 42 | download: function () { 43 | return true; 44 | }, 45 | fetch: null 46 | }); 47 | ``` 48 | 49 | *html:* 50 | 51 | ```html 52 | 59 | ``` 60 | 61 | ## Schema Example 62 | 63 | If you are not able to change the `afQuickField` attributes directly or you want to use a `quickForm`, you can put the attributes in the schema instead: 64 | 65 | ```js 66 | Docs.attachSchema(new SimpleSchema({ 67 | name: { 68 | type: String 69 | }, 70 | fileId: { 71 | type: String, 72 | autoform: { 73 | afFieldInput: { 74 | type: "cfs-file", 75 | collection: "files" 76 | } 77 | } 78 | } 79 | })); 80 | ``` 81 | 82 | ## Server Method Example 83 | 84 | In addition to the above, on a server method we need to reference the schema later. 85 | 86 | ```js 87 | mySchema = new SimpleSchema({ 88 | name: { 89 | type: String 90 | }, 91 | fileId: { 92 | type: String, 93 | autoform: { 94 | afFieldInput: { 95 | type: "cfs-file", 96 | collection: "files" 97 | } 98 | } 99 | } 100 | })); 101 | Docs.attachSchema(mySchema); 102 | ``` 103 | 104 | Change the html to reflect the server method type: 105 | ```html 106 | 113 | ``` 114 | 115 | Then manually add the required AutoForm hooks to the form: 116 | ```js 117 | AutoForm.addHooks( 118 | ["insertForm"], 119 | { 120 | before : { 121 | method: CfsAutoForm.Hooks.beforeInsert 122 | }, 123 | after : { 124 | method: CfsAutoForm.Hooks.afterInsert 125 | } 126 | } 127 | ); 128 | ``` 129 | 130 | And on the server-sde: 131 | ```js 132 | Meteor.methods({ 133 | myServerMethod: function(doc) { 134 | try { 135 | check(doc, mySchema); 136 | mySchema.clean(doc); 137 | }catch(e){ 138 | throw new Meteor.Error(e); 139 | } 140 | 141 | //do some stuff here and throw a new Meteor.Error if there is a problem 142 | }}); 143 | ``` 144 | Please note that myServerMethod, insertForm, and mySchema can (and should) be changed to whatever you like. 145 | 146 | ## Notes 147 | 148 | * Only insert and server method forms (`type="insert"` or `type="method"`) are supported 149 | * Use `type="cfs-file"` to allow one file to be selected or dropped. Use `type="cfs-files"` to allow multiple files to be selected or dropped. 150 | * The `collection` attribute must be the same as the first argument you passed to the FS.Collection constructor. 151 | * Files are uploaded only after you click submit and the form is valid. 152 | * If file upload fails, the form document is not inserted. 153 | * If one file fails to upload, any other files from that form that did upload are deleted. 154 | * If the form document insert fails on the server, the associated files are automatically deleted as part of the latency compensation rollback. 155 | 156 | ## TODO 157 | 158 | * Insert FS.File itself when using cfs-ejson-file package. 159 | * Display customizable progress bar template in place of each field while uploading. 160 | * Update forms 161 | 162 | [![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.1.3/dist/gratipay.png)](https://gratipay.com/aldeed/) 163 | -------------------------------------------------------------------------------- /cfs-autoform-hooks.js: -------------------------------------------------------------------------------- 1 | Hooks = { 2 | beforeInsert: function (doc) { 3 | var self = this, template = this.template; 4 | if (!AutoForm.validateForm(this.formId)) { 5 | return false; 6 | } 7 | 8 | // Loop through all hidden file inputs in the form. 9 | var totalFiles = 0; 10 | var arrayFields = {}; 11 | template.$('.cfsaf-hidden').each(function () { 12 | var elem = $(this); 13 | 14 | // Get schema key that this input is for 15 | var key = elem.attr("data-schema-key"); 16 | 17 | // no matter what, we want to delete the dummyId value 18 | //delete doc[key]; 19 | CfsAutoForm.Util.deepDelete(doc,key); 20 | 21 | // Get list of files that were attached for this key 22 | var fileList = elem.data("cfsaf_files"); 23 | 24 | // If we have some attached files 25 | if (fileList) { 26 | // add all files to total count 27 | totalFiles += fileList.length; 28 | } 29 | 30 | // Otherwise it might be a multiple files field 31 | else { 32 | var fileListList = elem.data("cfsaf_files_multi"); 33 | if (fileListList) { 34 | // make a note that it's an array field 35 | arrayFields[key] = true; 36 | // add all files to total count 37 | _.each(fileListList, function (fileList) { 38 | totalFiles += fileList.length; 39 | }); 40 | // prep the array 41 | doc[key] = []; 42 | } 43 | } 44 | }); 45 | 46 | // If no files were attached anywhere on the form, we're done. 47 | // We pass back the doc synchronously 48 | if (totalFiles === 0) { 49 | return doc; 50 | } 51 | 52 | // Create the callback that will be called either 53 | // upon file insert error or upon each file being uploaded. 54 | var doneFiles = 0; 55 | var failedFiles = 0; 56 | function cb(error, fileObj, key) { 57 | // Increment the done files count 58 | doneFiles++; 59 | 60 | // Increment the failed files count if it failed 61 | if (error) { 62 | failedFiles++; 63 | } 64 | 65 | // If it didn't fail, set the new ID as the property value in the doc, 66 | // or push it into the array of IDs if it's a multiple files field. 67 | else { 68 | if (arrayFields[key]) { 69 | CfsAutoForm.Util.deepFind(doc,key)[key].push(fileObj._id); 70 | } else { 71 | //doc[key] = fileObj._id; 72 | CfsAutoForm.Util.deepSet(doc,key,fileObj._id); 73 | } 74 | } 75 | 76 | // If this is the last file to be processed, pass execution back to autoform 77 | if (doneFiles === totalFiles) { 78 | // If any files failed 79 | if (failedFiles > 0) { 80 | // delete all that succeeded 81 | CfsAutoForm.deleteUploadedFiles(template); 82 | // pass back to autoform code, telling it we failed 83 | self.result(false); 84 | } 85 | // Otherwise if all files succeeded 86 | else { 87 | // pass updated doc back to autoform code, telling it we succeeded 88 | self.result(doc); 89 | } 90 | } 91 | } 92 | 93 | // Loop through all hidden file fields, inserting 94 | // and uploading all files that have been attached to them. 95 | template.$('.cfsaf-hidden').each(function () { 96 | var elem = $(this); 97 | 98 | // Get schema key that this input is for 99 | var key = elem.attr("data-schema-key"); 100 | 101 | // Get the FS.Collection instance 102 | var fsCollectionName = elem.attr("data-cfs-collection"); 103 | var fsCollection = FS._collections[fsCollectionName]; 104 | 105 | // Loop through all files that were attached to this field 106 | function loopFileList(fileList) { 107 | _.each(fileList, function (file) { 108 | // Create the FS.File instance 109 | var fileObj = new FS.File(file); 110 | 111 | // Listen for the "uploaded" event on this file, so that we 112 | // can call our callback. We want to wait until uploaded rather 113 | // than just inserted. XXX Maybe should wait for stored? 114 | fileObj.once("uploaded", function () { 115 | // track successful uploads so we can delete them if any 116 | // of the other files fail to upload 117 | var uploadedFiles = elem.data("cfsaf_uploaded-files") || []; 118 | uploadedFiles.push(fileObj); 119 | elem.data("cfsaf_uploaded-files", uploadedFiles); 120 | // call callback 121 | cb(null, fileObj, key); 122 | }); 123 | 124 | // Insert the FS.File instance into the FS.Collection 125 | fsCollection.insert(fileObj, function (error, fileObj) { 126 | // call callback if insert/upload failed 127 | if (error) { 128 | cb(error, fileObj, key); 129 | } 130 | // TODO progress bar during uploads 131 | }); 132 | }); 133 | } 134 | 135 | // single fields first 136 | loopFileList(elem.data("cfsaf_files")); 137 | // then multiple fields 138 | _.each(elem.data("cfsaf_files_multi"), function (fileList) { 139 | loopFileList(fileList); 140 | }); 141 | }); 142 | }, 143 | afterInsert: function (error) { 144 | var template = this.template, elems = template.$('.cfsaf-hidden'); 145 | if (error) { 146 | CfsAutoForm.deleteUploadedFiles(template); 147 | if (FS.debug || AutoForm._debug) 148 | console.log("There was an error inserting so all uploaded files were removed.", error); 149 | } else { 150 | // cleanup files data 151 | elems.removeData("cfsaf_files"); 152 | elems.removeData("cfsaf_files_multi"); 153 | } 154 | // cleanup uploaded files data 155 | elems.removeData("cfsaf_uploaded-files"); 156 | }, 157 | beforeUpdate: function(doc){ 158 | var self = this, template = this.template; 159 | if (!AutoForm.validateForm(this.formId)) { 160 | return false; 161 | } 162 | // Loop through all hidden file inputs in the form. 163 | var totalFiles = 0; 164 | var arrayFields = {}; 165 | template.$('.cfsaf-hidden').each(function () { 166 | var elem = $(this); 167 | 168 | // Get schema key that this input is for 169 | var key = elem.attr("data-schema-key"); 170 | 171 | //Maintain current key 172 | doc.$set[key] = self.currentDoc[key]; 173 | 174 | // Get list of files that were attached for this key 175 | var fileList = elem.data("cfsaf_files"); 176 | 177 | // If we have some attached files 178 | if (fileList) { 179 | // add all files to total count 180 | totalFiles += fileList.length; 181 | //we delete the id only if we uploaded another file 182 | //delete doc[key]; 183 | CfsAutoForm.Util.deepDelete(doc,key); 184 | } 185 | 186 | // Otherwise it might be a multiple files field 187 | else { 188 | var fileListList = elem.data("cfsaf_files_multi"); 189 | if (fileListList) { 190 | //we delete the id only if we uploaded another file 191 | //delete doc[key]; 192 | CfsAutoForm.Util.deepDelete(doc,key); 193 | // make a note that it's an array field 194 | arrayFields[key] = true; 195 | // add all files to total count 196 | _.each(fileListList, function (fileList) { 197 | totalFiles += fileList.length; 198 | }); 199 | // prep the array 200 | doc[key] = []; 201 | } 202 | } 203 | }); 204 | 205 | // If no files were attached anywhere on the form, we're done. 206 | // We pass back the doc synchronously 207 | if (totalFiles === 0) { 208 | return doc; 209 | } 210 | 211 | // Create the callback that will be called either 212 | // upon file insert error or upon each file being uploaded. 213 | var doneFiles = 0; 214 | var failedFiles = 0; 215 | function cb(error, fileObj, key) { 216 | // Increment the done files count 217 | doneFiles++; 218 | 219 | // Increment the failed files count if it failed 220 | if (error) { 221 | failedFiles++; 222 | } 223 | 224 | // If it didn't fail, set the new ID as the property value in the doc, 225 | // or push it into the array of IDs if it's a multiple files field. 226 | else { 227 | if (arrayFields[key]) { 228 | CfsAutoForm.Util.deepFind(doc.$set,key).push(fileObj._id); 229 | } else { 230 | //doc[key] = fileObj._id; 231 | CfsAutoForm.Util.deepSet(doc.$set,key,fileObj._id); 232 | } 233 | } 234 | 235 | // If this is the last file to be processed, pass execution back to autoform 236 | if (doneFiles === totalFiles) { 237 | // If any files failed 238 | if (failedFiles > 0) { 239 | // delete all that succeeded 240 | CfsAutoForm.deleteUploadedFiles(template); 241 | // pass back to autoform code, telling it we failed 242 | self.result(false); 243 | } 244 | // Otherwise if all files succeeded 245 | else { 246 | // pass updated doc back to autoform code, telling it we succeeded 247 | console.log(doc); 248 | self.result(doc); 249 | } 250 | } 251 | } 252 | 253 | // Loop through all hidden file fields, inserting 254 | // and uploading all files that have been attached to them. 255 | template.$('.cfsaf-hidden').each(function () { 256 | var elem = $(this); 257 | 258 | // Get schema key that this input is for 259 | var key = elem.attr("data-schema-key"); 260 | 261 | // Get the FS.Collection instance 262 | var fsCollectionName = elem.attr("data-cfs-collection"); 263 | var fsCollection = FS._collections[fsCollectionName]; 264 | 265 | // Loop through all files that were attached to this field 266 | function loopFileList(fileList) { 267 | _.each(fileList, function (file) { 268 | // Create the FS.File instance 269 | var fileObj = new FS.File(file); 270 | 271 | // Listen for the "uploaded" event on this file, so that we 272 | // can call our callback. We want to wait until uploaded rather 273 | // than just inserted. XXX Maybe should wait for stored? 274 | fileObj.once("uploaded", function () { 275 | // track successful uploads so we can delete them if any 276 | // of the other files fail to upload 277 | var uploadedFiles = elem.data("cfsaf_uploaded-files") || []; 278 | uploadedFiles.push(fileObj); 279 | elem.data("cfsaf_uploaded-files", uploadedFiles); 280 | // call callback 281 | cb(null, fileObj, key); 282 | }); 283 | 284 | // Insert the FS.File instance into the FS.Collection 285 | fsCollection.insert(fileObj, function (error, fileObj) { 286 | // call callback if insert/upload failed 287 | if (error) { 288 | cb(error, fileObj, key); 289 | } 290 | // TODO progress bar during uploads 291 | }); 292 | }); 293 | } 294 | 295 | // single fields first 296 | loopFileList(elem.data("cfsaf_files")); 297 | // then multiple fields 298 | _.each(elem.data("cfsaf_files_multi"), function (fileList) { 299 | loopFileList(fileList); 300 | }); 301 | }); 302 | }, 303 | afterUpdate: function(error, result){ 304 | var template = this.template; 305 | var elems = template.$('.cfsaf-hidden'); 306 | if (error) { 307 | CfsAutoForm.deleteUploadedFiles(template); 308 | if (FS.debug || AutoForm._debug) 309 | console.log("There was an error inserting so all uploaded files were removed.", error); 310 | } else { 311 | // cleanup files data 312 | elems.removeData("cfsaf_files"); 313 | elems.removeData("cfsaf_files_multi"); 314 | } 315 | // cleanup uploaded files data 316 | elems.removeData("cfsaf_uploaded-files"); 317 | } 318 | }; 319 | -------------------------------------------------------------------------------- /cfs-autoform-util.js: -------------------------------------------------------------------------------- 1 | Util = { 2 | //delete prop from obj 3 | //prop can be something like "obj.3.badprop 4 | deepDelete: function(obj, prop){ 5 | return CfsAutoForm.Util.deepDo(obj, prop, function(obj, prop){ 6 | delete obj[prop]; 7 | }); 8 | }, 9 | deepSet: function(obj, prop, value){ 10 | return CfsAutoForm.Util.deepDo(obj, prop, function(obj, prop){ 11 | obj[prop] = value; 12 | }); 13 | }, 14 | //returns the object that CONTAINS the last property 15 | deepFind: function(obj, path){ 16 | path = path.split('.'); 17 | for (i = 0; i < path.length - 1; i++) 18 | obj = obj[path[i]]; 19 | 20 | return obj; 21 | }, 22 | //executes closure(obj, prop) where prop might be a string of properties and array indices 23 | deepDo: function(obj, path, closure){ 24 | path = path.split('.'); 25 | for (i = 0; i < path.length - 1; i++) 26 | obj = obj[path[i]]; 27 | 28 | closure.apply(this, [obj, path[i]]); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /cfs-autoform.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | .cfsaf-hidden { 3 | display: none !important; 4 | } 5 | 6 | .cfsaf-field { 7 | cursor: pointer !important; 8 | } -------------------------------------------------------------------------------- /cfs-autoform.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /cfs-autoform.js: -------------------------------------------------------------------------------- 1 | CfsAutoForm = CfsAutoForm || {}; 2 | CfsAutoForm.Util = Util; 3 | CfsAutoForm.Hooks = Hooks; 4 | 5 | CfsAutoForm.deleteUploadedFiles = function(template) { 6 | template.$('.cfsaf-hidden').each(function () { 7 | var uploadedFiles = $(this).data("cfsaf_uploaded-files") || []; 8 | _.each(uploadedFiles, function ( f ) { 9 | f.remove(); 10 | }); 11 | }); 12 | }; 13 | 14 | 15 | if (Meteor.isClient) { 16 | // Adds a custom "cfs-file" input type that AutoForm will recognize 17 | AutoForm.addInputType("cfs-file", { 18 | template:"cfsFileField", 19 | valueOut: function () { 20 | return "dummyId"; 21 | }, 22 | contextAdjust: function (context) { 23 | context.atts.placeholder = context.atts.placeholder || "Click to upload a file or drop it here"; 24 | context.atts["class"] = (context.atts["class"] || "") + " cfsaf-field"; 25 | return context; 26 | } 27 | }); 28 | 29 | // Adds a custom "cfs-files" input type that AutoForm will recognize 30 | AutoForm.addInputType("cfs-files", { 31 | template:"cfsFilesField", 32 | valueOut: function () { 33 | return ["dummyId"]; 34 | }, 35 | contextAdjust: function (context) { 36 | context.atts.placeholder = context.atts.placeholder || "Click to upload files or drop them here"; 37 | context.atts["class"] = (context.atts["class"] || "") + " cfsaf-field"; 38 | return context; 39 | } 40 | }); 41 | 42 | function textInputAtts() { 43 | var atts = _.clone(this.atts); 44 | delete atts.collection; 45 | // we want the schema key tied to the hidden file field only 46 | delete atts["data-schema-key"]; 47 | atts["class"] = (atts["class"] || "") + " form-control"; 48 | return atts; 49 | } 50 | 51 | function fileInputAtts() { 52 | return {'data-schema-key': this.atts["data-schema-key"]}; 53 | } 54 | 55 | Template.cfsFileField_bootstrap3.helpers({ 56 | textInputAtts: textInputAtts, 57 | fileInputAtts: fileInputAtts 58 | }); 59 | 60 | Template.cfsFilesField_bootstrap3.helpers({ 61 | textInputAtts: textInputAtts, 62 | fileInputAtts: fileInputAtts 63 | }); 64 | 65 | var hookTracking = {}; 66 | Template.cfsFileField_bootstrap3.rendered = 67 | Template.cfsFilesField_bootstrap3.rendered = function () { 68 | var formId; 69 | 70 | // backwards compatibility with pre 5.0 autoform 71 | if (typeof AutoForm.find === 'function') { 72 | formId = AutoForm.find().formId; 73 | } else { 74 | formId = AutoForm.getFormId(); 75 | } 76 | 77 | // By adding hooks dynamically on render, hopefully any user hooks will have 78 | // been added before so we won't disrupt expected behavior. 79 | if (!hookTracking[formId]) { 80 | hookTracking[formId] = true; 81 | addAFHook(formId); 82 | } 83 | }; 84 | 85 | var singleHandler = function (event, template) { 86 | var fileList = []; 87 | FS.Utility.eachFile(event, function (f) { 88 | fileList.push(f.name); 89 | }); 90 | template.$('.cfsaf-field').val(fileList.join(", ")); 91 | var fileList = event.originalEvent.dataTransfer ? event.originalEvent.dataTransfer.files : event.currentTarget.files; 92 | // Store the FileList on the file input for later 93 | template.$('.cfsaf-hidden').data("cfsaf_files", fileList); 94 | }; 95 | 96 | Template.cfsFileField_bootstrap3.events({ 97 | 'click .cfsaf-field': function (event, template) { 98 | template.$('.cfsaf-hidden').click(); 99 | }, 100 | 'change .cfsaf-hidden': singleHandler, 101 | 'dropped .cfsaf-field': singleHandler 102 | }); 103 | 104 | var multipleHandler = function (event, template) { 105 | // Get the FileList object from the event object 106 | var fileList = event.originalEvent.dataTransfer ? event.originalEvent.dataTransfer.files : event.currentTarget.files; 107 | 108 | // Store the FileList on the file input for later. We store an array of 109 | // separate FileList objects. Browsers don't let you add/remove items from 110 | // an existing FileList. 111 | var fileListList = template.$('.cfsaf-hidden').data("cfsaf_files_multi") || []; 112 | fileListList.push(fileList); 113 | template.$('.cfsaf-hidden').data("cfsaf_files_multi", fileListList); 114 | 115 | // Get full list of files to display in the visible, read-only field 116 | var fullNameList = []; 117 | _.each(fileListList, function (fileList) { 118 | _.each(fileList, function (f) { 119 | fullNameList.push(f.name); 120 | }); 121 | }); 122 | template.$('.cfsaf-field').val(fullNameList.join(", ")); 123 | }; 124 | 125 | Template.cfsFilesField_bootstrap3.events({ 126 | 'click .cfsaf-field': function (event, template) { 127 | template.$('.cfsaf-hidden').click(); 128 | }, 129 | 'change .cfsaf-hidden': multipleHandler, 130 | 'dropped .cfsaf-field': multipleHandler 131 | }); 132 | 133 | function addAFHook(formId) { 134 | AutoForm.addHooks(formId, { 135 | before: { 136 | // We add a before.insert hook to upload all the files in the form. 137 | // This hook doesn't allow the form to continue submitting until 138 | // all the files are successfully uploaded. 139 | insert: CfsAutoForm.Hooks.beforeInsert, 140 | update: CfsAutoForm.Hooks.beforeUpdate 141 | }, 142 | after: { 143 | // We add an after.insert hook to delete uploaded files if the doc insert fails. 144 | insert: CfsAutoForm.Hooks.afterInsert, 145 | update: CfsAutoForm.Hooks.afterUpdate 146 | } 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "cfs:autoform", 3 | version: "2.2.1", 4 | summary: "Upload files as part of autoform submission", 5 | git: "https://github.com/aldeed/meteor-cfs-autoform.git" 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.use('underscore@1.0.1', 'client'); 10 | api.use('templating@1.0.9', 'client'); 11 | 12 | api.use('aldeed:autoform@4.0.0 || 5.0.0'); 13 | api.use('cfs:standard-packages@0.0.2', ['client', 'server'], {weak: true}); 14 | api.use('raix:ui-dropped-event@0.0.7', 'client'); 15 | 16 | // ensure standard packages are available globally incase the user didn't `meteor add cfs:standard-packages` 17 | api.imply('cfs:standard-packages'); 18 | 19 | api.export('CfsAutoForm', 'client'); 20 | 21 | api.add_files([ 22 | 'cfs-autoform.html', 23 | 'cfs-autoform-hooks.js', 24 | 'cfs-autoform-util.js', 25 | 'cfs-autoform.js', 26 | 'cfs-autoform.css' 27 | ], 'client'); 28 | }); 29 | --------------------------------------------------------------------------------