/** * Plupload - multi-runtime File Uploader * v3.1.2 * * Copyright 2018, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing * * Date: 2018-02-20 */ ;(function (global, factory) { var extract = function() { var ctx = {}; factory.apply(ctx, arguments); return ctx.plupload; }; if (typeof define === "function" && define.amd) { define("plupload", ['./moxie'], extract); } else if (typeof module === "object" && module.exports) { module.exports = extract(require('./moxie')); } else { global.plupload = extract(global.moxie); } }(this || window, function(moxie) { /** * Compiled inline version. (Library mode) */ /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ /*globals $code */ (function(exports, undefined) { "use strict"; var modules = {}; function require(ids, callback) { var module, defs = []; for (var i = 0; i < ids.length; ++i) { module = modules[ids[i]] || resolve(ids[i]); if (!module) { throw 'module definition dependecy not found: ' + ids[i]; } defs.push(module); } callback.apply(null, defs); } function define(id, dependencies, definition) { if (typeof id !== 'string') { throw 'invalid module definition, module id must be defined and be a string'; } if (dependencies === undefined) { throw 'invalid module definition, dependencies must be specified'; } if (definition === undefined) { throw 'invalid module definition, definition function must be specified'; } require(dependencies, function() { modules[id] = definition.apply(null, arguments); }); } function defined(id) { return !!modules[id]; } function resolve(id) { var target = exports; var fragments = id.split(/[.\/]/); for (var fi = 0; fi < fragments.length; ++fi) { if (!target[fragments[fi]]) { return; } target = target[fragments[fi]]; } return target; } function expose(ids) { for (var i = 0; i < ids.length; i++) { var target = exports; var id = ids[i]; var fragments = id.split(/[.\/]/); for (var fi = 0; fi < fragments.length - 1; ++fi) { if (target[fragments[fi]] === undefined) { target[fragments[fi]] = {}; } target = target[fragments[fi]]; } target[fragments[fragments.length - 1]] = modules[id]; } } // Included from: src/plupload.js /** * plupload.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** Namespace for all Plupload related classes, methods and properties. @class plupload @public @static */ define('plupload', [], function() { var o = moxie; var u = o.core.utils; // redifine event dispatcher for Flash/Silverlight runtimes u.Env.global_event_dispatcher = 'plupload.EventTarget.instance.dispatchEvent'; return { /** * Plupload version will be replaced on build. * * @property VERSION * @static * @final */ VERSION: '3.1.2', /** * The state of the queue before it has started and after it has finished * * @property STOPPED * @static * @final */ STOPPED: 1, /** * Upload process is running * * @property STARTED * @static * @final */ STARTED: 2, /** File is queued for upload @property QUEUED @static @final */ QUEUED: 1, /** File is being uploaded @property UPLOADING @static @final */ UPLOADING: 2, /** File has failed to be uploaded @property FAILED @static @final */ FAILED: 4, /** File has been uploaded successfully @property DONE @static @final */ DONE: 5, // Error constants used by the Error event /** * Generic error for example if an exception is thrown inside Silverlight. * * @property GENERIC_ERROR * @static * @final */ GENERIC_ERROR: -100, /** * HTTP transport error. For example if the server produces a HTTP status other than 200. * * @property HTTP_ERROR * @static * @final */ HTTP_ERROR: -200, /** * Generic I/O error. For example if it wasn't possible to open the file stream on local machine. * * @property IO_ERROR * @static * @final */ IO_ERROR: -300, /** * @property SECURITY_ERROR * @static * @final */ SECURITY_ERROR: -400, /** * Initialization error. Will be triggered if no runtime was initialized. * * @property INIT_ERROR * @static * @final */ INIT_ERROR: -500, /** * File size error. If the user selects a file that is too large it will be blocked and an error of this type will be triggered. * * @property FILE_SIZE_ERROR * @static * @final */ FILE_SIZE_ERROR: -600, /** * File extension error. If the user selects a file that isn't valid according to the filters setting. * * @property FILE_EXTENSION_ERROR * @static * @final */ FILE_EXTENSION_ERROR: -601, /** * Duplicate file error. If prevent_duplicates is set to true and user selects the same file again. * * @property FILE_DUPLICATE_ERROR * @static * @final */ FILE_DUPLICATE_ERROR: -602, /** * Runtime will try to detect if image is proper one. Otherwise will throw this error. * * @property IMAGE_FORMAT_ERROR * @static * @final */ IMAGE_FORMAT_ERROR: -700, /** * While working on files runtime may run out of memory and will throw this error. * * @since 2.1.2 * @property MEMORY_ERROR * @static * @final */ MEMORY_ERROR: -701, /** * Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error. * * @property IMAGE_DIMENSIONS_ERROR * @static * @final */ IMAGE_DIMENSIONS_ERROR: -702, /** Invalid option error. Will be thrown if user tries to alter the option that cannot be changed without uploader reinitialisation. @property OPTION_ERROR @static @final */ OPTION_ERROR: -800, /** * Expose whole moxie (#1469). * * @property moxie * @type Object * @final */ moxie: o, /** * In some cases sniffing is the only way around :( */ ua: u.Env, /** * Gets the true type of the built-in object (better version of typeof). * @credits Angus Croll (http://javascriptweblog.wordpress.com/) * * @method typeOf * @static * @param {Object} o Object to check. * @return {String} Object [[Class]] */ typeOf: u.Basic.typeOf, clone: u.Basic.clone, inherit: u.Basic.inherit, /** * Extends the specified object with another object. * * @method extend * @static * @param {Object} target Object to extend. * @param {Object..} obj Multiple objects to extend with. * @return {Object} Same as target, the extended object. */ extend: u.Basic.extend, extendImmutable: u.Basic.extendImmutable, /** Extends the specified object with another object(s), but only if the property exists in the target. @method extendIf @static @param {Object} target Object to extend. @param {Object} [obj]* Multiple objects to extend with. @return {Object} Same as target, the extended object. */ extendIf: u.Basic.extendIf, /** Recieve an array of functions (usually async) to call in sequence, each function receives a callback as first argument that it should call, when it completes. Finally, after everything is complete, main callback is called. Passing truthy value to the callback as a first argument will interrupt the sequence and invoke main callback immediately. @method inSeries @static @param {Array} queue Array of functions to call in sequence @param {Function} cb Main callback that is called in the end, or in case of error */ inSeries: u.Basic.inSeries, /** Recieve an array of functions (usually async) to call in parallel, each function receives a callback as first argument that it should call, when it completes. After everything is complete, main callback is called. Passing truthy value to the callback as a first argument will interrupt the process and invoke main callback immediately. @method inParallel @static @param {Array} queue Array of functions to call in sequence @param {Function} cb Main callback that is called in the end, or in case of erro */ inParallel: u.Basic.inParallel, /** * Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers. * The only way a user would be able to get the same ID is if the two persons at the same exact millisecond manages * to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique. * It's more probable for the earth to be hit with an asteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property * to an user unique key. * * @method guid * @static * @return {String} Virtually unique id. */ guid: u.Basic.guid, /** * Get array of DOM Elements by their ids. * * @method get * @param {String} id Identifier of the DOM Element * @return {Array} */ getAll: function get(ids) { var els = [], el; if (u.Basic.typeOf(ids) !== 'array') { ids = [ids]; } var i = ids.length; while (i--) { el = u.Dom.get(ids[i]); if (el) { els.push(el); } } return els.length ? els : null; }, /** Get DOM element by id @method get @param {String} id Identifier of the DOM Element @return {Node} */ get: u.Dom.get, /** * Executes the callback function for each item in array/object. If you return false in the * callback it will break the loop. * * @method each * @static * @param {Object} obj Object to iterate. * @param {function} callback Callback function to execute for each item. */ each: u.Basic.each, /** * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields. * * @method getPos * @static * @param {Element} node HTML element or element id to get x, y position from. * @param {Element} root Optional root element to stop calculations at. * @return {object} Absolute position of the specified element object with x, y fields. */ getPos: u.Dom.getPos, /** * Returns the size of the specified node in pixels. * * @method getSize * @static * @param {Node} node Node to get the size of. * @return {Object} Object with a w and h property. */ getSize: u.Dom.getSize, /** * Encodes the specified string. * * @method xmlEncode * @static * @param {String} s String to encode. * @return {String} Encoded string. */ xmlEncode: function(str) { var xmlEncodeChars = { '<': 'lt', '>': 'gt', '&': 'amp', '"': 'quot', '\'': '#39' }, xmlEncodeRegExp = /[<>&\"\']/g; return str ? ('' + str).replace(xmlEncodeRegExp, function(chr) { return xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr; }) : str; }, /** * Forces anything into an array. * * @method toArray * @static * @param {Object} obj Object with length field. * @return {Array} Array object containing all items. */ toArray: u.Basic.toArray, /** * Find an element in array and return its index if present, otherwise return -1. * * @method inArray * @static * @param {mixed} needle Element to find * @param {Array} array * @return {Int} Index of the element, or -1 if not found */ inArray: u.Basic.inArray, /** * Extends the language pack object with new items. * * @method addI18n * @static * @param {Object} pack Language pack items to add. * @return {Object} Extended language pack object. */ addI18n: o.core.I18n.addI18n, /** * Translates the specified string by checking for the english string in the language pack lookup. * * @method translate * @static * @param {String} str String to look for. * @return {String} Translated string or the input string if it wasn't found. */ translate: o.core.I18n.translate, /** * Pseudo sprintf implementation - simple way to replace tokens with specified values. * * @param {String} str String with tokens * @return {String} String with replaced tokens */ sprintf: u.Basic.sprintf, /** * Checks if object is empty. * * @method isEmptyObj * @static * @param {Object} obj Object to check. * @return {Boolean} */ isEmptyObj: u.Basic.isEmptyObj, /** * Checks if specified DOM element has specified class. * * @method hasClass * @static * @param {Object} obj DOM element like object to add handler to. * @param {String} name Class name */ hasClass: u.Dom.hasClass, /** * Adds specified className to specified DOM element. * * @method addClass * @static * @param {Object} obj DOM element like object to add handler to. * @param {String} name Class name */ addClass: u.Dom.addClass, /** * Removes specified className from specified DOM element. * * @method removeClass * @static * @param {Object} obj DOM element like object to add handler to. * @param {String} name Class name */ removeClass: u.Dom.removeClass, /** * Returns a given computed style of a DOM element. * * @method getStyle * @static * @param {Object} obj DOM element like object. * @param {String} name Style you want to get from the DOM element */ getStyle: u.Dom.getStyle, /** * Adds an event handler to the specified object and store reference to the handler * in objects internal Plupload registry (@see removeEvent). * * @method addEvent * @static * @param {Object} obj DOM element like object to add handler to. * @param {String} name Name to add event listener to. * @param {Function} callback Function to call when event occurs. * @param {String} (optional) key that might be used to add specifity to the event record. */ addEvent: u.Events.addEvent, /** * Remove event handler from the specified object. If third argument (callback) * is not specified remove all events with the specified name. * * @method removeEvent * @static * @param {Object} obj DOM element to remove event listener(s) from. * @param {String} name Name of event listener to remove. * @param {Function|String} (optional) might be a callback or unique key to match. */ removeEvent: u.Events.removeEvent, /** * Remove all kind of events from the specified object * * @method removeAllEvents * @static * @param {Object} obj DOM element to remove event listeners from. * @param {String} (optional) unique key to match, when removing events. */ removeAllEvents: u.Events.removeAllEvents, /** * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _. * * @method cleanName * @static * @param {String} s String to clean up. * @return {String} Cleaned string. */ cleanName: function(name) { var i, lookup; // Replace diacritics lookup = [ /[\300-\306]/g, 'A', /[\340-\346]/g, 'a', /\307/g, 'C', /\347/g, 'c', /[\310-\313]/g, 'E', /[\350-\353]/g, 'e', /[\314-\317]/g, 'I', /[\354-\357]/g, 'i', /\321/g, 'N', /\361/g, 'n', /[\322-\330]/g, 'O', /[\362-\370]/g, 'o', /[\331-\334]/g, 'U', /[\371-\374]/g, 'u' ]; for (i = 0; i < lookup.length; i += 2) { name = name.replace(lookup[i], lookup[i + 1]); } // Replace whitespace name = name.replace(/\s+/g, '_'); // Remove anything else name = name.replace(/[^a-z0-9_\-\.]+/gi, ''); return name; }, /** * Builds a full url out of a base URL and an object with items to append as query string items. * * @method buildUrl * @static * @param {String} url Base URL to append query string items to. * @param {Object} items Name/value object to serialize as a querystring. * @return {String} String with url + serialized query string items. */ buildUrl: function(url, items) { var query = ''; u.Basic.each(items, function(value, name) { query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value); }); if (query) { url += (url.indexOf('?') > 0 ? '&' : '?') + query; } return url; }, /** * Formats the specified number as a size string for example 1024 becomes 1 KB. * * @method formatSize * @static * @param {Number} size Size to format as string. * @return {String} Formatted size string. */ formatSize: function(size) { var self = this; function round(num, precision) { return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision); } size = parseInt(size, 10); if (isNaN(size)) { return self.translate('N/A'); } var boundary = Math.pow(1024, 4); // TB if (size > boundary) { return round(size / boundary, 1) + " " + self.translate('tb'); } // GB if (size > (boundary /= 1024)) { return round(size / boundary, 1) + " " + self.translate('gb'); } // MB if (size > (boundary /= 1024)) { return round(size / boundary, 1) + " " + self.translate('mb'); } // KB if (size > 1024) { return Math.round(size / 1024) + " " + self.translate('kb'); } return size + " " + self.translate('b'); }, /** * @private */ mimes2extList: moxie.core.utils.Mime.mimes2extList, /** Resolve url - among other things will turn relative url to absolute @method resolveUrl @static @param {String|Object} url Either absolute or relative, or a result of parseUrl call @return {String} Resolved, absolute url */ resolveUrl: u.Url.resolveUrl, /** * Parses the specified size string into a byte value. For example 10kb becomes 10240. * * @method parseSize * @static * @param {String|Number} size String to parse or number to just pass through. * @return {Number} Size in bytes. */ parseSize: u.Basic.parseSizeStr, delay: u.Basic.delay, /** Parent object for all event dispatching components and objects @class plupload.EventTarget @private @constructor */ EventTarget: moxie.core.EventTarget, /** Common set of methods and properties for every runtime instance @class plupload.Runtime @private @param {Object} options @param {String} type Sanitized name of the runtime @param {Object} [caps] Set of capabilities that differentiate specified runtime @param {Object} [modeCaps] Set of capabilities that do require specific operational mode @param {String} [preferredMode='browser'] Preferred operational mode to choose if no required capabilities were requested */ Runtime: moxie.runtime.Runtime, /** Provides a convenient way to create cross-browser file-picker. Generates file selection dialog on click, converts selected files to _File_ objects, to be used in conjunction with _Image_, preloaded in memory with _FileReader_ or uploaded to a server through _XMLHttpRequest_. @class plupload.FileInput @private @constructor @extends EventTarget @uses RuntimeClient @param {Object|String|DOMElement} options If options is string or node, argument is considered as _browse\_button_. @param {String|DOMElement} options.browse_button DOM Element to turn into file picker. @param {Array} [options.accept] Array of mime types to accept. By default accepts all. @param {String} [options.file='file'] Name of the file field (not the filename). @param {Boolean} [options.multiple=false] Enable selection of multiple files. @param {Boolean} [options.directory=false] Turn file input into the folder input (cannot be both at the same time). @param {String|DOMElement} [options.container] DOM Element to use as a container for file-picker. Defaults to parentNode for _browse\_button_. @param {Object|String} [options.required_caps] Set of required capabilities, that chosen runtime must support. */ FileInput: moxie.file.FileInput, /** Utility for preloading o.Blob/o.File objects in memory. By design closely follows [W3C FileReader](http://www.w3.org/TR/FileAPI/#dfn-filereader) interface. Where possible uses native FileReader, where - not falls back to shims. @class plupload.FileReader @private @constructor @extends EventTarget @uses RuntimeClient */ FileReader: moxie.file.FileReader }; }); // Included from: src/core/Collection.js /** * Collection.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** Helper collection class - in a way a mix of object and array @contsructor @class plupload.core.Collection @private */ define('plupload/core/Collection', [ 'plupload' ], function(Basic) { var Collection = function() { var _registry = {}; var _length = 0; var _last; plupload.extend(this, { count: function() { return _length; }, hasKey: function(key) { return _registry.hasOwnProperty(key) }, get: function(key) { return _registry[key]; }, first: function() { for (var key in _registry) { return _registry[key]; } }, last: function() { return _last; }, toObject: function() { return _registry; }, add: function(key, obj) { var self = this; if (typeof(key) === 'object' && !obj) { return plupload.each(key, function(obj, key) { self.add(key, obj); }); } if (_registry.hasOwnProperty(key)) { return self.update.apply(self, arguments); } _registry[key] = _last = obj; _length++; }, remove: function(key) { if (this.hasKey(key)) { var last = _registry[key]; delete _registry[key]; _length--; // renew ref to the last added item if necessary if (_last === last) { _last = findLast(); } } }, extract: function(key) { var item = this.get(key); this.remove(key); return item; }, shift: function() { var self = this, first, key; for (key in _registry) { first = _registry[key]; self.remove(key); return first; } }, update: function(key, obj) { _registry[key] = obj; }, each: function(cb) { plupload.each(_registry, cb); }, combineWith: function() { var newCol = new Collection(); newCol.add(_registry); plupload.each(arguments, function(col) { if (col instanceof Collection) { newCol.add(col.toObject()); } }); return newCol; }, clear: function() { _registry = {}; _last = null; _length = 0; } }); function findLast() { var key; for (key in _registry) {} return _registry[key]; } }; return Collection; }); // Included from: src/core/ArrCollection.js /** * ArrCollection.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @contsructor @class plupload.core.ArrCollection @private */ define('plupload/core/ArrCollection', [ 'plupload' ], function(plupload) { var ArrCollection = function() { var _registry = []; plupload.extend(this, { count: function() { return _registry.length; }, hasKey: function(key) { return this.getIdx(key) > -1; }, get: function(key) { var idx = this.getIdx(key); return idx > -1 ? _registry[idx] : null; }, getIdx: function(key) { for (var i = 0, length = _registry.length; i < length; i++) { if (_registry[i].uid === key) { return i; } } return -1; }, getByIdx: function(idx) { return _registry[idx] }, first: function() { return _registry[0]; }, last: function() { return _registry[_registry.length - 1]; }, add: function(obj) { obj = arguments[1] || obj; // make it compatible with Collection.add() var idx = this.getIdx(obj.uid); if (idx > -1) { _registry[idx] = obj; return idx; } _registry.push(obj); return _registry.length - 1; }, remove: function(key) { return !!this.extract(key); }, splice: function(start, length) { start = plupload.typeOf(start) === 'undefinded' ? 0 : Math.max(start, 0); length = plupload.typeOf(length) !== 'undefinded' && start + length < _registry.length ? length : _registry.length - start; return _registry.splice(start, length); }, extract: function(key) { var idx = this.getIdx(key); if (idx > -1) { return _registry.splice(idx, 1); } return null; }, shift: function() { return _registry.shift(); }, update: function(key, obj) { var idx = this.getIdx(key); if (idx > -1) { _registry[idx] = obj; return true; } return false; }, each: function(cb) { plupload.each(_registry, cb); }, combineWith: function() { return Array.prototype.concat.apply(this.toArray(), arguments); }, sort: function(cb) { _registry.sort(cb || function(a, b) { return a.priority - b.priority; }); }, clear: function() { _registry = []; }, toObject: function() { var obj = {}; for (var i = 0, length = _registry.length; i < length; i++) { obj[_registry[i].uid] = _registry[i]; } return obj; }, toArray: function() { return Array.prototype.slice.call(_registry); } }); }; return ArrCollection; }); // Included from: src/core/Optionable.js /** * Optionable.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @contsructor @class plupload.core.Optionable @private @since 3.0 */ define('plupload/core/Optionable', [ 'plupload' ], function(plupload) { var EventTarget = moxie.core.EventTarget; var dispatches = [ /** * Dispatched when option is being changed. * * @event OptionChanged * @param {Object} event * @param {String} name Name of the option being changed * @param {Mixed} value * @param {Mixed} oldValue */ 'OptionChanged' ]; return (function(Parent) { /** * @class Optionable * @constructor * @extends EventTarget */ function Optionable() { Parent.apply(this, arguments); this._options = {}; } plupload.inherit(Optionable, Parent); plupload.extend(Optionable.prototype, { /** * Set the value for the specified option(s). * * @method setOption * @since 2.1 * @param {String|Object} option Name of the option to change or the set of key/value pairs * @param {Mixed} [value] Value for the option (is ignored, if first argument is object) * @param {Boolean} [mustBeDefined] if truthy, any option that is not in defaults will be ignored */ setOption: function(option, value, mustBeDefined) { var self = this; var oldValue; if (typeof(option) === 'object') { mustBeDefined = value; plupload.each(option, function(value, option) { self.setOption(option, value, mustBeDefined); }); return; } if (mustBeDefined && !self._options.hasOwnProperty(option)) { return; } oldValue = plupload.clone(self._options[option]); //! basically if an option is of type object extend it rather than replace if (plupload.typeOf(value) === 'object' && plupload.typeOf(self._options[option]) === 'object') { // having some options as objects was a bad idea, prefixes is the way plupload.extend(self._options[option], value); } else { self._options[option] = value; } self.trigger('OptionChanged', option, value, oldValue); }, /** * Get the value for the specified option or the whole configuration, if not specified. * * @method getOption * @since 2.1 * @param {String} [option] Name of the option to get * @return {Mixed} Value for the option or the whole set */ getOption: function(option) { if (!option) { return this._options; } var value = this._options[option]; if (plupload.inArray(plupload.typeOf(value), ['array', 'object']) > -1) { return plupload.extendImmutable({}, value); } else { return value; } }, /** * Set many options as once. * * @method setOptions * @param {Object} options * @param {Boolean} [mustBeDefined] if truthy, any option that is not in defaults will be ignored */ setOptions: function(options, mustBeDefined) { if (typeof(options) !== 'object') { return; } this.setOption(options, mustBeDefined); }, /** Gets all options. @method getOptions @return {Object} */ getOptions: function() { return this.getOption(); } }); return Optionable; }(EventTarget)); }); // Included from: src/core/Queueable.js /** * Queueable.js * * Copyright 2017, Ephox * Released under AGPLv3 License.se. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** Every queue item must have properties, implement methods and fire events defined in this class @contsructor @class plupload.core.Queueable @private @decorator @extends EventTarget */ define('plupload/core/Queueable', [ 'plupload', 'plupload/core/Optionable' ], function(plupload, Optionable) { var dispatches = [ /** * Dispatched every time the state of queue changes * * @event statechanged * @param {Object} event * @param {Number} state New state * @param {Number} prevState Previous state */ 'statechanged', /** * Dispatched when the item is put on pending list * * @event queued * @param {Object} event */ 'queued', /** * Dispatched as soon as activity starts * * @event started * @param {Object} event */ 'started', 'paused', 'resumed', 'stopped', /** * Dispatched as the activity progresses * * @event * @param {Object} event * @param {Number} event.percent * @param {Number} [event.processed] * @param {Number} [event.total] */ 'progress', 'failed', 'done', 'processed', 'destroy' ]; return (function(Parent) { function Queueable() { Parent.apply(this, arguments); /** Unique identifier @property uid @type {String} */ this.uid = plupload.guid(); this.state = Queueable.IDLE; this.processed = 0; this.total = 0; this.percent = 0; this.retries = 0; /** * Can be 0-Infinity - item with higher priority will have well... higher priority * @property [priority=0] * @type {Number} */ this.priority = 0; this.startedTimestamp = 0; /** * Set when item becomes Queueable.DONE or Queueable.FAILED. * Used to calculate proper processedPerSec for the queue stats. * @property processedTimestamp * @type {Number} */ this.processedTimestamp = 0; if (MXI_DEBUG) { this.bind('StateChanged', function(e, state, oldState) { var self = this; var stateToString = function(code) { switch (code) { case Queueable.IDLE: return 'IDLE'; case Queueable.PROCESSING: return 'PROCESSING'; case Queueable.PAUSED: return 'PAUSED'; case Queueable.RESUMED: return 'RESUMED'; case Queueable.DONE: return 'DONE'; case Queueable.FAILED: return 'FAILED'; case Queueable.DESTROYED: return 'DESTROYED'; } }; var indent = function() { switch (self.ctorName) { case 'File': return "\t".repeat(2); case 'QueueUpload': case 'QueueResize': return "\t"; case 'FileUploader': return "\t".repeat(3); case 'ChunkUploader': return "\t".repeat(4); default: return "\t"; } }; plupload.ua.log("StateChanged:" + indent() + self.ctorName + '::' + self.uid + ' (' + stateToString(oldState) + ' to ' + stateToString(state) + ')'); }, 999); } } Queueable.IDLE = 1; Queueable.PROCESSING = 2; Queueable.PAUSED = 6; Queueable.RESUMED = 7; Queueable.DONE = 5; Queueable.FAILED = 4; Queueable.DESTROYED = 8; plupload.inherit(Queueable, Parent); plupload.extend(Queueable.prototype, { start: function() { var prevState = this.state; if (this.state === Queueable.PROCESSING) { return false; } if (!this.startedTimestamp) { this.startedTimestamp = +new Date(); } this.state = Queueable.PROCESSING; this.trigger('statechanged', this.state, prevState); this.trigger('started'); return true; }, pause: function() { var prevState = this.state; if (plupload.inArray(this.state, [Queueable.IDLE, Queueable.RESUMED, Queueable.PROCESSING]) === -1) { return false; } this.processed = this.percent = 0; // by default reset all progress this.loaded = this.processed; // for backward compatibility this.state = Queueable.PAUSED; this.trigger('statechanged', this.state, prevState); this.trigger('paused'); return true; }, resume: function() { var prevState = this.state; if (this.state !== Queueable.PAUSED && this.state !== Queueable.RESUMED) { return false; } this.state = Queueable.RESUMED; this.trigger('statechanged', this.state, prevState); this.trigger('resumed'); return true; }, stop: function() { var prevState = this.state; if (this.state === Queueable.IDLE) { return false; } this.processed = this.percent = 0; this.loaded = this.processed; // for backward compatibility this.startedTimestamp = 0; this.state = Queueable.IDLE; this.trigger('statechanged', this.state, prevState); this.trigger('stopped'); return true; }, done: function(result) { var prevState = this.state; if (this.state === Queueable.DONE) { return false; } this.processed = this.total; this.loaded = this.processed; // for backward compatibility this.percent = 100; this.processedTimestamp = +new Date(); this.state = Queueable.DONE; this.trigger('statechanged', this.state, prevState); this.trigger('done', result); this.trigger('processed'); return true; }, failed: function(result) { var prevState = this.state; if (this.state === Queueable.FAILED) { return false; } this.processed = this.percent = 0; // reset the progress this.loaded = this.processed; // for backward compatibility this.processedTimestamp = +new Date(); this.state = Queueable.FAILED; this.trigger('statechanged', this.state, prevState); this.trigger('failed', result); this.trigger('processed'); return true; }, progress: function(processed, total) { if (total) { this.total = total; // is this even required? } this.processed = Math.min(processed, this.total); this.loaded = this.processed; // for backward compatibility this.percent = Math.ceil(this.processed / this.total * 100); this.trigger({ type: 'progress', loaded: this.processed, total: this.total }); }, destroy: function() { var prevState = this.state; if (this.state === Queueable.DESTROYED) { return false; // already destroyed } this.state = Queueable.DESTROYED; this.trigger('statechanged', this.state, prevState); this.trigger('destroy'); this.unbindAll(); return true; } }); return Queueable; }(Optionable)); }); // Included from: src/core/Stats.js /** @class plupload.core.Stats @constructor @private */ define('plupload/core/Stats', [], function() { return function() { var self = this; /** * Total queue file size. * * @property size * @deprecated use total * @type Number */ self.size = 0; /** * Total size of the queue in units. * * @property total * @since 3.0 * @type Number */ self.total = 0; /** * Total bytes uploaded. * * @property loaded * @type Number */ self.loaded = 0; /** * Number of files uploaded successfully. * * @property uploaded * @deprecated use done * @type Number */ self.uploaded = 0; /** * Number of items processed successfully. * * @property done * @since 3.0 * @type Number */ self.done = 0; /** * Number of failed items. * * @property failed * @type Number */ self.failed = 0; /** * Number of items yet to be processed. * * @property queued * @type Number */ self.queued = 0; /** * Number of items currently paused. * * @property paused * @type Number */ self.paused = 0; /** * Number of items being processed. * * @property processing * @type Number */ self.processing = 0; /** * Number of items being paused. * * @property paused * @type Number */ self.paused = 0; /** * Percent of processed units. * * @property percent * @type Number */ self.percent = 0; /** * Bytes processed per second. * * @property bytesPerSec * @deprecated use processedPerSec * @type Number */ self.bytesPerSec = 0; /** * Units processed per second. * * @property processedPerSec * @since 3.0 * @type Number */ self.processedPerSec = 0; /** * Resets the progress to its initial values. * * @method reset */ self.reset = function() { self.size = // deprecated self.total = self.loaded = // deprecated self.processed = self.uploaded = // deprecated self.done = self.failed = self.queued = self.processing = self.paused = self.percent = self.bytesPerSec = // deprecated self.processedPerSec = 0; }; }; }); // Included from: src/core/Queue.js /** * Queue.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @contsructor @class plupload.core.Queue @private */ define('plupload/core/Queue', [ 'plupload', 'plupload/core/ArrCollection', 'plupload/core/Queueable', 'plupload/core/Stats' ], function(plupload, ArrCollection, Queueable, Stats) { var dispatches = [ /** * Dispatched as soon as activity starts * * @event started * @param {Object} event */ 'Started', /** * Dispatched as activity progresses * * @event progress * @param {Object} event * @param {Number} processed * @param {Number} total * @param {plupload.core.Stats} stats */ 'Progress', /** * Dispatched when activity is paused * * @event paused * @param {Object} event */ 'Paused', /** * Dispatched when there's no more items in processing * * @event done * @param {Object} event */ 'Done', /** * Dispatched as soon as activity ends * * @event stopped * @param {Object} event */ 'Stopped', /** * Dispatched when queue is destroyed * * @event destroy * @param {Object} event */ 'Destroy' ]; /** * @class Queue * @constructor * @extends EventTarget */ return (function(Parent) { plupload.inherit(Queue, Parent); function Queue(options) { Parent.apply(this, arguments); /** @property _queue @type {Collection} @private */ this._queue = new ArrCollection(); /** @property stats @type {Stats} @readOnly */ this.stats = new Stats(); this._options = plupload.extend({}, this._options, { max_slots: 1, max_retries: 0, auto_start: false, finish_active: false }, options); } plupload.extend(Queue.prototype, { /** * Returns number of items in the queue * * @method count * @returns {Number} */ count: function() { return this._queue.count(); }, /** * Start the queue * * @method start */ start: function() { if (!Queue.parent.start.call(this)) { return false; } return processNext.call(this); }, pause: function() { if (!Queue.parent.pause.call(this)) { return false; } this.forEachItem(function(item) { item.pause(); }); }, /** * Stop the queue. If `finish_active=true` the queue will wait until active items are done, before * stopping. * * @method stop */ stop: function() { if (!Queue.parent.stop.call(this) || this.getOption('finish_active')) { return false; } if (this.isActive()) { this.forEachItem(function(item) { item.stop(); }); } }, forEachItem: function(cb) { this._queue.each(cb); }, getItem: function(uid) { return this._queue.get(uid); }, /** * Add instance of Queueable to the queue. If `auto_start=true` queue will start as well. * * @method addItem * @param {Queueable} item */ addItem: function(item) { var self = this; item.bind('Started', function() { if (self.calcStats()) { plupload.delay.call(self, processNext); } }); item.bind('Resumed',function() { self.start(); }); item.bind('Paused', function() { if (self.calcStats()) { plupload.delay.call(self, function() { if (!processNext.call(self) && !self.stats.processing) { self.pause(); } }); } }); item.bind('Processed Stopped', function() { if (self.calcStats()) { plupload.delay.call(self, function() { if (!processNext.call(self) && !this.isStopped() && !this.isActive()) { self.stop(); } }); } }); item.bind('Progress', function() { if (self.calcStats()) { self.trigger('Progress', self.stats.processed, self.stats.total, self.stats); } }); item.bind('Failed', function() { if (self.getOption('max_retries') && this.retries < self.getOption('max_retries')) { this.stop(); this.retries++; } }); this._queue.add(item.uid, item); this.calcStats(); item.trigger('Queued'); if (self.getOption('auto_start') || self.state === Queueable.PAUSED) { plupload.delay.call(this, this.start); } }, /** * Extracts item from the queue by its uid and returns it. * * @method extractItem * @param {String} uid * @return {Queueable} Item that was removed */ extractItem: function(uid) { var item = this._queue.get(uid); if (item) { this.stopItem(item.uid); this._queue.remove(uid); this.calcStats(); } return item; }, /** * Removes item from the queue and destroys it * * @method removeItem * @param {String} uid * @returns {Boolean} Result of the operation */ removeItem: function(uid) { var item = this.extractItem(uid); if (item) { item.destroy(); return true; } return false; }, stopItem: function(uid) { var item = this._queue.get(uid); if (item) { return item.stop(); } else { return false; } }, pauseItem: function(uid) { var item = this._queue.get(uid); if (item) { return item.pause(); } else { return false; } }, resumeItem: function(uid) { var item = this._queue.get(uid); if (item) { plupload.delay.call(this, function() { this.start(); // start() will know if it needs to restart the queue }); return item.resume(); } else { return false; } }, splice: function(start, length) { return this._queue.splice(start, length); }, isActive: function() { return this.stats && (this.stats.processing || this.stats.paused); }, isStopped: function() { return this.state === Queueable.IDLE || this.state === Queueable.DESTROYED; }, countSpareSlots: function() { return Math.max(this.getOption('max_slots') - this.stats.processing, 0); }, toArray: function() { return this._queue.toArray(); }, clear: function() { var self = this; if (self.state !== Queueable.IDLE) { // stop the active queue first self.bindOnce('Stopped', function() { self.clear(); }); return self.stop(); } else { self._queue.clear(); self.stats.reset(); } }, calcStats: function() { var self = this; var stats = self.stats; var processed = 0; var processedDuringThisSession = 0; if (!stats) { return false; // maybe queue is destroyed } stats.reset(); self.forEachItem(function(item) { switch (item.state) { case Queueable.DONE: stats.done++; stats.uploaded = stats.done; // for backward compatibility break; case Queueable.FAILED: stats.failed++; break; case Queueable.PROCESSING: stats.processing++; break; case Queueable.PAUSED: stats.paused++; break; default: stats.queued++; } processed += item.processed; if (!item.processedTimestamp || item.processedTimestamp > self.startedTimestamp) { processedDuringThisSession += processed; } stats.processedPerSec = Math.ceil(processedDuringThisSession / ((+new Date() - self.startedTimestamp || 1) / 1000.0)); stats.processed = processed; stats.total += item.total; if (stats.total) { stats.percent = Math.ceil(stats.processed / stats.total * 100); } }); // enable properties inherited from Queueable /* TODO: this is good but it currently conflicts with deprecated total property in Uploader self.processed = stats.processed; self.total = stats.total; */ self.percent = stats.percent; // for backward compatibility stats.loaded = stats.processed; stats.size = stats.total; stats.bytesPerSec = stats.processedPerSec; return true; }, destroy: function() { var self = this; if (self.state === Queueable.DESTROYED) { return false; // already destroyed } if (self.state !== Queueable.IDLE) { // stop the active queue first self.bindOnce('Stopped', function() { plupload.delay.call(self, self.destroy); }); return self.stop(); } else { self.clear(); Queue.parent.destroy.call(this); self._queue = self.stats = null; } return true; } }); /** * Returns another Queueable.IDLE or Queueable.RESUMED item, or null. */ function getNextIdleItem() { var nextItem; this.forEachItem(function(item) { if (item.state === Queueable.IDLE || item.state === Queueable.RESUMED) { nextItem = item; return false; } }); return nextItem ? nextItem : null; } function processNext() { var item; if (this.state !== Queueable.PROCESSING && this.state !== Queueable.PAUSED) { return false; } if (this.stats.processing < this.getOption('max_slots')) { item = getNextIdleItem.call(this); if (item) { if (item.trigger('beforestart')) { item.setOptions(this.getOptions()); return item.start(); } else { item.pause(); // we need to call it sync, otherwise another thread may pick up the same file, while it is processed in beforestart handler processNext.call(this); } } } return false; } return Queue; }(Queueable)); }); // Included from: src/QueueUpload.js /** * QueueUpload.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @class plupload.QueueUpload @extends plupload.core.Queue @constructor @private @final @since 3.0 @param {Object} options */ define('plupload/QueueUpload', [ 'plupload', 'plupload/core/Queue' ], function(plupload, Queue) { return (function(Parent) { plupload.inherit(QueueUpload, Parent); function QueueUpload(options) { Queue.call(this, { max_slots: 1, max_retries: 0, auto_start: false, finish_active: false, url: false, chunk_size: 0, multipart: true, http_method: 'POST', params: {}, headers: false, file_data_name: 'file', send_file_name: true, stop_on_fail: true }); this.setOption = function(option, value) { if (typeof(option) !== 'object') { if (option == 'max_upload_slots') { option = 'max_slots'; } } QueueUpload.prototype.setOption.call(this, option, value, true); }; this.setOptions(options); } return QueueUpload; }(Queue)); }); // Included from: src/QueueResize.js /** * QueueResize.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @class plupload.QueueResize @extends plupload.core.Queue @constructor @private @final @since 3.0 @param {Object} options */ define('plupload/QueueResize', [ 'plupload', 'plupload/core/Queue' ], function(plupload, Queue) { return (function(Parent) { plupload.inherit(QueueResize, Parent); function QueueResize(options) { Queue.call(this, { max_slots: 1, max_retries: 0, auto_start: false, finish_active: false, resize: {} }); this.setOption = function(option, value) { if (typeof(option) !== 'object') { if (option == 'max_resize_slots') { option = 'max_slots'; } } QueueResize.prototype.setOption.call(this, option, value, true); }; this.setOptions(options); } return QueueResize; }(Queue)); }); // Included from: src/ChunkUploader.js /** * ChunkUploader.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** * @class plupload.ChunkUploader * @extends plupload.core.Queueable * @constructor * @private * @final * @constructor */ define('plupload/ChunkUploader', [ 'plupload', 'plupload/core/Collection', 'plupload/core/Queueable' ], function(plupload, Collection, Queueable) { var XMLHttpRequest = moxie.xhr.XMLHttpRequest; var FormData = moxie.xhr.FormData; function ChunkUploader(blob) { var _xhr; Queueable.call(this); this._options = { file_data_name: 'file', headers: false, http_method: 'POST', multipart: true, params: {}, send_file_name: true, url: false }; plupload.extend(this, { start: function() { var self = this; var url; var formData; var prevState = this.state; var options = self._options; if (this.state === Queueable.PROCESSING) { return false; } if (!this.startedTimestamp) { this.startedTimestamp = +new Date(); } this.state = Queueable.PROCESSING; this.trigger('statechanged', this.state, prevState); _xhr = new XMLHttpRequest(); if (_xhr.upload) { _xhr.upload.onprogress = function(e) { self.progress(e.loaded, e.total); }; } _xhr.onload = function() { var result = { response: this.responseText, status: this.status, responseHeaders: this.getAllResponseHeaders() }; if (this.status < 200 || this.status >= 400) { // assume error return self.failed(result); } self.done(result); }; _xhr.onerror = function() { self.failed(); // TODO: reason here }; _xhr.onloadend = function() { // we do not need _xhr anymore, so destroy it setTimeout(function() { // we detach to sustain reference until all handlers are done if (_xhr) { _xhr.destroy(); _xhr = null; } }, 1); }; try { url = options.multipart ? options.url : buildUrl(options.url, options.params); _xhr.open(options.http_method, url, true); // headers must be set after request is already opened, otherwise INVALID_STATE_ERR exception will raise if (!plupload.isEmptyObj(options.headers)) { plupload.each(options.headers, function(val, key) { _xhr.setRequestHeader(key, val); }); } if (options.multipart) { formData = new FormData(); if (!plupload.isEmptyObj(options.params)) { plupload.each(options.params, function(val, key) { formData.append(key, val); }); } formData.append(options.file_data_name, blob); _xhr.send(formData); } else { // if no multipart, send as binary stream if (plupload.isEmptyObj(options.headers) || !_xhr.hasRequestHeader('content-type')) { _xhr.setRequestHeader('content-type', 'application/octet-stream'); // binary stream header } _xhr.send(blob); } this.trigger('started'); } catch(ex) { self.failed(); } }, stop: function() { if (_xhr) { _xhr.abort(); _xhr.destroy(); _xhr = null; } ChunkUploader.prototype.stop.call(this); }, setOption: function(option, value) { ChunkUploader.prototype.setOption.call(this, option, value, true); }, setOptions: function(options) { ChunkUploader.prototype.setOption.call(this, options, true); }, destroy: function() { this.stop(); ChunkUploader.prototype.destroy.call(this); } }); /** * Builds a full url out of a base URL and an object with items to append as query string items. * * @method buildUrl * @private * @param {String} url Base URL to append query string items to. * @param {Object} items Name/value object to serialize as a querystring. * @return {String} String with url + serialized query string items. */ function buildUrl(url, items) { var query = ''; plupload.each(items, function(value, name) { query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value); }); if (query) { url += (url.indexOf('?') > 0 ? '&' : '?') + query; } return url; } } plupload.inherit(ChunkUploader, Queueable); return ChunkUploader; }); // Included from: src/FileUploader.js /** * FileUploader.js * * Copyright 2017, Ephox * Released under AGPLv3 License.se. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** * @class plupload.FileUploader * @extends plupload.core.Queueable * @constructor * @since 3.0 * @final */ define('plupload/FileUploader', [ 'plupload', 'plupload/core/Collection', 'plupload/core/Queueable', 'plupload/ChunkUploader' ], function(plupload, Collection, Queueable, ChunkUploader) { function FileUploader(file, queue) { var _chunks = new Collection(); var _totalChunks = 1; Queueable.call(this); this._options = { chunk_size: 0, params: {}, send_file_name: true, stop_on_fail: true }; plupload.extend(this, { /** When send_file_name is set to true, will be sent with the request as `name` param. Can be used on server-side to override original file name. @property name @type {String} */ name: file.name, start: function() { var self = this; var prevState = this.state; var up; if (this.state === Queueable.PROCESSING) { return false; } if (!this.startedTimestamp) { this.startedTimestamp = +new Date(); } this.state = Queueable.PROCESSING; this.trigger('statechanged', this.state, prevState); // send additional 'name' parameter only if required or explicitly requested if (self._options.send_file_name) { self._options.params.name = self.target_name || self.name; } if (self._options.chunk_size) { _totalChunks = Math.ceil(file.size / self._options.chunk_size); self.uploadChunk(false, true); } else { up = new ChunkUploader(file); up.bind('progress', function(e) { self.progress(e.loaded, e.total); }); up.bind('done', function(e, result) { self.done(result); }); up.bind('failed', function(e, result) { self.failed(result); }); up.setOptions(self._options); queue.addItem(up); } this.trigger('started'); }, uploadChunk: function(seq, dontStop) { var self = this; var chunkSize = this.getOption('chunk_size'); var up; var chunk = {}; var _options; chunk.seq = parseInt(seq, 10) || getNextChunk(); chunk.start = chunk.seq * chunkSize; chunk.end = Math.min(chunk.start + chunkSize, file.size); chunk.total = file.size; // do not proceed for weird chunks if (chunk.start < 0 || chunk.start >= file.size) { return false; } _options = plupload.extendImmutable({}, this.getOptions(), { params: { chunk: chunk.seq, chunks: _totalChunks } }); up = new ChunkUploader(file.slice(chunk.start, chunk.end, file.type)); up.bind('progress', function(e) { self.progress(calcProcessed() + e.loaded, file.size); }); up.bind('failed', function(e, result) { _chunks.add(chunk.seq, plupload.extend({ state: Queueable.FAILED }, chunk)); self.trigger('chunkuploadfailed', plupload.extendImmutable({}, chunk, result)); if (_options.stop_on_fail) { self.failed(result); } }); up.bind('done', function(e, result) { _chunks.add(chunk.seq, plupload.extend({ state: Queueable.DONE }, chunk)); self.trigger('chunkuploaded', plupload.extendImmutable({}, chunk, result)); if (calcProcessed() >= file.size) { self.progress(file.size, file.size); self.done(result); // obviously we are done } else if (dontStop) { plupload.delay(function() { self.uploadChunk(getNextChunk(), dontStop); }); } }); up.bind('processed', function() { this.destroy(); }); up.setOptions(_options); _chunks.add(chunk.seq, plupload.extend({ state: Queueable.PROCESSING }, chunk)); queue.addItem(up); // enqueue even more chunks if slots available if (dontStop && queue.countSpareSlots()) { self.uploadChunk(getNextChunk(), dontStop); } return true; }, destroy: function() { FileUploader.prototype.destroy.call(this); _chunks.clear(); } }); function calcProcessed() { var processed = 0; _chunks.each(function(item) { if (item.state === Queueable.DONE) { processed += (item.end - item.start); } }); return processed; } function getNextChunk() { var i = 0; while (i < _totalChunks && _chunks.hasKey(i)) { i++; } return i; } } plupload.inherit(FileUploader, Queueable); return FileUploader; }); // Included from: src/ImageResizer.js /** * ImageResizer.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @class plupload.ImageResizer @extends plupload.core.Queueable @constructor @private @final @since 3.0 @param {plupload.File} fileRef */ define("plupload/ImageResizer", [ 'plupload', 'plupload/core/Queueable' ], function(plupload, Queueable) { var mxiImage = moxie.image.Image; function ImageResizer(fileRef) { Queueable.call(this); this._options = { type: 'image/jpeg', quality: 90, crop: false, fit: true, preserveHeaders: true, resample: 'default', multipass: true }; this.setOption = function(option, value) { if (typeof(option) !== 'object' && !this._options.hasOwnProperty(option)) { return; } ImageResizer.prototype.setOption.apply(this, arguments); }; this.start = function(options) { var self = this; var img; if (options) { this.setOptions(options.resize); } img = new mxiImage(); img.bind('load', function() { this.resize(self.getOptions()); }); img.bind('resize', function() { self.done(this.getAsBlob(self.getOption('type'), self.getOption('quality'))); this.destroy(); }); img.bind('error', function() { self.failed(); this.destroy(); }); img.load(fileRef, self.getOption('runtimeOptions')); }; } plupload.inherit(ImageResizer, Queueable); // ImageResizer is only included for builds with Image manipulation support, so we add plupload.Image here manually plupload.Image = mxiImage; return ImageResizer; }); // Included from: src/File.js /** * File.js * * Copyright 2017, Ephox * Released under AGPLv3 License.se. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** * @class plupload.File * @extends plupload.core.Queueable * @constructor * @since 3.0 * @final */ define('plupload/File', [ 'plupload', 'plupload/core/Queueable', 'plupload/FileUploader', 'plupload/ImageResizer' ], function(plupload, Queueable, FileUploader, ImageResizer) { function File(file, queueUpload, queueResize) { Queueable.call(this); plupload.extend(this, { /** * For backward compatibility * * @property id * @type {String} * @deprecated */ id: this.uid, /** When send_file_name is set to true, will be sent with the request as `name` param. Can be used on server-side to override original file name. @property name @type {String} */ name: file.name, /** @property target_name @type {String} @deprecated use name */ target_name: null, /** * File type, `e.g image/jpeg` * * @property type * @type String */ type: file.type, /** * File size in bytes (may change after client-side manupilation). * * @property size * @type Number */ size: file.size, /** * Original file size in bytes. * * @property origSize * @type Number */ origSize: file.size, start: function() { var prevState = this.state; if (this.state === Queueable.PROCESSING) { return false; } this.state = Queueable.PROCESSING; this.trigger('statechanged', this.state, prevState); this.trigger('started'); if (!plupload.isEmptyObj(this._options.resize) && isImage(this.type) && runtimeCan(file, 'send_binary_string')) { this.resizeAndUpload(); } else { this.upload(); } return true; }, /** * Get the file for which this File is responsible * * @method getSource * @returns {moxie.file.File} */ getSource: function() { return file; }, /** * Returns file representation of the current runtime. For HTML5 runtime * this is going to be native browser File object * (for backward compatibility) * * @method getNative * @deprecated * @returns {File|Blob|Object} */ getNative: function() { return this.getFile().getSource(); }, resizeAndUpload: function() { var self = this; var opts = self.getOptions(); var rszr = new ImageResizer(file); rszr.bind('progress', function(e) { self.progress(e.loaded, e.total); }); rszr.bind('done', function(e, file) { file = file; self.upload(); }); rszr.bind('failed', function() { self.upload(); }); rszr.setOption('runtimeOptions', { runtime_order: opts.runtimes, required_caps: opts.required_features, preferred_caps: opts.preferred_caps, swf_url: opts.flash_swf_url, xap_url: opts.silverlight_xap_url }); queueResize.addItem(rszr); }, upload: function() { var self = this; var up = new FileUploader(file, queueUpload); up.bind('paused', function() { self.pause(); }); up.bind('resumed', function() { this.start(); }); up.bind('started', function() { self.trigger('startupload'); }); up.bind('progress', function(e) { self.progress(e.loaded, e.total); }); up.bind('done', function(e, result) { self.done(result); }); up.bind('failed', function(e, result) { self.failed(result); }); up.setOptions(self.getOptions()); up.start(); }, destroy: function() { File.prototype.destroy.call(this); file = null; } }); } function isImage(type) { return plupload.inArray(type, ['image/jpeg', 'image/png']) > -1; } function runtimeCan(blob, cap) { if (blob.ruid) { var info = plupload.Runtime.getInfo(blob.ruid); if (info) { return info.can(cap); } } return false; } plupload.inherit(File, Queueable); return File; }); // Included from: src/Uploader.js /** * Uploader.js * * Copyright 2017, Ephox * Released under AGPLv3 License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ /** @class plupload.Uploader @extends plupload.core.Queue @constructor @public @final @param {Object} settings For detailed information about each option check documentation. @param {String|DOMElement} settings.browse_button id of the DOM element or DOM element itself to use as file dialog trigger. @param {Number|String} [settings.chunk_size=0] Chunk size in bytes to slice the file into. Shorcuts with b, kb, mb, gb, tb suffixes also supported. `e.g. 204800 or "204800b" or "200kb"`. By default - disabled. @param {Boolean} [settings.send_chunk_number=true] Whether to send chunks and chunk numbers, or total and offset bytes. @param {String|DOMElement} [settings.container] id of the DOM element or DOM element itself that will be used to wrap uploader structures. Defaults to immediate parent of the `browse_button` element. @param {String|DOMElement} [settings.drop_element] id of the DOM element or DOM element itself to use as a drop zone for Drag-n-Drop. @param {String} [settings.file_data_name="file"] Name for the file field in Multipart formated message. @param {Object} [settings.filters={}] Set of file type filters. @param {Array} [settings.filters.mime_types=[]] List of file types to accept, each one defined by title and list of extensions. `e.g. {title : "Image files", extensions : "jpg,jpeg,gif,png"}`. Dispatches `plupload.FILE_EXTENSION_ERROR` @param {String|Number} [settings.filters.max_file_size=0] Maximum file size that the user can pick, in bytes. Optionally supports b, kb, mb, gb, tb suffixes. `e.g. "10mb" or "1gb"`. By default - not set. Dispatches `plupload.FILE_SIZE_ERROR`. @param {Boolean} [settings.filters.prevent_duplicates=false] Do not let duplicates into the queue. Dispatches `plupload.FILE_DUPLICATE_ERROR`. @param {String} [settings.flash_swf_url] URL of the Flash swf. @param {Object} [settings.headers] Custom headers to send with the upload. Hash of name/value pairs. @param {String} [settings.http_method="POST"] HTTP method to use during upload (only PUT or POST allowed). @param {Number} [settings.max_retries=0] How many times to retry the chunk or file, before triggering Error event. @param {Boolean} [settings.multipart=true] Whether to send file and additional parameters as Multipart formated message. @param {Boolean} [settings.multi_selection=true] Enable ability to select multiple files at once in file dialog. @param {Object} [settings.params] Hash of key/value pairs to send with every file upload. @param {String|Object} [settings.required_features] Either comma-separated list or hash of required features that chosen runtime should absolutely possess. @param {Object} [settings.resize] Enable resizing of images on client-side. Applies to `image/jpeg` and `image/png` only. `e.g. {width : 200, height : 200, quality : 90, crop: true}` @param {Number} settings.resize.width Resulting width @param {Number} [settings.resize.height=width] Resulting height (optional, if not supplied will default to width) @param {String} [settings.resize.type='image/jpeg'] MIME type of the resulting image @param {Number} [settings.resize.quality=90] In the case of JPEG, controls the quality of resulting image @param {Boolean} [settings.resize.crop='cc'] If not falsy, image will be cropped, by default from center @param {Boolean} [settings.resize.fit=true] In case of crop whether to upscale the image to fit the exact dimensions @param {Boolean} [settings.resize.preserveHeaders=true] Whether to preserve meta headers (on JPEGs after resize) @param {String} [settings.resize.resample='default'] Resampling algorithm to use during resize @param {Boolean} [settings.resize.multipass=true] Whether to scale the image in steps (results in better quality) @param {String} [settings.runtimes="html5,flash,silverlight,html4"] Comma separated list of runtimes, that Plupload will try in turn, moving to the next if previous fails. @param {Boolean} [settings.send_file_name=true] Whether to send file name as additional argument - 'name' (required for chunked uploads and some other cases where file name cannot be sent via normal ways). @param {String} [settings.silverlight_xap_url] URL of the Silverlight xap. @param {Boolean} [settings.unique_names=false] If true will generate unique filenames for uploaded files. @param {String} settings.url URL of the server-side upload handler. */ /** Fires when the current RunTime has been initialized. @event Init @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires after the init event incase you need to perform actions there. @event PostInit @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires when the option is changed in via uploader.setOption(). @event OptionChanged @since 2.1 @param {plupload.Uploader} uploader Uploader instance sending the event. @param {String} name Name of the option that was changed @param {Mixed} value New value for the specified option @param {Mixed} oldValue Previous value of the option */ /** Fires when the silverlight/flash or other shim needs to move. @event Refresh @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires when the overall state is being changed for the upload queue. @event StateChanged @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires when browse_button is clicked and browse dialog shows. @event Browse @since 2.1.2 @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires for every filtered file before it is added to the queue. @event FileFiltered @since 2.1 @param {plupload.Uploader} uploader Uploader instance sending the event. @param {plupload.File} file Another file that has to be added to the queue. */ /** Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance. @event QueueChanged @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires after files were filtered and added to the queue. @event FilesAdded @param {plupload.Uploader} uploader Uploader instance sending the event. @param {Array} files Array of FileUploader objects that were added to the queue by user. */ /** Fires when file is removed from the queue. @event FilesRemoved @param {plupload.Uploader} uploader Uploader instance sending the event. @param {Array} files Array of files that got removed. */ /** Fires just before a file is uploaded. Can be used to cancel upload of the current file by returning false from the handler. @event BeforeUpload @param {plupload.Uploader} uploader Uploader instance sending the event. @param {plupload.File} file File to be uploaded. */ /** Fires when a file is to be uploaded by the runtime. @event UploadFile @param {plupload.Uploader} uploader Uploader instance sending the event. @param {plupload.File} file File to be uploaded. */ /** Fires while a file is being uploaded. Use this event to update the current file upload progress. @event UploadProgress @param {plupload.Uploader} uploader Uploader instance sending the event. @param {plupload.File} file File that is currently being uploaded. */ /** Fires when file chunk is uploaded. @event ChunkUploaded @param {plupload.Uploader} uploader Uploader instance sending the event. @param {plupload.File} file File that the chunk was uploaded for. @param {Object} result Object with response properties. @param {Number} result.offset The amount of bytes the server has received so far, including this chunk. @param {Number} result.total The size of the file. @param {String} result.response The response body sent by the server. @param {Number} result.status The HTTP status code sent by the server. @param {String} result.responseHeaders All the response headers as a single string. */ /** Fires when a file is successfully uploaded. @event FileUploaded @param {plupload.Uploader} uploader Uploader instance sending the event. @param {plupload.File} file File that was uploaded. @param {Object} result Object with response properties. @param {String} result.response The response body sent by the server. @param {Number} result.status The HTTP status code sent by the server. @param {String} result.responseHeaders All the response headers as a single string. */ /** Fires when all files in a queue are uploaded @event UploadComplete @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires whenever upload is aborted for some reason @event CancelUpload @param {plupload.Uploader} uploader Uploader instance sending the event. */ /** Fires when a error occurs. @event Error @param {plupload.Uploader} uploader Uploader instance sending the event. @param {Object} error Contains code, message and sometimes file and other details. @param {Number} error.code The plupload error code. @param {String} error.message Description of the error (uses i18n). */ /** Fires when destroy method is called. @event Destroy @param {plupload.Uploader} uploader Uploader instance sending the event. */ define('plupload/Uploader', [ 'plupload', 'plupload/core/Collection', 'plupload/core/Queue', 'plupload/QueueUpload', 'plupload/QueueResize', 'plupload/File' ], function(plupload, Collection, Queue, QueueUpload, QueueResize, PluploadFile) { var fileFilters = {}; var undef; function Uploader(options) { var _fileInputs = []; var _fileDrops = []; var _queueUpload, _queueResize; var _initialized = false; var _disabled = false; var _options = normalizeOptions(plupload.extend({ backward_compatibility: true, chunk_size: 0, file_data_name: 'file', filters: { mime_types: '*', prevent_duplicates: false, max_file_size: 0 }, flash_swf_url: 'js/Moxie.swf', // @since 2.3 http_method: 'POST', // headers: false, // Plupload had a required feature with the same name, comment it to avoid confusion max_resize_slots: 1, max_retries: 0, max_upload_slots: 1, multipart: true, multipart_params: {}, // deprecated, use - params, multi_selection: true, // @since 3 params: {}, resize: false, runtimes: plupload.Runtime.order, send_chunk_number: true, // whether to send chunks and chunk numbers, instead of total and offset bytes send_file_name: true, silverlight_xap_url: 'js/Moxie.xap', // during normalization, these should be processed last required_features: false, preferred_caps: false }, options)); Queue.call(this); // Add public methods plupload.extend(this, { _options: _options, /** * Unique id for the Uploader instance. * * @property id * @type String */ id: this.uid, /** * Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED. * These states are controlled by the stop/start methods. The default value is STOPPED. * * @property state * @type Number */ state: plupload.STOPPED, /** * Map of features that are available for the uploader runtime. Features will be filled * before the init event is called, these features can then be used to alter the UI for the end user. * Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize. * * @property features * @type Object * @deprecated */ features: {}, /** * Object with name/value settings. * * @property settings * @type Object * @deprecated Use `getOption()/setOption()` */ settings : _options, /** * Current runtime name * * @property runtime * @type String * @deprecated There might be multiple runtimes per uploader */ runtime: null, /** * Current upload queue, an array of File instances * * @property files * @deprecated use forEachItem(callback) to cycle over the items in the queue * @type Array */ files: [], /** * Total progess information. How many files has been uploaded, total percent etc. * * @property total * @deprecated use stats */ total: this.stats, /** * Initializes the Uploader instance and adds internal event listeners. * * @method init */ init: function() { var self = this, preinitOpt, err; preinitOpt = self.getOption('preinit'); if (typeof(preinitOpt) == "function") { preinitOpt(self); } else { plupload.each(preinitOpt, function(func, name) { self.bind(name, func); }); } bindEventListeners.call(self); // Check for required options plupload.each(['container', 'browse_button', 'drop_element'], function(el) { if (self.getOption(el) === null) { err = { code: plupload.INIT_ERROR, message: plupload.sprintf(plupload.translate("%s specified, but cannot be found."), el) } return false; } }); if (err) { return self.trigger('Error', err); } if (!self.getOption('browse_button') && !self.getOption('drop_element')) { return self.trigger('Error', { code: plupload.INIT_ERROR, message: plupload.translate("You must specify either browse_button or drop_element.") }); } initControls.call(self, function(initialized) { var runtime; var initOpt = self.getOption('init'); var queueOpts = plupload.extendImmutable({}, self.getOption(), { auto_start: true }); if (typeof(initOpt) == "function") { initOpt(self); } else { plupload.each(initOpt, function(func, name) { self.bind(name, func); }); } if (initialized) { _initialized = true; runtime = plupload.Runtime.getInfo(getRUID()); _queueUpload = new QueueUpload(queueOpts); _queueResize = new QueueResize(queueOpts); self.trigger('Init', { ruid: runtime.uid, runtime: self.runtime = runtime.type }); self.trigger('PostInit'); } else { self.trigger('Error', { code: plupload.INIT_ERROR, message: plupload.translate('Init error.') }); } }); }, /** * Set the value for the specified option(s). * * @method setOption * @since 2.1 * @param {String|Object} option Name of the option to change or the set of key/value pairs * @param {Mixed} [value] Value for the option (is ignored, if first argument is object) */ setOption: function(option, value) { if (_initialized) { // following options cannot be changed after initialization if (plupload.inArray(option, [ 'container', 'browse_button', 'drop_element', 'runtimes', 'multi_selection', 'flash_swf_url', 'silverlight_xap_url' ]) > -1) { return this.trigger('Error', { code: plupload.OPTION_ERROR, message: plupload.sprintf(plupload.translate("%s option cannot be changed.")), option: option }); } } if (typeof(option) !== 'object') { value = normalizeOption(option, value, this._options); // queues will take in only appropriate options if (_queueUpload) { _queueUpload.setOption(option, value); } if (_queueResize) { _queueResize.setOption(option, value); } } Uploader.prototype.setOption.call(this, option, value); }, /** * Refreshes the upload instance by dispatching out a refresh event to all runtimes. * This would for example reposition flash/silverlight shims on the page. * * @method refresh */ refresh: function() { if (_fileInputs.length) { plupload.each(_fileInputs, function(fileInput) { fileInput.trigger('Refresh'); }); } if (_fileDrops.length) { plupload.each(_fileDrops, function(fileDrops) { fileDrops.trigger('Refresh'); }); } this.trigger('Refresh'); }, /** * Stops the upload of the queued files. * * @method stop */ stop: function() { if (Uploader.prototype.stop.call(this) && this.state != plupload.STOPPED) { this.trigger('CancelUpload'); } }, /** * Disables/enables browse button on request. * * @method disableBrowse * @param {Boolean} disable Whether to disable or enable (default: true) */ disableBrowse: function() { _disabled = arguments[0] !== undef ? arguments[0] : true; if (_fileInputs.length) { plupload.each(_fileInputs, function(fileInput) { fileInput.disable(_disabled); }); } this.trigger('DisableBrowse', _disabled); }, /** * Returns the specified FileUploader object by id * * @method getFile * @deprecated use getItem() * @param {String} id FileUploader id to look for * @return {plupload.FileUploader} */ getFile: function(id) { return this.getItem(id); }, /** * Adds file to the queue programmatically. Can be native file, instance of Plupload.File, * instance of mOxie.File, input[type="file"] element, or array of these. Fires FilesAdded, * if any files were added to the queue. Otherwise nothing happens. * * @method addFile * @since 2.0 * @param {plupload.File|mOxie.File|File|Node|Array} file File or files to add to the queue. * @param {String} [fileName] If specified, will be used as a name for the file */ addFile: function(file, fileName) { var self = this; var queue = []; var ruid; // spare runtime uid, for those files that do not have their own var filesAdded = []; // here we track the files that got filtered and are added to the queue function bindListeners(fileUp) { fileUp.bind('beforestart', function(e) { return self.trigger('BeforeUpload', e.target); }); fileUp.bind('startupload', function() { self.trigger('UploadFile', this); }); fileUp.bind('progress', function() { self.trigger('UploadProgress', this); }); fileUp.bind('done', function(e, args) { self.trigger('FileUploaded', this, args); }); fileUp.bind('failed', function(e, err) { self.trigger('Error', plupload.extend({ code: plupload.HTTP_ERROR, message: plupload.translate('HTTP Error.'), file: this }, err)); }); } function filterFile(file, cb) { var queue = []; plupload.each(self.getOption('filters'), function(rule, name) { if (fileFilters[name]) { queue.push(function(cb) { fileFilters[name].call(self, rule, file, function(res) { cb(!res); }); }); } }); plupload.inParallel(queue, cb); } /** * @method resolveFile * @private * @param {mxiFile|mxiBlob|FileUploader|File|Blob|input[type="file"]} file */ function resolveFile(file) { var type = plupload.typeOf(file); // mxiFile (final step for other conditional branches) if (file instanceof moxie.file.File) { if (!file.ruid && !file.isDetached()) { if (!ruid) { // weird case return false; } file.ruid = ruid; file.connectRuntime(ruid); } queue.push(function(cb) { // run through the internal and user-defined filters, if any filterFile(file, function(err) { var fileUp; if (!err) { fileUp = new PluploadFile(file, _queueUpload, _queueResize); if (fileName) { fileUp.name = fileName; } bindListeners(fileUp); self.addItem(fileUp); // make files available for the filters by updating the main queue directly filesAdded.push(fileUp); self.trigger("FileFiltered", fileUp); } plupload.delay(cb); // do not build up recursions or eventually we might hit the limits }); }); } // mxiBlob else if (file instanceof moxie.file.Blob) { resolveFile(file.getSource()); file.destroy(); } // native File or blob else if (plupload.inArray(type, ['file', 'blob']) !== -1) { resolveFile(new moxie.file.File(null, file)); } // input[type="file"] else if (type === 'node' && plupload.typeOf(file.files) === 'filelist') { // if we are dealing with input[type="file"] plupload.each(file.files, resolveFile); } // mixed array of any supported types (see above) else if (type === 'array') { fileName = null; // should never happen, but unset anyway to avoid funny situations plupload.each(file, resolveFile); } } ruid = getRUID(); resolveFile(file); if (queue.length) { plupload.inParallel(queue, function() { // if any files left after filtration, trigger FilesAdded if (filesAdded.length) { self.trigger("FilesAdded", filesAdded); } }); } }, /** * Removes a specific item from the queue * * @method removeFile * @param {plupload.FileUploader|String} file */ removeFile: function(file) { var item = this.extractItem(typeof(file) === 'string' ? file : file.uid); if (item) { this.trigger("FilesRemoved", [item]); item.destroy(); } }, /** * Removes part of the queue and returns removed files. * Triggers FilesRemoved and consequently QueueChanged events. * * @method splice * @param {Number} [start=0] Start index to remove from * @param {Number} [length] Length of items to remove */ splice: function() { var i = 0; var shouldRestart = plupload.STARTED == this.state; var removed = Queue.prototype.splice.apply(this, arguments); if (removed.length) { this.trigger("FilesRemoved", removed); if (shouldRestart) { this.stop(); } for (i = 0; i < removed.length; i++) { removed[i].destroy(); } if (shouldRestart) { this.start(); } } }, /** Dispatches the specified event name and its arguments to all listeners. @method trigger @param {String} name Event name to fire. @param {Object..} Multiple arguments to pass along to the listener functions. */ // override the parent method to match Plupload-like event logic dispatchEvent: function(type) { var list, args, result; type = type.toLowerCase(); list = this.hasEventListener(type); if (list) { // sort event list by priority list.sort(function(a, b) { return b.priority - a.priority; }); // first argument should be current plupload.Uploader instance args = [].slice.call(arguments); args.shift(); args.unshift(this); for (var i = 0; i < list.length; i++) { // Fire event, break chain if false is returned if (list[i].fn.apply(list[i].scope, args) === false) { return false; } } } return true; }, /** Check whether uploader has any listeners to the specified event. @method hasEventListener @param {String} name Event name to check for. */ /** Adds an event listener by name. @method bind @param {String} name Event name to listen for. @param {function} fn Function to call ones the event gets fired. @param {Object} [scope] Optional scope to execute the specified function in. @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first */ bind: function(name, fn, scope, priority) { // adapt moxie EventTarget style to Plupload-like plupload.Uploader.prototype.bind.call(this, name, fn, priority, scope); } /** Removes the specified event listener. @method unbind @param {String} name Name of event to remove. @param {function} fn Function to remove from listener. */ /** Removes all event listeners. @method unbindAll */ }); // keep alive deprecated properties if (_options.backward_compatibility) { this.bind('FilesAdded FilesRemoved', function (up) { up.files = up.toArray(); }, this, 999); this.bind('OptionChanged', function (up, name, value) { up.settings[name] = typeof(value) == 'object' ? plupload.extend({}, value) : value; }, this, 999); } function getRUID() { var ctrl = _fileInputs[0] || _fileDrops[0]; if (ctrl) { return ctrl.getRuntime().uid; } return false; } function bindEventListeners() { this.bind('FilesAdded FilesRemoved', function(up) { up.trigger('QueueChanged'); up.refresh(); }, this, 999); this.bind('BeforeUpload', onBeforeUpload); this.bind('Stopped', function(up) { up.trigger('UploadComplete'); }); this.bind('Error', onError); this.bind('Destroy', onDestroy); } function initControls(cb) { var self = this; var initialized = 0; var queue = []; // common settings var options = { runtime_order: self.getOption('runtimes'), required_caps: self.getOption('required_features'), preferred_caps: self.getOption('preferred_caps'), swf_url: self.getOption('flash_swf_url'), xap_url: self.getOption('silverlight_xap_url') }; // add runtime specific options if any plupload.each(self.getOption('runtimes').split(/\s*,\s*/), function(runtime) { if (self.getOption(runtime)) { options[runtime] = self.getOption(runtime); } }); // initialize file pickers - there can be many if (self.getOption('browse_button')) { plupload.each(self.getOption('browse_button'), function(el) { queue.push(function(cb) { var fileInput = new moxie.file.FileInput(plupload.extend({}, options, { accept: self.getOption('filters').mime_types, name: self.getOption('file_data_name'), multiple: self.getOption('multi_selection'), container: self.getOption('container'), browse_button: el })); fileInput.onready = function() { var info = plupload.Runtime.getInfo(this.ruid); // for backward compatibility plupload.extend(self.features, { chunks: info.can('slice_blob'), multipart: info.can('send_multipart'), multi_selection: info.can('select_multiple') }); initialized++; _fileInputs.push(this); cb(); }; fileInput.onchange = function() { self.addFile(this.files); }; fileInput.bind('mouseenter mouseleave mousedown mouseup', function(e) { if (!_disabled) { if (self.getOption('browse_button_hover')) { if ('mouseenter' === e.type) { plupload.addClass(el, self.getOption('browse_button_hover')); } else if ('mouseleave' === e.type) { plupload.removeClass(el, self.getOption('browse_button_hover')); } } if (self.getOption('browse_button_active')) { if ('mousedown' === e.type) { plupload.addClass(el, self.getOption('browse_button_active')); } else if ('mouseup' === e.type) { plupload.removeClass(el, self.getOption('browse_button_active')); } } } }); fileInput.bind('mousedown', function() { self.trigger('Browse'); }); fileInput.bind('error runtimeerror', function() { fileInput = null; cb(); }); fileInput.init(); }); }); } // initialize drop zones if (self.getOption('drop_element')) { plupload.each(self.getOption('drop_element'), function(el) { queue.push(function(cb) { var fileDrop = new moxie.file.FileDrop(plupload.extend({}, options, { drop_zone: el })); fileDrop.onready = function() { var info = plupload.Runtime.getInfo(this.ruid); // for backward compatibility plupload.extend(self.features, { chunks: info.can('slice_blob'), multipart: info.can('send_multipart'), dragdrop: info.can('drag_and_drop') }); initialized++; _fileDrops.push(this); cb(); }; fileDrop.ondrop = function() { self.addFile(this.files); }; fileDrop.bind('error runtimeerror', function() { fileDrop = null; cb(); }); fileDrop.init(); }); }); } plupload.inParallel(queue, function() { if (typeof(cb) === 'function') { cb(initialized); } }); } // Internal event handlers function onBeforeUpload(up, file) { // Generate unique target filenames if (up.getOption('unique_names')) { var matches = file.name.match(/\.([^.]+)$/), ext = "part"; if (matches) { ext = matches[1]; } file.target_name = file.id + '.' + ext; } } function onError(up, err) { if (err.code === plupload.INIT_ERROR) { up.destroy(); } else if (err.code === plupload.HTTP_ERROR && up.state == plupload.STARTED) { up.trigger('CancelUpload'); } } function onDestroy(up) { up.forEachItem(function(file) { file.destroy(); }); if (_fileInputs.length) { plupload.each(_fileInputs, function(fileInput) { fileInput.destroy(); }); _fileInputs = []; } if (_fileDrops.length) { plupload.each(_fileDrops, function(fileDrop) { fileDrop.destroy(); }); _fileDrops = []; } _initialized = false; if (_queueUpload) { _queueUpload.destroy(); } if (_queueResize) { _queueResize.destroy(); } _options = _queueUpload = _queueResize = null; // purge these exclusively } } // convert plupload features to caps acceptable by mOxie function normalizeCaps(settings) { var features = settings.required_features, caps = {}; function resolve(feature, value, strict) { // Feature notation is deprecated, use caps (this thing here is required for backward compatibility) var map = { chunks: 'slice_blob', jpgresize: 'send_binary_string', pngresize: 'send_binary_string', progress: 'report_upload_progress', multi_selection: 'select_multiple', dragdrop: 'drag_and_drop', drop_element: 'drag_and_drop', headers: 'send_custom_headers', urlstream_upload: 'send_binary_string', canSendBinary: 'send_binary', triggerDialog: 'summon_file_dialog' }; if (map[feature]) { caps[map[feature]] = value; } else if (!strict) { caps[feature] = value; } } if (typeof(features) === 'string') { plupload.each(features.split(/\s*,\s*/), function(feature) { resolve(feature, true); }); } else if (typeof(features) === 'object') { plupload.each(features, function(value, feature) { resolve(feature, value); }); } else if (features === true) { // check settings for required features if (settings.chunk_size && settings.chunk_size > 0) { caps.slice_blob = true; } if (!plupload.isEmptyObj(settings.resize) || settings.multipart === false) { caps.send_binary_string = true; } if (settings.http_method) { caps.use_http_method = settings.http_method; } plupload.each(settings, function(value, feature) { resolve(feature, !!value, true); // strict check }); } return caps; } function normalizeOptions(options) { plupload.each(options, function(value, option) { options[option] = normalizeOption(option, value, options); }); return options; } /** Normalize an option. @method normalizeOption @private @param {String} option Name of the option to normalize @param {Mixed} value @param {Object} options The whole set of options, that might be modified during normalization (see max_file_size or unique_names)! */ function normalizeOption(option, value, options) { switch (option) { case 'chunk_size': if (value = plupload.parseSize(value)) { options.send_file_name = true; } break; case 'headers': var headers = {}; if (typeof(value) === 'object') { plupload.each(value, function(value, key) { headers[key.toLowerCase()] = value; }); } return headers; case 'http_method': return value.toUpperCase() === 'PUT' ? 'PUT' : 'POST'; case 'filters': if (plupload.typeOf(value) === 'array') { // for backward compatibility value = { mime_types: value }; } // if file format filters are being updated, regenerate the matching expressions if (value.mime_types) { if (plupload.typeOf(value.mime_types) === 'string') { value.mime_types = plupload.mimes2extList(value.mime_types); } // generate and cache regular expression for filtering file extensions options.re_ext_filter = (function(filters) { var extensionsRegExp = []; plupload.each(filters, function(filter) { plupload.each(filter.extensions.split(/,/), function(ext) { if (/^\s*\*\s*$/.test(ext)) { extensionsRegExp.push('\\.*'); } else { extensionsRegExp.push('\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\'.replace(/./g, '\\$&')) + ']', 'g'), '\\$&')); } }); }); return new RegExp('(' + extensionsRegExp.join('|') + ')$', 'i'); }(value.mime_types)); } return value; case 'max_file_size': if (options && !options.filters) { options.filters = {}; } options.filters.max_file_size = value; break; case 'multipart': if (!value) { options.send_file_name = true; } break; case 'multipart_params': options.params = options.multipart_params = value; break; case 'resize': if (value) { return plupload.extend({ preserve_headers: true, crop: false }, value); } return false; case 'prevent_duplicates': if (options && !options.filters) { options.filters = {}; } options.filters.prevent_duplicates = !!value; break; case 'unique_names': if (value) { options.send_file_name = true; } break; case 'required_features': // Normalize the list of required capabilities return normalizeCaps(plupload.extend({}, options)); case 'preferred_caps': // Come up with the list of capabilities that can affect default mode in a multi-mode runtimes return normalizeCaps(plupload.extend({}, options, { required_features: true })); // options that require reinitialisation case 'container': case 'browse_button': case 'drop_element': return 'container' === option ? plupload.get(value) : plupload.getAll(value); } return value; } /** * Registers a filter that will be executed for each file added to the queue. * If callback returns false, file will not be added. * * Callback receives two arguments: a value for the filter as it was specified in settings.filters * and a file to be filtered. Callback is executed in the context of uploader instance. * * @method addFileFilter * @static * @param {String} name Name of the filter by which it can be referenced in settings.filters * @param {String} cb Callback - the actual routine that every added file must pass */ function addFileFilter(name, cb) { fileFilters[name] = cb; } /** * A way to predict what runtime will be choosen in the current environment with the * specified settings. * * @method predictRuntime * @static * @param {Object|String} config Plupload settings to check * @param {String} [runtimes] Comma-separated list of runtimes to check against * @return {String} Type of compatible runtime */ function predictRuntime(config, runtimes) { var up, runtime; up = new Uploader(config); runtime = plupload.Runtime.thatCan(up.getOption('required_features'), runtimes || config.runtimes); up.destroy(); return runtime; } addFileFilter('mime_types', function(filters, file, cb) { if (filters.length && !this.getOption('re_ext_filter').test(file.name)) { this.trigger('Error', { code: plupload.FILE_EXTENSION_ERROR, message: plupload.translate('File extension error.'), file: file }); cb(false); } else { cb(true); } }); addFileFilter('max_file_size', function(maxSize, file, cb) { var undef; maxSize = plupload.parseSize(maxSize); // Invalid file size if (file.size !== undef && maxSize && file.size > maxSize) { this.trigger('Error', { code: plupload.FILE_SIZE_ERROR, message: plupload.translate('File size error.'), file: file }); cb(false); } else { cb(true); } }); addFileFilter('prevent_duplicates', function(value, file, cb) { var self = this; if (value) { this.forEachItem(function(item) { // Compare by name and size (size might be 0 or undefined, but still equivalent for both) if (file.name === item.name && file.size === item.size) { self.trigger('Error', { code: plupload.FILE_DUPLICATE_ERROR, message: plupload.translate('Duplicate file error.'), file: file }); cb(false); return; } }); } cb(true); }); addFileFilter('prevent_empty', function(value, file, cb) { if (value && !file.size && file.size !== undef) { this.trigger('Error', { code : plupload.FILE_SIZE_ERROR, message : plupload.translate('File size error.'), file : file }); cb(false); } else { cb(true); } }); Uploader.addFileFilter = addFileFilter; plupload.inherit(Uploader, Queue); // for backward compatibility plupload.addFileFilter = addFileFilter; plupload.predictRuntime = predictRuntime; return Uploader; }); expose(["plupload","plupload/core/Collection","plupload/core/ArrCollection","plupload/core/Optionable","plupload/core/Queueable","plupload/core/Stats","plupload/core/Queue","plupload/QueueUpload","plupload/QueueResize","plupload/ChunkUploader","plupload/FileUploader","plupload/ImageResizer","plupload/File","plupload/Uploader"]); })(this); }));