/** * plupload.js * * Copyright 2009, Moxiecode Systems AB * Released under GPL License. * * License: http://www.plupload.com/license * Contributing: http://www.plupload.com/contributing */ // JSLint defined globals /*global window:false, escape:false */ /*!@@version@@*/ (function() { var count = 0, runtimes = [], i18n = {}, mimes = {}, xmlEncodeChars = {'<' : 'lt', '>' : 'gt', '&' : 'amp', '"' : 'quot', '\'' : '#39'}, xmlEncodeRegExp = /[<>&\"\']/g, undef, delay = window.setTimeout, // A place to store references to event handlers eventhash = {}, uid; // IE W3C like event funcs function preventDefault() { this.returnValue = false; } function stopPropagation() { this.cancelBubble = true; } // Parses the default mime types string into a mimes lookup map (function(mime_data) { var items = mime_data.split(/,/), i, y, ext; for (i = 0; i < items.length; i += 2) { ext = items[i + 1].split(/ /); for (y = 0; y < ext.length; y++) { mimes[ext[y]] = items[i]; } } })( "application/msword,doc dot," + "application/pdf,pdf," + "application/pgp-signature,pgp," + "application/postscript,ps ai eps," + "application/rtf,rtf," + "application/vnd.ms-excel,xls xlb," + "application/vnd.ms-powerpoint,ppt pps pot," + "application/zip,zip," + "application/x-shockwave-flash,swf swfl," + "application/vnd.openxmlformats,docx pptx xlsx," + "audio/mpeg,mpga mpega mp2 mp3," + "audio/x-wav,wav," + "audio/mp4,m4a," + "image/bmp,bmp," + "image/gif,gif," + "image/jpeg,jpeg jpg jpe," + "image/png,png," + "image/svg+xml,svg svgz," + "image/tiff,tiff tif," + "text/html,htm html xhtml," + "text/rtf,rtf," + "video/mpeg,mpeg mpg mpe," + "video/quicktime,qt mov," + "video/mp4,mp4," + "video/x-m4v,m4v," + "video/x-flv,flv," + "video/vnd.rn-realvideo,rv," + "text/plain,asc txt text diff log," + "application/octet-stream,exe" ); /** * Plupload class with some global constants and functions. * * @example * // Encode entities * console.log(plupload.xmlEncode("My string <>")); * * // Generate unique id * console.log(plupload.guid()); * * @static * @class plupload */ var plupload = { /** * Plupload version will be replaced on build. */ VERSION : '@@version@@', /** * Inital state of the queue and also the state ones it's finished all it's uploads. * * @property STOPPED * @final */ STOPPED : 1, /** * Upload process is running * * @property STARTED * @final */ STARTED : 2, /** * File is queued for upload * * @property QUEUED * @final */ QUEUED : 1, /** * File is being uploaded * * @property UPLOADING * @final */ UPLOADING : 2, /** * File has failed to be uploaded * * @property FAILED * @final */ FAILED : 4, /** * File has been uploaded successfully * * @property DONE * @final */ DONE : 5, // Error constants used by the Error event /** * Generic error for example if an exception is thrown inside Silverlight. * * @property GENERIC_ERROR * @final */ GENERIC_ERROR : -100, /** * HTTP transport error. For example if the server produces a HTTP status other than 200. * * @property HTTP_ERROR * @final */ HTTP_ERROR : -200, /** * Generic I/O error. For exampe if it wasn't possible to open the file stream on local machine. * * @property IO_ERROR * @final */ IO_ERROR : -300, /** * Generic I/O error. For exampe if it wasn't possible to open the file stream on local machine. * * @property SECURITY_ERROR * @final */ SECURITY_ERROR : -400, /** * Initialization error. Will be triggered if no runtime was initialized. * * @property INIT_ERROR * @final */ INIT_ERROR : -500, /** * File size error. If the user selects a file that is to large it will be blocked and an error of this type will be triggered. * * @property FILE_SIZE_ERROR * @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 * @final */ FILE_EXTENSION_ERROR : -601, /** * Runtime will try to detect if image is proper one. Otherwise will throw this error. * * @property IMAGE_FORMAT_ERROR * @final */ IMAGE_FORMAT_ERROR : -700, /** * While working on the image runtime will try to detect if the operation may potentially run out of memeory and will throw this error. * * @property IMAGE_MEMORY_ERROR * @final */ IMAGE_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 * @final */ IMAGE_DIMENSIONS_ERROR : -702, /** * Mime type lookup table. * * @property mimeTypes * @type Object * @final */ mimeTypes : mimes, /** * Extends the specified object with another object. * * @method extend * @param {Object} target Object to extend. * @param {Object..} obj Multiple objects to extend with. * @return {Object} Same as target, the extended object. */ extend : function(target) { plupload.each(arguments, function(arg, i) { if (i > 0) { plupload.each(arg, function(value, key) { target[key] = value; }); } }); return target; }, /** * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _. * * @method cleanName * @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; }, /** * Adds a specific upload runtime like for example flash or gears. * * @method addRuntime * @param {String} name Runtime name for example flash. * @param {Object} obj Object containing init/destroy method. */ addRuntime : function(name, runtime) { runtime.name = name; runtimes[name] = runtime; runtimes.push(runtime); return runtime; }, /** * 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 milisecond 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 ansteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property * to an user unique key. * * @method guid * @return {String} Virtually unique id. */ guid : function() { var guid = new Date().getTime().toString(32), i; for (i = 0; i < 5; i++) { guid += Math.floor(Math.random() * 65535).toString(32); } return (plupload.guidPrefix || 'p') + guid + (count++).toString(32); }, /** * Builds a full url out of a base URL and an object with items to append as query string items. * * @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 = ''; plupload.each(items, function(value, name) { query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value); }); if (query) { url += (url.indexOf('?') > 0 ? '&' : '?') + query; } return url; }, /** * Executes the callback function for each item in array/object. If you return false in the * callback it will break the loop. * * @param {Object} obj Object to iterate. * @param {function} callback Callback function to execute for each item. */ each : function(obj, callback) { var length, key, i; if (obj) { length = obj.length; if (length === undef) { // Loop object items for (key in obj) { if (obj.hasOwnProperty(key)) { if (callback(obj[key], key) === false) { return; } } } } else { // Loop array items for (i = 0; i < length; i++) { if (callback(obj[i], i) === false) { return; } } } } }, /** * Formats the specified number as a size string for example 1024 becomes 1 KB. * * @method formatSize * @param {Number} size Size to format as string. * @return {String} Formatted size string. */ formatSize : function(size) { if (size === undef || /\D/.test(size)) { return plupload.translate('N/A'); } // GB if (size > 1073741824) { return Math.round(size / 1073741824, 1) + " GB"; } // MB if (size > 1048576) { return Math.round(size / 1048576, 1) + " MB"; } // KB if (size > 1024) { return Math.round(size / 1024, 1) + " KB"; } return size + " b"; }, /** * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields. * * @method getPos * @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 : function(node, root) { var x = 0, y = 0, parent, doc = document, nodeRect, rootRect; node = node; root = root || doc.body; // Returns the x, y cordinate for an element on IE 6 and IE 7 function getIEPos(node) { var bodyElm, rect, x = 0, y = 0; if (node) { rect = node.getBoundingClientRect(); bodyElm = doc.compatMode === "CSS1Compat" ? doc.documentElement : doc.body; x = rect.left + bodyElm.scrollLeft; y = rect.top + bodyElm.scrollTop; } return { x : x, y : y }; } // Use getBoundingClientRect on IE 6 and IE 7 but not on IE 8 in standards mode if (node && node.getBoundingClientRect && (navigator.userAgent.indexOf('MSIE') > 0 && doc.documentMode !== 8)) { nodeRect = getIEPos(node); rootRect = getIEPos(root); return { x : nodeRect.x - rootRect.x, y : nodeRect.y - rootRect.y }; } parent = node; while (parent && parent != root && parent.nodeType) { x += parent.offsetLeft || 0; y += parent.offsetTop || 0; parent = parent.offsetParent; } parent = node.parentNode; while (parent && parent != root && parent.nodeType) { x -= parent.scrollLeft || 0; y -= parent.scrollTop || 0; parent = parent.parentNode; } return { x : x, y : y }; }, /** * Returns the size of the specified node in pixels. * * @param {Node} node Node to get the size of. * @return {Object} Object with a w and h property. */ getSize : function(node) { return { w : node.offsetWidth || node.clientWidth, h : node.offsetHeight || node.clientHeight }; }, /** * Parses the specified size string into a byte value. For example 10kb becomes 10240. * * @method parseSize * @param {String/Number} size String to parse or number to just pass through. * @return {Number} Size in bytes. */ parseSize : function(size) { var mul; if (typeof(size) == 'string') { size = /^([0-9]+)([mgk]+)$/.exec(size.toLowerCase().replace(/[^0-9mkg]/g, '')); mul = size[2]; size = +size[1]; if (mul == 'g') { size *= 1073741824; } if (mul == 'm') { size *= 1048576; } if (mul == 'k') { size *= 1024; } } return size; }, /** * Encodes the specified string. * * @method xmlEncode * @param {String} s String to encode. * @return {String} Encoded string. */ xmlEncode : function(str) { return str ? ('' + str).replace(xmlEncodeRegExp, function(chr) { return xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr; }) : str; }, /** * Forces anything into an array. * * @method toArray * @param {Object} obj Object with length field. * @return {Array} Array object containing all items. */ toArray : function(obj) { var i, arr = []; for (i = 0; i < obj.length; i++) { arr[i] = obj[i]; } return arr; }, /** * Extends the language pack object with new items. * * @param {Object} pack Language pack items to add. * @return {Object} Extended language pack object. */ addI18n : function(pack) { return plupload.extend(i18n, pack); }, /** * Translates the specified string by checking for the english string in the language pack lookup. * * @param {String} str String to look for. * @reutrn {String} Translated string or the input string if it wasn't found. */ translate : function(str) { return i18n[str] || str; }, /** * Checks if specified DOM element has specified class. * * @param {Object} obj DOM element like object to add handler to. * @param {String} name Class name */ hasClass : function(obj, name) { var regExp; if (obj.className == '') { return false; } regExp = new RegExp("(^|\\s+)"+name+"(\\s+|$)"); return regExp.test(obj.className); }, /** * Adds specified className to specified DOM element. * * @param {Object} obj DOM element like object to add handler to. * @param {String} name Class name */ addClass : function(obj, name) { if (!plupload.hasClass(obj, name)) { obj.className = obj.className == '' ? name : obj.className.replace(/\s+$/, '')+' '+name; } }, /** * Removes specified className from specified DOM element. * * @param {Object} obj DOM element like object to add handler to. * @param {String} name Class name */ removeClass : function(obj, name) { var regExp = new RegExp("(^|\\s+)"+name+"(\\s+|$)"); obj.className = obj.className.replace(regExp, function($0, $1, $2) { return $1 === ' ' && $2 === ' ' ? ' ' : ''; }); }, /** * Adds an event handler to the specified object and store reference to the handler * in objects internal Plupload registry (@see removeEvent). * * @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 : function(obj, name, callback) { var func, events, types, key; // if passed in, event will be locked with this key - one would need to provide it to removeEvent key = arguments[3]; name = name.toLowerCase(); // Initialize unique identifier if needed if (uid === undef) { uid = 'Plupload_' + plupload.guid(); } // Add event listener if (obj.attachEvent) { func = function() { var evt = window.event; if (!evt.target) { evt.target = evt.srcElement; } evt.preventDefault = preventDefault; evt.stopPropagation = stopPropagation; callback(evt); }; obj.attachEvent('on' + name, func); } else if (obj.addEventListener) { func = callback; obj.addEventListener(name, func, false); } // Log event handler to objects internal Plupload registry if (obj[uid] === undef) { obj[uid] = plupload.guid(); } if (!eventhash.hasOwnProperty(obj[uid])) { eventhash[obj[uid]] = {}; } events = eventhash[obj[uid]]; if (!events.hasOwnProperty(name)) { events[name] = []; } events[name].push({ func: func, orig: callback, // store original callback for IE key: key }); }, /** * Remove event handler from the specified object. If third argument (callback) * is not specified remove all events with the specified name. * * @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: function(obj, name) { var type, callback, key, // check if object is empty isEmptyObj = function(obj) { for (var prop in obj) { return false; } return true; }; // match the handler either by callback or by key if (typeof(arguments[2]) == "function") { callback = arguments[2]; } else { key = arguments[2]; } name = name.toLowerCase(); if (obj[uid] && eventhash[obj[uid]] && eventhash[obj[uid]][name]) { type = eventhash[obj[uid]][name]; } else { return; } for (var i=type.length-1; i>=0; i--) { // undefined or not, key should match if (type[i].key === key || type[i].orig === callback) { if (obj.detachEvent) { obj.detachEvent('on'+name, type[i].func); } else if (obj.removeEventListener) { obj.removeEventListener(name, type[i].func, false); } type[i].orig = null; type[i].func = null; type.splice(i, 1); // If callback was passed we are done here, otherwise proceed if (callback !== undef) { break; } } } // If event array got empty, remove it if (!type.length) { delete eventhash[obj[uid]][name]; } // If Plupload registry has become empty, remove it if (isEmptyObj(eventhash[obj[uid]])) { delete eventhash[obj[uid]]; // IE doesn't let you remove DOM object property with - delete try { delete obj[uid]; } catch(e) { obj[uid] = undef; } } }, /** * Remove all kind of events from the specified object * * @param {Object} obj DOM element to remove event listeners from. * @param {String} (optional) unique key to match, when removing events. */ removeAllEvents: function(obj) { var key = arguments[1]; if (obj[uid] === undef || !obj[uid]) { return; } plupload.each(eventhash[obj[uid]], function(events, name) { plupload.removeEvent(obj, name, key); }); } }; /** * Uploader class, an instance of this class will be created for each upload field. * * @example * var uploader = new plupload.Uploader({ * runtimes : 'gears,html5,flash', * browse_button : 'button_id' * }); * * uploader.bind('Init', function(up) { * alert('Supports drag/drop: ' + (!!up.features.dragdrop)); * }); * * uploader.bind('FilesAdded', function(up, files) { * alert('Selected files: ' + files.length); * }); * * uploader.bind('QueueChanged', function(up) { * alert('Queued files: ' + uploader.files.length); * }); * * uploader.init(); * * @class plupload.Uploader */ /** * Constructs a new uploader instance. * * @constructor * @method Uploader * @param {Object} settings Initialization settings, to be used by the uploader instance and runtimes. */ plupload.Uploader = function(settings) { var events = {}, total, files = [], startTime; // Inital total state total = new plupload.QueueProgress(); // Default settings settings = plupload.extend({ chunk_size : 0, multipart : true, multi_selection : true, file_data_name : 'file', filters : [] }, settings); // Private methods function uploadNext() { var file, count = 0, i; if (this.state == plupload.STARTED) { // Find first QUEUED file for (i = 0; i < files.length; i++) { if (!file && files[i].status == plupload.QUEUED) { file = files[i]; file.status = plupload.UPLOADING; this.trigger("BeforeUpload", file); this.trigger("UploadFile", file); } else { count++; } } // All files are DONE or FAILED if (count == files.length) { this.trigger("UploadComplete", files); this.stop(); } } } function calc() { var i, file; // Reset stats total.reset(); // Check status, size, loaded etc on all files for (i = 0; i < files.length; i++) { file = files[i]; if (file.size !== undef) { total.size += file.size; total.loaded += file.loaded; } else { total.size = undef; } if (file.status == plupload.DONE) { total.uploaded++; } else if (file.status == plupload.FAILED) { total.failed++; } else { total.queued++; } } // If we couldn't calculate a total file size then use the number of files to calc percent if (total.size === undef) { total.percent = files.length > 0 ? Math.ceil(total.uploaded / files.length * 100) : 0; } else { total.bytesPerSec = Math.ceil(total.loaded / ((+new Date() - startTime || 1) / 1000.0)); total.percent = total.size > 0 ? Math.ceil(total.loaded / total.size * 100) : 0; } } // Add public methods plupload.extend(this, { /** * 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, /** * Current runtime name. * * @property runtime * @type String */ runtime: '', /** * 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 */ features : {}, /** * Current upload queue, an array of File instances. * * @property files * @type Array * @see plupload.File */ files : files, /** * Object with name/value settings. * * @property settings * @type Object */ settings : settings, /** * Total progess information. How many files has been uploaded, total percent etc. * * @property total * @type plupload.QueueProgress */ total : total, /** * Unique id for the Uploader instance. * * @property id * @type String */ id : plupload.guid(), /** * Initializes the Uploader instance and adds internal event listeners. * * @method init */ init : function() { var self = this, i, runtimeList, a, runTimeIndex = 0, items; if (typeof(settings.preinit) == "function") { settings.preinit(self); } else { plupload.each(settings.preinit, function(func, name) { self.bind(name, func); }); } settings.page_url = settings.page_url || document.location.pathname.replace(/\/[^\/]+$/g, '/'); // If url is relative force it absolute to the current page if (!/^(\w+:\/\/|\/)/.test(settings.url)) { settings.url = settings.page_url + settings.url; } // Convert settings settings.chunk_size = plupload.parseSize(settings.chunk_size); settings.max_file_size = plupload.parseSize(settings.max_file_size); // Add files to queue self.bind('FilesAdded', function(up, selected_files) { var i, file, count = 0, extensionsRegExp, filters = settings.filters; // Convert extensions to regexp if (filters && filters.length) { extensionsRegExp = []; plupload.each(filters, function(filter) { plupload.each(filter.extensions.split(/,/), function(ext) { extensionsRegExp.push('\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\'.replace(/./g, '\\$&')) + ']', 'g'), '\\$&')); }); }); extensionsRegExp = new RegExp(extensionsRegExp.join('|') + '$', 'i'); } for (i = 0; i < selected_files.length; i++) { file = selected_files[i]; file.loaded = 0; file.percent = 0; file.status = plupload.QUEUED; // Invalid file extension if (extensionsRegExp && !extensionsRegExp.test(file.name)) { up.trigger('Error', { code : plupload.FILE_EXTENSION_ERROR, message : plupload.translate('File extension error.'), file : file }); continue; } // Invalid file size if (file.size !== undef && file.size > settings.max_file_size) { up.trigger('Error', { code : plupload.FILE_SIZE_ERROR, message : plupload.translate('File size error.'), file : file }); continue; } // Add valid file to list files.push(file); count++; } // Only trigger QueueChanged event if any files where added if (count) { delay(function() { self.trigger("QueueChanged"); self.refresh(); }, 1); } else { return false; // Stop the FilesAdded event from immediate propagation } }); // Generate unique target filenames if (settings.unique_names) { self.bind("UploadFile", function(up, file) { var matches = file.name.match(/\.([^.]+)$/), ext = "tmp"; if (matches) { ext = matches[1]; } file.target_name = file.id + '.' + ext; }); } self.bind('UploadProgress', function(up, file) { file.percent = file.size > 0 ? Math.ceil(file.loaded / file.size * 100) : 100; calc(); }); self.bind('StateChanged', function(up) { if (up.state == plupload.STARTED) { // Get start time to calculate bps startTime = (+new Date()); } }); self.bind('QueueChanged', calc); self.bind("Error", function(up, err) { // Set failed status if an error occured on a file if (err.file) { err.file.status = plupload.FAILED; calc(); // Upload next file but detach it from the error event // since other custom listeners might want to stop the queue if (up.state == plupload.STARTED) { delay(function() { uploadNext.call(self); }, 1); } } }); self.bind("FileUploaded", function(up, file) { file.status = plupload.DONE; file.loaded = file.size; up.trigger('UploadProgress', file); // Upload next file but detach it from the error event // since other custom listeners might want to stop the queue delay(function() { uploadNext.call(self); }, 1); }); // Setup runtimeList if (settings.runtimes) { runtimeList = []; items = settings.runtimes.split(/\s?,\s?/); for (i = 0; i < items.length; i++) { if (runtimes[items[i]]) { runtimeList.push(runtimes[items[i]]); } } } else { runtimeList = runtimes; } // Call init on each runtime in sequence function callNextInit() { var runtime = runtimeList[runTimeIndex++], features, requiredFeatures, i; if (runtime) { features = runtime.getFeatures(); // Check if runtime supports required features requiredFeatures = self.settings.required_features; if (requiredFeatures) { requiredFeatures = requiredFeatures.split(','); for (i = 0; i < requiredFeatures.length; i++) { // Specified feature doesn't exist if (!features[requiredFeatures[i]]) { callNextInit(); return; } } } // Try initializing the runtime runtime.init(self, function(res) { if (res && res.success) { // Successful initialization self.features = features; self.runtime = runtime.name; self.trigger('Init', {runtime : runtime.name}); self.trigger('PostInit'); self.refresh(); } else { callNextInit(); } }); } else { // Trigger an init error if we run out of runtimes self.trigger('Error', { code : plupload.INIT_ERROR, message : plupload.translate('Init error.') }); } } callNextInit(); if (typeof(settings.init) == "function") { settings.init(self); } else { plupload.each(settings.init, function(func, name) { self.bind(name, func); }); } }, /** * 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() { this.trigger("Refresh"); }, /** * Starts uploading the queued files. * * @method start */ start : function() { if (this.state != plupload.STARTED) { this.state = plupload.STARTED; this.trigger("StateChanged"); uploadNext.call(this); } }, /** * Stops the upload of the queued files. * * @method stop */ stop : function() { if (this.state != plupload.STOPPED) { this.state = plupload.STOPPED; this.trigger("StateChanged"); } }, /** * Returns the specified file object by id. * * @method getFile * @param {String} id File id to look for. * @return {plupload.File} File object or undefined if it wasn't found; */ getFile : function(id) { var i; for (i = files.length - 1; i >= 0; i--) { if (files[i].id === id) { return files[i]; } } }, /** * Removes a specific file. * * @method removeFile * @param {plupload.File} file File to remove from queue. */ removeFile : function(file) { var i; for (i = files.length - 1; i >= 0; i--) { if (files[i].id === file.id) { return this.splice(i, 1)[0]; } } }, /** * Removes part of the queue and returns the files removed. This will also trigger the FilesRemoved and QueueChanged events. * * @method splice * @param {Number} start (Optional) Start index to remove from. * @param {Number} length (Optional) Lengh of items to remove. * @return {Array} Array of files that was removed. */ splice : function(start, length) { var removed; // Splice and trigger events removed = files.splice(start === undef ? 0 : start, length === undef ? files.length : length); this.trigger("FilesRemoved", removed); this.trigger("QueueChanged"); return removed; }, /** * Dispatches the specified event name and it's arguments to all listeners. * * * @method trigger * @param {String} name Event name to fire. * @param {Object..} Multiple arguments to pass along to the listener functions. */ trigger : function(name) { var list = events[name.toLowerCase()], i, args; // console.log(name, arguments); if (list) { // Replace name with sender in args args = Array.prototype.slice.call(arguments); args[0] = this; // Dispatch event to all listeners for (i = 0; i < list.length; i++) { // Fire event, break chain if false is returned if (list[i].func.apply(list[i].scope, args) === false) { return false; } } } return true; }, /** * Adds an event listener by name. * * @method bind * @param {String} name Event name to listen for. * @param {function} func Function to call ones the event gets fired. * @param {Object} scope Optional scope to execute the specified function in. */ bind : function(name, func, scope) { var list; name = name.toLowerCase(); list = events[name] || []; list.push({func : func, scope : scope || this}); events[name] = list; }, /** * Removes the specified event listener. * * @method unbind * @param {String} name Name of event to remove. * @param {function} func Function to remove from listener. */ unbind : function(name) { name = name.toLowerCase(); var list = events[name], i, func = arguments[1]; if (list) { if (func !== undef) { for (i = list.length - 1; i >= 0; i--) { if (list[i].func === func) { list.splice(i, 1); break; } } } else { list = []; } // delete event list if it has become empty if (!list.length) { delete events[name]; } } }, /** * Removes all event listeners. * * @method unbindAll */ unbindAll : function() { var self = this; plupload.each(events, function(list, name) { self.unbind(name); }); }, /** * Destroys Plupload instance and cleans after itself. * * @method destroy */ destroy : function() { this.trigger('Destroy'); // Clean-up after uploader itself this.unbindAll(); } /** * 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 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 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 when just before a file is uploaded. This event enables you to override settings * on the uploader instance before the file is uploaded. * * @event BeforeUpload * @param {plupload.Uploader} uploader Uploader instance sending the event. * @param {plupload.File} file File to be uploaded. */ /** * 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 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 while a file was removed from queue. * * @event FilesRemoved * @param {plupload.Uploader} uploader Uploader instance sending the event. * @param {Array} files Array of files that got removed. */ /** * Fires while when the user selects files to upload. * * @event FilesAdded * @param {plupload.Uploader} uploader Uploader instance sending the event. * @param {Array} files Array of file objects that was added to queue/selected by the user. */ /** * 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} response Object with response properties. */ /** * 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} response Object with response properties. */ /** * Fires when all files in a queue are uploaded. * * @event UploadComplete * @param {plupload.Uploader} uploader Uploader instance sending the event. * @param {Array} files Array of file objects that was added to queue/selected by the user. */ /** * 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. */ /** * Fires when destroy method is called. * * @event Destroy * @param {plupload.Uploader} uploader Uploader instance sending the event. */ }); }; /** * File instance. * * @class plupload.File * @param {String} name Name of the file. * @param {Number} size File size. */ /** * Constructs a new file instance. * * @constructor * @method File * @param {String} id Unique file id. * @param {String} name File name. * @param {Number} size File size in bytes. */ plupload.File = function(id, name, size) { var self = this; // Setup alias for self to reduce code size when it's compressed /** * File id this is a globally unique id for the specific file. * * @property id * @type String */ self.id = id; /** * File name for example "myfile.gif". * * @property name * @type String */ self.name = name; /** * File size in bytes. * * @property size * @type Number */ self.size = size; /** * Number of bytes uploaded of the files total size. * * @property loaded * @type Number */ self.loaded = 0; /** * Number of percentage uploaded of the file. * * @property percent * @type Number */ self.percent = 0; /** * Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE. * * @property status * @type Number * @see plupload */ self.status = 0; }; /** * Runtime class gets implemented by each upload runtime. * * @class plupload.Runtime * @static */ plupload.Runtime = function() { /** * Returns a list of supported features for the runtime. * * @return {Object} Name/value object with supported features. */ this.getFeatures = function() { }; /** * Initializes the upload runtime. This method should add necessary items to the DOM and register events needed for operation. * * @method init * @param {plupload.Uploader} uploader Uploader instance that needs to be initialized. * @param {function} callback Callback function to execute when the runtime initializes or fails to initialize. If it succeeds an object with a parameter name success will be set to true. */ this.init = function(uploader, callback) { }; }; /** * Runtime class gets implemented by each upload runtime. * * @class plupload.QueueProgress */ /** * Constructs a queue progress. * * @constructor * @method QueueProgress */ plupload.QueueProgress = function() { var self = this; // Setup alias for self to reduce code size when it's compressed /** * Total queue file size. * * @property size * @type Number */ self.size = 0; /** * Total bytes uploaded. * * @property loaded * @type Number */ self.loaded = 0; /** * Number of files uploaded. * * @property uploaded * @type Number */ self.uploaded = 0; /** * Number of files failed to upload. * * @property failed * @type Number */ self.failed = 0; /** * Number of files yet to be uploaded. * * @property queued * @type Number */ self.queued = 0; /** * Total percent of the uploaded bytes. * * @property percent * @type Number */ self.percent = 0; /** * Bytes uploaded per second. * * @property bytesPerSec * @type Number */ self.bytesPerSec = 0; /** * Resets the progress to it's initial values. * * @method reset */ self.reset = function() { self.size = self.loaded = self.uploaded = self.failed = self.queued = self.percent = self.bytesPerSec = 0; }; }; // Create runtimes namespace plupload.runtimes = {}; // Expose plupload namespace window.plupload = plupload; })();