diff --git a/Gruntfile.js b/Gruntfile.js index a74cccc..187f6cb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,7 +4,7 @@ module.exports = function(grunt) { run_node: { start: { - files: { src: [ 'tests/mock-file-server.js'] } + files: { src: [ 'test/mock-file-server.js'] } } }, @@ -35,41 +35,50 @@ module.exports = function(grunt) { }, jasmine: { - host: 'http://localhost:8888/', - options: { - specs: ['tests/*spec.js'], - //keepRunner: true, - vendor: [ - "bower_components/Blob/Blob.js", - "bower_components/jquery/dist/jquery.min.js", - "bower_components/underscore/underscore-min.js", - "bower_components/backbone/backbone.js", - "backbone-model-file-upload.js" - ] - } - }, + amd: { + src: 'backbone-model-file-upload.js', + host: 'http://localhost:8888/', + options: { + specs: ['test/*spec.js'], + helpers: 'bower_components/Blob/Blob.js', + //keepRunner: true, + template: require('grunt-template-jasmine-requirejs'), + templateOptions: { + requireConfig: { + paths: { + "jquery": "bower_components/jquery/dist/jquery.min", + "underscore": "bower_components/underscore/underscore-min", + "backbone": "bower_components/backbone/backbone" + } + } + }, - parallel: { - runTest: { - tasks: [{ - grunt: true, - args: ['run:mockServer'] - }, { - grunt: true, - args: ['jasmine'] - }, { - cmd: 'node:kill' - }] + } + }, + browserGlobal: { + src: 'backbone-model-file-upload.js', + host: 'http://localhost:8888/', + options: { + specs: ['test/*spec.js'], + //keepRunner: true, + vendor: [ + "bower_components/Blob/Blob.js", + "bower_components/jquery/dist/jquery.min.js", + "bower_components/underscore/underscore-min.js", + "bower_components/backbone/backbone.js" + ] + } } } + }); grunt.loadNpmTasks('grunt-contrib-jasmine'); grunt.loadNpmTasks('grunt-run'); - grunt.loadNpmTasks('grunt-parallel'); grunt.loadNpmTasks('grunt-run-node'); - grunt.registerTask('test', ['run:installBower','run_node','jasmine','stop_node']); + grunt.registerTask('test', ['build','run_node','jasmine','stop_node']); + grunt.registerTask('build', ['run:installBower']); grunt.registerTask('resetNodeWin', ['run:killAllNodeWindows']); grunt.registerTask('resetNodeMac', ['run:killAllNodeMac']); diff --git a/README.md b/README.md index fb86609..0931299 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ Backbone.Model File Upload ========================== +![alt tag](https://travis-ci.org/homeslicesolutions/backbone-model-file-upload.svg?branch=master) A concise, non-iframe, & pure XHR2/AJAX Backbone.model file upload. (Good for IE >= 10, FF, Chrome.) -![alt tag](https://travis-ci.org/homeslicesolutions/backbone-model-file-upload.svg?branch=master) + This plugin upgrades the current `save` method to be able to upload files using the HTML5's File API and FormData class. @@ -162,18 +163,20 @@ c Version v0.1 #### Version 0.5.2 - Add jQuery to the UMD dependency model - - + +#### Version 0.5.3 + - Added CommonJS support ### Dev/Installation If you want to work on this plugin, test it, etc., it just needs an install of `node` and `grunt`. ``` npm i -d ``` +To build +``` +grunt build +``` To test ``` -npm test +npm test OR grunt test ``` - -### Future plans -As I'm looking at this plugin and all the plugins, I'm not really extending out the Backbone class like how it should. Also people have been asking me to write a mixin version instead. Also there should be a CommonJS version as well. If any of you guys want to help please do. That would be great! - diff --git a/backbone-model-file-upload.js b/backbone-model-file-upload.js index 5ce0504..377972e 100644 --- a/backbone-model-file-upload.js +++ b/backbone-model-file-upload.js @@ -1,4 +1,4 @@ -// Backbone.Model File Upload v0.5.2 +// Backbone.Model File Upload v0.5.3 // by Joe Vu - joe.vu@homeslicesolutions.com // For all details and documentation: // https://github.com/homeslicesolutions/backbone-model-file-upload @@ -7,21 +7,32 @@ // bildja - Dima Bildin - github.com/bildja // Minjung - Alejandro - github.com/Minjung -(function (root, factory) { +(function(root, factory) { + + // AMD if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['underscore', 'jquery', 'backbone'], factory); + define(['underscore', 'jquery', 'backbone'], function(_, $, Backbone){ + factory(root, Backbone, _, $); + }); + + // NodeJS/CommonJS + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'), $ = require('jquery'), Backbone = require('backbone'); + factory(root, _, $, Backbone); + + // Browser global } else { - // Browser globals - factory(_, $, Backbone); + factory(root, root.Backbone, root._, root.$); } -}(this, function(_, $, Backbone){ - // Clone the original Backbone.Model.prototype - var backboneModelClone = _.clone( Backbone.Model.prototype ); +}(this, function(root, Backbone, _, $) { + 'use strict'; + + // Clone the original Backbone.Model.prototype as superClass + var _superClass = _.clone( Backbone.Model.prototype ); // Extending out - _.extend(Backbone.Model.prototype, { + var BackboneModelFileUpload = Backbone.Model.extend({ // ! Default file attribute - can be overwritten fileAttribute: 'file', @@ -32,7 +43,7 @@ // Variables var attrs, attributes = this.attributes; - // Signature parsing - taken directly from original Backbone.Model.save + // Signature parsing - taken directly from original Backbone.Model.save // and it states: 'Handle both "key", value and {key: value} -style arguments.' if (key == null || typeof key === 'object') { attrs = key; @@ -57,16 +68,16 @@ } // Check for "formData" flag and check for if file exist. - if ( options.formData === true - || options.formData !== false - && mergedAttrs[ this.fileAttribute ] - && mergedAttrs[ this.fileAttribute ] instanceof File - || mergedAttrs[ this.fileAttribute ] instanceof FileList - || mergedAttrs[ this.fileAttribute ] instanceof Blob ) { - + if ( options.formData === true + || options.formData !== false + && mergedAttrs[ this.fileAttribute ] + && mergedAttrs[ this.fileAttribute ] instanceof File + || mergedAttrs[ this.fileAttribute ] instanceof FileList + || mergedAttrs[ this.fileAttribute ] instanceof Blob ) { + // Flatten Attributes reapplying File Object var formAttrs = _.clone( mergedAttrs ), - fileAttr = mergedAttrs[ this.fileAttribute ]; + fileAttr = mergedAttrs[ this.fileAttribute ]; formAttrs = this._flatten( formAttrs ); formAttrs[ this.fileAttribute ] = fileAttr; @@ -93,15 +104,15 @@ var xhr = $.ajaxSettings.xhr(); xhr.upload.addEventListener('progress', that._progressHandler.bind(that), false); return xhr; - } + } } // Resume back to original state if (attrs && options.wait) this.attributes = attributes; // Continue to call the existing "save" method - return backboneModelClone.save.call(this, attrs, options); - + return _superClass.save.call(this, attrs, options); + }, // _ FlattenObject gist by "penguinboy". Thank You! @@ -123,7 +134,7 @@ return output; }, - + // _ Get the Progress of the uploading file _progressHandler: function( event ) { if (event.lengthComputable) { @@ -131,7 +142,9 @@ this.trigger( 'progress', percentComplete ); } } - }); + // Export out to override Backbone Model + Backbone.Model = BackboneModelFileUpload; + })); diff --git a/bower.json b/bower.json index f18beec..e06f360 100644 --- a/bower.json +++ b/bower.json @@ -3,6 +3,7 @@ "dependencies": { "jquery": "~2.1.1", "backbone": "~1.1.2", - "Blob": "*" + "Blob": "*", + "requirejs": "~2.1.15" } } diff --git a/package.json b/package.json index 33d1df4..09f5a40 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "grunt-contrib-jasmine": "^0.6.5", "grunt-run": "^0.3.0", "grunt-run-node": "^0.1.0", + "grunt-template-jasmine-requirejs": "^0.2.0", "lodash": "~2.4.1" }, "scripts": { diff --git a/tests/backbone-model-file-upload.spec.js b/test/backbone-model-file-upload.spec.js similarity index 99% rename from tests/backbone-model-file-upload.spec.js rename to test/backbone-model-file-upload.spec.js index dd5e96f..f2981ea 100644 --- a/tests/backbone-model-file-upload.spec.js +++ b/test/backbone-model-file-upload.spec.js @@ -14,6 +14,8 @@ fileAttribute: 'fileAttachment' }); + + var fileModel; var simulatedFileObj; @@ -136,7 +138,7 @@ // Act fileModel.set({from: 'somethingelse@email.com'}); - fileModel.save(null); + fileModel.save(); }); diff --git a/test/mock-file-server.js b/test/mock-file-server.js new file mode 100644 index 0000000..9751442 --- /dev/null +++ b/test/mock-file-server.js @@ -0,0 +1,91 @@ +'use strict'; + +var http = require('http'), + formidable = require('formidable'), + fs = require('fs'), + _ = require('lodash'); + + +var options = { + host: 'localhost', + port: 8989 +}; + +var server = http.createServer(function(req,res) { + + console.log('POST Detected with incoming Form-data...'); + + var form = new formidable.IncomingForm(); + + form.parse(req, function(err, fields, files) { + console.log('POST Finished and publishing object back to browser...'); + + var headers = {}; + headers['Content-Type'] = 'application/json'; + headers["Access-Control-Allow-Origin"] = req.headers.origin; + headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS"; + headers["Access-Control-Allow-Credentials"] = true; + headers["Access-Control-Max-Age"] = '86400'; // 24 hours + headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Access-Control-Allow-Origin, Content-Type, Accept"; + + res.writeHead(200, headers); + + var output = {}; + + _.extend(output, unflatten(fields)); + + if (_.isEmpty(files)) return res.end(JSON.stringify(output)); + + for (var i in files) { + if (files.hasOwnProperty(i)) { + + output[i] = {}; + + fs.readFile(files[i].path, function (err, data) { + + output[i].type = files[i].type; + output[i].size = files[i].size; + output[i].name = files[i].name; + output[i].lastModifiedDate = files[i].lastModifiedDate; + output[i].data = 'data:' + files[i].type + ';base64,' + data.toString('base64'); + + res.end(JSON.stringify(output)); + + }); + + } + } + + }); + +}); + +var unflatten = function(path, obj, value) { + var key, child, output; + if (Object.prototype.toString.call(path) == '[object Object]') { + output = {}; + for (key in path) { + if (path.hasOwnProperty(key)) { + unflatten(key.split('.'), output, path[key]); + } + } + return output; + } + key = path.shift(); + + if (!path.length) { + obj[key] = value; + return obj; + } + + if ((child = obj[key]) == void 0) { + child = obj[key] = {}; + } + + unflatten(path, child, value); + + return obj; +}; + +server.listen(options.port, options.host); +console.log("listening on " + options.host + ':' + options.port); diff --git a/tests/mock-file-server.js b/tests/mock-file-server.js deleted file mode 100644 index d48ccdd..0000000 --- a/tests/mock-file-server.js +++ /dev/null @@ -1,122 +0,0 @@ -'use strict'; - -var http = require('http'), - formidable = require('formidable'), - fs = require('fs'), - _ = require('lodash'); - - -var options = { - host: 'localhost', - port: 8989 -}; - - -var unflatten = function(path, obj, value) { - var key, child, output; - if (Object.prototype.toString.call(path) == '[object Object]') { - output = {}; - for (key in path) { - if (path.hasOwnProperty(key)) { - unflatten(key.split('.'), output, path[key]); - } - } - return output; - } - key = path.shift(); - - if (!path.length) { - obj[key] = value; - return obj; - } - - if ((child = obj[key]) == void 0) { - child = obj[key] = {}; - } - - unflatten(path, child, value); - - return obj; -}; - - -var server = http.createServer(function(req,res) { - - // Capture POST call - //if (req.method == 'POST' && req.headers['content-type'].match('application/json')) { - // - // console.log('POST Detected with incoming JSON...'); - // - // var body = ''; - // req.on('data', function (data) { - // body += data; - // console.log('POST Data: ' + data); - // }); - // - // req.on('end', function () { - // console.log('POST Finished and publishing object back to browser...'); - // res.writeHead(200, {'Content-Type': 'application/json'}); - // res.end(body); - // }); - // - // - // - //} else if ((req.method == 'POST' || req.method == 'OPTIONS') && req.headers['content-type'].match('multipart/form-data') > 0) { - - console.log('POST Detected with incoming Form-data...'); - - var form = new formidable.IncomingForm(); - - form.parse(req, function(err, fields, files) { - console.log('POST Finished and publishing object back to browser...'); - - var headers = {}; - headers['Content-Type'] = 'application/json'; - headers["Access-Control-Allow-Origin"] = req.headers.origin; - headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS"; - headers["Access-Control-Allow-Credentials"] = true; - headers["Access-Control-Max-Age"] = '86400'; // 24 hours - headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Access-Control-Allow-Origin, Content-Type, Accept"; - - res.writeHead(200, headers); - - var output = {}; - - _.extend(output, unflatten(fields)); - - if (_.isEmpty(files)) return res.end(JSON.stringify(output)); - - for (var i in files) { - if (files.hasOwnProperty(i)) { - - output[i] = {}; - - fs.readFile(files[i].path, function (err, data) { - - output[i].type = files[i].type; - output[i].size = files[i].size; - output[i].name = files[i].name; - output[i].lastModifiedDate = files[i].lastModifiedDate; - output[i].data = 'data:' + files[i].type + ';base64,' + data.toString('base64'); - - res.end(JSON.stringify(output)); - - }); - - } - } - - }); - - - //} else { - // console.log('GET Detected...'); - // console.log('GET Returning OK'); - // res.writeHead(200, {'Content-Type': 'text/html'}) - // res.end('OK'); - //} - -}); - -server.listen(options.port, options.host); -console.log("listening on " + options.host + ':' + options.port);