UNPKG

simple-ajax-uploader

Version:

A Javascript plugin for cross-browser Ajax file uploading. Supports drag and drop, CORS, and multiple file uploading with progress bars. Works in IE7-9, mobile, and all modern browsers.

1,694 lines (1,355 loc) 68.2 kB
/** * Simple Ajax Uploader * Version 2.6.7 * https://github.com/LPology/Simple-Ajax-Uploader * * Copyright 2012-2019 LPology, LLC * Released under the MIT license */ ;(function( global, factory ) { /*globals define, module */ if ( typeof define === 'function' && define.amd ) { define( function() { return factory( global ); }); } else if ( typeof module === 'object' && module.exports ) { module.exports = factory( global ); } else { global.ss = factory( global ); } }( typeof window !== 'undefined' ? window : this, function( window ) { var ss = window.ss || {}, // ss.trim() rLWhitespace = /^\s+/, rTWhitespace = /\s+$/, // ss.getUID uidReplace = /[xy]/g, // ss.getFilename() rPath = /.*(\/|\\)/, // ss.getExt() rExt = /.*[.]/, // ss.hasClass() rHasClass = /[\t\r\n]/g, // Check for Safari -- it doesn't like multi file uploading. At all. // http://stackoverflow.com/a/9851769/1091949 isSafari = Object.prototype.toString.call( window.HTMLElement ).indexOf( 'Constructor' ) > 0, // Detect IE7-9 isIE7to9 = ( navigator.userAgent.indexOf('MSIE') !== -1 && navigator.userAgent.indexOf('MSIE 1') === -1 ), isIE7 = ( navigator.userAgent.indexOf('MSIE 7') !== -1 ), // Check whether XHR uploads are supported input = document.createElement( 'input' ), XhrOk; input.type = 'file'; XhrOk = ( 'multiple' in input && typeof File !== 'undefined' && typeof ( new XMLHttpRequest() ).upload !== 'undefined' ); /** * Converts object to query string */ ss.obj2string = function( obj, prefix ) { "use strict"; var str = []; for ( var prop in obj ) { if ( obj.hasOwnProperty( prop ) ) { var k = prefix ? prefix + '[' + prop + ']' : prop, v = obj[prop]; str.push( typeof v === 'object' ? ss.obj2string( v, k ) : encodeURIComponent( k ) + '=' + encodeURIComponent( v ) ); } } return str.join( '&' ); }; /** * Copies all missing properties from second object to first object */ ss.extendObj = function( first, second ) { "use strict"; for ( var prop in second ) { if ( second.hasOwnProperty( prop ) ) { first[prop] = second[prop]; } } }; ss.addEvent = function( elem, type, fn ) { "use strict"; if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); } else { elem.attachEvent( 'on' + type, fn ); } return function() { ss.removeEvent( elem, type, fn ); }; }; ss.removeEvent = document.removeEventListener ? function( elem, type, fn ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, fn, false ); } } : function( elem, type, fn ) { var name = 'on' + type; if ( typeof elem[ name ] === 'undefined' ) { elem[ name ] = null; } elem.detachEvent( name, fn ); }; ss.newXHR = function() { "use strict"; if ( typeof XMLHttpRequest !== 'undefined' ) { return new window.XMLHttpRequest(); } else if ( window.ActiveXObject ) { try { return new window.ActiveXObject( 'Microsoft.XMLHTTP' ); } catch ( err ) { return false; } } }; ss.encodeUTF8 = function( str ) { "use strict"; /*jshint nonstandard:true*/ return unescape( encodeURIComponent( str ) ); }; ss.getIFrame = function() { "use strict"; var id = ss.getUID(), iframe; // IE7 can only create an iframe this way, all others are the other way if ( isIE7 ) { iframe = document.createElement('<iframe src="javascript:false;" name="' + id + '">'); } else { iframe = document.createElement('iframe'); /*jshint scripturl:true*/ iframe.src = 'javascript:false;'; iframe.name = id; } iframe.style.display = 'none'; iframe.id = id; return iframe; }; ss.getForm = function( opts ) { "use strict"; var form = document.createElement('form'); form.encoding = 'multipart/form-data'; // IE form.enctype = 'multipart/form-data'; form.style.display = 'none'; for ( var prop in opts ) { if ( opts.hasOwnProperty( prop ) ) { form[prop] = opts[prop]; } } return form; }; ss.getHidden = function( name, value ) { "use strict"; var input = document.createElement( 'input' ); input.type = 'hidden'; input.name = name; input.value = value; return input; }; /** * Parses a JSON string and returns a Javascript object * Borrowed from www.jquery.com */ ss.parseJSON = function( data ) { "use strict"; /*jshint evil:true*/ if ( !data ) { return false; } data = ss.trim( data + '' ); if ( window.JSON && window.JSON.parse ) { try { // Support: Android 2.3 // Workaround failure to string-cast null input return window.JSON.parse( data + '' ); } catch ( err ) { return false; } } var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g, depth = null, requireNonComma; // Guard against invalid (and possibly dangerous) input by ensuring that nothing remains // after removing valid tokens return data && !ss.trim( data.replace( rvalidtokens, function( token, comma, open, close ) { // Force termination if we see a misplaced comma if ( requireNonComma && comma ) { depth = 0; } // Perform no more replacements after returning to outermost depth if ( depth === 0 ) { return token; } // Commas must not follow "[", "{", or "," requireNonComma = open || comma; // Determine new depth // array/object open ("[" or "{"): depth += true - false (increment) // array/object close ("]" or "}"): depth += false - true (decrement) // other cases ("," or primitive): depth += true - true (numeric cast) depth += !close - !open; // Remove this token return ''; }) ) ? ( Function( 'return ' + data ) )() : false; }; ss.getBox = function( elem ) { "use strict"; var box, docElem, top = 0, left = 0; if ( elem.getBoundingClientRect ) { box = elem.getBoundingClientRect(); docElem = document.documentElement; top = box.top + ( window.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ); left = box.left + ( window.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ); } else { do { left += elem.offsetLeft; top += elem.offsetTop; } while ( ( elem = elem.offsetParent ) ); } return { top: Math.round( top ), left: Math.round( left ) }; }; /** * Helper that takes object literal * and add all properties to element.style * @param {Element} el * @param {Object} styles */ ss.addStyles = function( elem, styles ) { "use strict"; for ( var name in styles ) { if ( styles.hasOwnProperty( name ) ) { elem.style[name] = styles[name]; } } }; /** * Function places an absolutely positioned * element on top of the specified element * copying position and dimensions. */ ss.copyLayout = function( from, to ) { "use strict"; var box = ss.getBox( from ); ss.addStyles( to, { position: 'absolute', display: 'block', left : box.left + 'px', top : box.top + 'px', width : from.offsetWidth + 'px', height : from.offsetHeight + 'px' }); }; /** * Generates unique ID * Complies with RFC 4122 version 4 * http://stackoverflow.com/a/2117523/1091949 * ID begins with letter "a" to be safe for HTML elem ID/name attr (can't start w/ number) */ ss.getUID = function() { "use strict"; /*jshint bitwise: false*/ return 'axxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(uidReplace, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); }; /** * Removes white space from left and right of string * Uses native String.trim if available * Adapted from www.jquery.com */ var trim = "".trim; ss.trim = trim && !trim.call("\uFEFF\xA0") ? function( text ) { return text === null ? "" : trim.call( text ); } : function( text ) { return text === null ? "" : text.toString().replace( rLWhitespace, '' ).replace( rTWhitespace, '' ); }; var arr = []; ss.indexOf = arr.indexOf ? function( array, elem ) { return array.indexOf( elem ); } : function( array, elem ) { for ( var i = 0, len = array.length; i < len; i++ ) { if ( array[i] === elem ) { return i; } } return -1; }; /** * Removes an element from an array */ ss.arrayDelete = function( array, elem ) { var index = ss.indexOf( array, elem ); if ( index > -1 ) { array.splice( index, 1 ); } }; /** * Extract file name from path */ ss.getFilename = function( path ) { "use strict"; return path.replace( rPath, '' ); }; /** * Get file extension */ ss.getExt = function( file ) { "use strict"; return ( -1 !== file.indexOf( '.' ) ) ? file.replace( rExt, '' ) : ''; }; /** * Checks whether an element is visible */ ss.isVisible = function( elem ) { "use strict"; if ( !elem ) { return false; } if ( elem.nodeType !== 1 || elem == document.body ) { elem = null; return true; } if ( elem.parentNode && ( elem.offsetWidth > 0 || elem.offsetHeight > 0 || ss.getStyle( elem, 'display' ).toLowerCase() != 'none' ) ) { return ss.isVisible( elem.parentNode ); } elem = null; return false; }; ss.getStyle = function( elem, style ) { "use strict"; if ( window.getComputedStyle ) { var cs = elem.ownerDocument.defaultView.getComputedStyle( elem, null ); return cs.getPropertyValue( style ); } else if ( elem.currentStyle && elem.currentStyle[ style ] ) { return elem.currentStyle[ style ]; } }; /** * Accepts a form element and returns an object with key/value pairs for the form fields */ ss.getFormObj = function( form ) { "use strict"; var elems = form.elements, ignore = ['button', 'submit', 'image', 'reset'], inputs = {}, obj; for ( var i = 0, len = elems.length; i < len; i++ ) { obj = {}; if ( elems[ i ].name && !elems[ i ].disabled && ss.indexOf( ignore, elems[ i ].type ) === -1 ) { if ( (elems[ i ].type == 'checkbox' || elems[ i ].type == 'radio') && !elems[ i ].checked ) { continue; } obj[ elems[ i ].name ] = ss.val( elems[ i ] ); ss.extendObj( inputs, obj ); } } return inputs; }; /** * Accepts a form input element and returns its value */ ss.val = function( elem ) { "use strict"; if ( !elem ) { return; } if ( elem.nodeName.toUpperCase() == 'SELECT' ) { var options = elem.options, index = elem.selectedIndex, one = elem.type === 'select-one' || index < 0, values = one ? null : [], value; for ( var i = 0, len = options.length; i < len; i++ ) { if ( ( options[ i ].selected || i === index ) && !options[ i ].disabled ) { value = !options[ i ].value ? options[ i ].text : options[ i ].value; if ( one ) { return value; } values.push( value ); } } return values; } else { return elem.value; } }; /** * Check whether element has a particular CSS class * Parts borrowed from www.jquery.com */ ss.hasClass = function( elem, name ) { "use strict"; if ( !elem || !name ) { return false; } return ( ' ' + elem.className + ' ' ).replace( rHasClass, ' ' ).indexOf( ' ' + name + ' ' ) >= 0; }; /** * Adds CSS class to an element */ ss.addClass = function( elem, name ) { "use strict"; if ( !elem || !name ) { return false; } if ( !ss.hasClass( elem, name ) ) { elem.className += ' ' + name; } }; /** * Removes CSS class from an element */ ss.removeClass = (function() { "use strict"; var c = {}; //cache regexps for performance return function( e, name ) { if ( !e || !name ) { return false; } if ( !c[name] ) { c[name] = new RegExp('(?:^|\\s)' + name + '(?!\\S)'); } e.className = e.className.replace( c[name], '' ); }; })(); /** * Nulls out event handlers to prevent memory leaks in IE6/IE7 * http://javascript.crockford.com/memory/leak.html * @param {Element} d * @return void */ ss.purge = function( d ) { "use strict"; var a = d.attributes, i, l, n; if ( a ) { for ( i = a.length - 1; i >= 0; i -= 1 ) { n = a[i].name; if ( typeof d[n] === 'function' ) { d[n] = null; } } } a = d.childNodes; if ( a ) { l = a.length; for ( i = 0; i < l; i += 1 ) { ss.purge( d.childNodes[i] ); } } }; /** * Removes element from the DOM */ ss.remove = function( elem ) { "use strict"; if ( elem && elem.parentNode ) { // null out event handlers for IE ss.purge( elem ); elem.parentNode.removeChild( elem ); } elem = null; }; /** * Accepts either a jQuery object, a string containing an element ID, or an element, * verifies that it exists, and returns the element. * @param {Mixed} elem * @return {Element} */ ss.verifyElem = function( elem ) { "use strict"; /*globals jQuery */ if ( typeof jQuery !== 'undefined' && elem instanceof jQuery ) { elem = elem[0]; } else if ( typeof elem === 'string' ) { if ( elem.charAt( 0 ) == '#' ) { elem = elem.substr( 1 ); } elem = document.getElementById( elem ); } if ( !elem || elem.nodeType !== 1 ) { return false; } if ( elem.nodeName.toUpperCase() == 'A' ) { elem.style.cursor = 'pointer'; ss.addEvent( elem, 'click', function( e ) { if ( e && e.preventDefault ) { e.preventDefault(); } else if ( window.event ) { window.event.returnValue = false; } }); } return elem; }; ss._options = {}; ss.uploadSetup = function( options ) { "use strict"; ss.extendObj( ss._options, options ); }; ss.SimpleUpload = function( options ) { "use strict"; this._opts = { button: '', url: '', dropzone: '', dragClass: '', form: '', overrideSubmit: true, cors: false, withCredentials: false, progressUrl: false, sessionProgressUrl: false, nginxProgressUrl: false, multiple: false, // allow multiple, concurrent file uploads multipleSelect: false, // allow multiple file selection maxUploads: 3, queue: true, checkProgressInterval: 500, keyParamName: 'APC_UPLOAD_PROGRESS', sessionProgressName: 'PHP_SESSION_UPLOAD_PROGRESS', nginxProgressHeader: 'X-Progress-ID', customProgressHeaders: {}, corsInputName: 'XHR_CORS_TARGETORIGIN', allowedExtensions: [], accept: '', maxSize: false, name: '', data: {}, noParams: true, autoSubmit: true, multipart: true, method: 'POST', responseType: '', debug: false, hoverClass: '', focusClass: '', disabledClass: '', customHeaders: {}, encodeHeaders: true, autoCalibrate: true, onBlankSubmit: function() {}, onAbort: function( filename, uploadBtn, size ) {}, onChange: function( filename, extension, uploadBtn, size, file ) {}, onSubmit: function( filename, extension, uploadBtn, size ) {}, onProgress: function( pct ) {}, onUpdateFileSize: function( filesize ) {}, onComplete: function( filename, response, uploadBtn, size ) {}, onDone: function( filename, status, textStatus, response, uploadBtn, size ) {}, onAllDone: function() {}, onExtError: function( filename, extension ) {}, onSizeError: function( filename, fileSize, uploadBtn ) {}, onError: function( filename, type, status, statusText, response, uploadBtn, size ) {}, startXHR: function( filename, fileSize, uploadBtn ) {}, endXHR: function( filename, fileSize, uploadBtn ) {}, startNonXHR: function( filename, uploadBtn ) {}, endNonXHR: function( filename, uploadBtn ) {} }; ss.extendObj( this._opts, ss._options ); // Include any setup options ss.extendObj( this._opts, options ); // Then add options for this instance // An array of objects, each containing two items: a file and a reference // to the button which triggered the upload: { file: uploadFile, btn: button } this._queue = []; this._active = 0; this._disabled = false; // if disabled, clicking on button won't do anything this._maxFails = 10; // max allowed failed progress updates requests in iframe mode this._progKeys = {}; // contains the currently active upload ID progress keys this._sizeFlags = {}; // Cache progress keys after setting sizeBox for fewer trips to the DOM this._btns = []; this.addButton( this._opts.button ); delete this._opts.button; this._opts.button = options = null; if ( this._opts.multiple === false ) { this._opts.maxUploads = 1; } if ( this._opts.dropzone !== '' ) { this.addDZ( this._opts.dropzone ); } if ( this._opts.dropzone === '' && this._btns.length < 1 ) { throw new Error( "Invalid upload button. Make sure the element you're passing exists." ); } if ( this._opts.form !== '' ) { this.setForm( this._opts.form ); } this._createInput(); this._manDisabled = false; this.enable( true ); }; ss.SimpleUpload.prototype = { destroy: function() { "use strict"; // # of upload buttons var i = this._btns.length; // Put upload buttons back to the way we found them while ( i-- ) { // Remove event listener if ( this._btns[i].off ) { this._btns[i].off(); } // Remove any lingering classes ss.removeClass( this._btns[i], this._opts.hoverClass ); ss.removeClass( this._btns[i], this._opts.focusClass ); ss.removeClass( this._btns[i], this._opts.disabledClass ); // In case we disabled it this._btns[i].disabled = false; } this._killInput(); // Set a flag to be checked in _last() this._destroy = true; }, /** * Send data to browser console if debug is set to true */ log: function( str ) { "use strict"; if ( this._opts && this._opts.debug && window.console && window.console.log ) { window.console.log( '[Uploader] ' + str ); } }, /** * Replaces user data * Note that all previously set data is entirely removed and replaced */ setData: function( data ) { "use strict"; this._opts.data = data; }, /** * Set or change uploader options * @param {Object} options */ setOptions: function( options ) { "use strict"; ss.extendObj( this._opts, options ); }, /** * Designate an element as an upload button */ addButton: function( button ) { var btn; // An array of buttons was passed if ( button instanceof Array ) { for ( var i = 0, len = button.length; i < len; i++ ) { btn = ss.verifyElem( button[i] ); if ( btn !== false ) { this._btns.push( this.rerouteClicks( btn ) ); } else { this.log( 'Button with array index ' + i + ' is invalid' ); } } // A single button was passed } else { btn = ss.verifyElem( button ); if ( btn !== false ) { this._btns.push( this.rerouteClicks( btn ) ); } } }, /** * Designate an element as a drop zone */ addDZ: function( dropzone ) { if ( !XhrOk ) { return; } dropzone = ss.verifyElem( dropzone ); if ( !dropzone ) { this.log( 'Invalid or nonexistent element passed for drop zone' ); } else { this.addDropZone( dropzone ); } }, /** * Designate an element as a progress bar * The CSS width % of the element will be updated as the upload progresses */ setProgressBar: function( elem ) { "use strict"; this._progBar = ss.verifyElem( elem ); }, /** * Designate an element to receive a string containing progress % during upload * Note: Uses innerHTML, so any existing child elements will be wiped out */ setPctBox: function( elem ) { "use strict"; this._pctBox = ss.verifyElem( elem ); }, /** * Designate an element to receive a string containing file size at start of upload * Note: Uses innerHTML, so any existing child elements will be wiped out */ setFileSizeBox: function( elem ) { "use strict"; this._sizeBox = ss.verifyElem( elem ); }, /** * Designate an element to be removed from DOM when upload is completed * Useful for removing progress bar, file size, etc. after upload */ setProgressContainer: function( elem ) { "use strict"; this._progBox = ss.verifyElem( elem ); }, /** * Designate an element to serve as the upload abort button */ setAbortBtn: function( elem, remove ) { "use strict"; this._abortBtn = ss.verifyElem( elem ); this._removeAbort = false; if ( remove ) { this._removeAbort = true; } }, setForm: function( form ) { "use strict"; this._form = ss.verifyElem( form ); if ( !this._form || this._form.nodeName.toUpperCase() != 'FORM' ) { this.log( 'Invalid or nonexistent element passed for form' ); } else { var self = this; this._opts.autoSubmit = false; if ( this._opts.overrideSubmit ) { ss.addEvent( this._form, 'submit', function( e ) { if ( e.preventDefault ) { e.preventDefault(); } else if ( window.event ) { window.event.returnValue = false; } if ( self._validateForm() ) { self.submit(); } }); this._form.submit = function() { if ( self._validateForm() ) { self.submit(); } }; } } }, /** * Returns number of files currently in queue */ getQueueSize: function() { "use strict"; return this._queue.length; }, /** * Remove current file from upload queue, reset props, cycle to next upload */ removeCurrent: function( id ) { "use strict"; if ( id ) { var i = this._queue.length; while ( i-- ) { if ( this._queue[i].id === id ) { this._queue.splice( i, 1 ); break; } } } else { this._queue.splice( 0, 1 ); } this._cycleQueue(); }, /** * Clears Queue so only most recent select file is uploaded */ clearQueue: function() { "use strict"; this._queue.length = 0; }, /** * Disables upload functionality */ disable: function( _self ) { "use strict"; var i = this._btns.length, nodeName; // _self is always true when disable() is called internally this._manDisabled = !_self || this._manDisabled === true ? true : false; this._disabled = true; while ( i-- ) { nodeName = this._btns[i].nodeName.toUpperCase(); if ( nodeName == 'INPUT' || nodeName == 'BUTTON' ) { this._btns[i].disabled = true; } if ( this._opts.disabledClass !== '' ) { ss.addClass( this._btns[i], this._opts.disabledClass ); } } // Hide file input if ( this._input && this._input.parentNode ) { this._input.parentNode.style.visibility = 'hidden'; } }, /** * Enables upload functionality */ enable: function( _self ) { "use strict"; // _self will always be true when enable() is called internally if ( !_self ) { this._manDisabled = false; } // Don't enable uploader if it was manually disabled if ( this._manDisabled === true ) { return; } var i = this._btns.length; this._disabled = false; while ( i-- ) { ss.removeClass( this._btns[i], this._opts.disabledClass ); this._btns[i].disabled = false; } }, /** * Updates invisible button position */ updatePosition: function( btn ) { "use strict"; btn = !btn ? this._btns[0] : btn; if ( btn && this._input && this._input.parentNode ) { ss.copyLayout( btn, this._input.parentNode ); } btn = null; }, rerouteClicks: function( elem ) { "use strict"; var self = this, detachOver, detachClick; detachOver = ss.addEvent( elem, 'mouseover', function() { if ( self._disabled ) { return; } if ( !self._input ) { self._createInput(); } self._overBtn = elem; ss.copyLayout( elem, self._input.parentNode ); self._input.parentNode.style.visibility = 'visible'; }); // Support keyboard interaction detachClick = ss.addEvent( elem, 'click', function( e ) { if ( e && e.preventDefault ) { e.preventDefault(); } if ( self._disabled ) { return; } if ( !self._input ) { self._createInput(); } self._overBtn = elem; if ( !isIE7to9 ) { self._input.click(); } }); // ss.addEvent() returns a function to detach, which // allows us to call elem.off() to remove mouseover listener elem.off = function() { detachOver(); detachClick(); }; if ( self._opts.autoCalibrate && !ss.isVisible( elem ) ) { self.log('Upload button not visible'); var interval = function() { if ( ss.isVisible( elem ) ) { self.log('Upload button now visible'); window.setTimeout(function() { self.updatePosition( elem ); if ( self._btns.length === 1 ) { self._input.parentNode.style.visibility = 'hidden'; } }, 200); } else { window.setTimeout( interval, 500 ); } }; window.setTimeout( interval, 500 ); } return elem; }, /** * Validates input and directs to either XHR method or iFrame method */ submit: function( _, auto ) { "use strict"; if ( !auto && this._queue.length < 1 ) { this._opts.onBlankSubmit.call( this ); return; } if ( this._disabled || this._active >= this._opts.maxUploads || this._queue.length < 1 ) { return; } if ( !this._checkFile( this._queue[0] ) ) { return; } // User returned false to cancel upload if ( false === this._opts.onSubmit.call( this, this._queue[0].name, this._queue[0].ext, this._queue[0].btn, this._queue[0].size ) ) { this.removeCurrent( this._queue[0].id ); return; } // Increment the active upload counter this._active++; // Disable uploading if multiple file uploads are not enabled // or if queue is disabled and we've reached max uploads if ( this._opts.multiple === false || this._opts.queue === false && this._active >= this._opts.maxUploads ) { this.disable( true ); } this._initUpload( this._queue[0] ); } }; ss.IframeUpload = { _detachEvents: {}, _detach: function( id ) { if ( this._detachEvents[ id ] ) { this._detachEvents[ id ](); delete this._detachEvents[ id ]; } }, /** * Accepts a URI string and returns the hostname */ _getHost: function( uri ) { var a = document.createElement( 'a' ); a.href = uri; if ( a.hostname ) { return a.hostname.toLowerCase(); } return uri; }, _addFiles: function( file ) { var filename = ss.getFilename( file.value ), ext = ss.getExt( filename ); if ( false === this._opts.onChange.call( this, filename, ext, this._overBtn, undefined, file ) ) { return false; } this._queue.push({ id: ss.getUID(), file: file, name: filename, ext: ext, btn: this._overBtn, size: null }); return true; }, /** * Handles uploading with iFrame */ _uploadIframe: function( fileObj, progBox, sizeBox, progBar, pctBox, abortBtn, removeAbort ) { "use strict"; var self = this, opts = this._opts, key = ss.getUID(), iframe = ss.getIFrame(), form, url, msgLoaded = false, iframeLoaded = false, cancel; if ( opts.noParams === true ) { url = opts.url; } else { // If we're using Nginx Upload Progress Module, append upload key to the URL // Also, preserve query string if there is one url = !opts.nginxProgressUrl ? opts.url : url + ( ( url.indexOf( '?' ) > -1 ) ? '&' : '?' ) + encodeURIComponent( opts.nginxProgressHeader ) + '=' + encodeURIComponent( key ); } form = ss.getForm({ action: url, target: iframe.name, method: opts.method }); opts.onProgress.call( this, 0 ); if ( pctBox ) { pctBox.innerHTML = '0%'; } if ( progBar ) { progBar.style.width = '0%'; } // For CORS, add a listener for the "message" event, which will be // triggered by the Javascript snippet in the server response if ( opts.cors ) { var msgId = ss.getUID(); self._detachEvents[ msgId ] = ss.addEvent( window, 'message', function( event ) { // Make sure event.origin matches the upload URL if ( self._getHost( event.origin ) != self._getHost( opts.url ) ) { self.log('Non-matching origin: ' + event.origin); return; } msgLoaded = true; self._detach( msgId ); opts.endNonXHR.call( self, fileObj.name, fileObj.btn ); self._finish( fileObj, '', '', event.data, sizeBox, progBox, pctBox, abortBtn, removeAbort ); }); } self._detachEvents[ iframe.id ] = ss.addEvent( iframe, 'load', function() { self._detach( iframe.id ); if ( opts.sessionProgressUrl ) { form.appendChild( ss.getHidden( opts.sessionProgressName, key ) ); } // PHP APC upload progress key field must come before the file field else if ( opts.progressUrl ) { form.appendChild( ss.getHidden( opts.keyParamName, key ) ); } if ( self._form ) { ss.extendObj( opts.data, ss.getFormObj( self._form ) ); } // Get additional data after startNonXHR() in case setData() was called prior to submitting for ( var prop in opts.data ) { if ( opts.data.hasOwnProperty( prop ) ) { form.appendChild( ss.getHidden( prop, opts.data[prop] ) ); } } // Add a field (default name: "XHR_CORS_TRARGETORIGIN") to tell server this is a CORS request // Value of the field is targetOrigin parameter of postMessage(message, targetOrigin) if ( opts.cors ) { form.appendChild( ss.getHidden( opts.corsInputName, window.location.href ) ); } form.appendChild( fileObj.file ); self._detachEvents[ fileObj.id ] = ss.addEvent( iframe, 'load', function() { if ( !iframe || !iframe.parentNode || iframeLoaded ) { return; } self._detach( fileObj.id ); iframeLoaded = true; delete self._progKeys[ key ]; delete self._sizeFlags[ key ]; if ( abortBtn ) { ss.removeEvent( abortBtn, 'click', cancel ); } // After a CORS response, we wait briefly for the "message" event to finish, // during which time the msgLoaded var will be set to true, signalling success. // If iframe loads without "message" event, we assume there was an error if ( opts.cors ) { window.setTimeout(function() { ss.remove( iframe ); // If msgLoaded has not been set to true after "message" event fires, we // infer that an error must have occurred and respond accordingly if ( !msgLoaded ) { self._errorFinish( fileObj, '', '', false, 'error', progBox, sizeBox, pctBox, abortBtn, removeAbort ); } opts = key = iframe = sizeBox = progBox = pctBox = abortBtn = removeAbort = null; }, 600); } // Non-CORS upload else { try { var doc = iframe.contentDocument ? iframe.contentDocument : iframe.contentWindow.document, response = doc.body.innerHTML; ss.remove( iframe ); iframe = null; opts.endNonXHR.call( self, fileObj.name, fileObj.btn ); // No way to get status and statusText for an iframe so return empty strings self._finish( fileObj, '', '', response, sizeBox, progBox, pctBox, abortBtn, removeAbort ); } catch ( e ) { self._errorFinish( fileObj, '', e.message, false, 'error', progBox, sizeBox, pctBox, abortBtn, removeAbort ); } opts = key = sizeBox = progBox = pctBox = null; } });// end load if ( abortBtn ) { cancel = function() { ss.removeEvent( abortBtn, 'click', cancel ); delete self._progKeys[key]; delete self._sizeFlags[key]; if ( iframe ) { iframeLoaded = true; self._detach( fileObj.id ); try { if ( iframe.contentWindow.document.execCommand ) { iframe.contentWindow.document.execCommand('Stop'); } } catch( err ) {} try { iframe.src = 'javascript'.concat(':false;'); } catch( err ) {} window.setTimeout(function() { ss.remove( iframe ); iframe = null; }, 1); } self.log('Upload aborted'); opts.onAbort.call( self, fileObj.name, fileObj.btn, fileObj.size ); self._last( sizeBox, progBox, pctBox, abortBtn, removeAbort, fileObj, 'abort' ); }; ss.addEvent( abortBtn, 'click', cancel ); } self.log( 'Commencing upload using iframe' ); form.submit(); // Remove form and begin next upload window.setTimeout(function() { ss.remove( form ); form = null; self.removeCurrent( fileObj.id ); }, 1); if ( self._hasProgUrl ) { // Add progress key to active key array self._progKeys[key] = 1; window.setTimeout( function() { self._getProg( key, progBar, sizeBox, pctBox, 1 ); progBar = sizeBox = pctBox = null; }, 600 ); } });// end load document.body.appendChild( form ); document.body.appendChild( iframe ); }, /** * Retrieves upload progress updates from the server * (For fallback upload progress support) */ _getProg: function( key, progBar, sizeBox, pctBox, counter ) { "use strict"; /*jshint sub:true*/ var self = this, opts = this._opts, time = new Date().getTime(), xhr, url, callback; if ( !key || !opts ) { return; } // Nginx Upload Progress Module if ( opts.nginxProgressUrl ) { url = opts.nginxProgressUrl + '?' + encodeURIComponent( opts.nginxProgressHeader ) + '=' + encodeURIComponent( key ) + '&_=' + time; } else if ( opts.sessionProgressUrl ) { url = opts.sessionProgressUrl; } // PHP APC upload progress else if ( opts.progressUrl ) { url = opts.progressUrl + '?progresskey=' + encodeURIComponent( key ) + '&_=' + time; } callback = function() { var response, size, pct, status, statusText; try { // XDR doesn't have readyState so we just assume that it finished correctly if ( callback && ( opts.cors || xhr.readyState === 4 ) ) { callback = undefined; xhr.onreadystatechange = function() {}; try { statusText = xhr.statusText; status = xhr.status; } catch( e ) { statusText = ''; status = ''; } // XDR also doesn't have status, so just assume that everything is fine if ( opts.cors || ( status >= 200 && status < 300 ) ) { response = ss.parseJSON( xhr.responseText ); if ( response === false ) { self.log( 'Error parsing progress response (expecting JSON)' ); return; } // Handle response if using Nginx Upload Progress Module if ( opts.nginxProgressUrl ) { if ( response.state == 'uploading' ) { size = parseInt( response.size, 10 ); if ( size > 0 ) { pct = Math.round( ( parseInt( response.received, 10 ) / size ) * 100 ); size = Math.round( size / 1024 ); // convert to kilobytes } } else if ( response.state == 'done' ) { pct = 100; } else if ( response.state == 'error' ) { self.log( 'Error requesting upload progress: ' + response.status ); return; } } // Handle response if using PHP APC else if ( opts.sessionProgressUrl || opts.progressUrl ) { if ( response.success === true ) { size = parseInt( response.size, 10 ); pct = parseInt( response.pct, 10 ); } } // Update progress bar width if ( pct ) { if ( pctBox ) { pctBox.innerHTML = pct + '%'; } if ( progBar ) { progBar.style.width = pct + '%'; } opts.onProgress.call( self, pct ); } if ( size && !self._sizeFlags[key] ) { if ( sizeBox ) { sizeBox.innerHTML = size + 'K'; } self._sizeFlags[key] = 1; opts.onUpdateFileSize.call( self, size ); } // Stop attempting progress checks if we keep failing if ( !pct && !size && counter >= self._maxFails ) { counter++; self.log( 'Failed progress request limit reached. Count: ' + counter ); return; } // Begin countdown until next progress update check if ( pct < 100 && self._progKeys[key] ) { window.setTimeout( function() { self._getProg( key, progBar, sizeBox, pctBox, counter ); key = progBar = sizeBox = pctBox = counter = null; }, opts.checkProgressInterval ); } // We didn't get a 2xx status so don't continue sending requests } else { delete self._progKeys[key]; self.log( 'Error requesting upload progress: ' + status + ' ' + statusText ); } xhr = size = pct = status = statusText = response = null; } } catch( e ) { self.log( 'Error requesting upload progress: ' + e.message ); } }; // CORS requests in IE8 and IE9 must use XDomainRequest if ( opts.cors && !opts.sessionProgressUrl ) { if ( window.XDomainRequest ) { xhr = new window.XDomainRequest(); xhr.open( 'GET', url, true ); xhr.onprogress = xhr.ontimeout = function() {}; xhr.onload = callback; xhr.onerror = function() { delete self._progKeys[key]; key = null; self.log('Error requesting upload progress'); }; // IE7 or some other dinosaur -- just give up } else { return; } } else { var method = !opts.sessionProgressUrl ? 'GET' : 'POST', headers = {}, params; xhr = ss.newXHR(); xhr.onreadystatechange = callback; xhr.open( method, url, true ); // PHP session progress updates must be a POST request if ( opts.sessionProgressUrl ) { params = encodeURIComponent( opts.sessionProgressName ) + '=' + encodeURIComponent( key ); headers['Content-Type'] = 'application/x-www-form-urlencoded'; } // Set the upload progress header for Nginx if ( opts.nginxProgressUrl ) { headers[opts.nginxProgressHeader] = key; } headers['X-Requested-With'] = 'XMLHttpRequest'; headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'; ss.extendObj( headers, opts.customProgressHeaders ); for ( var i in headers ) { if ( headers.hasOwnProperty( i ) ) { if ( opts.encodeHeaders ) { xhr.setRequestHeader( i, ss.encodeUTF8( headers[ i ] + '' ) ); } else { xhr.setRequestHeader( i, headers[ i ] + '' ); } } } xhr.send( ( opts.sessionProgressUrl && params ) || null ); } }, _initUpload: function( fileObj ) { if ( false === this._opts.startNonXHR.call( this, fileObj.name, fileObj.btn ) ) { if ( this._disabled ) { this.enable( true ); } this._active--; return; } this._hasProgUrl = ( this._opts.progressUrl || this._opts.sessionProgressUrl || this._opts.nginxProgressUrl ) ? true : false; this._uploadIframe( fileObj, this._progBox, this._sizeBox, this._progBar, this._pctBox, this._abortBtn, this._removeAbort ); fileObj = this._progBox = this._sizeBox = this._progBar = this._pctBox = this._abortBtn = this._removeAbort = null; } }; ss.XhrUpload = { _addFiles: function( files ) { var total = files.length, filename, ext, size, i; if ( !this._opts.multiple ) { total = 1; } for ( i = 0; i < total; i++ ) { filename = ss.getFilename( files[i].name ); ext = ss.getExt( filename ); size = Math.round( files[i].size / 1024 ); if ( false === this._opts.onChange.call( this, filename, ext, this._overBtn, size, files[i] ) ) { return false; } this._queue.push({ id: ss.getUID(), file: files[i], name: filename, ext: ext, btn: this._overBtn, size: size }); } return true; }, /** * Handles uploading with XHR */ _uploadXhr: function( fileObj, url, params, headers, sizeBox, progBar, progBox, pctBox, abortBtn, removeAbort ) { "use strict"; var self = this, opts = this._opts, xhr = ss.