├── public ├── images │ └── image-example.png ├── stylesheets │ └── style.css ├── cloudinary_cors.html ├── jquery.ui.widget.js ├── jquery.iframe-transport.js ├── jquery.cloudinary.js └── jquery.fileupload.js ├── routes ├── index.js └── user.js ├── package.json ├── README.md ├── views ├── layout.jade └── index.jade └── app.js /public/images/image-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/dailyjs-cloudinary-gallery/master/public/images/image-example.png -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET home page. 4 | */ 5 | 6 | exports.index = function(req, res){ 7 | res.render('index', { title: 'Express' }); 8 | }; -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET users listing. 4 | */ 5 | 6 | exports.list = function(req, res){ 7 | res.send("respond with a resource"); 8 | }; -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app" 7 | }, 8 | "dependencies": { 9 | "express": "3.0.3", 10 | "jade": "*", 11 | "cloudinary": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ###DailyJS Cloudinary Tutorial 2 | 3 | This repository is source code for a [tutorial on DailyJS](http://dailyjs.com/2013/02/21/cloudinary). 4 | 5 | To run it, ensure that you first create an account at [Cloudinary](http://cloudinary.com/) and fill in your `cloud_name`, `api_key`, and `api_secret` in `app.js`. 6 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype 5 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | script(src='http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js') 9 | script(src='/jquery.ui.widget.js') 10 | script(src='/jquery.iframe-transport.js') 11 | script(src='/jquery.fileupload.js') 12 | script(src='/jquery.cloudinary.js') 13 | block scripts 14 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | 7 | form(action="/upload", method="post", enctype="multipart/form-data") 8 | input(type="file", name="image") 9 | input(type="submit", value="Upload Image") 10 | 11 | - if (images && images.length) 12 | h2 Standard 13 | 14 | - images.forEach(function(image){ 15 | a(href=image.url) 16 | img(src=cloudinary.url(image.public_id + '.' + image.format, { width: 100, height: 100, crop: 'fill', version: image.version })) 17 | - }) 18 | 19 | h2 Special Effects 20 | 21 | - images.forEach(function(image){ 22 | a(href=image.url) 23 | img(src=cloudinary.url(image.public_id + '.' + image.format, { width: 100, height: 100, crop: 'fill', effect: 'vignette', version: image.version })) 24 | - }) 25 | 26 | h2 jQuery Uploads 27 | 28 | .preview 29 | 30 | form(enctype="multipart/form-data")!=cloudinary.uploader.image_upload_tag('image') 31 | 32 | block scripts 33 | script(type="text/javascript") 34 | // Configure Cloudinary 35 | $.cloudinary.config({ api_key: '!{api_key}', cloud_name: '!{cloud_name}' }); 36 | 37 | $('.cloudinary-fileupload').bind('fileuploadstart', function(e){ 38 | $('.preview').html('Upload started...'); 39 | }); 40 | 41 | // Upload finished 42 | $('.cloudinary-fileupload').bind('cloudinarydone', function(e, data){ 43 | $('.preview').html( 44 | $.cloudinary.image(data.result.public_id, { format: data.result.format, version: data.result.version, crop: 'scale', width: 100, height: 100 }) 45 | ); 46 | return true; 47 | }); 48 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express') 7 | , routes = require('./routes') 8 | , user = require('./routes/user') 9 | , http = require('http') 10 | , path = require('path') 11 | , cloudinary = require('cloudinary') 12 | , fs = require('fs') 13 | , crypto = require('crypto') 14 | ; 15 | 16 | var app = express(); 17 | 18 | app.locals.title = "Alex's Awesome Gallery"; 19 | 20 | app.configure(function(){ 21 | app.set('port', process.env.PORT || 3000); 22 | app.set('views', __dirname + '/views'); 23 | app.set('view engine', 'jade'); 24 | app.use(express.favicon()); 25 | app.use(express.logger('dev')); 26 | app.use(express.bodyParser()); 27 | app.use(express.methodOverride()); 28 | app.use(app.router); 29 | app.use(express.static(path.join(__dirname, 'public'))); 30 | }); 31 | 32 | app.configure('development', function(){ 33 | app.use(express.errorHandler()); 34 | cloudinary.config({ cloud_name: 'YOURS', api_key: 'YOURS', api_secret: 'YOURS' }); 35 | }); 36 | 37 | app.locals.api_key = cloudinary.config().api_key; 38 | app.locals.cloud_name = cloudinary.config().cloud_name; 39 | 40 | app.get('/', function(req, res, next){ 41 | cloudinary.api.resources(function(items){ 42 | res.render('index', { images: items.resources, cloudinary: cloudinary }); 43 | }); 44 | }); 45 | 46 | app.post('/upload', function(req, res){ 47 | var imageStream = fs.createReadStream(req.files.image.path, { encoding: 'binary' }) 48 | , cloudStream = cloudinary.uploader.upload_stream(function() { res.redirect('/'); }); 49 | 50 | imageStream.on('data', cloudStream.write).on('end', cloudStream.end); 51 | }); 52 | 53 | http.createServer(app).listen(app.get('port'), function(){ 54 | console.log('Express server listening on port', app.get('port')); 55 | }); 56 | -------------------------------------------------------------------------------- /public/cloudinary_cors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/jquery.ui.widget.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI Widget 1.8.18+amd 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://jquery.org/license 8 | * 9 | * http://docs.jquery.com/UI/Widget 10 | */ 11 | 12 | (function (factory) { 13 | if (typeof define === "function" && define.amd) { 14 | // Register as an anonymous AMD module: 15 | define(["jquery"], factory); 16 | } else { 17 | // Browser globals: 18 | factory(jQuery); 19 | } 20 | }(function( $, undefined ) { 21 | 22 | // jQuery 1.4+ 23 | if ( $.cleanData ) { 24 | var _cleanData = $.cleanData; 25 | $.cleanData = function( elems ) { 26 | for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { 27 | try { 28 | $( elem ).triggerHandler( "remove" ); 29 | // http://bugs.jquery.com/ticket/8235 30 | } catch( e ) {} 31 | } 32 | _cleanData( elems ); 33 | }; 34 | } else { 35 | var _remove = $.fn.remove; 36 | $.fn.remove = function( selector, keepData ) { 37 | return this.each(function() { 38 | if ( !keepData ) { 39 | if ( !selector || $.filter( selector, [ this ] ).length ) { 40 | $( "*", this ).add( [ this ] ).each(function() { 41 | try { 42 | $( this ).triggerHandler( "remove" ); 43 | // http://bugs.jquery.com/ticket/8235 44 | } catch( e ) {} 45 | }); 46 | } 47 | } 48 | return _remove.call( $(this), selector, keepData ); 49 | }); 50 | }; 51 | } 52 | 53 | $.widget = function( name, base, prototype ) { 54 | var namespace = name.split( "." )[ 0 ], 55 | fullName; 56 | name = name.split( "." )[ 1 ]; 57 | fullName = namespace + "-" + name; 58 | 59 | if ( !prototype ) { 60 | prototype = base; 61 | base = $.Widget; 62 | } 63 | 64 | // create selector for plugin 65 | $.expr[ ":" ][ fullName ] = function( elem ) { 66 | return !!$.data( elem, name ); 67 | }; 68 | 69 | $[ namespace ] = $[ namespace ] || {}; 70 | $[ namespace ][ name ] = function( options, element ) { 71 | // allow instantiation without initializing for simple inheritance 72 | if ( arguments.length ) { 73 | this._createWidget( options, element ); 74 | } 75 | }; 76 | 77 | var basePrototype = new base(); 78 | // we need to make the options hash a property directly on the new instance 79 | // otherwise we'll modify the options hash on the prototype that we're 80 | // inheriting from 81 | // $.each( basePrototype, function( key, val ) { 82 | // if ( $.isPlainObject(val) ) { 83 | // basePrototype[ key ] = $.extend( {}, val ); 84 | // } 85 | // }); 86 | basePrototype.options = $.extend( true, {}, basePrototype.options ); 87 | $[ namespace ][ name ].prototype = $.extend( true, basePrototype, { 88 | namespace: namespace, 89 | widgetName: name, 90 | widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name, 91 | widgetBaseClass: fullName 92 | }, prototype ); 93 | 94 | $.widget.bridge( name, $[ namespace ][ name ] ); 95 | }; 96 | 97 | $.widget.bridge = function( name, object ) { 98 | $.fn[ name ] = function( options ) { 99 | var isMethodCall = typeof options === "string", 100 | args = Array.prototype.slice.call( arguments, 1 ), 101 | returnValue = this; 102 | 103 | // allow multiple hashes to be passed on init 104 | options = !isMethodCall && args.length ? 105 | $.extend.apply( null, [ true, options ].concat(args) ) : 106 | options; 107 | 108 | // prevent calls to internal methods 109 | if ( isMethodCall && options.charAt( 0 ) === "_" ) { 110 | return returnValue; 111 | } 112 | 113 | if ( isMethodCall ) { 114 | this.each(function() { 115 | var instance = $.data( this, name ), 116 | methodValue = instance && $.isFunction( instance[options] ) ? 117 | instance[ options ].apply( instance, args ) : 118 | instance; 119 | // TODO: add this back in 1.9 and use $.error() (see #5972) 120 | // if ( !instance ) { 121 | // throw "cannot call methods on " + name + " prior to initialization; " + 122 | // "attempted to call method '" + options + "'"; 123 | // } 124 | // if ( !$.isFunction( instance[options] ) ) { 125 | // throw "no such method '" + options + "' for " + name + " widget instance"; 126 | // } 127 | // var methodValue = instance[ options ].apply( instance, args ); 128 | if ( methodValue !== instance && methodValue !== undefined ) { 129 | returnValue = methodValue; 130 | return false; 131 | } 132 | }); 133 | } else { 134 | this.each(function() { 135 | var instance = $.data( this, name ); 136 | if ( instance ) { 137 | instance.option( options || {} )._init(); 138 | } else { 139 | $.data( this, name, new object( options, this ) ); 140 | } 141 | }); 142 | } 143 | 144 | return returnValue; 145 | }; 146 | }; 147 | 148 | $.Widget = function( options, element ) { 149 | // allow instantiation without initializing for simple inheritance 150 | if ( arguments.length ) { 151 | this._createWidget( options, element ); 152 | } 153 | }; 154 | 155 | $.Widget.prototype = { 156 | widgetName: "widget", 157 | widgetEventPrefix: "", 158 | options: { 159 | disabled: false 160 | }, 161 | _createWidget: function( options, element ) { 162 | // $.widget.bridge stores the plugin instance, but we do it anyway 163 | // so that it's stored even before the _create function runs 164 | $.data( element, this.widgetName, this ); 165 | this.element = $( element ); 166 | this.options = $.extend( true, {}, 167 | this.options, 168 | this._getCreateOptions(), 169 | options ); 170 | 171 | var self = this; 172 | this.element.bind( "remove." + this.widgetName, function() { 173 | self.destroy(); 174 | }); 175 | 176 | this._create(); 177 | this._trigger( "create" ); 178 | this._init(); 179 | }, 180 | _getCreateOptions: function() { 181 | return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ]; 182 | }, 183 | _create: function() {}, 184 | _init: function() {}, 185 | 186 | destroy: function() { 187 | this.element 188 | .unbind( "." + this.widgetName ) 189 | .removeData( this.widgetName ); 190 | this.widget() 191 | .unbind( "." + this.widgetName ) 192 | .removeAttr( "aria-disabled" ) 193 | .removeClass( 194 | this.widgetBaseClass + "-disabled " + 195 | "ui-state-disabled" ); 196 | }, 197 | 198 | widget: function() { 199 | return this.element; 200 | }, 201 | 202 | option: function( key, value ) { 203 | var options = key; 204 | 205 | if ( arguments.length === 0 ) { 206 | // don't return a reference to the internal hash 207 | return $.extend( {}, this.options ); 208 | } 209 | 210 | if (typeof key === "string" ) { 211 | if ( value === undefined ) { 212 | return this.options[ key ]; 213 | } 214 | options = {}; 215 | options[ key ] = value; 216 | } 217 | 218 | this._setOptions( options ); 219 | 220 | return this; 221 | }, 222 | _setOptions: function( options ) { 223 | var self = this; 224 | $.each( options, function( key, value ) { 225 | self._setOption( key, value ); 226 | }); 227 | 228 | return this; 229 | }, 230 | _setOption: function( key, value ) { 231 | this.options[ key ] = value; 232 | 233 | if ( key === "disabled" ) { 234 | this.widget() 235 | [ value ? "addClass" : "removeClass"]( 236 | this.widgetBaseClass + "-disabled" + " " + 237 | "ui-state-disabled" ) 238 | .attr( "aria-disabled", value ); 239 | } 240 | 241 | return this; 242 | }, 243 | 244 | enable: function() { 245 | return this._setOption( "disabled", false ); 246 | }, 247 | disable: function() { 248 | return this._setOption( "disabled", true ); 249 | }, 250 | 251 | _trigger: function( type, event, data ) { 252 | var prop, orig, 253 | callback = this.options[ type ]; 254 | 255 | data = data || {}; 256 | event = $.Event( event ); 257 | event.type = ( type === this.widgetEventPrefix ? 258 | type : 259 | this.widgetEventPrefix + type ).toLowerCase(); 260 | // the original event may come from any element 261 | // so we need to reset the target on the new event 262 | event.target = this.element[ 0 ]; 263 | 264 | // copy original event properties over to the new event 265 | orig = event.originalEvent; 266 | if ( orig ) { 267 | for ( prop in orig ) { 268 | if ( !( prop in event ) ) { 269 | event[ prop ] = orig[ prop ]; 270 | } 271 | } 272 | } 273 | 274 | this.element.trigger( event, data ); 275 | 276 | return !( $.isFunction(callback) && 277 | callback.call( this.element[0], event, data ) === false || 278 | event.isDefaultPrevented() ); 279 | } 280 | }; 281 | 282 | })); 283 | -------------------------------------------------------------------------------- /public/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Iframe Transport Plugin 1.4 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /*jslint unparam: true, nomen: true */ 13 | /*global define, window, document */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define(['jquery'], factory); 20 | } else { 21 | // Browser globals: 22 | factory(window.jQuery); 23 | } 24 | }(function ($) { 25 | 'use strict'; 26 | 27 | // Helper variable to create unique names for the transport iframes: 28 | var counter = 0; 29 | 30 | // The iframe transport accepts three additional options: 31 | // options.fileInput: a jQuery collection of file input fields 32 | // options.paramName: the parameter name for the file form data, 33 | // overrides the name property of the file input field(s), 34 | // can be a string or an array of strings. 35 | // options.formData: an array of objects with name and value properties, 36 | // equivalent to the return data of .serializeArray(), e.g.: 37 | // [{name: 'a', value: 1}, {name: 'b', value: 2}] 38 | $.ajaxTransport('iframe', function (options) { 39 | if (options.async && (options.type === 'POST' || options.type === 'GET')) { 40 | var form, 41 | iframe; 42 | return { 43 | send: function (_, completeCallback) { 44 | form = $('
'); 45 | // javascript:false as initial iframe src 46 | // prevents warning popups on HTTPS in IE6. 47 | // IE versions below IE8 cannot set the name property of 48 | // elements that have already been added to the DOM, 49 | // so we set the name along with the iframe HTML markup: 50 | iframe = $( 51 | '' 53 | ).bind('load', function () { 54 | var fileInputClones, 55 | paramNames = $.isArray(options.paramName) ? 56 | options.paramName : [options.paramName]; 57 | iframe 58 | .unbind('load') 59 | .bind('load', function () { 60 | var response; 61 | // Wrap in a try/catch block to catch exceptions thrown 62 | // when trying to access cross-domain iframe contents: 63 | try { 64 | response = iframe.contents(); 65 | // Google Chrome and Firefox do not throw an 66 | // exception when calling iframe.contents() on 67 | // cross-domain requests, so we unify the response: 68 | if (!response.length || !response[0].firstChild) { 69 | throw new Error(); 70 | } 71 | } catch (e) { 72 | response = undefined; 73 | } 74 | // The complete callback returns the 75 | // iframe content document as response object: 76 | completeCallback( 77 | 200, 78 | 'success', 79 | {'iframe': response} 80 | ); 81 | // Fix for IE endless progress bar activity bug 82 | // (happens on form submits to iframe targets): 83 | $('') 84 | .appendTo(form); 85 | form.remove(); 86 | }); 87 | form 88 | .prop('target', iframe.prop('name')) 89 | .prop('action', options.url) 90 | .prop('method', options.type); 91 | if (options.formData) { 92 | $.each(options.formData, function (index, field) { 93 | $('') 94 | .prop('name', field.name) 95 | .val(field.value) 96 | .appendTo(form); 97 | }); 98 | } 99 | if (options.fileInput && options.fileInput.length && 100 | options.type === 'POST') { 101 | fileInputClones = options.fileInput.clone(); 102 | // Insert a clone for each file input field: 103 | options.fileInput.after(function (index) { 104 | return fileInputClones[index]; 105 | }); 106 | if (options.paramName) { 107 | options.fileInput.each(function (index) { 108 | $(this).prop( 109 | 'name', 110 | paramNames[index] || options.paramName 111 | ); 112 | }); 113 | } 114 | // Appending the file input fields to the hidden form 115 | // removes them from their original location: 116 | form 117 | .append(options.fileInput) 118 | .prop('enctype', 'multipart/form-data') 119 | // enctype must be set as encoding for IE: 120 | .prop('encoding', 'multipart/form-data'); 121 | } 122 | form.submit(); 123 | // Insert the file input fields at their original location 124 | // by replacing the clones with the originals: 125 | if (fileInputClones && fileInputClones.length) { 126 | options.fileInput.each(function (index, input) { 127 | var clone = $(fileInputClones[index]); 128 | $(input).prop('name', clone.prop('name')); 129 | clone.replaceWith(input); 130 | }); 131 | } 132 | }); 133 | form.append(iframe).appendTo(document.body); 134 | }, 135 | abort: function () { 136 | if (iframe) { 137 | // javascript:false as iframe src aborts the request 138 | // and prevents warning popups on HTTPS in IE6. 139 | // concat is used to avoid the "Script URL" JSLint error: 140 | iframe 141 | .unbind('load') 142 | .prop('src', 'javascript'.concat(':false;')); 143 | } 144 | if (form) { 145 | form.remove(); 146 | } 147 | } 148 | }; 149 | } 150 | }); 151 | 152 | // The iframe transport returns the iframe content document as response. 153 | // The following adds converters from iframe to text, json, html, and script: 154 | $.ajaxSetup({ 155 | converters: { 156 | 'iframe text': function (iframe) { 157 | return $(iframe[0].body).text(); 158 | }, 159 | 'iframe json': function (iframe) { 160 | return $.parseJSON($(iframe[0].body).text()); 161 | }, 162 | 'iframe html': function (iframe) { 163 | return $(iframe[0].body).html(); 164 | }, 165 | 'iframe script': function (iframe) { 166 | return $.globalEval($(iframe[0].body).text()); 167 | } 168 | } 169 | }); 170 | 171 | })); 172 | -------------------------------------------------------------------------------- /public/jquery.cloudinary.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Cloudinary's jQuery library - v1.0.2 3 | * Copyright Cloudinary 4 | * see https://github.com/cloudinary/cloudinary_js 5 | */ 6 | 7 | (function( $ ) { 8 | var SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"; 9 | function option_consume(options, option_name, default_value) { 10 | var result = options[option_name]; 11 | delete options[option_name]; 12 | return typeof(result) == 'undefined' ? default_value : result; 13 | } 14 | function build_array(arg) { 15 | return $.isArray(arg) ? arg : [arg]; 16 | } 17 | function present(value) { 18 | return typeof value != 'undefined' && ("" + value).length > 0; 19 | } 20 | function generate_transformation_string(options) { 21 | var width = options['width']; 22 | var height = options['height']; 23 | var size = option_consume(options, 'size'); 24 | if (size) { 25 | var split_size = size.split("x"); 26 | options['width'] = width = split_size[0]; 27 | options['height'] = height = split_size[1]; 28 | } 29 | var has_layer = options.overlay || options.underlay; 30 | 31 | var crop = option_consume(options, 'crop'); 32 | var angle = build_array(option_consume(options, 'angle')).join("."); 33 | 34 | var no_html_sizes = has_layer || present(angle) || crop == "fit" || crop == "limit"; 35 | 36 | if (width && (no_html_sizes || parseFloat(width) < 1)) delete options['width']; 37 | if (height && (no_html_sizes || parseFloat(height) < 1)) delete options['height']; 38 | if (!crop && !has_layer) width = height = undefined; 39 | 40 | var background = option_consume(options, 'background'); 41 | background = background && background.replace(/^#/, 'rgb:'); 42 | 43 | var base_transformations = build_array(option_consume(options, 'transformation', [])); 44 | var named_transformation = []; 45 | if ($.grep(base_transformations, function(bs) {return typeof(bs) == 'object';}).length > 0) { 46 | base_transformations = $.map(base_transformations, function(base_transformation) { 47 | return typeof(base_transformation) == 'object' ? generate_transformation_string($.extend({}, base_transformation)) : generate_transformation_string({transformation: base_transformation}); 48 | }); 49 | } else { 50 | named_transformation = base_transformations.join("."); 51 | base_transformations = []; 52 | } 53 | var effect = option_consume(options, "effect"); 54 | if ($.isArray(effect)) effect = effect.join(":"); 55 | 56 | var border = option_consume(options, "border") 57 | if ($.isPlainObject(border)) { 58 | var border_width = "" + (border.width || 2); 59 | var border_color = (border.color || "black").replace(/^#/, 'rgb:'); 60 | border = border_width + "px_solid_" + border_color; 61 | } 62 | 63 | var flags = build_array(option_consume(options, 'flags')).join("."); 64 | 65 | var params = [['c', crop], ['t', named_transformation], ['w', width], ['h', height], ['b', background], ['e', effect], ['a', angle], ['bo', border], ['fl', flags]]; 66 | var simple_params = { 67 | x: 'x', 68 | y: 'y', 69 | radius: 'r', 70 | gravity: 'g', 71 | quality: 'q', 72 | prefix: 'p', 73 | default_image: 'd', 74 | underlay: 'u', 75 | overlay: 'l', 76 | fetch_format: 'f', 77 | density: 'dn', 78 | page: 'pg', 79 | color_space: 'cl', 80 | delay: 'dl' 81 | }; 82 | for (var param in simple_params) { 83 | params.push([simple_params[param], option_consume(options, param)]); 84 | } 85 | params.sort(function(a, b){return a[0]b[0] ? 1 : 0);}); 86 | params.push([option_consume(options, 'raw_transformation')]); 87 | var transformation = $.map($.grep(params, function(param) { 88 | var value = param[param.length-1]; 89 | return present(value); 90 | }), function(param) { 91 | return param.join("_"); 92 | }).join(","); 93 | base_transformations.push(transformation); 94 | return $.grep(base_transformations, present).join("/"); 95 | } 96 | var dummyImg = undefined; 97 | function absolutize(url) { 98 | if (!dummyImg) dummyImg = document.createElement("img"); 99 | dummyImg.src = url; 100 | url = dummyImg.src; 101 | dummyImg.src = null; 102 | return url; 103 | } 104 | function cloudinary_url(public_id, options) { 105 | options = options || {}; 106 | var type = option_consume(options, 'type', 'upload'); 107 | if (type == 'fetch') { 108 | options.fetch_format = options.fetch_format || option_consume(options, 'format'); 109 | } 110 | var transformation = generate_transformation_string(options); 111 | var resource_type = option_consume(options, 'resource_type', "image"); 112 | var version = option_consume(options, 'version'); 113 | var format = option_consume(options, 'format'); 114 | var cloud_name = option_consume(options, 'cloud_name', $.cloudinary.config().cloud_name); 115 | if (!cloud_name) throw "Unknown cloud_name"; 116 | var private_cdn = option_consume(options, 'private_cdn', $.cloudinary.config().private_cdn); 117 | var secure_distribution = option_consume(options, 'secure_distribution', $.cloudinary.config().secure_distribution); 118 | var cname = option_consume(options, 'cname', $.cloudinary.config().cname); 119 | var cdn_subdomain = option_consume(options, 'cdn_subdomain', $.cloudinary.config().cdn_subdomain); 120 | var secure = window.location.protocol == 'https:'; 121 | if (secure && !secure_distribution) { 122 | if (private_cdn) { 123 | throw "secure_distribution not defined"; 124 | } else { 125 | secure_distribution = SHARED_CDN; 126 | } 127 | } 128 | 129 | if (type == 'fetch') { 130 | public_id = absolutize(public_id); 131 | } 132 | 133 | if (public_id.match(/^https?:/)) { 134 | if (type == "upload" || type == "asset") return public_id; 135 | public_id = encodeURIComponent(public_id).replace(/%3A/g, ":").replace(/%2F/g, "/"); 136 | } else if (format) { 137 | public_id += "." + format; 138 | } 139 | 140 | prefix = window.location.protocol + "//"; 141 | var subdomain = cdn_subdomain ? "a" + ((crc32(public_id) % 5) + 1) + "." : ""; 142 | 143 | if (secure) { 144 | prefix += secure_distribution; 145 | } else { 146 | host = cname || (private_cdn ? cloud_name + "-res.cloudinary.com" : "res.cloudinary.com" ); 147 | prefix += subdomain + host; 148 | } 149 | if (!private_cdn) prefix += "/" + cloud_name; 150 | var url = [prefix, resource_type, type, transformation, version ? "v" + version : "", 151 | public_id].join("/").replace(/([^:])\/+/g, '$1/'); 152 | return url; 153 | } 154 | function html_only_attributes(options) { 155 | var width = option_consume(options, 'html_width'); 156 | var height = option_consume(options, 'html_height'); 157 | if (width) options['width'] = width; 158 | if (height) options['height'] = height; 159 | } 160 | var cloudinary_config = undefined; 161 | $.cloudinary = { 162 | config: function(new_config, new_value) { 163 | if (!cloudinary_config) { 164 | cloudinary_config = {}; 165 | $('meta[name^="cloudinary_"]').each(function() { 166 | cloudinary_config[$(this).attr('name').replace("cloudinary_", '')] = $(this).attr('content'); 167 | }); 168 | } 169 | if (typeof(new_value) != 'undefined') { 170 | cloudinary_config[new_config] = new_value; 171 | } else if (typeof(new_config) == 'string') { 172 | return cloudinary_config[new_config]; 173 | } else if (new_config) { 174 | cloudinary_config = new_config; 175 | } 176 | return cloudinary_config; 177 | }, 178 | url: function(public_id, options) { 179 | options = $.extend({}, options); 180 | return cloudinary_url(public_id, options); 181 | }, 182 | url_internal: cloudinary_url, 183 | image: function(public_id, options) { 184 | options = $.extend({}, options); 185 | var url = cloudinary_url(public_id, options); 186 | html_only_attributes(options); 187 | return $('').attr(options).attr('src', url); 188 | }, 189 | facebook_profile_image: function(public_id, options) { 190 | return $.cloudinary.image(public_id, $.extend({type: 'facebook'}, options)); 191 | }, 192 | twitter_profile_image: function(public_id, options) { 193 | return $.cloudinary.image(public_id, $.extend({type: 'twitter'}, options)); 194 | }, 195 | twitter_name_profile_image: function(public_id, options) { 196 | return $.cloudinary.image(public_id, $.extend({type: 'twitter_name'}, options)); 197 | }, 198 | gravatar_image: function(public_id, options) { 199 | return $.cloudinary.image(public_id, $.extend({type: 'gravatar'}, options)); 200 | }, 201 | fetch_image: function(public_id, options) { 202 | return $.cloudinary.image(public_id, $.extend({type: 'fetch'}, options)); 203 | } 204 | }; 205 | $.fn.cloudinary = function(options) { 206 | this.filter('img').each(function() { 207 | var img_options = $.extend({width: $(this).attr('width'), height: $(this).attr('height'), 208 | src: $(this).attr('src')}, 209 | $.extend($(this).data(), options)); 210 | var public_id = option_consume(img_options, 'source', option_consume(img_options, 'src')); 211 | var url = cloudinary_url(public_id, img_options); 212 | html_only_attributes(img_options); 213 | $(this).attr({src: url, width: img_options['width'], height: img_options['height']}); 214 | }); 215 | return this; 216 | }; 217 | $.fn.fetchify = function(options) { 218 | return this.cloudinary($.extend(options, {'type': 'fetch'})); 219 | }; 220 | })( jQuery ); 221 | 222 | 223 | function utf8_encode (argString) { 224 | // http://kevin.vanzonneveld.net 225 | // + original by: Webtoolkit.info (http://www.webtoolkit.info/) 226 | // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 227 | // + improved by: sowberry 228 | // + tweaked by: Jack 229 | // + bugfixed by: Onno Marsman 230 | // + improved by: Yves Sucaet 231 | // + bugfixed by: Onno Marsman 232 | // + bugfixed by: Ulrich 233 | // + bugfixed by: Rafal Kukawski 234 | // + improved by: kirilloid 235 | // * example 1: utf8_encode('Kevin van Zonneveld'); 236 | // * returns 1: 'Kevin van Zonneveld' 237 | 238 | if (argString === null || typeof argString === "undefined") { 239 | return ""; 240 | } 241 | 242 | var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); 243 | var utftext = '', 244 | start, end, stringl = 0; 245 | 246 | start = end = 0; 247 | stringl = string.length; 248 | for (var n = 0; n < stringl; n++) { 249 | var c1 = string.charCodeAt(n); 250 | var enc = null; 251 | 252 | if (c1 < 128) { 253 | end++; 254 | } else if (c1 > 127 && c1 < 2048) { 255 | enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); 256 | } else { 257 | enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); 258 | } 259 | if (enc !== null) { 260 | if (end > start) { 261 | utftext += string.slice(start, end); 262 | } 263 | utftext += enc; 264 | start = end = n + 1; 265 | } 266 | } 267 | 268 | if (end > start) { 269 | utftext += string.slice(start, stringl); 270 | } 271 | 272 | return utftext; 273 | } 274 | 275 | function crc32 (str) { 276 | // http://kevin.vanzonneveld.net 277 | // + original by: Webtoolkit.info (http://www.webtoolkit.info/) 278 | // + improved by: T0bsn 279 | // + improved by: http://stackoverflow.com/questions/2647935/javascript-crc32-function-and-php-crc32-not-matching 280 | // - depends on: utf8_encode 281 | // * example 1: crc32('Kevin van Zonneveld'); 282 | // * returns 1: 1249991249 283 | str = utf8_encode(str); 284 | var table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D"; 285 | 286 | var crc = 0; 287 | var x = 0; 288 | var y = 0; 289 | 290 | crc = crc ^ (-1); 291 | for (var i = 0, iTop = str.length; i < iTop; i++) { 292 | y = (crc ^ str.charCodeAt(i)) & 0xFF; 293 | x = "0x" + table.substr(y * 9, 8); 294 | crc = (crc >>> 8) ^ x; 295 | } 296 | 297 | crc = crc ^ (-1); 298 | //convert to unsigned 32-bit int if needed 299 | if (crc < 0) {crc += 4294967296} 300 | return crc; 301 | } 302 | 303 | (function( $ ) { 304 | if (!$.fn.fileupload) { 305 | return; 306 | } 307 | $.fn.cloudinary_fileupload = function(options) { 308 | options = $.extend({ 309 | maxFileSize: 10000000, 310 | dataType: 'json', 311 | acceptFileTypes: /(\.|\/)(gif|jpe?g|png|bmp|ico)$/i, 312 | headers: {"X-Requested-With": "XMLHttpRequest"} 313 | }, options); 314 | this.fileupload(options).bind("fileuploaddone", function(e, data) { 315 | if (data.result.error) return; 316 | data.result.path = ["v", data.result.version, "/", data.result.public_id, 317 | data.result.format ? "." + data.result.format : ""].join(""); 318 | 319 | if (data.cloudinaryField && data.form.length > 0) { 320 | var upload_info = [data.result.resource_type, "upload", data.result.path].join("/") + "#" + data.result.signature; 321 | var field = $(data.form).find('input[name="' + data.cloudinaryField + '"]'); 322 | if (field.length > 0) { 323 | field.val(upload_info); 324 | } else { 325 | $('').attr({type: "hidden", name: data.cloudinaryField}).val(upload_info).appendTo(data.form); 326 | } 327 | } 328 | $(e.target).trigger('cloudinarydone', data); 329 | }); 330 | if (!this.fileupload('option').url) { 331 | var upload_url = "https://api.cloudinary.com/v1_1/" + $.cloudinary.config().cloud_name + "/upload"; 332 | this.fileupload('option', 'url', upload_url); 333 | } 334 | return this; 335 | }; 336 | 337 | $(function() { 338 | $("input.cloudinary-fileupload[type=file]").cloudinary_fileupload(); 339 | }); 340 | })( jQuery ); 341 | -------------------------------------------------------------------------------- /public/jquery.fileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin 5.10.1 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /*jslint nomen: true, unparam: true, regexp: true */ 13 | /*global define, window, document, Blob, FormData, location */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'jquery.ui.widget' 22 | ], factory); 23 | } else { 24 | // Browser globals: 25 | factory(window.jQuery); 26 | } 27 | }(function ($) { 28 | 'use strict'; 29 | 30 | // The FileReader API is not actually used, but works as feature detection, 31 | // as e.g. Safari supports XHR file uploads via the FormData API, 32 | // but not non-multipart XHR file uploads: 33 | $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); 34 | $.support.xhrFormDataFileUpload = !!window.FormData; 35 | 36 | // The fileupload widget listens for change events on file input fields defined 37 | // via fileInput setting and paste or drop events of the given dropZone. 38 | // In addition to the default jQuery Widget methods, the fileupload widget 39 | // exposes the "add" and "send" methods, to add or directly send files using 40 | // the fileupload API. 41 | // By default, files added via file input selection, paste, drag & drop or 42 | // "add" method are uploaded immediately, but it is possible to override 43 | // the "add" callback option to queue file uploads. 44 | $.widget('blueimp.fileupload', { 45 | 46 | options: { 47 | // The namespace used for event handler binding on the dropZone and 48 | // fileInput collections. 49 | // If not set, the name of the widget ("fileupload") is used. 50 | namespace: undefined, 51 | // The drop target collection, by the default the complete document. 52 | // Set to null or an empty collection to disable drag & drop support: 53 | dropZone: $(document), 54 | // The file input field collection, that is listened for change events. 55 | // If undefined, it is set to the file input fields inside 56 | // of the widget element on plugin initialization. 57 | // Set to null or an empty collection to disable the change listener. 58 | fileInput: undefined, 59 | // By default, the file input field is replaced with a clone after 60 | // each input field change event. This is required for iframe transport 61 | // queues and allows change events to be fired for the same file 62 | // selection, but can be disabled by setting the following option to false: 63 | replaceFileInput: true, 64 | // The parameter name for the file form data (the request argument name). 65 | // If undefined or empty, the name property of the file input field is 66 | // used, or "files[]" if the file input name property is also empty, 67 | // can be a string or an array of strings: 68 | paramName: undefined, 69 | // By default, each file of a selection is uploaded using an individual 70 | // request for XHR type uploads. Set to false to upload file 71 | // selections in one request each: 72 | singleFileUploads: true, 73 | // To limit the number of files uploaded with one XHR request, 74 | // set the following option to an integer greater than 0: 75 | limitMultiFileUploads: undefined, 76 | // Set the following option to true to issue all file upload requests 77 | // in a sequential order: 78 | sequentialUploads: false, 79 | // To limit the number of concurrent uploads, 80 | // set the following option to an integer greater than 0: 81 | limitConcurrentUploads: undefined, 82 | // Set the following option to true to force iframe transport uploads: 83 | forceIframeTransport: false, 84 | // Set the following option to the location of a redirect url on the 85 | // origin server, for cross-domain iframe transport uploads: 86 | redirect: undefined, 87 | // The parameter name for the redirect url, sent as part of the form 88 | // data and set to 'redirect' if this option is empty: 89 | redirectParamName: undefined, 90 | // Set the following option to the location of a postMessage window, 91 | // to enable postMessage transport uploads: 92 | postMessage: undefined, 93 | // By default, XHR file uploads are sent as multipart/form-data. 94 | // The iframe transport is always using multipart/form-data. 95 | // Set to false to enable non-multipart XHR uploads: 96 | multipart: true, 97 | // To upload large files in smaller chunks, set the following option 98 | // to a preferred maximum chunk size. If set to 0, null or undefined, 99 | // or the browser does not support the required Blob API, files will 100 | // be uploaded as a whole. 101 | maxChunkSize: undefined, 102 | // When a non-multipart upload or a chunked multipart upload has been 103 | // aborted, this option can be used to resume the upload by setting 104 | // it to the size of the already uploaded bytes. This option is most 105 | // useful when modifying the options object inside of the "add" or 106 | // "send" callbacks, as the options are cloned for each file upload. 107 | uploadedBytes: undefined, 108 | // By default, failed (abort or error) file uploads are removed from the 109 | // global progress calculation. Set the following option to false to 110 | // prevent recalculating the global progress data: 111 | recalculateProgress: true, 112 | 113 | // Additional form data to be sent along with the file uploads can be set 114 | // using this option, which accepts an array of objects with name and 115 | // value properties, a function returning such an array, a FormData 116 | // object (for XHR file uploads), or a simple object. 117 | // The form of the first fileInput is given as parameter to the function: 118 | formData: function (form) { 119 | return form.serializeArray(); 120 | }, 121 | 122 | // The add callback is invoked as soon as files are added to the fileupload 123 | // widget (via file input selection, drag & drop, paste or add API call). 124 | // If the singleFileUploads option is enabled, this callback will be 125 | // called once for each file in the selection for XHR file uplaods, else 126 | // once for each file selection. 127 | // The upload starts when the submit method is invoked on the data parameter. 128 | // The data object contains a files property holding the added files 129 | // and allows to override plugin options as well as define ajax settings. 130 | // Listeners for this callback can also be bound the following way: 131 | // .bind('fileuploadadd', func); 132 | // data.submit() returns a Promise object and allows to attach additional 133 | // handlers using jQuery's Deferred callbacks: 134 | // data.submit().done(func).fail(func).always(func); 135 | add: function (e, data) { 136 | data.submit(); 137 | }, 138 | 139 | // Other callbacks: 140 | // Callback for the submit event of each file upload: 141 | // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); 142 | // Callback for the start of each file upload request: 143 | // send: function (e, data) {}, // .bind('fileuploadsend', func); 144 | // Callback for successful uploads: 145 | // done: function (e, data) {}, // .bind('fileuploaddone', func); 146 | // Callback for failed (abort or error) uploads: 147 | // fail: function (e, data) {}, // .bind('fileuploadfail', func); 148 | // Callback for completed (success, abort or error) requests: 149 | // always: function (e, data) {}, // .bind('fileuploadalways', func); 150 | // Callback for upload progress events: 151 | // progress: function (e, data) {}, // .bind('fileuploadprogress', func); 152 | // Callback for global upload progress events: 153 | // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); 154 | // Callback for uploads start, equivalent to the global ajaxStart event: 155 | // start: function (e) {}, // .bind('fileuploadstart', func); 156 | // Callback for uploads stop, equivalent to the global ajaxStop event: 157 | // stop: function (e) {}, // .bind('fileuploadstop', func); 158 | // Callback for change events of the fileInput collection: 159 | // change: function (e, data) {}, // .bind('fileuploadchange', func); 160 | // Callback for paste events to the dropZone collection: 161 | // paste: function (e, data) {}, // .bind('fileuploadpaste', func); 162 | // Callback for drop events of the dropZone collection: 163 | // drop: function (e, data) {}, // .bind('fileuploaddrop', func); 164 | // Callback for dragover events of the dropZone collection: 165 | // dragover: function (e) {}, // .bind('fileuploaddragover', func); 166 | 167 | // The plugin options are used as settings object for the ajax calls. 168 | // The following are jQuery ajax settings required for the file uploads: 169 | processData: false, 170 | contentType: false, 171 | cache: false 172 | }, 173 | 174 | // A list of options that require a refresh after assigning a new value: 175 | _refreshOptionsList: [ 176 | 'namespace', 177 | 'dropZone', 178 | 'fileInput', 179 | 'multipart', 180 | 'forceIframeTransport' 181 | ], 182 | 183 | _isXHRUpload: function (options) { 184 | return !options.forceIframeTransport && 185 | ((!options.multipart && $.support.xhrFileUpload) || 186 | $.support.xhrFormDataFileUpload); 187 | }, 188 | 189 | _getFormData: function (options) { 190 | var formData; 191 | if (typeof options.formData === 'function') { 192 | return options.formData(options.form); 193 | } else if ($.isArray(options.formData)) { 194 | return options.formData; 195 | } else if (options.formData) { 196 | formData = []; 197 | $.each(options.formData, function (name, value) { 198 | formData.push({name: name, value: value}); 199 | }); 200 | return formData; 201 | } 202 | return []; 203 | }, 204 | 205 | _getTotal: function (files) { 206 | var total = 0; 207 | $.each(files, function (index, file) { 208 | total += file.size || 1; 209 | }); 210 | return total; 211 | }, 212 | 213 | _onProgress: function (e, data) { 214 | if (e.lengthComputable) { 215 | var total = data.total || this._getTotal(data.files), 216 | loaded = parseInt( 217 | e.loaded / e.total * (data.chunkSize || total), 218 | 10 219 | ) + (data.uploadedBytes || 0); 220 | this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); 221 | data.lengthComputable = true; 222 | data.loaded = loaded; 223 | data.total = total; 224 | // Trigger a custom progress event with a total data property set 225 | // to the file size(s) of the current upload and a loaded data 226 | // property calculated accordingly: 227 | this._trigger('progress', e, data); 228 | // Trigger a global progress event for all current file uploads, 229 | // including ajax calls queued for sequential file uploads: 230 | this._trigger('progressall', e, { 231 | lengthComputable: true, 232 | loaded: this._loaded, 233 | total: this._total 234 | }); 235 | } 236 | }, 237 | 238 | _initProgressListener: function (options) { 239 | var that = this, 240 | xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 241 | // Accesss to the native XHR object is required to add event listeners 242 | // for the upload progress event: 243 | if (xhr.upload) { 244 | $(xhr.upload).bind('progress', function (e) { 245 | var oe = e.originalEvent; 246 | // Make sure the progress event properties get copied over: 247 | e.lengthComputable = oe.lengthComputable; 248 | e.loaded = oe.loaded; 249 | e.total = oe.total; 250 | that._onProgress(e, options); 251 | }); 252 | options.xhr = function () { 253 | return xhr; 254 | }; 255 | } 256 | }, 257 | 258 | _initXHRData: function (options) { 259 | var formData, 260 | file = options.files[0], 261 | // Ignore non-multipart setting if not supported: 262 | multipart = options.multipart || !$.support.xhrFileUpload, 263 | paramName = options.paramName[0]; 264 | if (!multipart || options.blob) { 265 | // For non-multipart uploads and chunked uploads, 266 | // file meta data is not part of the request body, 267 | // so we transmit this data as part of the HTTP headers. 268 | // For cross domain requests, these headers must be allowed 269 | // via Access-Control-Allow-Headers or removed using 270 | // the beforeSend callback: 271 | options.headers = $.extend(options.headers, { 272 | 'X-File-Name': file.name, 273 | 'X-File-Type': file.type, 274 | 'X-File-Size': file.size 275 | }); 276 | if (!options.blob) { 277 | // Non-chunked non-multipart upload: 278 | options.contentType = file.type; 279 | options.data = file; 280 | } else if (!multipart) { 281 | // Chunked non-multipart upload: 282 | options.contentType = 'application/octet-stream'; 283 | options.data = options.blob; 284 | } 285 | } 286 | if (multipart && $.support.xhrFormDataFileUpload) { 287 | if (options.postMessage) { 288 | // window.postMessage does not allow sending FormData 289 | // objects, so we just add the File/Blob objects to 290 | // the formData array and let the postMessage window 291 | // create the FormData object out of this array: 292 | formData = this._getFormData(options); 293 | if (options.blob) { 294 | formData.push({ 295 | name: paramName, 296 | value: options.blob 297 | }); 298 | } else { 299 | $.each(options.files, function (index, file) { 300 | formData.push({ 301 | name: options.paramName[index] || paramName, 302 | value: file 303 | }); 304 | }); 305 | } 306 | } else { 307 | if (options.formData instanceof FormData) { 308 | formData = options.formData; 309 | } else { 310 | formData = new FormData(); 311 | $.each(this._getFormData(options), function (index, field) { 312 | formData.append(field.name, field.value); 313 | }); 314 | } 315 | if (options.blob) { 316 | formData.append(paramName, options.blob, file.name); 317 | } else { 318 | $.each(options.files, function (index, file) { 319 | // File objects are also Blob instances. 320 | // This check allows the tests to run with 321 | // dummy objects: 322 | if (file instanceof Blob) { 323 | formData.append( 324 | options.paramName[index] || paramName, 325 | file, 326 | file.name 327 | ); 328 | } 329 | }); 330 | } 331 | } 332 | options.data = formData; 333 | } 334 | // Blob reference is not needed anymore, free memory: 335 | options.blob = null; 336 | }, 337 | 338 | _initIframeSettings: function (options) { 339 | // Setting the dataType to iframe enables the iframe transport: 340 | options.dataType = 'iframe ' + (options.dataType || ''); 341 | // The iframe transport accepts a serialized array as form data: 342 | options.formData = this._getFormData(options); 343 | // Add redirect url to form data on cross-domain uploads: 344 | if (options.redirect && $('').prop('href', options.url) 345 | .prop('host') !== location.host) { 346 | options.formData.push({ 347 | name: options.redirectParamName || 'redirect', 348 | value: options.redirect 349 | }); 350 | } 351 | }, 352 | 353 | _initDataSettings: function (options) { 354 | if (this._isXHRUpload(options)) { 355 | if (!this._chunkedUpload(options, true)) { 356 | if (!options.data) { 357 | this._initXHRData(options); 358 | } 359 | this._initProgressListener(options); 360 | } 361 | if (options.postMessage) { 362 | // Setting the dataType to postmessage enables the 363 | // postMessage transport: 364 | options.dataType = 'postmessage ' + (options.dataType || ''); 365 | } 366 | } else { 367 | this._initIframeSettings(options, 'iframe'); 368 | } 369 | }, 370 | 371 | _getParamName: function (options) { 372 | var fileInput = $(options.fileInput), 373 | paramName = options.paramName; 374 | if (!paramName) { 375 | paramName = []; 376 | fileInput.each(function () { 377 | var input = $(this), 378 | name = input.prop('name') || 'files[]', 379 | i = (input.prop('files') || [1]).length; 380 | while (i) { 381 | paramName.push(name); 382 | i -= 1; 383 | } 384 | }); 385 | if (!paramName.length) { 386 | paramName = [fileInput.prop('name') || 'files[]']; 387 | } 388 | } else if (!$.isArray(paramName)) { 389 | paramName = [paramName]; 390 | } 391 | return paramName; 392 | }, 393 | 394 | _initFormSettings: function (options) { 395 | // Retrieve missing options from the input field and the 396 | // associated form, if available: 397 | if (!options.form || !options.form.length) { 398 | options.form = $(options.fileInput.prop('form')); 399 | } 400 | options.paramName = this._getParamName(options); 401 | if (!options.url) { 402 | options.url = options.form.prop('action') || location.href; 403 | } 404 | // The HTTP request method must be "POST" or "PUT": 405 | options.type = (options.type || options.form.prop('method') || '') 406 | .toUpperCase(); 407 | if (options.type !== 'POST' && options.type !== 'PUT') { 408 | options.type = 'POST'; 409 | } 410 | }, 411 | 412 | _getAJAXSettings: function (data) { 413 | var options = $.extend({}, this.options, data); 414 | this._initFormSettings(options); 415 | this._initDataSettings(options); 416 | return options; 417 | }, 418 | 419 | // Maps jqXHR callbacks to the equivalent 420 | // methods of the given Promise object: 421 | _enhancePromise: function (promise) { 422 | promise.success = promise.done; 423 | promise.error = promise.fail; 424 | promise.complete = promise.always; 425 | return promise; 426 | }, 427 | 428 | // Creates and returns a Promise object enhanced with 429 | // the jqXHR methods abort, success, error and complete: 430 | _getXHRPromise: function (resolveOrReject, context, args) { 431 | var dfd = $.Deferred(), 432 | promise = dfd.promise(); 433 | context = context || this.options.context || promise; 434 | if (resolveOrReject === true) { 435 | dfd.resolveWith(context, args); 436 | } else if (resolveOrReject === false) { 437 | dfd.rejectWith(context, args); 438 | } 439 | promise.abort = dfd.promise; 440 | return this._enhancePromise(promise); 441 | }, 442 | 443 | // Uploads a file in multiple, sequential requests 444 | // by splitting the file up in multiple blob chunks. 445 | // If the second parameter is true, only tests if the file 446 | // should be uploaded in chunks, but does not invoke any 447 | // upload requests: 448 | _chunkedUpload: function (options, testOnly) { 449 | var that = this, 450 | file = options.files[0], 451 | fs = file.size, 452 | ub = options.uploadedBytes = options.uploadedBytes || 0, 453 | mcs = options.maxChunkSize || fs, 454 | // Use the Blob methods with the slice implementation 455 | // according to the W3C Blob API specification: 456 | slice = file.webkitSlice || file.mozSlice || file.slice, 457 | upload, 458 | n, 459 | jqXHR, 460 | pipe; 461 | if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || 462 | options.data) { 463 | return false; 464 | } 465 | if (testOnly) { 466 | return true; 467 | } 468 | if (ub >= fs) { 469 | file.error = 'uploadedBytes'; 470 | return this._getXHRPromise( 471 | false, 472 | options.context, 473 | [null, 'error', file.error] 474 | ); 475 | } 476 | // n is the number of blobs to upload, 477 | // calculated via filesize, uploaded bytes and max chunk size: 478 | n = Math.ceil((fs - ub) / mcs); 479 | // The chunk upload method accepting the chunk number as parameter: 480 | upload = function (i) { 481 | if (!i) { 482 | return that._getXHRPromise(true, options.context); 483 | } 484 | // Upload the blobs in sequential order: 485 | return upload(i -= 1).pipe(function () { 486 | // Clone the options object for each chunk upload: 487 | var o = $.extend({}, options); 488 | o.blob = slice.call( 489 | file, 490 | ub + i * mcs, 491 | ub + (i + 1) * mcs 492 | ); 493 | // Store the current chunk size, as the blob itself 494 | // will be dereferenced after data processing: 495 | o.chunkSize = o.blob.size; 496 | // Process the upload data (the blob and potential form data): 497 | that._initXHRData(o); 498 | // Add progress listeners for this chunk upload: 499 | that._initProgressListener(o); 500 | jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) 501 | .done(function () { 502 | // Create a progress event if upload is done and 503 | // no progress event has been invoked for this chunk: 504 | if (!o.loaded) { 505 | that._onProgress($.Event('progress', { 506 | lengthComputable: true, 507 | loaded: o.chunkSize, 508 | total: o.chunkSize 509 | }), o); 510 | } 511 | options.uploadedBytes = o.uploadedBytes += 512 | o.chunkSize; 513 | }); 514 | return jqXHR; 515 | }); 516 | }; 517 | // Return the piped Promise object, enhanced with an abort method, 518 | // which is delegated to the jqXHR object of the current upload, 519 | // and jqXHR callbacks mapped to the equivalent Promise methods: 520 | pipe = upload(n); 521 | pipe.abort = function () { 522 | return jqXHR.abort(); 523 | }; 524 | return this._enhancePromise(pipe); 525 | }, 526 | 527 | _beforeSend: function (e, data) { 528 | if (this._active === 0) { 529 | // the start callback is triggered when an upload starts 530 | // and no other uploads are currently running, 531 | // equivalent to the global ajaxStart event: 532 | this._trigger('start'); 533 | } 534 | this._active += 1; 535 | // Initialize the global progress values: 536 | this._loaded += data.uploadedBytes || 0; 537 | this._total += this._getTotal(data.files); 538 | }, 539 | 540 | _onDone: function (result, textStatus, jqXHR, options) { 541 | if (!this._isXHRUpload(options)) { 542 | // Create a progress event for each iframe load: 543 | this._onProgress($.Event('progress', { 544 | lengthComputable: true, 545 | loaded: 1, 546 | total: 1 547 | }), options); 548 | } 549 | options.result = result; 550 | options.textStatus = textStatus; 551 | options.jqXHR = jqXHR; 552 | this._trigger('done', null, options); 553 | }, 554 | 555 | _onFail: function (jqXHR, textStatus, errorThrown, options) { 556 | options.jqXHR = jqXHR; 557 | options.textStatus = textStatus; 558 | options.errorThrown = errorThrown; 559 | this._trigger('fail', null, options); 560 | if (options.recalculateProgress) { 561 | // Remove the failed (error or abort) file upload from 562 | // the global progress calculation: 563 | this._loaded -= options.loaded || options.uploadedBytes || 0; 564 | this._total -= options.total || this._getTotal(options.files); 565 | } 566 | }, 567 | 568 | _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { 569 | this._active -= 1; 570 | options.textStatus = textStatus; 571 | if (jqXHRorError && jqXHRorError.always) { 572 | options.jqXHR = jqXHRorError; 573 | options.result = jqXHRorResult; 574 | } else { 575 | options.jqXHR = jqXHRorResult; 576 | options.errorThrown = jqXHRorError; 577 | } 578 | this._trigger('always', null, options); 579 | if (this._active === 0) { 580 | // The stop callback is triggered when all uploads have 581 | // been completed, equivalent to the global ajaxStop event: 582 | this._trigger('stop'); 583 | // Reset the global progress values: 584 | this._loaded = this._total = 0; 585 | } 586 | }, 587 | 588 | _onSend: function (e, data) { 589 | var that = this, 590 | jqXHR, 591 | slot, 592 | pipe, 593 | options = that._getAJAXSettings(data), 594 | send = function (resolve, args) { 595 | that._sending += 1; 596 | jqXHR = jqXHR || ( 597 | (resolve !== false && 598 | that._trigger('send', e, options) !== false && 599 | (that._chunkedUpload(options) || $.ajax(options))) || 600 | that._getXHRPromise(false, options.context, args) 601 | ).done(function (result, textStatus, jqXHR) { 602 | that._onDone(result, textStatus, jqXHR, options); 603 | }).fail(function (jqXHR, textStatus, errorThrown) { 604 | that._onFail(jqXHR, textStatus, errorThrown, options); 605 | }).always(function (jqXHRorResult, textStatus, jqXHRorError) { 606 | that._sending -= 1; 607 | that._onAlways( 608 | jqXHRorResult, 609 | textStatus, 610 | jqXHRorError, 611 | options 612 | ); 613 | if (options.limitConcurrentUploads && 614 | options.limitConcurrentUploads > that._sending) { 615 | // Start the next queued upload, 616 | // that has not been aborted: 617 | var nextSlot = that._slots.shift(); 618 | while (nextSlot) { 619 | if (!nextSlot.isRejected()) { 620 | nextSlot.resolve(); 621 | break; 622 | } 623 | nextSlot = that._slots.shift(); 624 | } 625 | } 626 | }); 627 | return jqXHR; 628 | }; 629 | this._beforeSend(e, options); 630 | if (this.options.sequentialUploads || 631 | (this.options.limitConcurrentUploads && 632 | this.options.limitConcurrentUploads <= this._sending)) { 633 | if (this.options.limitConcurrentUploads > 1) { 634 | slot = $.Deferred(); 635 | this._slots.push(slot); 636 | pipe = slot.pipe(send); 637 | } else { 638 | pipe = (this._sequence = this._sequence.pipe(send, send)); 639 | } 640 | // Return the piped Promise object, enhanced with an abort method, 641 | // which is delegated to the jqXHR object of the current upload, 642 | // and jqXHR callbacks mapped to the equivalent Promise methods: 643 | pipe.abort = function () { 644 | var args = [undefined, 'abort', 'abort']; 645 | if (!jqXHR) { 646 | if (slot) { 647 | slot.rejectWith(args); 648 | } 649 | return send(false, args); 650 | } 651 | return jqXHR.abort(); 652 | }; 653 | return this._enhancePromise(pipe); 654 | } 655 | return send(); 656 | }, 657 | 658 | _onAdd: function (e, data) { 659 | var that = this, 660 | result = true, 661 | options = $.extend({}, this.options, data), 662 | limit = options.limitMultiFileUploads, 663 | paramName = this._getParamName(options), 664 | paramNameSet, 665 | paramNameSlice, 666 | fileSet, 667 | i; 668 | if (!(options.singleFileUploads || limit) || 669 | !this._isXHRUpload(options)) { 670 | fileSet = [data.files]; 671 | paramNameSet = [paramName]; 672 | } else if (!options.singleFileUploads && limit) { 673 | fileSet = []; 674 | paramNameSet = []; 675 | for (i = 0; i < data.files.length; i += limit) { 676 | fileSet.push(data.files.slice(i, i + limit)); 677 | paramNameSlice = paramName.slice(i, i + limit); 678 | if (!paramNameSlice.length) { 679 | paramNameSlice = paramName; 680 | } 681 | paramNameSet.push(paramNameSlice); 682 | } 683 | } else { 684 | paramNameSet = paramName; 685 | } 686 | data.originalFiles = data.files; 687 | $.each(fileSet || data.files, function (index, element) { 688 | var newData = $.extend({}, data); 689 | newData.files = fileSet ? element : [element]; 690 | newData.paramName = paramNameSet[index]; 691 | newData.submit = function () { 692 | newData.jqXHR = this.jqXHR = 693 | (that._trigger('submit', e, this) !== false) && 694 | that._onSend(e, this); 695 | return this.jqXHR; 696 | }; 697 | return (result = that._trigger('add', e, newData)); 698 | }); 699 | return result; 700 | }, 701 | 702 | // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: 703 | _normalizeFile: function (index, file) { 704 | if (file.name === undefined && file.size === undefined) { 705 | file.name = file.fileName; 706 | file.size = file.fileSize; 707 | } 708 | }, 709 | 710 | _replaceFileInput: function (input) { 711 | var inputClone = input.clone(true); 712 | $('
').append(inputClone)[0].reset(); 713 | // Detaching allows to insert the fileInput on another form 714 | // without loosing the file input value: 715 | input.after(inputClone).detach(); 716 | // Avoid memory leaks with the detached file input: 717 | $.cleanData(input.unbind('remove')); 718 | // Replace the original file input element in the fileInput 719 | // collection with the clone, which has been copied including 720 | // event handlers: 721 | this.options.fileInput = this.options.fileInput.map(function (i, el) { 722 | if (el === input[0]) { 723 | return inputClone[0]; 724 | } 725 | return el; 726 | }); 727 | // If the widget has been initialized on the file input itself, 728 | // override this.element with the file input clone: 729 | if (input[0] === this.element[0]) { 730 | this.element = inputClone; 731 | } 732 | }, 733 | 734 | _onChange: function (e) { 735 | var that = e.data.fileupload, 736 | data = { 737 | files: $.each($.makeArray(e.target.files), that._normalizeFile), 738 | fileInput: $(e.target), 739 | form: $(e.target.form) 740 | }; 741 | if (!data.files.length) { 742 | // If the files property is not available, the browser does not 743 | // support the File API and we add a pseudo File object with 744 | // the input value as name with path information removed: 745 | data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; 746 | } 747 | if (that.options.replaceFileInput) { 748 | that._replaceFileInput(data.fileInput); 749 | } 750 | if (that._trigger('change', e, data) === false || 751 | that._onAdd(e, data) === false) { 752 | return false; 753 | } 754 | }, 755 | 756 | _onPaste: function (e) { 757 | var that = e.data.fileupload, 758 | cbd = e.originalEvent.clipboardData, 759 | items = (cbd && cbd.items) || [], 760 | data = {files: []}; 761 | $.each(items, function (index, item) { 762 | var file = item.getAsFile && item.getAsFile(); 763 | if (file) { 764 | data.files.push(file); 765 | } 766 | }); 767 | if (that._trigger('paste', e, data) === false || 768 | that._onAdd(e, data) === false) { 769 | return false; 770 | } 771 | }, 772 | 773 | _onDrop: function (e) { 774 | var that = e.data.fileupload, 775 | dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, 776 | data = { 777 | files: $.each( 778 | $.makeArray(dataTransfer && dataTransfer.files), 779 | that._normalizeFile 780 | ) 781 | }; 782 | if (that._trigger('drop', e, data) === false || 783 | that._onAdd(e, data) === false) { 784 | return false; 785 | } 786 | e.preventDefault(); 787 | }, 788 | 789 | _onDragOver: function (e) { 790 | var that = e.data.fileupload, 791 | dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; 792 | if (that._trigger('dragover', e) === false) { 793 | return false; 794 | } 795 | if (dataTransfer) { 796 | dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; 797 | } 798 | e.preventDefault(); 799 | }, 800 | 801 | _initEventHandlers: function () { 802 | var ns = this.options.namespace; 803 | if (this._isXHRUpload(this.options)) { 804 | this.options.dropZone 805 | .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) 806 | .bind('drop.' + ns, {fileupload: this}, this._onDrop) 807 | .bind('paste.' + ns, {fileupload: this}, this._onPaste); 808 | } 809 | this.options.fileInput 810 | .bind('change.' + ns, {fileupload: this}, this._onChange); 811 | }, 812 | 813 | _destroyEventHandlers: function () { 814 | var ns = this.options.namespace; 815 | this.options.dropZone 816 | .unbind('dragover.' + ns, this._onDragOver) 817 | .unbind('drop.' + ns, this._onDrop) 818 | .unbind('paste.' + ns, this._onPaste); 819 | this.options.fileInput 820 | .unbind('change.' + ns, this._onChange); 821 | }, 822 | 823 | _setOption: function (key, value) { 824 | var refresh = $.inArray(key, this._refreshOptionsList) !== -1; 825 | if (refresh) { 826 | this._destroyEventHandlers(); 827 | } 828 | $.Widget.prototype._setOption.call(this, key, value); 829 | if (refresh) { 830 | this._initSpecialOptions(); 831 | this._initEventHandlers(); 832 | } 833 | }, 834 | 835 | _initSpecialOptions: function () { 836 | var options = this.options; 837 | if (options.fileInput === undefined) { 838 | options.fileInput = this.element.is('input:file') ? 839 | this.element : this.element.find('input:file'); 840 | } else if (!(options.fileInput instanceof $)) { 841 | options.fileInput = $(options.fileInput); 842 | } 843 | if (!(options.dropZone instanceof $)) { 844 | options.dropZone = $(options.dropZone); 845 | } 846 | }, 847 | 848 | _create: function () { 849 | var options = this.options; 850 | // Initialize options set via HTML5 data-attributes: 851 | $.extend(options, $(this.element[0].cloneNode(false)).data()); 852 | options.namespace = options.namespace || this.widgetName; 853 | this._initSpecialOptions(); 854 | this._slots = []; 855 | this._sequence = this._getXHRPromise(true); 856 | this._sending = this._active = this._loaded = this._total = 0; 857 | this._initEventHandlers(); 858 | }, 859 | 860 | destroy: function () { 861 | this._destroyEventHandlers(); 862 | $.Widget.prototype.destroy.call(this); 863 | }, 864 | 865 | enable: function () { 866 | $.Widget.prototype.enable.call(this); 867 | this._initEventHandlers(); 868 | }, 869 | 870 | disable: function () { 871 | this._destroyEventHandlers(); 872 | $.Widget.prototype.disable.call(this); 873 | }, 874 | 875 | // This method is exposed to the widget API and allows adding files 876 | // using the fileupload API. The data parameter accepts an object which 877 | // must have a files property and can contain additional options: 878 | // .fileupload('add', {files: filesList}); 879 | add: function (data) { 880 | if (!data || this.options.disabled) { 881 | return; 882 | } 883 | data.files = $.each($.makeArray(data.files), this._normalizeFile); 884 | this._onAdd(null, data); 885 | }, 886 | 887 | // This method is exposed to the widget API and allows sending files 888 | // using the fileupload API. The data parameter accepts an object which 889 | // must have a files property and can contain additional options: 890 | // .fileupload('send', {files: filesList}); 891 | // The method returns a Promise object for the file upload call. 892 | send: function (data) { 893 | if (data && !this.options.disabled) { 894 | data.files = $.each($.makeArray(data.files), this._normalizeFile); 895 | if (data.files.length) { 896 | return this._onSend(null, data); 897 | } 898 | } 899 | return this._getXHRPromise(false, data && data.context); 900 | } 901 | 902 | }); 903 | 904 | })); 905 | --------------------------------------------------------------------------------