| | |
| | | /*! WebUploader 0.1.2 */ |
| | | |
| | | |
| | | /** |
| | | * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 |
| | | * |
| | | * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 |
| | | */ |
| | | (function( root, factory ) { |
| | | var modules = {}, |
| | | |
| | | // 内部require, 简单不完全实现。 |
| | | // https://github.com/amdjs/amdjs-api/wiki/require |
| | | _require = function( deps, callback ) { |
| | | var args, len, i; |
| | | |
| | | // 如果deps不是数组,则直接返回指定module |
| | | if ( typeof deps === 'string' ) { |
| | | return getModule( deps ); |
| | | } else { |
| | | args = []; |
| | | for( len = deps.length, i = 0; i < len; i++ ) { |
| | | args.push( getModule( deps[ i ] ) ); |
| | | } |
| | | |
| | | return callback.apply( null, args ); |
| | | } |
| | | }, |
| | | |
| | | // 内部define,暂时不支持不指定id. |
| | | _define = function( id, deps, factory ) { |
| | | if ( arguments.length === 2 ) { |
| | | factory = deps; |
| | | deps = null; |
| | | } |
| | | |
| | | _require( deps || [], function() { |
| | | setModule( id, factory, arguments ); |
| | | }); |
| | | }, |
| | | |
| | | // 设置module, 兼容CommonJs写法。 |
| | | setModule = function( id, factory, args ) { |
| | | var module = { |
| | | exports: factory |
| | | }, |
| | | returned; |
| | | |
| | | if ( typeof factory === 'function' ) { |
| | | args.length || (args = [ _require, module.exports, module ]); |
| | | returned = factory.apply( null, args ); |
| | | returned !== undefined && (module.exports = returned); |
| | | } |
| | | |
| | | modules[ id ] = module.exports; |
| | | }, |
| | | |
| | | // 根据id获取module |
| | | getModule = function( id ) { |
| | | var module = modules[ id ] || root[ id ]; |
| | | |
| | | if ( !module ) { |
| | | throw new Error( '`' + id + '` is undefined' ); |
| | | } |
| | | |
| | | return module; |
| | | }, |
| | | |
| | | // 将所有modules,将路径ids装换成对象。 |
| | | exportsTo = function( obj ) { |
| | | var key, host, parts, part, last, ucFirst; |
| | | |
| | | // make the first character upper case. |
| | | ucFirst = function( str ) { |
| | | return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); |
| | | }; |
| | | |
| | | for ( key in modules ) { |
| | | host = obj; |
| | | |
| | | if ( !modules.hasOwnProperty( key ) ) { |
| | | continue; |
| | | } |
| | | |
| | | parts = key.split('/'); |
| | | last = ucFirst( parts.pop() ); |
| | | |
| | | while( (part = ucFirst( parts.shift() )) ) { |
| | | host[ part ] = host[ part ] || {}; |
| | | host = host[ part ]; |
| | | } |
| | | |
| | | host[ last ] = modules[ key ]; |
| | | } |
| | | }, |
| | | |
| | | exports = factory( root, _define, _require ), |
| | | origin; |
| | | |
| | | // exports every module. |
| | | exportsTo( exports ); |
| | | |
| | | if ( typeof module === 'object' && typeof module.exports === 'object' ) { |
| | | |
| | | // For CommonJS and CommonJS-like environments where a proper window is present, |
| | | module.exports = exports; |
| | | } else if ( typeof define === 'function' && define.amd ) { |
| | | |
| | | // Allow using this built library as an AMD module |
| | | // in another project. That other project will only |
| | | // see this AMD call, not the internal modules in |
| | | // the closure below. |
| | | define([], exports ); |
| | | } else { |
| | | |
| | | // Browser globals case. Just assign the |
| | | // result to a property on the global. |
| | | origin = root.WebUploader; |
| | | root.WebUploader = exports; |
| | | root.WebUploader.noConflict = function() { |
| | | root.WebUploader = origin; |
| | | }; |
| | | } |
| | | })( this, function( window, define, require ) { |
| | | |
| | | |
| | | /** |
| | | * @fileOverview jQuery or Zepto |
| | | */ |
| | | define('dollar-third',[],function() { |
| | | return window.jQuery || window.Zepto; |
| | | }); |
| | | /** |
| | | * @fileOverview Dom 操作相关 |
| | | */ |
| | | define('dollar',[ |
| | | 'dollar-third' |
| | | ], function( _ ) { |
| | | return _; |
| | | }); |
| | | /** |
| | | * @fileOverview 使用jQuery的Promise |
| | | */ |
| | | define('promise-third',[ |
| | | 'dollar' |
| | | ], function( $ ) { |
| | | return { |
| | | Deferred: $.Deferred, |
| | | when: $.when, |
| | | |
| | | isPromise: function( anything ) { |
| | | return anything && typeof anything.then === 'function'; |
| | | } |
| | | }; |
| | | }); |
| | | /** |
| | | * @fileOverview Promise/A+ |
| | | */ |
| | | define('promise',[ |
| | | 'promise-third' |
| | | ], function( _ ) { |
| | | return _; |
| | | }); |
| | | /** |
| | | * @fileOverview 基础类方法。 |
| | | */ |
| | | |
| | | /** |
| | | * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 |
| | | * |
| | | * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. |
| | | * 默认module id该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: |
| | | * |
| | | * * module `base`:WebUploader.Base |
| | | * * module `file`: WebUploader.File |
| | | * * module `lib/dnd`: WebUploader.Lib.Dnd |
| | | * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd |
| | | * |
| | | * |
| | | * 以下文档将可能省略`WebUploader`前缀。 |
| | | * @module WebUploader |
| | | * @title WebUploader API文档 |
| | | */ |
| | | define('base',[ |
| | | 'dollar', |
| | | 'promise' |
| | | ], function( $, promise ) { |
| | | |
| | | var noop = function() {}, |
| | | call = Function.call; |
| | | |
| | | // http://jsperf.com/uncurrythis |
| | | // 反科里化 |
| | | function uncurryThis( fn ) { |
| | | return function() { |
| | | return call.apply( fn, arguments ); |
| | | }; |
| | | } |
| | | |
| | | function bindFn( fn, context ) { |
| | | return function() { |
| | | return fn.apply( context, arguments ); |
| | | }; |
| | | } |
| | | |
| | | function createObject( proto ) { |
| | | var f; |
| | | |
| | | if ( Object.create ) { |
| | | return Object.create( proto ); |
| | | } else { |
| | | f = function() {}; |
| | | f.prototype = proto; |
| | | return new f(); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 基础类,提供一些简单常用的方法。 |
| | | * @class Base |
| | | */ |
| | | return { |
| | | |
| | | /** |
| | | * @property {String} version 当前版本号。 |
| | | */ |
| | | version: '0.1.2', |
| | | |
| | | /** |
| | | * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 |
| | | */ |
| | | $: $, |
| | | |
| | | Deferred: promise.Deferred, |
| | | |
| | | isPromise: promise.isPromise, |
| | | |
| | | when: promise.when, |
| | | |
| | | /** |
| | | * @description 简单的浏览器检查结果。 |
| | | * |
| | | * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 |
| | | * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 |
| | | * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** |
| | | * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 |
| | | * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 |
| | | * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 |
| | | * |
| | | * @property {Object} [browser] |
| | | */ |
| | | browser: (function( ua ) { |
| | | var ret = {}, |
| | | webkit = ua.match( /WebKit\/([\d.]+)/ ), |
| | | chrome = ua.match( /Chrome\/([\d.]+)/ ) || |
| | | ua.match( /CriOS\/([\d.]+)/ ), |
| | | |
| | | ie = ua.match( /MSIE\s([\d\.]+)/ ) || |
| | | ua.match(/(?:trident)(?:.*rv:([\w.]+))?/i), |
| | | firefox = ua.match( /Firefox\/([\d.]+)/ ), |
| | | safari = ua.match( /Safari\/([\d.]+)/ ), |
| | | opera = ua.match( /OPR\/([\d.]+)/ ); |
| | | |
| | | webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); |
| | | chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); |
| | | ie && (ret.ie = parseFloat( ie[ 1 ] )); |
| | | firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); |
| | | safari && (ret.safari = parseFloat( safari[ 1 ] )); |
| | | opera && (ret.opera = parseFloat( opera[ 1 ] )); |
| | | |
| | | return ret; |
| | | })( navigator.userAgent ), |
| | | |
| | | /** |
| | | * @description 操作系统检查结果。 |
| | | * |
| | | * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 |
| | | * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 |
| | | * @property {Object} [os] |
| | | */ |
| | | os: (function( ua ) { |
| | | var ret = {}, |
| | | |
| | | // osx = !!ua.match( /\(Macintosh\; Intel / ), |
| | | android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), |
| | | ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); |
| | | |
| | | // osx && (ret.osx = true); |
| | | android && (ret.android = parseFloat( android[ 1 ] )); |
| | | ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); |
| | | |
| | | return ret; |
| | | })( navigator.userAgent ), |
| | | |
| | | /** |
| | | * 实现类与类之间的继承。 |
| | | * @method inherits |
| | | * @grammar Base.inherits( super ) => child |
| | | * @grammar Base.inherits( super, protos ) => child |
| | | * @grammar Base.inherits( super, protos, statics ) => child |
| | | * @param {Class} super 父类 |
| | | * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 |
| | | * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 |
| | | * @param {Object} [statics] 静态属性或方法。 |
| | | * @return {Class} 返回子类。 |
| | | * @example |
| | | * function Person() { |
| | | * console.log( 'Super' ); |
| | | * } |
| | | * Person.prototype.hello = function() { |
| | | * console.log( 'hello' ); |
| | | * }; |
| | | * |
| | | * var Manager = Base.inherits( Person, { |
| | | * world: function() { |
| | | * console.log( 'World' ); |
| | | * } |
| | | * }); |
| | | * |
| | | * // 因为没有指定构造器,父类的构造器将会执行。 |
| | | * var instance = new Manager(); // => Super |
| | | * |
| | | * // 继承子父类的方法 |
| | | * instance.hello(); // => hello |
| | | * instance.world(); // => World |
| | | * |
| | | * // 子类的__super__属性指向父类 |
| | | * console.log( Manager.__super__ === Person ); // => true |
| | | */ |
| | | inherits: function( Super, protos, staticProtos ) { |
| | | var child; |
| | | |
| | | if ( typeof protos === 'function' ) { |
| | | child = protos; |
| | | protos = null; |
| | | } else if ( protos && protos.hasOwnProperty('constructor') ) { |
| | | child = protos.constructor; |
| | | } else { |
| | | child = function() { |
| | | return Super.apply( this, arguments ); |
| | | }; |
| | | } |
| | | |
| | | // 复制静态方法 |
| | | $.extend( true, child, Super, staticProtos || {} ); |
| | | |
| | | /* jshint camelcase: false */ |
| | | |
| | | // 让子类的__super__属性指向父类。 |
| | | child.__super__ = Super.prototype; |
| | | |
| | | // 构建原型,添加原型方法或属性。 |
| | | // 暂时用Object.create实现。 |
| | | child.prototype = createObject( Super.prototype ); |
| | | protos && $.extend( true, child.prototype, protos ); |
| | | |
| | | return child; |
| | | }, |
| | | |
| | | /** |
| | | * 一个不做任何事情的方法。可以用来赋值给默认的callback. |
| | | * @method noop |
| | | */ |
| | | noop: noop, |
| | | |
| | | /** |
| | | * 返回一个新的方法,此方法将已指定的`context`来执行。 |
| | | * @grammar Base.bindFn( fn, context ) => Function |
| | | * @method bindFn |
| | | * @example |
| | | * var doSomething = function() { |
| | | * console.log( this.name ); |
| | | * }, |
| | | * obj = { |
| | | * name: 'Object Name' |
| | | * }, |
| | | * aliasFn = Base.bind( doSomething, obj ); |
| | | * |
| | | * aliasFn(); // => Object Name |
| | | * |
| | | */ |
| | | bindFn: bindFn, |
| | | |
| | | /** |
| | | * 引用Console.log如果存在的话,否则引用一个[空函数loop](#WebUploader:Base.log)。 |
| | | * @grammar Base.log( args... ) => undefined |
| | | * @method log |
| | | */ |
| | | log: (function() { |
| | | if ( window.console ) { |
| | | return bindFn( console.log, console ); |
| | | } |
| | | return noop; |
| | | })(), |
| | | |
| | | nextTick: (function() { |
| | | |
| | | return function( cb ) { |
| | | setTimeout( cb, 1 ); |
| | | }; |
| | | |
| | | // @bug 当浏览器不在当前窗口时就停了。 |
| | | // var next = window.requestAnimationFrame || |
| | | // window.webkitRequestAnimationFrame || |
| | | // window.mozRequestAnimationFrame || |
| | | // function( cb ) { |
| | | // window.setTimeout( cb, 1000 / 60 ); |
| | | // }; |
| | | |
| | | // // fix: Uncaught TypeError: Illegal invocation |
| | | // return bindFn( next, window ); |
| | | })(), |
| | | |
| | | /** |
| | | * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 |
| | | * 将用来将非数组对象转化成数组对象。 |
| | | * @grammar Base.slice( target, start[, end] ) => Array |
| | | * @method slice |
| | | * @example |
| | | * function doSomthing() { |
| | | * var args = Base.slice( arguments, 1 ); |
| | | * console.log( args ); |
| | | * } |
| | | * |
| | | * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] |
| | | */ |
| | | slice: uncurryThis( [].slice ), |
| | | |
| | | /** |
| | | * 生成唯一的ID |
| | | * @method guid |
| | | * @grammar Base.guid() => String |
| | | * @grammar Base.guid( prefx ) => String |
| | | */ |
| | | guid: (function() { |
| | | var counter = 0; |
| | | |
| | | return function( prefix ) { |
| | | var guid = (+new Date()).toString( 32 ), |
| | | i = 0; |
| | | |
| | | for ( ; i < 5; i++ ) { |
| | | guid += Math.floor( Math.random() * 65535 ).toString( 32 ); |
| | | } |
| | | |
| | | return (prefix || 'wu_') + guid + (counter++).toString( 32 ); |
| | | }; |
| | | })(), |
| | | |
| | | /** |
| | | * 格式化文件大小, 输出成带单位的字符串 |
| | | * @method formatSize |
| | | * @grammar Base.formatSize( size ) => String |
| | | * @grammar Base.formatSize( size, pointLength ) => String |
| | | * @grammar Base.formatSize( size, pointLength, units ) => String |
| | | * @param {Number} size 文件大小 |
| | | * @param {Number} [pointLength=2] 精确到的小数点数。 |
| | | * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. |
| | | * @example |
| | | * console.log( Base.formatSize( 100 ) ); // => 100B |
| | | * console.log( Base.formatSize( 1024 ) ); // => 1.00K |
| | | * console.log( Base.formatSize( 1024, 0 ) ); // => 1K |
| | | * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M |
| | | * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G |
| | | * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB |
| | | */ |
| | | formatSize: function( size, pointLength, units ) { |
| | | var unit; |
| | | |
| | | units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; |
| | | |
| | | while ( (unit = units.shift()) && size > 1024 ) { |
| | | size = size / 1024; |
| | | } |
| | | |
| | | return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + |
| | | unit; |
| | | } |
| | | }; |
| | | }); |
| | | /** |
| | | * 事件处理类,可以独立使用,也可以扩展给对象使用。 |
| | | * @fileOverview Mediator |
| | | */ |
| | | define('mediator',[ |
| | | 'base' |
| | | ], function( Base ) { |
| | | var $ = Base.$, |
| | | slice = [].slice, |
| | | separator = /\s+/, |
| | | protos; |
| | | |
| | | // 根据条件过滤出事件handlers. |
| | | function findHandlers( arr, name, callback, context ) { |
| | | return $.grep( arr, function( handler ) { |
| | | return handler && |
| | | (!name || handler.e === name) && |
| | | (!callback || handler.cb === callback || |
| | | handler.cb._cb === callback) && |
| | | (!context || handler.ctx === context); |
| | | }); |
| | | } |
| | | |
| | | function eachEvent( events, callback, iterator ) { |
| | | // 不支持对象,只支持多个event用空格隔开 |
| | | $.each( (events || '').split( separator ), function( _, key ) { |
| | | iterator( key, callback ); |
| | | }); |
| | | } |
| | | |
| | | function triggerHanders( events, args ) { |
| | | var stoped = false, |
| | | i = -1, |
| | | len = events.length, |
| | | handler; |
| | | |
| | | while ( ++i < len ) { |
| | | handler = events[ i ]; |
| | | |
| | | if ( handler.cb.apply( handler.ctx2, args ) === false ) { |
| | | stoped = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | return !stoped; |
| | | } |
| | | |
| | | protos = { |
| | | |
| | | /** |
| | | * 绑定事件。 |
| | | * |
| | | * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 |
| | | * ```javascript |
| | | * var obj = {}; |
| | | * |
| | | * // 使得obj有事件行为 |
| | | * Mediator.installTo( obj ); |
| | | * |
| | | * obj.on( 'testa', function( arg1, arg2 ) { |
| | | * console.log( arg1, arg2 ); // => 'arg1', 'arg2' |
| | | * }); |
| | | * |
| | | * obj.trigger( 'testa', 'arg1', 'arg2' ); |
| | | * ``` |
| | | * |
| | | * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 |
| | | * 切会影响到`trigger`方法的返回值,为`false`。 |
| | | * |
| | | * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, |
| | | * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 |
| | | * ```javascript |
| | | * obj.on( 'all', function( type, arg1, arg2 ) { |
| | | * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' |
| | | * }); |
| | | * ``` |
| | | * |
| | | * @method on |
| | | * @grammar on( name, callback[, context] ) => self |
| | | * @param {String} name 事件名,支持多个事件用空格隔开 |
| | | * @param {Function} callback 事件处理器 |
| | | * @param {Object} [context] 事件处理器的上下文。 |
| | | * @return {self} 返回自身,方便链式 |
| | | * @chainable |
| | | * @class Mediator |
| | | */ |
| | | on: function( name, callback, context ) { |
| | | var me = this, |
| | | set; |
| | | |
| | | if ( !callback ) { |
| | | return this; |
| | | } |
| | | |
| | | set = this._events || (this._events = []); |
| | | |
| | | eachEvent( name, callback, function( name, callback ) { |
| | | var handler = { e: name }; |
| | | |
| | | handler.cb = callback; |
| | | handler.ctx = context; |
| | | handler.ctx2 = context || me; |
| | | handler.id = set.length; |
| | | |
| | | set.push( handler ); |
| | | }); |
| | | |
| | | return this; |
| | | }, |
| | | |
| | | /** |
| | | * 绑定事件,且当handler执行完后,自动解除绑定。 |
| | | * @method once |
| | | * @grammar once( name, callback[, context] ) => self |
| | | * @param {String} name 事件名 |
| | | * @param {Function} callback 事件处理器 |
| | | * @param {Object} [context] 事件处理器的上下文。 |
| | | * @return {self} 返回自身,方便链式 |
| | | * @chainable |
| | | */ |
| | | once: function( name, callback, context ) { |
| | | var me = this; |
| | | |
| | | if ( !callback ) { |
| | | return me; |
| | | } |
| | | |
| | | eachEvent( name, callback, function( name, callback ) { |
| | | var once = function() { |
| | | me.off( name, once ); |
| | | return callback.apply( context || me, arguments ); |
| | | }; |
| | | |
| | | once._cb = callback; |
| | | me.on( name, once, context ); |
| | | }); |
| | | |
| | | return me; |
| | | }, |
| | | |
| | | /** |
| | | * 解除事件绑定 |
| | | * @method off |
| | | * @grammar off( [name[, callback[, context] ] ] ) => self |
| | | * @param {String} [name] 事件名 |
| | | * @param {Function} [callback] 事件处理器 |
| | | * @param {Object} [context] 事件处理器的上下文。 |
| | | * @return {self} 返回自身,方便链式 |
| | | * @chainable |
| | | */ |
| | | off: function( name, cb, ctx ) { |
| | | var events = this._events; |
| | | |
| | | if ( !events ) { |
| | | return this; |
| | | } |
| | | |
| | | if ( !name && !cb && !ctx ) { |
| | | this._events = []; |
| | | return this; |
| | | } |
| | | |
| | | eachEvent( name, cb, function( name, cb ) { |
| | | $.each( findHandlers( events, name, cb, ctx ), function() { |
| | | delete events[ this.id ]; |
| | | }); |
| | | }); |
| | | |
| | | return this; |
| | | }, |
| | | |
| | | /** |
| | | * 触发事件 |
| | | * @method trigger |
| | | * @grammar trigger( name[, args...] ) => self |
| | | * @param {String} type 事件名 |
| | | * @param {*} [...] 任意参数 |
| | | * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true |
| | | */ |
| | | trigger: function( type ) { |
| | | var args, events, allEvents; |
| | | |
| | | if ( !this._events || !type ) { |
| | | return this; |
| | | } |
| | | |
| | | args = slice.call( arguments, 1 ); |
| | | events = findHandlers( this._events, type ); |
| | | allEvents = findHandlers( this._events, 'all' ); |
| | | |
| | | return triggerHanders( events, args ) && |
| | | triggerHanders( allEvents, arguments ); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 |
| | | * 主要目的是负责模块与模块之间的合作,降低耦合度。 |
| | | * |
| | | * @class Mediator |
| | | */ |
| | | return $.extend({ |
| | | |
| | | /** |
| | | * 可以通过这个接口,使任何对象具备事件功能。 |
| | | * @method installTo |
| | | * @param {Object} obj 需要具备事件行为的对象。 |
| | | * @return {Object} 返回obj. |
| | | */ |
| | | installTo: function( obj ) { |
| | | return $.extend( obj, protos ); |
| | | } |
| | | |
| | | }, protos ); |
| | | }); |
| | | /** |
| | | * @fileOverview Uploader上传类 |
| | | */ |
| | | define('uploader',[ |
| | | 'base', |
| | | 'mediator' |
| | | ], function( Base, Mediator ) { |
| | | |
| | | var $ = Base.$; |
| | | |
| | | /** |
| | | * 上传入口类。 |
| | | * @class Uploader |
| | | * @constructor |
| | | * @grammar new Uploader( opts ) => Uploader |
| | | * @example |
| | | * var uploader = WebUploader.Uploader({ |
| | | * swf: 'path_of_swf/Uploader.swf', |
| | | * |
| | | * // 开起分片上传。 |
| | | * chunked: true |
| | | * }); |
| | | */ |
| | | function Uploader( opts ) { |
| | | this.options = $.extend( true, {}, Uploader.options, opts ); |
| | | this._init( this.options ); |
| | | } |
| | | |
| | | // default Options |
| | | // widgets中有相应扩展 |
| | | Uploader.options = {}; |
| | | Mediator.installTo( Uploader.prototype ); |
| | | |
| | | // 批量添加纯命令式方法。 |
| | | $.each({ |
| | | upload: 'start-upload', |
| | | stop: 'stop-upload', |
| | | getFile: 'get-file', |
| | | getFiles: 'get-files', |
| | | addFile: 'add-file', |
| | | addFiles: 'add-file', |
| | | sort: 'sort-files', |
| | | removeFile: 'remove-file', |
| | | skipFile: 'skip-file', |
| | | retry: 'retry', |
| | | isInProgress: 'is-in-progress', |
| | | makeThumb: 'make-thumb', |
| | | getDimension: 'get-dimension', |
| | | addButton: 'add-btn', |
| | | getRuntimeType: 'get-runtime-type', |
| | | refresh: 'refresh', |
| | | disable: 'disable', |
| | | enable: 'enable', |
| | | reset: 'reset' |
| | | }, function( fn, command ) { |
| | | Uploader.prototype[ fn ] = function() { |
| | | return this.request( command, arguments ); |
| | | }; |
| | | }); |
| | | |
| | | $.extend( Uploader.prototype, { |
| | | state: 'pending', |
| | | |
| | | _init: function( opts ) { |
| | | var me = this; |
| | | |
| | | me.request( 'init', opts, function() { |
| | | me.state = 'ready'; |
| | | me.trigger('ready'); |
| | | }); |
| | | }, |
| | | |
| | | /** |
| | | * 获取或者设置Uploader配置项。 |
| | | * @method option |
| | | * @grammar option( key ) => * |
| | | * @grammar option( key, val ) => self |
| | | * @example |
| | | * |
| | | * // 初始状态图片上传前不会压缩 |
| | | * var uploader = new WebUploader.Uploader({ |
| | | * resize: null; |
| | | * }); |
| | | * |
| | | * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 |
| | | * uploader.options( 'resize', { |
| | | * width: 1600, |
| | | * height: 1600 |
| | | * }); |
| | | */ |
| | | option: function( key, val ) { |
| | | var opts = this.options; |
| | | |
| | | // setter |
| | | if ( arguments.length > 1 ) { |
| | | |
| | | if ( $.isPlainObject( val ) && |
| | | $.isPlainObject( opts[ key ] ) ) { |
| | | $.extend( opts[ key ], val ); |
| | | } else { |
| | | opts[ key ] = val; |
| | | } |
| | | |
| | | } else { // getter |
| | | return key ? opts[ key ] : opts; |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * 获取文件统计信息。返回一个包含一下信息的对象。 |
| | | * * `successNum` 上传成功的文件数 |
| | | * * `uploadFailNum` 上传失败的文件数 |
| | | * * `cancelNum` 被删除的文件数 |
| | | * * `invalidNum` 无效的文件数 |
| | | * * `queueNum` 还在队列中的文件数 |
| | | * @method getStats |
| | | * @grammar getStats() => Object |
| | | */ |
| | | getStats: function() { |
| | | // return this._mgr.getStats.apply( this._mgr, arguments ); |
| | | var stats = this.request('get-stats'); |
| | | |
| | | return { |
| | | successNum: stats.numOfSuccess, |
| | | |
| | | // who care? |
| | | // queueFailNum: 0, |
| | | cancelNum: stats.numOfCancel, |
| | | invalidNum: stats.numOfInvalid, |
| | | uploadFailNum: stats.numOfUploadFailed, |
| | | queueNum: stats.numOfQueue |
| | | }; |
| | | }, |
| | | |
| | | // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 |
| | | trigger: function( type/*, args...*/ ) { |
| | | var args = [].slice.call( arguments, 1 ), |
| | | opts = this.options, |
| | | name = 'on' + type.substring( 0, 1 ).toUpperCase() + |
| | | type.substring( 1 ); |
| | | |
| | | if ( |
| | | // 调用通过on方法注册的handler. |
| | | Mediator.trigger.apply( this, arguments ) === false || |
| | | |
| | | // 调用opts.onEvent |
| | | $.isFunction( opts[ name ] ) && |
| | | opts[ name ].apply( this, args ) === false || |
| | | |
| | | // 调用this.onEvent |
| | | $.isFunction( this[ name ] ) && |
| | | this[ name ].apply( this, args ) === false || |
| | | |
| | | // 广播所有uploader的事件。 |
| | | Mediator.trigger.apply( Mediator, |
| | | [ this, type ].concat( args ) ) === false ) { |
| | | |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }, |
| | | |
| | | // widgets/widget.js将补充此方法的详细文档。 |
| | | request: Base.noop |
| | | }); |
| | | |
| | | /** |
| | | * 创建Uploader实例,等同于new Uploader( opts ); |
| | | * @method create |
| | | * @class Base |
| | | * @static |
| | | * @grammar Base.create( opts ) => Uploader |
| | | */ |
| | | Base.create = Uploader.create = function( opts ) { |
| | | return new Uploader( opts ); |
| | | }; |
| | | |
| | | // 暴露Uploader,可以通过它来扩展业务逻辑。 |
| | | Base.Uploader = Uploader; |
| | | |
| | | return Uploader; |
| | | }); |
| | | /** |
| | | * @fileOverview Runtime管理器,负责Runtime的选择, 连接 |
| | | */ |
| | | define('runtime/runtime',[ |
| | | 'base', |
| | | 'mediator' |
| | | ], function( Base, Mediator ) { |
| | | |
| | | var $ = Base.$, |
| | | factories = {}, |
| | | |
| | | // 获取对象的第一个key |
| | | getFirstKey = function( obj ) { |
| | | for ( var key in obj ) { |
| | | if ( obj.hasOwnProperty( key ) ) { |
| | | return key; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | // 接口类。 |
| | | function Runtime( options ) { |
| | | this.options = $.extend({ |
| | | container: document.body |
| | | }, options ); |
| | | this.uid = Base.guid('rt_'); |
| | | } |
| | | |
| | | $.extend( Runtime.prototype, { |
| | | |
| | | getContainer: function() { |
| | | var opts = this.options, |
| | | parent, container; |
| | | |
| | | if ( this._container ) { |
| | | return this._container; |
| | | } |
| | | |
| | | parent = $( opts.container || document.body ); |
| | | container = $( document.createElement('div') ); |
| | | |
| | | container.attr( 'id', 'rt_' + this.uid ); |
| | | container.css({ |
| | | position: 'absolute', |
| | | top: '0px', |
| | | left: '0px', |
| | | width: '1px', |
| | | height: '1px', |
| | | overflow: 'hidden' |
| | | }); |
| | | |
| | | parent.append( container ); |
| | | parent.addClass('webuploader-container'); |
| | | this._container = container; |
| | | return container; |
| | | }, |
| | | |
| | | init: Base.noop, |
| | | exec: Base.noop, |
| | | |
| | | destroy: function() { |
| | | if ( this._container ) { |
| | | this._container.parentNode.removeChild( this.__container ); |
| | | } |
| | | |
| | | this.off(); |
| | | } |
| | | }); |
| | | |
| | | Runtime.orders = 'html5,flash'; |
| | | |
| | | |
| | | /** |
| | | * 添加Runtime实现。 |
| | | * @param {String} type 类型 |
| | | * @param {Runtime} factory 具体Runtime实现。 |
| | | */ |
| | | Runtime.addRuntime = function( type, factory ) { |
| | | factories[ type ] = factory; |
| | | }; |
| | | |
| | | Runtime.hasRuntime = function( type ) { |
| | | return !!(type ? factories[ type ] : getFirstKey( factories )); |
| | | }; |
| | | |
| | | Runtime.create = function( opts, orders ) { |
| | | var type, runtime; |
| | | |
| | | orders = orders || Runtime.orders; |
| | | $.each( orders.split( /\s*,\s*/g ), function() { |
| | | if ( factories[ this ] ) { |
| | | type = this; |
| | | return false; |
| | | } |
| | | }); |
| | | |
| | | type = type || getFirstKey( factories ); |
| | | |
| | | if ( !type ) { |
| | | throw new Error('Runtime Error'); |
| | | } |
| | | |
| | | runtime = new factories[ type ]( opts ); |
| | | return runtime; |
| | | }; |
| | | |
| | | Mediator.installTo( Runtime.prototype ); |
| | | return Runtime; |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview Runtime管理器,负责Runtime的选择, 连接 |
| | | */ |
| | | define('runtime/client',[ |
| | | 'base', |
| | | 'mediator', |
| | | 'runtime/runtime' |
| | | ], function( Base, Mediator, Runtime ) { |
| | | |
| | | var cache; |
| | | |
| | | cache = (function() { |
| | | var obj = {}; |
| | | |
| | | return { |
| | | add: function( runtime ) { |
| | | obj[ runtime.uid ] = runtime; |
| | | }, |
| | | |
| | | get: function( ruid, standalone ) { |
| | | var i; |
| | | |
| | | if ( ruid ) { |
| | | return obj[ ruid ]; |
| | | } |
| | | |
| | | for ( i in obj ) { |
| | | // 有些类型不能重用,比如filepicker. |
| | | if ( standalone && obj[ i ].__standalone ) { |
| | | continue; |
| | | } |
| | | |
| | | return obj[ i ]; |
| | | } |
| | | |
| | | return null; |
| | | }, |
| | | |
| | | remove: function( runtime ) { |
| | | delete obj[ runtime.uid ]; |
| | | } |
| | | }; |
| | | })(); |
| | | |
| | | function RuntimeClient( component, standalone ) { |
| | | var deferred = Base.Deferred(), |
| | | runtime; |
| | | |
| | | this.uid = Base.guid('client_'); |
| | | |
| | | // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 |
| | | this.runtimeReady = function( cb ) { |
| | | return deferred.done( cb ); |
| | | }; |
| | | |
| | | this.connectRuntime = function( opts, cb ) { |
| | | |
| | | // already connected. |
| | | if ( runtime ) { |
| | | throw new Error('already connected!'); |
| | | } |
| | | |
| | | deferred.done( cb ); |
| | | |
| | | if ( typeof opts === 'string' && cache.get( opts ) ) { |
| | | runtime = cache.get( opts ); |
| | | } |
| | | |
| | | // 像filePicker只能独立存在,不能公用。 |
| | | runtime = runtime || cache.get( null, standalone ); |
| | | |
| | | // 需要创建 |
| | | if ( !runtime ) { |
| | | runtime = Runtime.create( opts, opts.runtimeOrder ); |
| | | runtime.__promise = deferred.promise(); |
| | | runtime.once( 'ready', deferred.resolve ); |
| | | runtime.init(); |
| | | cache.add( runtime ); |
| | | runtime.__client = 1; |
| | | } else { |
| | | // 来自cache |
| | | Base.$.extend( runtime.options, opts ); |
| | | runtime.__promise.then( deferred.resolve ); |
| | | runtime.__client++; |
| | | } |
| | | |
| | | standalone && (runtime.__standalone = standalone); |
| | | return runtime; |
| | | }; |
| | | |
| | | this.getRuntime = function() { |
| | | return runtime; |
| | | }; |
| | | |
| | | this.disconnectRuntime = function() { |
| | | if ( !runtime ) { |
| | | return; |
| | | } |
| | | |
| | | runtime.__client--; |
| | | |
| | | if ( runtime.__client <= 0 ) { |
| | | cache.remove( runtime ); |
| | | delete runtime.__promise; |
| | | runtime.destroy(); |
| | | } |
| | | |
| | | runtime = null; |
| | | }; |
| | | |
| | | this.exec = function() { |
| | | if ( !runtime ) { |
| | | return; |
| | | } |
| | | |
| | | var args = Base.slice( arguments ); |
| | | component && args.unshift( component ); |
| | | |
| | | return runtime.exec.apply( this, args ); |
| | | }; |
| | | |
| | | this.getRuid = function() { |
| | | return runtime && runtime.uid; |
| | | }; |
| | | |
| | | this.destroy = (function( destroy ) { |
| | | return function() { |
| | | destroy && destroy.apply( this, arguments ); |
| | | this.trigger('destroy'); |
| | | this.off(); |
| | | this.exec('destroy'); |
| | | this.disconnectRuntime(); |
| | | }; |
| | | })( this.destroy ); |
| | | } |
| | | |
| | | Mediator.installTo( RuntimeClient.prototype ); |
| | | return RuntimeClient; |
| | | }); |
| | | /** |
| | | * @fileOverview 错误信息 |
| | | */ |
| | | define('lib/dnd',[ |
| | | 'base', |
| | | 'mediator', |
| | | 'runtime/client' |
| | | ], function( Base, Mediator, RuntimeClent ) { |
| | | |
| | | var $ = Base.$; |
| | | |
| | | function DragAndDrop( opts ) { |
| | | opts = this.options = $.extend({}, DragAndDrop.options, opts ); |
| | | |
| | | opts.container = $( opts.container ); |
| | | |
| | | if ( !opts.container.length ) { |
| | | return; |
| | | } |
| | | |
| | | RuntimeClent.call( this, 'DragAndDrop' ); |
| | | } |
| | | |
| | | DragAndDrop.options = { |
| | | accept: null, |
| | | disableGlobalDnd: false |
| | | }; |
| | | |
| | | Base.inherits( RuntimeClent, { |
| | | constructor: DragAndDrop, |
| | | |
| | | init: function() { |
| | | var me = this; |
| | | |
| | | me.connectRuntime( me.options, function() { |
| | | me.exec('init'); |
| | | me.trigger('ready'); |
| | | }); |
| | | }, |
| | | |
| | | destroy: function() { |
| | | this.disconnectRuntime(); |
| | | } |
| | | }); |
| | | |
| | | Mediator.installTo( DragAndDrop.prototype ); |
| | | |
| | | return DragAndDrop; |
| | | }); |
| | | /** |
| | | * @fileOverview 组件基类。 |
| | | */ |
| | | define('widgets/widget',[ |
| | | 'base', |
| | | 'uploader' |
| | | ], function( Base, Uploader ) { |
| | | |
| | | var $ = Base.$, |
| | | _init = Uploader.prototype._init, |
| | | IGNORE = {}, |
| | | widgetClass = []; |
| | | |
| | | function isArrayLike( obj ) { |
| | | if ( !obj ) { |
| | | return false; |
| | | } |
| | | |
| | | var length = obj.length, |
| | | type = $.type( obj ); |
| | | |
| | | if ( obj.nodeType === 1 && length ) { |
| | | return true; |
| | | } |
| | | |
| | | return type === 'array' || type !== 'function' && type !== 'string' && |
| | | (length === 0 || typeof length === 'number' && length > 0 && |
| | | (length - 1) in obj); |
| | | } |
| | | |
| | | function Widget( uploader ) { |
| | | this.owner = uploader; |
| | | this.options = uploader.options; |
| | | } |
| | | |
| | | $.extend( Widget.prototype, { |
| | | |
| | | init: Base.noop, |
| | | |
| | | // 类Backbone的事件监听声明,监听uploader实例上的事件 |
| | | // widget直接无法监听事件,事件只能通过uploader来传递 |
| | | invoke: function( apiName, args ) { |
| | | |
| | | /* |
| | | { |
| | | 'make-thumb': 'makeThumb' |
| | | } |
| | | */ |
| | | var map = this.responseMap; |
| | | |
| | | // 如果无API响应声明则忽略 |
| | | if ( !map || !(apiName in map) || !(map[ apiName ] in this) || |
| | | !$.isFunction( this[ map[ apiName ] ] ) ) { |
| | | |
| | | return IGNORE; |
| | | } |
| | | |
| | | return this[ map[ apiName ] ].apply( this, args ); |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 |
| | | * @method request |
| | | * @grammar request( command, args ) => * | Promise |
| | | * @grammar request( command, args, callback ) => Promise |
| | | * @for Uploader |
| | | */ |
| | | request: function() { |
| | | return this.owner.request.apply( this.owner, arguments ); |
| | | } |
| | | }); |
| | | |
| | | // 扩展Uploader. |
| | | $.extend( Uploader.prototype, { |
| | | |
| | | // 覆写_init用来初始化widgets |
| | | _init: function() { |
| | | var me = this, |
| | | widgets = me._widgets = []; |
| | | |
| | | $.each( widgetClass, function( _, klass ) { |
| | | widgets.push( new klass( me ) ); |
| | | }); |
| | | |
| | | return _init.apply( me, arguments ); |
| | | }, |
| | | |
| | | request: function( apiName, args, callback ) { |
| | | var i = 0, |
| | | widgets = this._widgets, |
| | | len = widgets.length, |
| | | rlts = [], |
| | | dfds = [], |
| | | widget, rlt, promise, key; |
| | | |
| | | args = isArrayLike( args ) ? args : [ args ]; |
| | | |
| | | for ( ; i < len; i++ ) { |
| | | widget = widgets[ i ]; |
| | | rlt = widget.invoke( apiName, args ); |
| | | |
| | | if ( rlt !== IGNORE ) { |
| | | |
| | | // Deferred对象 |
| | | if ( Base.isPromise( rlt ) ) { |
| | | dfds.push( rlt ); |
| | | } else { |
| | | rlts.push( rlt ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 如果有callback,则用异步方式。 |
| | | if ( callback || dfds.length ) { |
| | | promise = Base.when.apply( Base, dfds ); |
| | | key = promise.pipe ? 'pipe' : 'then'; |
| | | |
| | | // 很重要不能删除。删除了会死循环。 |
| | | // 保证执行顺序。让callback总是在下一个tick中执行。 |
| | | return promise[ key ](function() { |
| | | var deferred = Base.Deferred(), |
| | | args = arguments; |
| | | |
| | | setTimeout(function() { |
| | | deferred.resolve.apply( deferred, args ); |
| | | }, 1 ); |
| | | |
| | | return deferred.promise(); |
| | | })[ key ]( callback || Base.noop ); |
| | | } else { |
| | | return rlts[ 0 ]; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * 添加组件 |
| | | * @param {object} widgetProto 组件原型,构造函数通过constructor属性定义 |
| | | * @param {object} responseMap API名称与函数实现的映射 |
| | | * @example |
| | | * Uploader.register( { |
| | | * init: function( options ) {}, |
| | | * makeThumb: function() {} |
| | | * }, { |
| | | * 'make-thumb': 'makeThumb' |
| | | * } ); |
| | | */ |
| | | Uploader.register = Widget.register = function( responseMap, widgetProto ) { |
| | | var map = { init: 'init' }, |
| | | klass; |
| | | |
| | | if ( arguments.length === 1 ) { |
| | | widgetProto = responseMap; |
| | | widgetProto.responseMap = map; |
| | | } else { |
| | | widgetProto.responseMap = $.extend( map, responseMap ); |
| | | } |
| | | |
| | | klass = Base.inherits( Widget, widgetProto ); |
| | | widgetClass.push( klass ); |
| | | |
| | | return klass; |
| | | }; |
| | | |
| | | return Widget; |
| | | }); |
| | | /** |
| | | * @fileOverview DragAndDrop Widget。 |
| | | */ |
| | | define('widgets/filednd',[ |
| | | 'base', |
| | | 'uploader', |
| | | 'lib/dnd', |
| | | 'widgets/widget' |
| | | ], function( Base, Uploader, Dnd ) { |
| | | var $ = Base.$; |
| | | |
| | | Uploader.options.dnd = ''; |
| | | |
| | | /** |
| | | * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 |
| | | * @namespace options |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @event dndAccept |
| | | * @param {DataTransferItemList} items DataTransferItem |
| | | * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 |
| | | * @for Uploader |
| | | */ |
| | | return Uploader.register({ |
| | | init: function( opts ) { |
| | | |
| | | if ( !opts.dnd || |
| | | this.request('predict-runtime-type') !== 'html5' ) { |
| | | return; |
| | | } |
| | | |
| | | var me = this, |
| | | deferred = Base.Deferred(), |
| | | options = $.extend({}, { |
| | | disableGlobalDnd: opts.disableGlobalDnd, |
| | | container: opts.dnd, |
| | | accept: opts.accept |
| | | }), |
| | | dnd; |
| | | |
| | | dnd = new Dnd( options ); |
| | | |
| | | dnd.once( 'ready', deferred.resolve ); |
| | | dnd.on( 'drop', function( files ) { |
| | | me.request( 'add-file', [ files ]); |
| | | }); |
| | | |
| | | // 检测文件是否全部允许添加。 |
| | | dnd.on( 'accept', function( items ) { |
| | | return me.owner.trigger( 'dndAccept', items ); |
| | | }); |
| | | |
| | | dnd.init(); |
| | | |
| | | return deferred.promise(); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview 错误信息 |
| | | */ |
| | | define('lib/filepaste',[ |
| | | 'base', |
| | | 'mediator', |
| | | 'runtime/client' |
| | | ], function( Base, Mediator, RuntimeClent ) { |
| | | |
| | | var $ = Base.$; |
| | | |
| | | function FilePaste( opts ) { |
| | | opts = this.options = $.extend({}, opts ); |
| | | opts.container = $( opts.container || document.body ); |
| | | RuntimeClent.call( this, 'FilePaste' ); |
| | | } |
| | | |
| | | Base.inherits( RuntimeClent, { |
| | | constructor: FilePaste, |
| | | |
| | | init: function() { |
| | | var me = this; |
| | | |
| | | me.connectRuntime( me.options, function() { |
| | | me.exec('init'); |
| | | me.trigger('ready'); |
| | | }); |
| | | }, |
| | | |
| | | destroy: function() { |
| | | this.exec('destroy'); |
| | | this.disconnectRuntime(); |
| | | this.off(); |
| | | } |
| | | }); |
| | | |
| | | Mediator.installTo( FilePaste.prototype ); |
| | | |
| | | return FilePaste; |
| | | }); |
| | | /** |
| | | * @fileOverview 组件基类。 |
| | | */ |
| | | define('widgets/filepaste',[ |
| | | 'base', |
| | | 'uploader', |
| | | 'lib/filepaste', |
| | | 'widgets/widget' |
| | | ], function( Base, Uploader, FilePaste ) { |
| | | var $ = Base.$; |
| | | |
| | | /** |
| | | * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. |
| | | * @namespace options |
| | | * @for Uploader |
| | | */ |
| | | return Uploader.register({ |
| | | init: function( opts ) { |
| | | |
| | | if ( !opts.paste || |
| | | this.request('predict-runtime-type') !== 'html5' ) { |
| | | return; |
| | | } |
| | | |
| | | var me = this, |
| | | deferred = Base.Deferred(), |
| | | options = $.extend({}, { |
| | | container: opts.paste, |
| | | accept: opts.accept |
| | | }), |
| | | paste; |
| | | |
| | | paste = new FilePaste( options ); |
| | | |
| | | paste.once( 'ready', deferred.resolve ); |
| | | paste.on( 'paste', function( files ) { |
| | | me.owner.request( 'add-file', [ files ]); |
| | | }); |
| | | paste.init(); |
| | | |
| | | return deferred.promise(); |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview Blob |
| | | */ |
| | | define('lib/blob',[ |
| | | 'base', |
| | | 'runtime/client' |
| | | ], function( Base, RuntimeClient ) { |
| | | |
| | | function Blob( ruid, source ) { |
| | | var me = this; |
| | | |
| | | me.source = source; |
| | | me.ruid = ruid; |
| | | |
| | | RuntimeClient.call( me, 'Blob' ); |
| | | |
| | | this.uid = source.uid || this.uid; |
| | | this.type = source.type || ''; |
| | | this.size = source.size || 0; |
| | | |
| | | if ( ruid ) { |
| | | me.connectRuntime( ruid ); |
| | | } |
| | | } |
| | | |
| | | Base.inherits( RuntimeClient, { |
| | | constructor: Blob, |
| | | |
| | | slice: function( start, end ) { |
| | | return this.exec( 'slice', start, end ); |
| | | }, |
| | | |
| | | getSource: function() { |
| | | return this.source; |
| | | } |
| | | }); |
| | | |
| | | return Blob; |
| | | }); |
| | | /** |
| | | * 为了统一化Flash的File和HTML5的File而存在。 |
| | | * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 |
| | | * @fileOverview File |
| | | */ |
| | | define('lib/file',[ |
| | | 'base', |
| | | 'lib/blob' |
| | | ], function( Base, Blob ) { |
| | | |
| | | var uid = 1, |
| | | rExt = /\.([^.]+)$/; |
| | | |
| | | function File( ruid, file ) { |
| | | var ext; |
| | | |
| | | Blob.apply( this, arguments ); |
| | | this.name = file.name || ('untitled' + uid++); |
| | | ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; |
| | | |
| | | // todo 支持其他类型文件的转换。 |
| | | |
| | | // 如果有mimetype, 但是文件名里面没有找出后缀规律 |
| | | if ( !ext && this.type ) { |
| | | ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( this.type ) ? |
| | | RegExp.$1.toLowerCase() : ''; |
| | | this.name += '.' + ext; |
| | | } |
| | | |
| | | // 如果没有指定mimetype, 但是知道文件后缀。 |
| | | if ( !this.type && ~'jpg,jpeg,png,gif,bmp'.indexOf( ext ) ) { |
| | | this.type = 'image/' + (ext === 'jpg' ? 'jpeg' : ext); |
| | | } |
| | | |
| | | this.ext = ext; |
| | | this.lastModifiedDate = file.lastModifiedDate || |
| | | (new Date()).toLocaleString(); |
| | | } |
| | | |
| | | return Base.inherits( Blob, File ); |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview 错误信息 |
| | | */ |
| | | define('lib/filepicker',[ |
| | | 'base', |
| | | 'runtime/client', |
| | | 'lib/file' |
| | | ], function( Base, RuntimeClent, File ) { |
| | | |
| | | var $ = Base.$; |
| | | |
| | | function FilePicker( opts ) { |
| | | opts = this.options = $.extend({}, FilePicker.options, opts ); |
| | | |
| | | opts.container = $( opts.id ); |
| | | |
| | | if ( !opts.container.length ) { |
| | | throw new Error('按钮指定错误'); |
| | | } |
| | | |
| | | opts.innerHTML = opts.innerHTML || opts.label || |
| | | opts.container.html() || ''; |
| | | |
| | | opts.button = $( opts.button || document.createElement('div') ); |
| | | opts.button.html( opts.innerHTML ); |
| | | opts.container.html( opts.button ); |
| | | |
| | | RuntimeClent.call( this, 'FilePicker', true ); |
| | | } |
| | | |
| | | FilePicker.options = { |
| | | button: null, |
| | | container: null, |
| | | label: null, |
| | | innerHTML: null, |
| | | multiple: true, |
| | | accept: null, |
| | | name: 'file' |
| | | }; |
| | | |
| | | Base.inherits( RuntimeClent, { |
| | | constructor: FilePicker, |
| | | |
| | | init: function() { |
| | | var me = this, |
| | | opts = me.options, |
| | | button = opts.button; |
| | | |
| | | button.addClass('webuploader-pick'); |
| | | |
| | | me.on( 'all', function( type ) { |
| | | var files; |
| | | |
| | | switch ( type ) { |
| | | case 'mouseenter': |
| | | button.addClass('webuploader-pick-hover'); |
| | | break; |
| | | |
| | | case 'mouseleave': |
| | | button.removeClass('webuploader-pick-hover'); |
| | | break; |
| | | |
| | | case 'change': |
| | | files = me.exec('getFiles'); |
| | | me.trigger( 'select', $.map( files, function( file ) { |
| | | file = new File( me.getRuid(), file ); |
| | | |
| | | // 记录来源。 |
| | | file._refer = opts.container; |
| | | return file; |
| | | }), opts.container ); |
| | | break; |
| | | } |
| | | }); |
| | | |
| | | me.connectRuntime( opts, function() { |
| | | me.refresh(); |
| | | me.exec( 'init', opts ); |
| | | me.trigger('ready'); |
| | | }); |
| | | |
| | | $( window ).on( 'resize', function() { |
| | | me.refresh(); |
| | | }); |
| | | }, |
| | | |
| | | refresh: function() { |
| | | var shimContainer = this.getRuntime().getContainer(), |
| | | button = this.options.button, |
| | | width = button.outerWidth ? |
| | | button.outerWidth() : button.width(), |
| | | |
| | | height = button.outerHeight ? |
| | | button.outerHeight() : button.height(), |
| | | |
| | | pos = button.offset(); |
| | | |
| | | width && height && shimContainer.css({ |
| | | bottom: 'auto', |
| | | right: 'auto', |
| | | width: width + 'px', |
| | | height: height + 'px' |
| | | }).offset( pos ); |
| | | }, |
| | | |
| | | enable: function() { |
| | | var btn = this.options.button; |
| | | |
| | | btn.removeClass('webuploader-pick-disable'); |
| | | this.refresh(); |
| | | }, |
| | | |
| | | disable: function() { |
| | | var btn = this.options.button; |
| | | |
| | | this.getRuntime().getContainer().css({ |
| | | top: '-99999px' |
| | | }); |
| | | |
| | | btn.addClass('webuploader-pick-disable'); |
| | | }, |
| | | |
| | | destroy: function() { |
| | | if ( this.runtime ) { |
| | | this.exec('destroy'); |
| | | this.disconnectRuntime(); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | return FilePicker; |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview 文件选择相关 |
| | | */ |
| | | define('widgets/filepicker',[ |
| | | 'base', |
| | | 'uploader', |
| | | 'lib/filepicker', |
| | | 'widgets/widget' |
| | | ], function( Base, Uploader, FilePicker ) { |
| | | var $ = Base.$; |
| | | |
| | | $.extend( Uploader.options, { |
| | | |
| | | /** |
| | | * @property {Selector | Object} [pick=undefined] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 指定选择文件的按钮容器,不指定则不创建按钮。 |
| | | * |
| | | * * `id` {Seletor} 指定选择文件的按钮容器,不指定则不创建按钮。 |
| | | * * `label` {String} 请采用 `innerHTML` 代替 |
| | | * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 |
| | | * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 |
| | | */ |
| | | pick: null, |
| | | |
| | | /** |
| | | * @property {Arroy} [accept=null] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 |
| | | * |
| | | * * `title` {String} 文字描述 |
| | | * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 |
| | | * * `mimeTypes` {String} 多个用逗号分割。 |
| | | * |
| | | * 如: |
| | | * |
| | | * ``` |
| | | * { |
| | | * title: 'Images', |
| | | * extensions: 'gif,jpg,jpeg,bmp,png', |
| | | * mimeTypes: 'image/*' |
| | | * } |
| | | * ``` |
| | | */ |
| | | accept: null/*{ |
| | | title: 'Images', |
| | | extensions: 'gif,jpg,jpeg,bmp,png', |
| | | mimeTypes: 'image/*' |
| | | }*/ |
| | | }); |
| | | |
| | | return Uploader.register({ |
| | | 'add-btn': 'addButton', |
| | | refresh: 'refresh', |
| | | disable: 'disable', |
| | | enable: 'enable' |
| | | }, { |
| | | |
| | | init: function( opts ) { |
| | | this.pickers = []; |
| | | return opts.pick && this.addButton( opts.pick ); |
| | | }, |
| | | |
| | | refresh: function() { |
| | | $.each( this.pickers, function() { |
| | | this.refresh(); |
| | | }); |
| | | }, |
| | | |
| | | /** |
| | | * @method addButton |
| | | * @for Uploader |
| | | * @grammar addButton( pick ) => Promise |
| | | * @description |
| | | * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 |
| | | * @example |
| | | * uploader.addButton({ |
| | | * id: '#btnContainer', |
| | | * innerHTML: '选择文件' |
| | | * }); |
| | | */ |
| | | addButton: function( pick ) { |
| | | var me = this, |
| | | opts = me.options, |
| | | accept = opts.accept, |
| | | options, picker, deferred; |
| | | |
| | | if ( !pick ) { |
| | | return; |
| | | } |
| | | |
| | | deferred = Base.Deferred(); |
| | | $.isPlainObject( pick ) || (pick = { |
| | | id: pick |
| | | }); |
| | | |
| | | options = $.extend({}, pick, { |
| | | accept: $.isPlainObject( accept ) ? [ accept ] : accept, |
| | | swf: opts.swf, |
| | | runtimeOrder: opts.runtimeOrder |
| | | }); |
| | | |
| | | picker = new FilePicker( options ); |
| | | |
| | | picker.once( 'ready', deferred.resolve ); |
| | | picker.on( 'select', function( files ) { |
| | | me.owner.request( 'add-file', [ files ]); |
| | | }); |
| | | picker.init(); |
| | | |
| | | this.pickers.push( picker ); |
| | | |
| | | return deferred.promise(); |
| | | }, |
| | | |
| | | disable: function() { |
| | | $.each( this.pickers, function() { |
| | | this.disable(); |
| | | }); |
| | | }, |
| | | |
| | | enable: function() { |
| | | $.each( this.pickers, function() { |
| | | this.enable(); |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview 文件属性封装 |
| | | */ |
| | | define('file',[ |
| | | 'base', |
| | | 'mediator' |
| | | ], function( Base, Mediator ) { |
| | | |
| | | var $ = Base.$, |
| | | idPrefix = 'WU_FILE_', |
| | | idSuffix = 0, |
| | | rExt = /\.([^.]+)$/, |
| | | statusMap = {}; |
| | | |
| | | function gid() { |
| | | return idPrefix + idSuffix++; |
| | | } |
| | | |
| | | /** |
| | | * 文件类 |
| | | * @class File |
| | | * @constructor 构造函数 |
| | | * @grammar new File( source ) => File |
| | | * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 |
| | | */ |
| | | function WUFile( source ) { |
| | | |
| | | /** |
| | | * 文件名,包括扩展名(后缀) |
| | | * @property name |
| | | * @type {string} |
| | | */ |
| | | this.name = source.name || 'Untitled'; |
| | | |
| | | /** |
| | | * 文件体积(字节) |
| | | * @property size |
| | | * @type {uint} |
| | | * @default 0 |
| | | */ |
| | | this.size = source.size || 0; |
| | | |
| | | /** |
| | | * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) |
| | | * @property type |
| | | * @type {string} |
| | | * @default 'application' |
| | | */ |
| | | this.type = source.type || 'application'; |
| | | |
| | | /** |
| | | * 文件最后修改日期 |
| | | * @property lastModifiedDate |
| | | * @type {int} |
| | | * @default 当前时间戳 |
| | | */ |
| | | this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); |
| | | |
| | | /** |
| | | * 文件ID,每个对象具有唯一ID,与文件名无关 |
| | | * @property id |
| | | * @type {string} |
| | | */ |
| | | this.id = gid(); |
| | | |
| | | /** |
| | | * 文件扩展名,通过文件名获取,例如test.png的扩展名为png |
| | | * @property ext |
| | | * @type {string} |
| | | */ |
| | | this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; |
| | | |
| | | |
| | | /** |
| | | * 状态文字说明。在不同的status语境下有不同的用途。 |
| | | * @property statusText |
| | | * @type {string} |
| | | */ |
| | | this.statusText = ''; |
| | | |
| | | // 存储文件状态,防止通过属性直接修改 |
| | | statusMap[ this.id ] = WUFile.Status.INITED; |
| | | |
| | | this.source = source; |
| | | this.loaded = 0; |
| | | |
| | | this.on( 'error', function( msg ) { |
| | | this.setStatus( WUFile.Status.ERROR, msg ); |
| | | }); |
| | | } |
| | | |
| | | $.extend( WUFile.prototype, { |
| | | |
| | | /** |
| | | * 设置状态,状态变化时会触发`change`事件。 |
| | | * @method setStatus |
| | | * @grammar setStatus( status[, statusText] ); |
| | | * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) |
| | | * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 |
| | | */ |
| | | setStatus: function( status, text ) { |
| | | |
| | | var prevStatus = statusMap[ this.id ]; |
| | | |
| | | typeof text !== 'undefined' && (this.statusText = text); |
| | | |
| | | if ( status !== prevStatus ) { |
| | | statusMap[ this.id ] = status; |
| | | /** |
| | | * 文件状态变化 |
| | | * @event statuschange |
| | | */ |
| | | this.trigger( 'statuschange', status, prevStatus ); |
| | | } |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * 获取文件状态 |
| | | * @return {File.Status} |
| | | * @example |
| | | 文件状态具体包括以下几种类型: |
| | | { |
| | | // 初始化 |
| | | INITED: 0, |
| | | // 已入队列 |
| | | QUEUED: 1, |
| | | // 正在上传 |
| | | PROGRESS: 2, |
| | | // 上传出错 |
| | | ERROR: 3, |
| | | // 上传成功 |
| | | COMPLETE: 4, |
| | | // 上传取消 |
| | | CANCELLED: 5 |
| | | } |
| | | */ |
| | | getStatus: function() { |
| | | return statusMap[ this.id ]; |
| | | }, |
| | | |
| | | /** |
| | | * 获取文件原始信息。 |
| | | * @return {*} |
| | | */ |
| | | getSource: function() { |
| | | return this.source; |
| | | }, |
| | | |
| | | destory: function() { |
| | | delete statusMap[ this.id ]; |
| | | } |
| | | }); |
| | | |
| | | Mediator.installTo( WUFile.prototype ); |
| | | |
| | | /** |
| | | * 文件状态值,具体包括以下几种类型: |
| | | * * `inited` 初始状态 |
| | | * * `queued` 已经进入队列, 等待上传 |
| | | * * `progress` 上传中 |
| | | * * `complete` 上传完成。 |
| | | * * `error` 上传出错,可重试 |
| | | * * `interrupt` 上传中断,可续传。 |
| | | * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 |
| | | * * `cancelled` 文件被移除。 |
| | | * @property {Object} Status |
| | | * @namespace File |
| | | * @class File |
| | | * @static |
| | | */ |
| | | WUFile.Status = { |
| | | INITED: 'inited', // 初始状态 |
| | | QUEUED: 'queued', // 已经进入队列, 等待上传 |
| | | PROGRESS: 'progress', // 上传中 |
| | | ERROR: 'error', // 上传出错,可重试 |
| | | COMPLETE: 'complete', // 上传完成。 |
| | | CANCELLED: 'cancelled', // 上传取消。 |
| | | INTERRUPT: 'interrupt', // 上传中断,可续传。 |
| | | INVALID: 'invalid' // 文件不合格,不能重试上传。 |
| | | }; |
| | | |
| | | return WUFile; |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview 文件队列 |
| | | */ |
| | | define('queue',[ |
| | | 'base', |
| | | 'mediator', |
| | | 'file' |
| | | ], function( Base, Mediator, WUFile ) { |
| | | |
| | | var $ = Base.$, |
| | | STATUS = WUFile.Status; |
| | | |
| | | /** |
| | | * 文件队列, 用来存储各个状态中的文件。 |
| | | * @class Queue |
| | | * @extends Mediator |
| | | */ |
| | | function Queue() { |
| | | |
| | | /** |
| | | * 统计文件数。 |
| | | * * `numOfQueue` 队列中的文件数。 |
| | | * * `numOfSuccess` 上传成功的文件数 |
| | | * * `numOfCancel` 被移除的文件数 |
| | | * * `numOfProgress` 正在上传中的文件数 |
| | | * * `numOfUploadFailed` 上传错误的文件数。 |
| | | * * `numOfInvalid` 无效的文件数。 |
| | | * @property {Object} stats |
| | | */ |
| | | this.stats = { |
| | | numOfQueue: 0, |
| | | numOfSuccess: 0, |
| | | numOfCancel: 0, |
| | | numOfProgress: 0, |
| | | numOfUploadFailed: 0, |
| | | numOfInvalid: 0 |
| | | }; |
| | | |
| | | // 上传队列,仅包括等待上传的文件 |
| | | this._queue = []; |
| | | |
| | | // 存储所有文件 |
| | | this._map = {}; |
| | | } |
| | | |
| | | $.extend( Queue.prototype, { |
| | | |
| | | /** |
| | | * 将新文件加入对队列尾部 |
| | | * |
| | | * @method append |
| | | * @param {File} file 文件对象 |
| | | */ |
| | | append: function( file ) { |
| | | this._queue.push( file ); |
| | | this._fileAdded( file ); |
| | | return this; |
| | | }, |
| | | |
| | | /** |
| | | * 将新文件加入对队列头部 |
| | | * |
| | | * @method prepend |
| | | * @param {File} file 文件对象 |
| | | */ |
| | | prepend: function( file ) { |
| | | this._queue.unshift( file ); |
| | | this._fileAdded( file ); |
| | | return this; |
| | | }, |
| | | |
| | | /** |
| | | * 获取文件对象 |
| | | * |
| | | * @method getFile |
| | | * @param {String} fileId 文件ID |
| | | * @return {File} |
| | | */ |
| | | getFile: function( fileId ) { |
| | | if ( typeof fileId !== 'string' ) { |
| | | return fileId; |
| | | } |
| | | return this._map[ fileId ]; |
| | | }, |
| | | |
| | | /** |
| | | * 从队列中取出一个指定状态的文件。 |
| | | * @grammar fetch( status ) => File |
| | | * @method fetch |
| | | * @param {String} status [文件状态值](#WebUploader:File:File.Status) |
| | | * @return {File} [File](#WebUploader:File) |
| | | */ |
| | | fetch: function( status ) { |
| | | var len = this._queue.length, |
| | | i, file; |
| | | |
| | | status = status || STATUS.QUEUED; |
| | | |
| | | for ( i = 0; i < len; i++ ) { |
| | | file = this._queue[ i ]; |
| | | |
| | | if ( status === file.getStatus() ) { |
| | | return file; |
| | | } |
| | | } |
| | | |
| | | return null; |
| | | }, |
| | | |
| | | /** |
| | | * 对队列进行排序,能够控制文件上传顺序。 |
| | | * @grammar sort( fn ) => undefined |
| | | * @method sort |
| | | * @param {Function} fn 排序方法 |
| | | */ |
| | | sort: function( fn ) { |
| | | if ( typeof fn === 'function' ) { |
| | | this._queue.sort( fn ); |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 |
| | | * @grammar getFiles( [status1[, status2 ...]] ) => Array |
| | | * @method getFiles |
| | | * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) |
| | | */ |
| | | getFiles: function() { |
| | | var sts = [].slice.call( arguments, 0 ), |
| | | ret = [], |
| | | i = 0, |
| | | len = this._queue.length, |
| | | file; |
| | | |
| | | for ( ; i < len; i++ ) { |
| | | file = this._queue[ i ]; |
| | | |
| | | if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { |
| | | continue; |
| | | } |
| | | |
| | | ret.push( file ); |
| | | } |
| | | |
| | | return ret; |
| | | }, |
| | | |
| | | _fileAdded: function( file ) { |
| | | var me = this, |
| | | existing = this._map[ file.id ]; |
| | | |
| | | if ( !existing ) { |
| | | this._map[ file.id ] = file; |
| | | |
| | | file.on( 'statuschange', function( cur, pre ) { |
| | | me._onFileStatusChange( cur, pre ); |
| | | }); |
| | | } |
| | | |
| | | file.setStatus( STATUS.QUEUED ); |
| | | }, |
| | | |
| | | _onFileStatusChange: function( curStatus, preStatus ) { |
| | | var stats = this.stats; |
| | | |
| | | switch ( preStatus ) { |
| | | case STATUS.PROGRESS: |
| | | stats.numOfProgress--; |
| | | break; |
| | | |
| | | case STATUS.QUEUED: |
| | | stats.numOfQueue --; |
| | | break; |
| | | |
| | | case STATUS.ERROR: |
| | | stats.numOfUploadFailed--; |
| | | break; |
| | | |
| | | case STATUS.INVALID: |
| | | stats.numOfInvalid--; |
| | | break; |
| | | } |
| | | |
| | | switch ( curStatus ) { |
| | | case STATUS.QUEUED: |
| | | stats.numOfQueue++; |
| | | break; |
| | | |
| | | case STATUS.PROGRESS: |
| | | stats.numOfProgress++; |
| | | break; |
| | | |
| | | case STATUS.ERROR: |
| | | stats.numOfUploadFailed++; |
| | | break; |
| | | |
| | | case STATUS.COMPLETE: |
| | | stats.numOfSuccess++; |
| | | break; |
| | | |
| | | case STATUS.CANCELLED: |
| | | stats.numOfCancel++; |
| | | break; |
| | | |
| | | case STATUS.INVALID: |
| | | stats.numOfInvalid++; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | }); |
| | | |
| | | Mediator.installTo( Queue.prototype ); |
| | | |
| | | return Queue; |
| | | }); |
| | | /** |
| | | * @fileOverview 队列 |
| | | */ |
| | | define('widgets/queue',[ |
| | | 'base', |
| | | 'uploader', |
| | | 'queue', |
| | | 'file', |
| | | 'lib/file', |
| | | 'runtime/client', |
| | | 'widgets/widget' |
| | | ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { |
| | | |
| | | var $ = Base.$, |
| | | rExt = /\.\w+$/, |
| | | Status = WUFile.Status; |
| | | |
| | | return Uploader.register({ |
| | | 'sort-files': 'sortFiles', |
| | | 'add-file': 'addFiles', |
| | | 'get-file': 'getFile', |
| | | 'fetch-file': 'fetchFile', |
| | | 'get-stats': 'getStats', |
| | | 'get-files': 'getFiles', |
| | | 'remove-file': 'removeFile', |
| | | 'retry': 'retry', |
| | | 'reset': 'reset', |
| | | 'accept-file': 'acceptFile' |
| | | }, { |
| | | |
| | | init: function( opts ) { |
| | | var me = this, |
| | | deferred, len, i, item, arr, accept, runtime; |
| | | |
| | | if ( $.isPlainObject( opts.accept ) ) { |
| | | opts.accept = [ opts.accept ]; |
| | | } |
| | | |
| | | // accept中的中生成匹配正则。 |
| | | if ( opts.accept ) { |
| | | arr = []; |
| | | |
| | | for ( i = 0, len = opts.accept.length; i < len; i++ ) { |
| | | item = opts.accept[ i ].extensions; |
| | | item && arr.push( item ); |
| | | } |
| | | |
| | | if ( arr.length ) { |
| | | accept = '\\.' + arr.join(',') |
| | | .replace( /,/g, '$|\\.' ) |
| | | .replace( /\*/g, '.*' ) + '$'; |
| | | } |
| | | |
| | | me.accept = new RegExp( accept, 'i' ); |
| | | } |
| | | |
| | | me.queue = new Queue(); |
| | | me.stats = me.queue.stats; |
| | | |
| | | // 如果当前不是html5运行时,那就算了。 |
| | | // 不执行后续操作 |
| | | if ( this.request('predict-runtime-type') !== 'html5' ) { |
| | | return; |
| | | } |
| | | |
| | | // 创建一个 html5 运行时的 placeholder |
| | | // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 |
| | | deferred = Base.Deferred(); |
| | | runtime = new RuntimeClient('Placeholder'); |
| | | runtime.connectRuntime({ |
| | | runtimeOrder: 'html5' |
| | | }, function() { |
| | | me._ruid = runtime.getRuid(); |
| | | deferred.resolve(); |
| | | }); |
| | | return deferred.promise(); |
| | | }, |
| | | |
| | | |
| | | // 为了支持外部直接添加一个原生File对象。 |
| | | _wrapFile: function( file ) { |
| | | if ( !(file instanceof WUFile) ) { |
| | | |
| | | if ( !(file instanceof File) ) { |
| | | if ( !this._ruid ) { |
| | | throw new Error('Can\'t add external files.'); |
| | | } |
| | | file = new File( this._ruid, file ); |
| | | } |
| | | |
| | | file = new WUFile( file ); |
| | | } |
| | | |
| | | return file; |
| | | }, |
| | | |
| | | // 判断文件是否可以被加入队列 |
| | | acceptFile: function( file ) { |
| | | var invalid = !file || file.size < 6 || this.accept && |
| | | |
| | | // 如果名字中有后缀,才做后缀白名单处理。 |
| | | rExt.exec( file.name ) && !this.accept.test( file.name ); |
| | | |
| | | return !invalid; |
| | | }, |
| | | |
| | | |
| | | /** |
| | | * @event beforeFileQueued |
| | | * @param {File} file File对象 |
| | | * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @event fileQueued |
| | | * @param {File} file File对象 |
| | | * @description 当文件被加入队列以后触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | _addFile: function( file ) { |
| | | var me = this; |
| | | |
| | | file = me._wrapFile( file ); |
| | | |
| | | // 不过类型判断允许不允许,先派送 `beforeFileQueued` |
| | | if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { |
| | | return; |
| | | } |
| | | |
| | | // 类型不匹配,则派送错误事件,并返回。 |
| | | if ( !me.acceptFile( file ) ) { |
| | | me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); |
| | | return; |
| | | } |
| | | |
| | | me.queue.append( file ); |
| | | me.owner.trigger( 'fileQueued', file ); |
| | | return file; |
| | | }, |
| | | |
| | | getFile: function( fileId ) { |
| | | return this.queue.getFile( fileId ); |
| | | }, |
| | | |
| | | /** |
| | | * @event filesQueued |
| | | * @param {File} files 数组,内容为原始File(lib/File)对象。 |
| | | * @description 当一批文件添加进队列以后触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @method addFiles |
| | | * @grammar addFiles( file ) => undefined |
| | | * @grammar addFiles( [file1, file2 ...] ) => undefined |
| | | * @param {Array of File or File} [files] Files 对象 数组 |
| | | * @description 添加文件到队列 |
| | | * @for Uploader |
| | | */ |
| | | addFiles: function( files ) { |
| | | var me = this; |
| | | |
| | | if ( !files.length ) { |
| | | files = [ files ]; |
| | | } |
| | | |
| | | files = $.map( files, function( file ) { |
| | | return me._addFile( file ); |
| | | }); |
| | | |
| | | me.owner.trigger( 'filesQueued', files ); |
| | | |
| | | if ( me.options.auto ) { |
| | | me.request('start-upload'); |
| | | } |
| | | }, |
| | | |
| | | getStats: function() { |
| | | return this.stats; |
| | | }, |
| | | |
| | | /** |
| | | * @event fileDequeued |
| | | * @param {File} file File对象 |
| | | * @description 当文件被移除队列后触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @method removeFile |
| | | * @grammar removeFile( file ) => undefined |
| | | * @grammar removeFile( id ) => undefined |
| | | * @param {File|id} file File对象或这File对象的id |
| | | * @description 移除某一文件。 |
| | | * @for Uploader |
| | | * @example |
| | | * |
| | | * $li.on('click', '.remove-this', function() { |
| | | * uploader.removeFile( file ); |
| | | * }) |
| | | */ |
| | | removeFile: function( file ) { |
| | | var me = this; |
| | | |
| | | file = file.id ? file : me.queue.getFile( file ); |
| | | |
| | | file.setStatus( Status.CANCELLED ); |
| | | me.owner.trigger( 'fileDequeued', file ); |
| | | }, |
| | | |
| | | /** |
| | | * @method getFiles |
| | | * @grammar getFiles() => Array |
| | | * @grammar getFiles( status1, status2, status... ) => Array |
| | | * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 |
| | | * @for Uploader |
| | | * @example |
| | | * console.log( uploader.getFiles() ); // => all files |
| | | * console.log( uploader.getFiles('error') ) // => all error files. |
| | | */ |
| | | getFiles: function() { |
| | | return this.queue.getFiles.apply( this.queue, arguments ); |
| | | }, |
| | | |
| | | fetchFile: function() { |
| | | return this.queue.fetch.apply( this.queue, arguments ); |
| | | }, |
| | | |
| | | /** |
| | | * @method retry |
| | | * @grammar retry() => undefined |
| | | * @grammar retry( file ) => undefined |
| | | * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 |
| | | * @for Uploader |
| | | * @example |
| | | * function retry() { |
| | | * uploader.retry(); |
| | | * } |
| | | */ |
| | | retry: function( file, noForceStart ) { |
| | | var me = this, |
| | | files, i, len; |
| | | |
| | | if ( file ) { |
| | | file = file.id ? file : me.queue.getFile( file ); |
| | | file.setStatus( Status.QUEUED ); |
| | | noForceStart || me.request('start-upload'); |
| | | return; |
| | | } |
| | | |
| | | files = me.queue.getFiles( Status.ERROR ); |
| | | i = 0; |
| | | len = files.length; |
| | | |
| | | for ( ; i < len; i++ ) { |
| | | file = files[ i ]; |
| | | file.setStatus( Status.QUEUED ); |
| | | } |
| | | |
| | | me.request('start-upload'); |
| | | }, |
| | | |
| | | /** |
| | | * @method sort |
| | | * @grammar sort( fn ) => undefined |
| | | * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 |
| | | * @for Uploader |
| | | */ |
| | | sortFiles: function() { |
| | | return this.queue.sort.apply( this.queue, arguments ); |
| | | }, |
| | | |
| | | /** |
| | | * @method reset |
| | | * @grammar reset() => undefined |
| | | * @description 重置uploader。目前只重置了队列。 |
| | | * @for Uploader |
| | | * @example |
| | | * uploader.reset(); |
| | | */ |
| | | reset: function() { |
| | | this.queue = new Queue(); |
| | | this.stats = this.queue.stats; |
| | | } |
| | | }); |
| | | |
| | | }); |
| | | /** |
| | | * @fileOverview 添加获取Runtime相关信息的方法。 |
| | | */ |
| | | define('widgets/runtime',[ |
| | | 'uploader', |
| | | 'runtime/runtime', |
| | | 'widgets/widget' |
| | | ], function( Uploader, Runtime ) { |
| | | |
| | | Uploader.support = function() { |
| | | return Runtime.hasRuntime.apply( Runtime, arguments ); |
| | | }; |
| | | |
| | | return Uploader.register({ |
| | | 'predict-runtime-type': 'predictRuntmeType' |
| | | }, { |
| | | |
| | | init: function() { |
| | | if ( !this.predictRuntmeType() ) { |
| | | throw Error('Runtime Error'); |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * 预测Uploader将采用哪个`Runtime` |
| | | * @grammar predictRuntmeType() => String |
| | | * @method predictRuntmeType |
| | | * @for Uploader |
| | | */ |
| | | predictRuntmeType: function() { |
| | | var orders = this.options.runtimeOrder || Runtime.orders, |
| | | type = this.type, |
| | | i, len; |
| | | |
| | | if ( !type ) { |
| | | orders = orders.split( /\s*,\s*/g ); |
| | | |
| | | for ( i = 0, len = orders.length; i < len; i++ ) { |
| | | if ( Runtime.hasRuntime( orders[ i ] ) ) { |
| | | this.type = type = orders[ i ]; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return type; |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview Transport |
| | | */ |
| | | define('lib/transport',[ |
| | | 'base', |
| | | 'runtime/client', |
| | | 'mediator' |
| | | ], function( Base, RuntimeClient, Mediator ) { |
| | | |
| | | var $ = Base.$; |
| | | |
| | | function Transport( opts ) { |
| | | var me = this; |
| | | |
| | | opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); |
| | | RuntimeClient.call( this, 'Transport' ); |
| | | |
| | | this._blob = null; |
| | | this._formData = opts.formData || {}; |
| | | this._headers = opts.headers || {}; |
| | | |
| | | this.on( 'progress', this._timeout ); |
| | | this.on( 'load error', function() { |
| | | me.trigger( 'progress', 1 ); |
| | | clearTimeout( me._timer ); |
| | | }); |
| | | } |
| | | |
| | | Transport.options = { |
| | | server: '', |
| | | method: 'POST', |
| | | |
| | | // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 |
| | | withCredentials: false, |
| | | fileVal: 'file', |
| | | timeout: 2 * 60 * 1000, // 2分钟 |
| | | formData: {}, |
| | | headers: {}, |
| | | sendAsBinary: false |
| | | }; |
| | | |
| | | $.extend( Transport.prototype, { |
| | | |
| | | // 添加Blob, 只能添加一次,最后一次有效。 |
| | | appendBlob: function( key, blob, filename ) { |
| | | var me = this, |
| | | opts = me.options; |
| | | |
| | | if ( me.getRuid() ) { |
| | | me.disconnectRuntime(); |
| | | } |
| | | |
| | | // 连接到blob归属的同一个runtime. |
| | | me.connectRuntime( blob.ruid, function() { |
| | | me.exec('init'); |
| | | }); |
| | | |
| | | me._blob = blob; |
| | | opts.fileVal = key || opts.fileVal; |
| | | opts.filename = filename || opts.filename; |
| | | }, |
| | | |
| | | // 添加其他字段 |
| | | append: function( key, value ) { |
| | | if ( typeof key === 'object' ) { |
| | | $.extend( this._formData, key ); |
| | | } else { |
| | | this._formData[ key ] = value; |
| | | } |
| | | }, |
| | | |
| | | setRequestHeader: function( key, value ) { |
| | | if ( typeof key === 'object' ) { |
| | | $.extend( this._headers, key ); |
| | | } else { |
| | | this._headers[ key ] = value; |
| | | } |
| | | }, |
| | | |
| | | send: function( method ) { |
| | | this.exec( 'send', method ); |
| | | this._timeout(); |
| | | }, |
| | | |
| | | abort: function() { |
| | | clearTimeout( this._timer ); |
| | | return this.exec('abort'); |
| | | }, |
| | | |
| | | destroy: function() { |
| | | this.trigger('destroy'); |
| | | this.off(); |
| | | this.exec('destroy'); |
| | | this.disconnectRuntime(); |
| | | }, |
| | | |
| | | getResponse: function() { |
| | | return this.exec('getResponse'); |
| | | }, |
| | | |
| | | getResponseAsJson: function() { |
| | | return this.exec('getResponseAsJson'); |
| | | }, |
| | | |
| | | getStatus: function() { |
| | | return this.exec('getStatus'); |
| | | }, |
| | | |
| | | _timeout: function() { |
| | | var me = this, |
| | | duration = me.options.timeout; |
| | | |
| | | if ( !duration ) { |
| | | return; |
| | | } |
| | | |
| | | clearTimeout( me._timer ); |
| | | me._timer = setTimeout(function() { |
| | | me.abort(); |
| | | me.trigger( 'error', 'timeout' ); |
| | | }, duration ); |
| | | } |
| | | |
| | | }); |
| | | |
| | | // 让Transport具备事件功能。 |
| | | Mediator.installTo( Transport.prototype ); |
| | | |
| | | return Transport; |
| | | }); |
| | | /** |
| | | * @fileOverview 负责文件上传相关。 |
| | | */ |
| | | define('widgets/upload',[ |
| | | 'base', |
| | | 'uploader', |
| | | 'file', |
| | | 'lib/transport', |
| | | 'widgets/widget' |
| | | ], function( Base, Uploader, WUFile, Transport ) { |
| | | |
| | | var $ = Base.$, |
| | | isPromise = Base.isPromise, |
| | | Status = WUFile.Status; |
| | | |
| | | // 添加默认配置项 |
| | | $.extend( Uploader.options, { |
| | | |
| | | |
| | | /** |
| | | * @property {Boolean} [prepareNextFile=false] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 是否允许在文件传输时提前把下一个文件准备好。 |
| | | * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 |
| | | * 如果能提前在当前文件传输期处理,可以节省总体耗时。 |
| | | */ |
| | | prepareNextFile: false, |
| | | |
| | | /** |
| | | * @property {Boolean} [chunked=false] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 是否要分片处理大文件上传。 |
| | | */ |
| | | chunked: false, |
| | | |
| | | /** |
| | | * @property {Boolean} [chunkSize=5242880] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 如果要分片,分多大一片? 默认大小为5M. |
| | | */ |
| | | chunkSize: 5 * 1024 * 1024, |
| | | |
| | | /** |
| | | * @property {Boolean} [chunkRetry=2] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 如果某个分片由于网络问题出错,允许自动重传多少次? |
| | | */ |
| | | chunkRetry: 2, |
| | | |
| | | /** |
| | | * @property {Boolean} [threads=3] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 上传并发数。允许同时最大上传进程数。 |
| | | */ |
| | | threads: 3, |
| | | |
| | | |
| | | /** |
| | | * @property {Object} [formData] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 |
| | | */ |
| | | formData: null |
| | | |
| | | /** |
| | | * @property {Object} [fileVal='file'] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 设置文件上传域的name。 |
| | | */ |
| | | |
| | | /** |
| | | * @property {Object} [method='POST'] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 文件上传方式,`POST`或者`GET`。 |
| | | */ |
| | | |
| | | /** |
| | | * @property {Object} [sendAsBinary=false] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, |
| | | * 其他参数在$_GET数组中。 |
| | | */ |
| | | }); |
| | | |
| | | // 负责将文件切片。 |
| | | function CuteFile( file, chunkSize ) { |
| | | var pending = [], |
| | | blob = file.source, |
| | | total = blob.size, |
| | | chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, |
| | | start = 0, |
| | | index = 0, |
| | | len; |
| | | |
| | | while ( index < chunks ) { |
| | | len = Math.min( chunkSize, total - start ); |
| | | |
| | | pending.push({ |
| | | file: file, |
| | | start: start, |
| | | end: chunkSize ? (start + len) : total, |
| | | total: total, |
| | | chunks: chunks, |
| | | chunk: index++ |
| | | }); |
| | | start += len; |
| | | } |
| | | |
| | | file.blocks = pending.concat(); |
| | | file.remaning = pending.length; |
| | | |
| | | return { |
| | | file: file, |
| | | |
| | | has: function() { |
| | | return !!pending.length; |
| | | }, |
| | | |
| | | fetch: function() { |
| | | return pending.shift(); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | Uploader.register({ |
| | | 'start-upload': 'start', |
| | | 'stop-upload': 'stop', |
| | | 'skip-file': 'skipFile', |
| | | 'is-in-progress': 'isInProgress' |
| | | }, { |
| | | |
| | | init: function() { |
| | | var owner = this.owner; |
| | | |
| | | this.runing = false; |
| | | |
| | | // 记录当前正在传的数据,跟threads相关 |
| | | this.pool = []; |
| | | |
| | | // 缓存即将上传的文件。 |
| | | this.pending = []; |
| | | |
| | | // 跟踪还有多少分片没有完成上传。 |
| | | this.remaning = 0; |
| | | this.__tick = Base.bindFn( this._tick, this ); |
| | | |
| | | owner.on( 'uploadComplete', function( file ) { |
| | | // 把其他块取消了。 |
| | | file.blocks && $.each( file.blocks, function( _, v ) { |
| | | v.transport && (v.transport.abort(), v.transport.destroy()); |
| | | delete v.transport; |
| | | }); |
| | | |
| | | delete file.blocks; |
| | | delete file.remaning; |
| | | }); |
| | | }, |
| | | |
| | | /** |
| | | * @event startUpload |
| | | * @description 当开始上传流程时触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 |
| | | * @grammar upload() => undefined |
| | | * @method upload |
| | | * @for Uploader |
| | | */ |
| | | start: function() { |
| | | var me = this; |
| | | |
| | | // 移出invalid的文件 |
| | | $.each( me.request( 'get-files', Status.INVALID ), function() { |
| | | me.request( 'remove-file', this ); |
| | | }); |
| | | |
| | | if ( me.runing ) { |
| | | return; |
| | | } |
| | | |
| | | me.runing = true; |
| | | |
| | | // 如果有暂停的,则续传 |
| | | $.each( me.pool, function( _, v ) { |
| | | var file = v.file; |
| | | |
| | | if ( file.getStatus() === Status.INTERRUPT ) { |
| | | file.setStatus( Status.PROGRESS ); |
| | | me._trigged = false; |
| | | v.transport && v.transport.send(); |
| | | } |
| | | }); |
| | | |
| | | me._trigged = false; |
| | | me.owner.trigger('startUpload'); |
| | | Base.nextTick( me.__tick ); |
| | | }, |
| | | |
| | | /** |
| | | * @event stopUpload |
| | | * @description 当开始上传流程暂停时触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 |
| | | * @grammar stop() => undefined |
| | | * @grammar stop( true ) => undefined |
| | | * @method stop |
| | | * @for Uploader |
| | | */ |
| | | stop: function( interrupt ) { |
| | | var me = this; |
| | | |
| | | if ( me.runing === false ) { |
| | | return; |
| | | } |
| | | |
| | | me.runing = false; |
| | | |
| | | interrupt && $.each( me.pool, function( _, v ) { |
| | | v.transport && v.transport.abort(); |
| | | v.file.setStatus( Status.INTERRUPT ); |
| | | }); |
| | | |
| | | me.owner.trigger('stopUpload'); |
| | | }, |
| | | |
| | | /** |
| | | * 判断`Uplaode`r是否正在上传中。 |
| | | * @grammar isInProgress() => Boolean |
| | | * @method isInProgress |
| | | * @for Uploader |
| | | */ |
| | | isInProgress: function() { |
| | | return !!this.runing; |
| | | }, |
| | | |
| | | getStats: function() { |
| | | return this.request('get-stats'); |
| | | }, |
| | | |
| | | /** |
| | | * 掉过一个文件上传,直接标记指定文件为已上传状态。 |
| | | * @grammar skipFile( file ) => undefined |
| | | * @method skipFile |
| | | * @for Uploader |
| | | */ |
| | | skipFile: function( file, status ) { |
| | | file = this.request( 'get-file', file ); |
| | | |
| | | file.setStatus( status || Status.COMPLETE ); |
| | | file.skipped = true; |
| | | |
| | | // 如果正在上传。 |
| | | file.blocks && $.each( file.blocks, function( _, v ) { |
| | | var _tr = v.transport; |
| | | |
| | | if ( _tr ) { |
| | | _tr.abort(); |
| | | _tr.destroy(); |
| | | delete v.transport; |
| | | } |
| | | }); |
| | | |
| | | this.owner.trigger( 'uploadSkip', file ); |
| | | }, |
| | | |
| | | /** |
| | | * @event uploadFinished |
| | | * @description 当所有文件上传结束时触发。 |
| | | * @for Uploader |
| | | */ |
| | | _tick: function() { |
| | | var me = this, |
| | | opts = me.options, |
| | | fn, val; |
| | | |
| | | // 上一个promise还没有结束,则等待完成后再执行。 |
| | | if ( me._promise ) { |
| | | return me._promise.always( me.__tick ); |
| | | } |
| | | |
| | | // 还有位置,且还有文件要处理的话。 |
| | | if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { |
| | | me._trigged = false; |
| | | |
| | | fn = function( val ) { |
| | | me._promise = null; |
| | | |
| | | // 有可能是reject过来的,所以要检测val的类型。 |
| | | val && val.file && me._startSend( val ); |
| | | Base.nextTick( me.__tick ); |
| | | }; |
| | | |
| | | me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); |
| | | |
| | | // 没有要上传的了,且没有正在传输的了。 |
| | | } else if ( !me.remaning && !me.getStats().numOfQueue ) { |
| | | me.runing = false; |
| | | |
| | | me._trigged || Base.nextTick(function() { |
| | | me.owner.trigger('uploadFinished'); |
| | | }); |
| | | me._trigged = true; |
| | | } |
| | | }, |
| | | |
| | | _nextBlock: function() { |
| | | var me = this, |
| | | act = me._act, |
| | | opts = me.options, |
| | | next, done; |
| | | |
| | | // 如果当前文件还有没有需要传输的,则直接返回剩下的。 |
| | | if ( act && act.has() && |
| | | act.file.getStatus() === Status.PROGRESS ) { |
| | | |
| | | // 是否提前准备下一个文件 |
| | | if ( opts.prepareNextFile && !me.pending.length ) { |
| | | me._prepareNextFile(); |
| | | } |
| | | |
| | | return act.fetch(); |
| | | |
| | | // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 |
| | | } else if ( me.runing ) { |
| | | |
| | | // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 |
| | | if ( !me.pending.length && me.getStats().numOfQueue ) { |
| | | me._prepareNextFile(); |
| | | } |
| | | |
| | | next = me.pending.shift(); |
| | | done = function( file ) { |
| | | if ( !file ) { |
| | | return null; |
| | | } |
| | | |
| | | act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); |
| | | me._act = act; |
| | | return act.fetch(); |
| | | }; |
| | | |
| | | // 文件可能还在prepare中,也有可能已经完全准备好了。 |
| | | return isPromise( next ) ? |
| | | next[ next.pipe ? 'pipe' : 'then']( done ) : |
| | | done( next ); |
| | | } |
| | | }, |
| | | |
| | | |
| | | /** |
| | | * @event uploadStart |
| | | * @param {File} file File对象 |
| | | * @description 某个文件开始上传前触发,一个文件只会触发一次。 |
| | | * @for Uploader |
| | | */ |
| | | _prepareNextFile: function() { |
| | | var me = this, |
| | | file = me.request('fetch-file'), |
| | | pending = me.pending, |
| | | promise; |
| | | |
| | | if ( file ) { |
| | | promise = me.request( 'before-send-file', file, function() { |
| | | |
| | | // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. |
| | | if ( file.getStatus() === Status.QUEUED ) { |
| | | me.owner.trigger( 'uploadStart', file ); |
| | | file.setStatus( Status.PROGRESS ); |
| | | return file; |
| | | } |
| | | |
| | | return me._finishFile( file ); |
| | | }); |
| | | |
| | | // 如果还在pending中,则替换成文件本身。 |
| | | promise.done(function() { |
| | | var idx = $.inArray( promise, pending ); |
| | | |
| | | ~idx && pending.splice( idx, 1, file ); |
| | | }); |
| | | |
| | | // befeore-send-file的钩子就有错误发生。 |
| | | promise.fail(function( reason ) { |
| | | file.setStatus( Status.ERROR, reason ); |
| | | me.owner.trigger( 'uploadError', file, reason ); |
| | | me.owner.trigger( 'uploadComplete', file ); |
| | | }); |
| | | |
| | | pending.push( promise ); |
| | | } |
| | | }, |
| | | |
| | | // 让出位置了,可以让其他分片开始上传 |
| | | _popBlock: function( block ) { |
| | | var idx = $.inArray( block, this.pool ); |
| | | |
| | | this.pool.splice( idx, 1 ); |
| | | block.file.remaning--; |
| | | this.remaning--; |
| | | }, |
| | | |
| | | // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 |
| | | _startSend: function( block ) { |
| | | var me = this, |
| | | file = block.file, |
| | | promise; |
| | | |
| | | me.pool.push( block ); |
| | | me.remaning++; |
| | | |
| | | // 如果没有分片,则直接使用原始的。 |
| | | // 不会丢失content-type信息。 |
| | | block.blob = block.chunks === 1 ? file.source : |
| | | file.source.slice( block.start, block.end ); |
| | | |
| | | // hook, 每个分片发送之前可能要做些异步的事情。 |
| | | promise = me.request( 'before-send', block, function() { |
| | | |
| | | // 有可能文件已经上传出错了,所以不需要再传输了。 |
| | | if ( file.getStatus() === Status.PROGRESS ) { |
| | | me._doSend( block ); |
| | | } else { |
| | | me._popBlock( block ); |
| | | Base.nextTick( me.__tick ); |
| | | } |
| | | }); |
| | | |
| | | // 如果为fail了,则跳过此分片。 |
| | | promise.fail(function() { |
| | | if ( file.remaning === 1 ) { |
| | | me._finishFile( file ).always(function() { |
| | | block.percentage = 1; |
| | | me._popBlock( block ); |
| | | me.owner.trigger( 'uploadComplete', file ); |
| | | Base.nextTick( me.__tick ); |
| | | }); |
| | | } else { |
| | | block.percentage = 1; |
| | | me._popBlock( block ); |
| | | Base.nextTick( me.__tick ); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | |
| | | /** |
| | | * @event uploadBeforeSend |
| | | * @param {Object} object |
| | | * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 |
| | | * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @event uploadAccept |
| | | * @param {Object} object |
| | | * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 |
| | | * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @event uploadProgress |
| | | * @param {File} file File对象 |
| | | * @param {Number} percentage 上传进度 |
| | | * @description 上传过程中触发,携带上传进度。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | |
| | | /** |
| | | * @event uploadError |
| | | * @param {File} file File对象 |
| | | * @param {String} reason 出错的code |
| | | * @description 当文件上传出错时触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @event uploadSuccess |
| | | * @param {File} file File对象 |
| | | * @param {Object} response 服务端返回的数据 |
| | | * @description 当文件上传成功时触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | /** |
| | | * @event uploadComplete |
| | | * @param {File} [file] File对象 |
| | | * @description 不管成功或者失败,文件上传完成时触发。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | // 做上传操作。 |
| | | _doSend: function( block ) { |
| | | var me = this, |
| | | owner = me.owner, |
| | | opts = me.options, |
| | | file = block.file, |
| | | tr = new Transport( opts ), |
| | | data = $.extend({}, opts.formData ), |
| | | headers = $.extend({}, opts.headers ), |
| | | requestAccept, ret; |
| | | |
| | | block.transport = tr; |
| | | |
| | | tr.on( 'destroy', function() { |
| | | delete block.transport; |
| | | me._popBlock( block ); |
| | | Base.nextTick( me.__tick ); |
| | | }); |
| | | |
| | | // 广播上传进度。以文件为单位。 |
| | | tr.on( 'progress', function( percentage ) { |
| | | var totalPercent = 0, |
| | | uploaded = 0; |
| | | |
| | | // 可能没有abort掉,progress还是执行进来了。 |
| | | // if ( !file.blocks ) { |
| | | // return; |
| | | // } |
| | | |
| | | totalPercent = block.percentage = percentage; |
| | | |
| | | if ( block.chunks > 1 ) { // 计算文件的整体速度。 |
| | | $.each( file.blocks, function( _, v ) { |
| | | uploaded += (v.percentage || 0) * (v.end - v.start); |
| | | }); |
| | | |
| | | totalPercent = uploaded / file.size; |
| | | } |
| | | |
| | | owner.trigger( 'uploadProgress', file, totalPercent || 0 ); |
| | | }); |
| | | |
| | | // 用来询问,是否返回的结果是有错误的。 |
| | | requestAccept = function( reject ) { |
| | | var fn; |
| | | |
| | | ret = tr.getResponseAsJson() || {}; |
| | | ret._raw = tr.getResponse(); |
| | | fn = function( value ) { |
| | | reject = value; |
| | | }; |
| | | |
| | | // 服务端响应了,不代表成功了,询问是否响应正确。 |
| | | if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { |
| | | reject = reject || 'server'; |
| | | } |
| | | |
| | | return reject; |
| | | }; |
| | | |
| | | // 尝试重试,然后广播文件上传出错。 |
| | | tr.on( 'error', function( type, flag ) { |
| | | block.retried = block.retried || 0; |
| | | |
| | | // 自动重试 |
| | | if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && |
| | | block.retried < opts.chunkRetry ) { |
| | | |
| | | block.retried++; |
| | | tr.send(); |
| | | |
| | | } else { |
| | | |
| | | // http status 500 ~ 600 |
| | | if ( !flag && type === 'server' ) { |
| | | type = requestAccept( type ); |
| | | } |
| | | |
| | | file.setStatus( Status.ERROR, type ); |
| | | owner.trigger( 'uploadError', file, type ); |
| | | owner.trigger( 'uploadComplete', file ); |
| | | } |
| | | }); |
| | | |
| | | // 上传成功 |
| | | tr.on( 'load', function() { |
| | | var reason; |
| | | |
| | | // 如果非预期,转向上传出错。 |
| | | if ( (reason = requestAccept()) ) { |
| | | tr.trigger( 'error', reason, true ); |
| | | return; |
| | | } |
| | | |
| | | // 全部上传完成。 |
| | | if ( file.remaning === 1 ) { |
| | | me._finishFile( file, ret ); |
| | | } else { |
| | | tr.destroy(); |
| | | } |
| | | }); |
| | | |
| | | // 配置默认的上传字段。 |
| | | data = $.extend( data, { |
| | | id: file.id, |
| | | name: file.name, |
| | | type: file.type, |
| | | lastModifiedDate: file.lastModifiedDate, |
| | | size: file.size |
| | | }); |
| | | |
| | | block.chunks > 1 && $.extend( data, { |
| | | chunks: block.chunks, |
| | | chunk: block.chunk |
| | | }); |
| | | |
| | | // 在发送之间可以添加字段什么的。。。 |
| | | // 如果默认的字段不够使用,可以通过监听此事件来扩展 |
| | | owner.trigger( 'uploadBeforeSend', block, data, headers ); |
| | | |
| | | // 开始发送。 |
| | | tr.appendBlob( opts.fileVal, block.blob, file.name ); |
| | | tr.append( data ); |
| | | tr.setRequestHeader( headers ); |
| | | tr.send(); |
| | | }, |
| | | |
| | | // 完成上传。 |
| | | _finishFile: function( file, ret, hds ) { |
| | | var owner = this.owner; |
| | | |
| | | return owner |
| | | .request( 'after-send-file', arguments, function() { |
| | | file.setStatus( Status.COMPLETE ); |
| | | owner.trigger( 'uploadSuccess', file, ret, hds ); |
| | | }) |
| | | .fail(function( reason ) { |
| | | |
| | | // 如果外部已经标记为invalid什么的,不再改状态。 |
| | | if ( file.getStatus() === Status.PROGRESS ) { |
| | | file.setStatus( Status.ERROR, reason ); |
| | | } |
| | | |
| | | owner.trigger( 'uploadError', file, reason ); |
| | | }) |
| | | .always(function() { |
| | | owner.trigger( 'uploadComplete', file ); |
| | | }); |
| | | } |
| | | |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 |
| | | */ |
| | | |
| | | define('widgets/validator',[ |
| | | 'base', |
| | | 'uploader', |
| | | 'file', |
| | | 'widgets/widget' |
| | | ], function( Base, Uploader, WUFile ) { |
| | | |
| | | var $ = Base.$, |
| | | validators = {}, |
| | | api; |
| | | |
| | | /** |
| | | * @event error |
| | | * @param {String} type 错误类型。 |
| | | * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 |
| | | * |
| | | * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 |
| | | * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 |
| | | * @for Uploader |
| | | */ |
| | | |
| | | // 暴露给外面的api |
| | | api = { |
| | | |
| | | // 添加验证器 |
| | | addValidator: function( type, cb ) { |
| | | validators[ type ] = cb; |
| | | }, |
| | | |
| | | // 移除验证器 |
| | | removeValidator: function( type ) { |
| | | delete validators[ type ]; |
| | | } |
| | | }; |
| | | |
| | | // 在Uploader初始化的时候启动Validators的初始化 |
| | | Uploader.register({ |
| | | init: function() { |
| | | var me = this; |
| | | $.each( validators, function() { |
| | | this.call( me.owner ); |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * @property {int} [fileNumLimit=undefined] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 验证文件总数量, 超出则不允许加入队列。 |
| | | */ |
| | | api.addValidator( 'fileNumLimit', function() { |
| | | var uploader = this, |
| | | opts = uploader.options, |
| | | count = 0, |
| | | max = opts.fileNumLimit >> 0, |
| | | flag = true; |
| | | |
| | | if ( !max ) { |
| | | return; |
| | | } |
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) { |
| | | |
| | | if ( count >= max && flag ) { |
| | | flag = false; |
| | | this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); |
| | | setTimeout(function() { |
| | | flag = true; |
| | | }, 1 ); |
| | | } |
| | | |
| | | return count >= max ? false : true; |
| | | }); |
| | | |
| | | uploader.on( 'fileQueued', function() { |
| | | count++; |
| | | }); |
| | | |
| | | uploader.on( 'fileDequeued', function() { |
| | | count--; |
| | | }); |
| | | |
| | | uploader.on( 'uploadFinished', function() { |
| | | count = 0; |
| | | }); |
| | | }); |
| | | |
| | | |
| | | /** |
| | | * @property {int} [fileSizeLimit=undefined] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 |
| | | */ |
| | | api.addValidator( 'fileSizeLimit', function() { |
| | | var uploader = this, |
| | | opts = uploader.options, |
| | | count = 0, |
| | | max = opts.fileSizeLimit >> 0, |
| | | flag = true; |
| | | |
| | | if ( !max ) { |
| | | return; |
| | | } |
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) { |
| | | var invalid = count + file.size > max; |
| | | |
| | | if ( invalid && flag ) { |
| | | flag = false; |
| | | this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); |
| | | setTimeout(function() { |
| | | flag = true; |
| | | }, 1 ); |
| | | } |
| | | |
| | | return invalid ? false : true; |
| | | }); |
| | | |
| | | uploader.on( 'fileQueued', function( file ) { |
| | | count += file.size; |
| | | }); |
| | | |
| | | uploader.on( 'fileDequeued', function( file ) { |
| | | count -= file.size; |
| | | }); |
| | | |
| | | uploader.on( 'uploadFinished', function() { |
| | | count = 0; |
| | | }); |
| | | }); |
| | | |
| | | /** |
| | | * @property {int} [fileSingleSizeLimit=undefined] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 |
| | | */ |
| | | api.addValidator( 'fileSingleSizeLimit', function() { |
| | | var uploader = this, |
| | | opts = uploader.options, |
| | | max = opts.fileSingleSizeLimit; |
| | | |
| | | if ( !max ) { |
| | | return; |
| | | } |
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) { |
| | | |
| | | if ( file.size > max ) { |
| | | file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); |
| | | this.trigger( 'error', 'F_EXCEED_SIZE', file ); |
| | | return false; |
| | | } |
| | | |
| | | }); |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * @property {int} [duplicate=undefined] |
| | | * @namespace options |
| | | * @for Uploader |
| | | * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. |
| | | */ |
| | | api.addValidator( 'duplicate', function() { |
| | | var uploader = this, |
| | | opts = uploader.options, |
| | | mapping = {}; |
| | | |
| | | if ( opts.duplicate ) { |
| | | return; |
| | | } |
| | | |
| | | function hashString( str ) { |
| | | var hash = 0, |
| | | i = 0, |
| | | len = str.length, |
| | | _char; |
| | | |
| | | for ( ; i < len; i++ ) { |
| | | _char = str.charCodeAt( i ); |
| | | hash = _char + (hash << 6) + (hash << 16) - hash; |
| | | } |
| | | |
| | | return hash; |
| | | } |
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) { |
| | | var hash = file.__hash || (file.__hash = hashString( file.name + |
| | | file.size + file.lastModifiedDate )); |
| | | |
| | | // 已经重复了 |
| | | if ( mapping[ hash ] ) { |
| | | this.trigger( 'error', 'F_DUPLICATE', file ); |
| | | return false; |
| | | } |
| | | }); |
| | | |
| | | uploader.on( 'fileQueued', function( file ) { |
| | | var hash = file.__hash; |
| | | |
| | | hash && (mapping[ hash ] = true); |
| | | }); |
| | | |
| | | uploader.on( 'fileDequeued', function( file ) { |
| | | var hash = file.__hash; |
| | | |
| | | hash && (delete mapping[ hash ]); |
| | | }); |
| | | }); |
| | | |
| | | return api; |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview Runtime管理器,负责Runtime的选择, 连接 |
| | | */ |
| | | define('runtime/compbase',[],function() { |
| | | |
| | | function CompBase( owner, runtime ) { |
| | | |
| | | this.owner = owner; |
| | | this.options = owner.options; |
| | | |
| | | this.getRuntime = function() { |
| | | return runtime; |
| | | }; |
| | | |
| | | this.getRuid = function() { |
| | | return runtime.uid; |
| | | }; |
| | | |
| | | this.trigger = function() { |
| | | return owner.trigger.apply( owner, arguments ); |
| | | }; |
| | | } |
| | | |
| | | return CompBase; |
| | | }); |
| | | /** |
| | | * @fileOverview Html5Runtime |
| | | */ |
| | | define('runtime/html5/runtime',[ |
| | | 'base', |
| | | 'runtime/runtime', |
| | | 'runtime/compbase' |
| | | ], function( Base, Runtime, CompBase ) { |
| | | |
| | | var type = 'html5', |
| | | components = {}; |
| | | |
| | | function Html5Runtime() { |
| | | var pool = {}, |
| | | me = this, |
| | | destory = this.destory; |
| | | |
| | | Runtime.apply( me, arguments ); |
| | | me.type = type; |
| | | |
| | | |
| | | // 这个方法的调用者,实际上是RuntimeClient |
| | | me.exec = function( comp, fn/*, args...*/) { |
| | | var client = this, |
| | | uid = client.uid, |
| | | args = Base.slice( arguments, 2 ), |
| | | instance; |
| | | |
| | | if ( components[ comp ] ) { |
| | | instance = pool[ uid ] = pool[ uid ] || |
| | | new components[ comp ]( client, me ); |
| | | |
| | | if ( instance[ fn ] ) { |
| | | return instance[ fn ].apply( instance, args ); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | me.destory = function() { |
| | | // @todo 删除池子中的所有实例 |
| | | return destory && destory.apply( this, arguments ); |
| | | }; |
| | | } |
| | | |
| | | Base.inherits( Runtime, { |
| | | constructor: Html5Runtime, |
| | | |
| | | // 不需要连接其他程序,直接执行callback |
| | | init: function() { |
| | | var me = this; |
| | | setTimeout(function() { |
| | | me.trigger('ready'); |
| | | }, 1 ); |
| | | } |
| | | |
| | | }); |
| | | |
| | | // 注册Components |
| | | Html5Runtime.register = function( name, component ) { |
| | | var klass = components[ name ] = Base.inherits( CompBase, component ); |
| | | return klass; |
| | | }; |
| | | |
| | | // 注册html5运行时。 |
| | | // 只有在支持的前提下注册。 |
| | | if ( window.Blob && window.FileReader && window.DataView ) { |
| | | Runtime.addRuntime( type, Html5Runtime ); |
| | | } |
| | | |
| | | return Html5Runtime; |
| | | }); |
| | | /** |
| | | * @fileOverview Blob Html实现 |
| | | */ |
| | | define('runtime/html5/blob',[ |
| | | 'runtime/html5/runtime', |
| | | 'lib/blob' |
| | | ], function( Html5Runtime, Blob ) { |
| | | |
| | | return Html5Runtime.register( 'Blob', { |
| | | slice: function( start, end ) { |
| | | var blob = this.owner.source, |
| | | slice = blob.slice || blob.webkitSlice || blob.mozSlice; |
| | | |
| | | blob = slice.call( blob, start, end ); |
| | | |
| | | return new Blob( this.getRuid(), blob ); |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview FilePaste |
| | | */ |
| | | define('runtime/html5/dnd',[ |
| | | 'base', |
| | | 'runtime/html5/runtime', |
| | | 'lib/file' |
| | | ], function( Base, Html5Runtime, File ) { |
| | | |
| | | var $ = Base.$, |
| | | prefix = 'webuploader-dnd-'; |
| | | |
| | | return Html5Runtime.register( 'DragAndDrop', { |
| | | init: function() { |
| | | var elem = this.elem = this.options.container; |
| | | |
| | | this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); |
| | | this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); |
| | | this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); |
| | | this.dropHandler = Base.bindFn( this._dropHandler, this ); |
| | | this.dndOver = false; |
| | | |
| | | elem.on( 'dragenter', this.dragEnterHandler ); |
| | | elem.on( 'dragover', this.dragOverHandler ); |
| | | elem.on( 'dragleave', this.dragLeaveHandler ); |
| | | elem.on( 'drop', this.dropHandler ); |
| | | |
| | | if ( this.options.disableGlobalDnd ) { |
| | | $( document ).on( 'dragover', this.dragOverHandler ); |
| | | $( document ).on( 'drop', this.dropHandler ); |
| | | } |
| | | }, |
| | | |
| | | _dragEnterHandler: function( e ) { |
| | | var me = this, |
| | | denied = me._denied || false, |
| | | items; |
| | | |
| | | e = e.originalEvent || e; |
| | | |
| | | if ( !me.dndOver ) { |
| | | me.dndOver = true; |
| | | |
| | | // 注意只有 chrome 支持。 |
| | | items = e.dataTransfer.items; |
| | | |
| | | if ( items && items.length ) { |
| | | me._denied = denied = !me.trigger( 'accept', items ); |
| | | } |
| | | |
| | | me.elem.addClass( prefix + 'over' ); |
| | | me.elem[ denied ? 'addClass' : |
| | | 'removeClass' ]( prefix + 'denied' ); |
| | | } |
| | | |
| | | |
| | | e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; |
| | | |
| | | return false; |
| | | }, |
| | | |
| | | _dragOverHandler: function( e ) { |
| | | // 只处理框内的。 |
| | | var parentElem = this.elem.parent().get( 0 ); |
| | | if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { |
| | | return false; |
| | | } |
| | | |
| | | clearTimeout( this._leaveTimer ); |
| | | this._dragEnterHandler.call( this, e ); |
| | | |
| | | return false; |
| | | }, |
| | | |
| | | _dragLeaveHandler: function() { |
| | | var me = this, |
| | | handler; |
| | | |
| | | handler = function() { |
| | | me.dndOver = false; |
| | | me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); |
| | | }; |
| | | |
| | | clearTimeout( me._leaveTimer ); |
| | | me._leaveTimer = setTimeout( handler, 100 ); |
| | | return false; |
| | | }, |
| | | |
| | | _dropHandler: function( e ) { |
| | | var me = this, |
| | | ruid = me.getRuid(), |
| | | parentElem = me.elem.parent().get( 0 ); |
| | | |
| | | // 只处理框内的。 |
| | | if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { |
| | | return false; |
| | | } |
| | | |
| | | me._getTansferFiles( e, function( results ) { |
| | | me.trigger( 'drop', $.map( results, function( file ) { |
| | | return new File( ruid, file ); |
| | | }) ); |
| | | }); |
| | | |
| | | me.dndOver = false; |
| | | me.elem.removeClass( prefix + 'over' ); |
| | | return false; |
| | | }, |
| | | |
| | | // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 |
| | | _getTansferFiles: function( e, callback ) { |
| | | var results = [], |
| | | promises = [], |
| | | items, files, dataTransfer, file, item, i, len, canAccessFolder; |
| | | |
| | | e = e.originalEvent || e; |
| | | |
| | | dataTransfer = e.dataTransfer; |
| | | items = dataTransfer.items; |
| | | files = dataTransfer.files; |
| | | |
| | | canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); |
| | | |
| | | for ( i = 0, len = files.length; i < len; i++ ) { |
| | | file = files[ i ]; |
| | | item = items && items[ i ]; |
| | | |
| | | if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { |
| | | |
| | | promises.push( this._traverseDirectoryTree( |
| | | item.webkitGetAsEntry(), results ) ); |
| | | } else { |
| | | results.push( file ); |
| | | } |
| | | } |
| | | |
| | | Base.when.apply( Base, promises ).done(function() { |
| | | |
| | | if ( !results.length ) { |
| | | return; |
| | | } |
| | | |
| | | callback( results ); |
| | | }); |
| | | }, |
| | | |
| | | _traverseDirectoryTree: function( entry, results ) { |
| | | var deferred = Base.Deferred(), |
| | | me = this; |
| | | |
| | | if ( entry.isFile ) { |
| | | entry.file(function( file ) { |
| | | results.push( file ); |
| | | deferred.resolve(); |
| | | }); |
| | | } else if ( entry.isDirectory ) { |
| | | entry.createReader().readEntries(function( entries ) { |
| | | var len = entries.length, |
| | | promises = [], |
| | | arr = [], // 为了保证顺序。 |
| | | i; |
| | | |
| | | for ( i = 0; i < len; i++ ) { |
| | | promises.push( me._traverseDirectoryTree( |
| | | entries[ i ], arr ) ); |
| | | } |
| | | |
| | | Base.when.apply( Base, promises ).then(function() { |
| | | results.push.apply( results, arr ); |
| | | deferred.resolve(); |
| | | }, deferred.reject ); |
| | | }); |
| | | } |
| | | |
| | | return deferred.promise(); |
| | | }, |
| | | |
| | | destroy: function() { |
| | | var elem = this.elem; |
| | | |
| | | elem.off( 'dragenter', this.dragEnterHandler ); |
| | | elem.off( 'dragover', this.dragEnterHandler ); |
| | | elem.off( 'dragleave', this.dragLeaveHandler ); |
| | | elem.off( 'drop', this.dropHandler ); |
| | | |
| | | if ( this.options.disableGlobalDnd ) { |
| | | $( document ).off( 'dragover', this.dragOverHandler ); |
| | | $( document ).off( 'drop', this.dropHandler ); |
| | | } |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview FilePaste |
| | | */ |
| | | define('runtime/html5/filepaste',[ |
| | | 'base', |
| | | 'runtime/html5/runtime', |
| | | 'lib/file' |
| | | ], function( Base, Html5Runtime, File ) { |
| | | |
| | | return Html5Runtime.register( 'FilePaste', { |
| | | init: function() { |
| | | var opts = this.options, |
| | | elem = this.elem = opts.container, |
| | | accept = '.*', |
| | | arr, i, len, item; |
| | | |
| | | // accetp的mimeTypes中生成匹配正则。 |
| | | if ( opts.accept ) { |
| | | arr = []; |
| | | |
| | | for ( i = 0, len = opts.accept.length; i < len; i++ ) { |
| | | item = opts.accept[ i ].mimeTypes; |
| | | item && arr.push( item ); |
| | | } |
| | | |
| | | if ( arr.length ) { |
| | | accept = arr.join(','); |
| | | accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); |
| | | } |
| | | } |
| | | this.accept = accept = new RegExp( accept, 'i' ); |
| | | this.hander = Base.bindFn( this._pasteHander, this ); |
| | | elem.on( 'paste', this.hander ); |
| | | }, |
| | | |
| | | _pasteHander: function( e ) { |
| | | var allowed = [], |
| | | ruid = this.getRuid(), |
| | | items, item, blob, i, len; |
| | | |
| | | e = e.originalEvent || e; |
| | | items = e.clipboardData.items; |
| | | |
| | | for ( i = 0, len = items.length; i < len; i++ ) { |
| | | item = items[ i ]; |
| | | |
| | | if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { |
| | | continue; |
| | | } |
| | | |
| | | allowed.push( new File( ruid, blob ) ); |
| | | } |
| | | |
| | | if ( allowed.length ) { |
| | | // 不阻止非文件粘贴(文字粘贴)的事件冒泡 |
| | | e.preventDefault(); |
| | | e.stopPropagation(); |
| | | this.trigger( 'paste', allowed ); |
| | | } |
| | | }, |
| | | |
| | | destroy: function() { |
| | | this.elem.off( 'paste', this.hander ); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | /** |
| | | * @fileOverview FilePicker |
| | | */ |
| | | define('runtime/html5/filepicker',[ |
| | | 'base', |
| | | 'runtime/html5/runtime' |
| | | ], function( Base, Html5Runtime ) { |
| | | |
| | | var $ = Base.$; |
| | | |
| | | return Html5Runtime.register( 'FilePicker', { |
| | | init: function() { |
| | | var container = this.getRuntime().getContainer(), |
| | | me = this, |
| | | owner = me.owner, |
| | | opts = me.options, |
| | | lable = $( document.createElement('label') ), |
| | | input = $( document.createElement('input') ), |
| | | arr, i, len, mouseHandler; |
| | | |
| | | input.attr( 'type', 'file' ); |
| | | input.attr( 'name', opts.name ); |
| | | input.addClass('webuploader-element-invisible'); |
| | | |
| | | lable.on( 'click', function() { |
| | | input.trigger('click'); |
| | | }); |
| | | |
| | | lable.css({ |
| | | opacity: 0, |
| | | width: '100%', |
| | | height: '100%', |
| | | display: 'block', |
| | | cursor: 'pointer', |
| | | background: '#ffffff' |
| | | }); |
| | | |
| | | if ( opts.multiple ) { |
| | | input.attr( 'multiple', 'multiple' ); |
| | | } |
| | | |
| | | // @todo Firefox不支持单独指定后缀 |
| | | if ( opts.accept && opts.accept.length > 0 ) { |
| | | arr = []; |
| | | |
| | | for ( i = 0, len = opts.accept.length; i < len; i++ ) { |
| | | arr.push( opts.accept[ i ].mimeTypes ); |
| | | } |
| | | |
| | | input.attr( 'accept', arr.join(',') ); |
| | | } |
| | | |
| | | container.append( input ); |
| | | container.append( lable ); |
| | | |
| | | mouseHandler = function( e ) { |
| | | owner.trigger( e.type ); |
| | | }; |
| | | |
| | | input.on( 'change', function( e ) { |
| | | var fn = arguments.callee, |
| | | clone; |
| | | |
| | | me.files = e.target.files; |
| | | |
| | | // reset input |
| | | clone = this.cloneNode( true ); |
| | | this.parentNode.replaceChild( clone, this ); |
| | | |
| | | input.off(); |
| | | input = $( clone ).on( 'change', fn ) |
| | | .on( 'mouseenter mouseleave', mouseHandler ); |
| | | |
| | | owner.trigger('change'); |
| | | }); |
| | | |
| | | lable.on( 'mouseenter mouseleave', mouseHandler ); |
| | | |
| | | }, |
| | | |
| | | |
| | | getFiles: function() { |
| | | return this.files; |
| | | }, |
| | | |
| | | destroy: function() { |
| | | // todo |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview Transport |
| | | * @todo 支持chunked传输,优势: |
| | | * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, |
| | | * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 |
| | | */ |
| | | define('runtime/html5/transport',[ |
| | | 'base', |
| | | 'runtime/html5/runtime' |
| | | ], function( Base, Html5Runtime ) { |
| | | |
| | | var noop = Base.noop, |
| | | $ = Base.$; |
| | | |
| | | return Html5Runtime.register( 'Transport', { |
| | | init: function() { |
| | | this._status = 0; |
| | | this._response = null; |
| | | }, |
| | | |
| | | send: function() { |
| | | var owner = this.owner, |
| | | opts = this.options, |
| | | xhr = this._initAjax(), |
| | | blob = owner._blob, |
| | | server = opts.server, |
| | | formData, binary, fr; |
| | | |
| | | if ( opts.sendAsBinary ) { |
| | | server += (/\?/.test( server ) ? '&' : '?') + |
| | | $.param( owner._formData ); |
| | | |
| | | binary = blob.getSource(); |
| | | } else { |
| | | formData = new FormData(); |
| | | $.each( owner._formData, function( k, v ) { |
| | | formData.append( k, v ); |
| | | }); |
| | | |
| | | formData.append( opts.fileVal, blob.getSource(), |
| | | opts.filename || owner._formData.name || '' ); |
| | | } |
| | | |
| | | if ( opts.withCredentials && 'withCredentials' in xhr ) { |
| | | xhr.open( opts.method, server, true ); |
| | | xhr.withCredentials = true; |
| | | } else { |
| | | xhr.open( opts.method, server ); |
| | | } |
| | | |
| | | this._setRequestHeader( xhr, opts.headers ); |
| | | |
| | | if ( binary ) { |
| | | xhr.overrideMimeType('application/octet-stream'); |
| | | |
| | | // android直接发送blob会导致服务端接收到的是空文件。 |
| | | // bug详情。 |
| | | // https://code.google.com/p/android/issues/detail?id=39882 |
| | | // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 |
| | | if ( Base.os.android ) { |
| | | fr = new FileReader(); |
| | | |
| | | fr.onload = function() { |
| | | xhr.send( this.result ); |
| | | fr = fr.onload = null; |
| | | }; |
| | | |
| | | fr.readAsArrayBuffer( binary ); |
| | | } else { |
| | | xhr.send( binary ); |
| | | } |
| | | } else { |
| | | xhr.send( formData ); |
| | | } |
| | | }, |
| | | |
| | | getResponse: function() { |
| | | return this._response; |
| | | }, |
| | | |
| | | getResponseAsJson: function() { |
| | | return this._parseJson( this._response ); |
| | | }, |
| | | |
| | | getStatus: function() { |
| | | return this._status; |
| | | }, |
| | | |
| | | abort: function() { |
| | | var xhr = this._xhr; |
| | | |
| | | if ( xhr ) { |
| | | xhr.upload.onprogress = noop; |
| | | xhr.onreadystatechange = noop; |
| | | xhr.abort(); |
| | | |
| | | this._xhr = xhr = null; |
| | | } |
| | | }, |
| | | |
| | | destroy: function() { |
| | | this.abort(); |
| | | }, |
| | | |
| | | _initAjax: function() { |
| | | var me = this, |
| | | xhr = new XMLHttpRequest(), |
| | | opts = this.options; |
| | | |
| | | if ( opts.withCredentials && !('withCredentials' in xhr) && |
| | | typeof XDomainRequest !== 'undefined' ) { |
| | | xhr = new XDomainRequest(); |
| | | } |
| | | |
| | | xhr.upload.onprogress = function( e ) { |
| | | var percentage = 0; |
| | | |
| | | if ( e.lengthComputable ) { |
| | | percentage = e.loaded / e.total; |
| | | } |
| | | |
| | | return me.trigger( 'progress', percentage ); |
| | | }; |
| | | |
| | | xhr.onreadystatechange = function() { |
| | | |
| | | if ( xhr.readyState !== 4 ) { |
| | | return; |
| | | } |
| | | |
| | | xhr.upload.onprogress = noop; |
| | | xhr.onreadystatechange = noop; |
| | | me._xhr = null; |
| | | me._status = xhr.status; |
| | | |
| | | if ( xhr.status >= 200 && xhr.status < 300 ) { |
| | | me._response = xhr.responseText; |
| | | return me.trigger('load'); |
| | | } else if ( xhr.status >= 500 && xhr.status < 600 ) { |
| | | me._response = xhr.responseText; |
| | | return me.trigger( 'error', 'server' ); |
| | | } |
| | | |
| | | |
| | | return me.trigger( 'error', me._status ? 'http' : 'abort' ); |
| | | }; |
| | | |
| | | me._xhr = xhr; |
| | | return xhr; |
| | | }, |
| | | |
| | | _setRequestHeader: function( xhr, headers ) { |
| | | $.each( headers, function( key, val ) { |
| | | xhr.setRequestHeader( key, val ); |
| | | }); |
| | | }, |
| | | |
| | | _parseJson: function( str ) { |
| | | var json; |
| | | |
| | | try { |
| | | json = JSON.parse( str ); |
| | | } catch ( ex ) { |
| | | json = {}; |
| | | } |
| | | |
| | | return json; |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview FlashRuntime |
| | | */ |
| | | define('runtime/flash/runtime',[ |
| | | 'base', |
| | | 'runtime/runtime', |
| | | 'runtime/compbase' |
| | | ], function( Base, Runtime, CompBase ) { |
| | | |
| | | var $ = Base.$, |
| | | type = 'flash', |
| | | components = {}; |
| | | |
| | | |
| | | function getFlashVersion() { |
| | | var version; |
| | | |
| | | try { |
| | | version = navigator.plugins[ 'Shockwave Flash' ]; |
| | | version = version.description; |
| | | } catch ( ex ) { |
| | | try { |
| | | version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') |
| | | .GetVariable('$version'); |
| | | } catch ( ex2 ) { |
| | | version = '0.0'; |
| | | } |
| | | } |
| | | version = version.match( /\d+/g ); |
| | | return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); |
| | | } |
| | | |
| | | function FlashRuntime() { |
| | | var pool = {}, |
| | | clients = {}, |
| | | destory = this.destory, |
| | | me = this, |
| | | jsreciver = Base.guid('webuploader_'); |
| | | |
| | | Runtime.apply( me, arguments ); |
| | | me.type = type; |
| | | |
| | | |
| | | // 这个方法的调用者,实际上是RuntimeClient |
| | | me.exec = function( comp, fn/*, args...*/ ) { |
| | | var client = this, |
| | | uid = client.uid, |
| | | args = Base.slice( arguments, 2 ), |
| | | instance; |
| | | |
| | | clients[ uid ] = client; |
| | | |
| | | if ( components[ comp ] ) { |
| | | if ( !pool[ uid ] ) { |
| | | pool[ uid ] = new components[ comp ]( client, me ); |
| | | } |
| | | |
| | | instance = pool[ uid ]; |
| | | |
| | | if ( instance[ fn ] ) { |
| | | return instance[ fn ].apply( instance, args ); |
| | | } |
| | | } |
| | | |
| | | return me.flashExec.apply( client, arguments ); |
| | | }; |
| | | |
| | | function handler( evt, obj ) { |
| | | var type = evt.type || evt, |
| | | parts, uid; |
| | | |
| | | parts = type.split('::'); |
| | | uid = parts[ 0 ]; |
| | | type = parts[ 1 ]; |
| | | |
| | | // console.log.apply( console, arguments ); |
| | | |
| | | if ( type === 'Ready' && uid === me.uid ) { |
| | | me.trigger('ready'); |
| | | } else if ( clients[ uid ] ) { |
| | | clients[ uid ].trigger( type.toLowerCase(), evt, obj ); |
| | | } |
| | | |
| | | // Base.log( evt, obj ); |
| | | } |
| | | |
| | | // flash的接受器。 |
| | | window[ jsreciver ] = function() { |
| | | var args = arguments; |
| | | |
| | | // 为了能捕获得到。 |
| | | setTimeout(function() { |
| | | handler.apply( null, args ); |
| | | }, 1 ); |
| | | }; |
| | | |
| | | this.jsreciver = jsreciver; |
| | | |
| | | this.destory = function() { |
| | | // @todo 删除池子中的所有实例 |
| | | return destory && destory.apply( this, arguments ); |
| | | }; |
| | | |
| | | this.flashExec = function( comp, fn ) { |
| | | var flash = me.getFlash(), |
| | | args = Base.slice( arguments, 2 ); |
| | | |
| | | return flash.exec( this.uid, comp, fn, args ); |
| | | }; |
| | | |
| | | // @todo |
| | | } |
| | | |
| | | Base.inherits( Runtime, { |
| | | constructor: FlashRuntime, |
| | | |
| | | init: function() { |
| | | var container = this.getContainer(), |
| | | opts = this.options, |
| | | html; |
| | | |
| | | // if not the minimal height, shims are not initialized |
| | | // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) |
| | | container.css({ |
| | | position: 'absolute', |
| | | top: '-8px', |
| | | left: '-8px', |
| | | width: '9px', |
| | | height: '9px', |
| | | overflow: 'hidden' |
| | | }); |
| | | |
| | | // insert flash object |
| | | html = '<object id="' + this.uid + '" type="application/' + |
| | | 'x-shockwave-flash" data="' + opts.swf + '" '; |
| | | |
| | | if ( Base.browser.ie ) { |
| | | html += 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" '; |
| | | } |
| | | |
| | | html += 'width="100%" height="100%" style="outline:0">' + |
| | | '<param name="movie" value="' + opts.swf + '" />' + |
| | | '<param name="flashvars" value="uid=' + this.uid + |
| | | '&jsreciver=' + this.jsreciver + '" />' + |
| | | '<param name="wmode" value="transparent" />' + |
| | | '<param name="allowscriptaccess" value="always" />' + |
| | | '</object>'; |
| | | |
| | | container.html( html ); |
| | | }, |
| | | |
| | | getFlash: function() { |
| | | if ( this._flash ) { |
| | | return this._flash; |
| | | } |
| | | |
| | | this._flash = $( '#' + this.uid ).get( 0 ); |
| | | return this._flash; |
| | | } |
| | | |
| | | }); |
| | | |
| | | FlashRuntime.register = function( name, component ) { |
| | | component = components[ name ] = Base.inherits( CompBase, $.extend({ |
| | | |
| | | // @todo fix this later |
| | | flashExec: function() { |
| | | var owner = this.owner, |
| | | runtime = this.getRuntime(); |
| | | |
| | | return runtime.flashExec.apply( owner, arguments ); |
| | | } |
| | | }, component ) ); |
| | | |
| | | return component; |
| | | }; |
| | | |
| | | if ( getFlashVersion() >= 11.4 ) { |
| | | Runtime.addRuntime( type, FlashRuntime ); |
| | | } |
| | | |
| | | return FlashRuntime; |
| | | }); |
| | | /** |
| | | * @fileOverview FilePicker |
| | | */ |
| | | define('runtime/flash/filepicker',[ |
| | | 'base', |
| | | 'runtime/flash/runtime' |
| | | ], function( Base, FlashRuntime ) { |
| | | var $ = Base.$; |
| | | |
| | | return FlashRuntime.register( 'FilePicker', { |
| | | init: function( opts ) { |
| | | var copy = $.extend({}, opts ), |
| | | len, i; |
| | | |
| | | // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. |
| | | len = copy.accept && copy.accept.length; |
| | | for ( i = 0; i < len; i++ ) { |
| | | if ( !copy.accept[ i ].title ) { |
| | | copy.accept[ i ].title = 'Files'; |
| | | } |
| | | } |
| | | |
| | | delete copy.button; |
| | | delete copy.container; |
| | | |
| | | this.flashExec( 'FilePicker', 'init', copy ); |
| | | }, |
| | | |
| | | destroy: function() { |
| | | // todo |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview Transport flash实现 |
| | | */ |
| | | define('runtime/flash/transport',[ |
| | | 'base', |
| | | 'runtime/flash/runtime', |
| | | 'runtime/client' |
| | | ], function( Base, FlashRuntime, RuntimeClient ) { |
| | | var $ = Base.$; |
| | | |
| | | return FlashRuntime.register( 'Transport', { |
| | | init: function() { |
| | | this._status = 0; |
| | | this._response = null; |
| | | this._responseJson = null; |
| | | }, |
| | | |
| | | send: function() { |
| | | var owner = this.owner, |
| | | opts = this.options, |
| | | xhr = this._initAjax(), |
| | | blob = owner._blob, |
| | | server = opts.server, |
| | | binary; |
| | | |
| | | xhr.connectRuntime( blob.ruid ); |
| | | |
| | | if ( opts.sendAsBinary ) { |
| | | server += (/\?/.test( server ) ? '&' : '?') + |
| | | $.param( owner._formData ); |
| | | |
| | | binary = blob.uid; |
| | | } else { |
| | | $.each( owner._formData, function( k, v ) { |
| | | xhr.exec( 'append', k, v ); |
| | | }); |
| | | |
| | | xhr.exec( 'appendBlob', opts.fileVal, blob.uid, |
| | | opts.filename || owner._formData.name || '' ); |
| | | } |
| | | |
| | | this._setRequestHeader( xhr, opts.headers ); |
| | | xhr.exec( 'send', { |
| | | method: opts.method, |
| | | url: server |
| | | }, binary ); |
| | | }, |
| | | |
| | | getStatus: function() { |
| | | return this._status; |
| | | }, |
| | | |
| | | getResponse: function() { |
| | | return this._response; |
| | | }, |
| | | |
| | | getResponseAsJson: function() { |
| | | return this._responseJson; |
| | | }, |
| | | |
| | | abort: function() { |
| | | var xhr = this._xhr; |
| | | |
| | | if ( xhr ) { |
| | | xhr.exec('abort'); |
| | | xhr.destroy(); |
| | | this._xhr = xhr = null; |
| | | } |
| | | }, |
| | | |
| | | destroy: function() { |
| | | this.abort(); |
| | | }, |
| | | |
| | | _initAjax: function() { |
| | | var me = this, |
| | | xhr = new RuntimeClient('XMLHttpRequest'); |
| | | |
| | | xhr.on( 'uploadprogress progress', function( e ) { |
| | | return me.trigger( 'progress', e.loaded / e.total ); |
| | | }); |
| | | |
| | | xhr.on( 'load', function() { |
| | | var status = xhr.exec('getStatus'), |
| | | err = ''; |
| | | |
| | | xhr.off(); |
| | | me._xhr = null; |
| | | |
| | | if ( status >= 200 && status < 300 ) { |
| | | me._response = xhr.exec('getResponse'); |
| | | me._responseJson = xhr.exec('getResponseAsJson'); |
| | | } else if ( status >= 500 && status < 600 ) { |
| | | me._response = xhr.exec('getResponse'); |
| | | me._responseJson = xhr.exec('getResponseAsJson'); |
| | | err = 'server'; |
| | | } else { |
| | | err = 'http'; |
| | | } |
| | | |
| | | xhr.destroy(); |
| | | xhr = null; |
| | | |
| | | return err ? me.trigger( 'error', err ) : me.trigger('load'); |
| | | }); |
| | | |
| | | xhr.on( 'error', function() { |
| | | xhr.off(); |
| | | me._xhr = null; |
| | | me.trigger( 'error', 'http' ); |
| | | }); |
| | | |
| | | me._xhr = xhr; |
| | | return xhr; |
| | | }, |
| | | |
| | | _setRequestHeader: function( xhr, headers ) { |
| | | $.each( headers, function( key, val ) { |
| | | xhr.exec( 'setRequestHeader', key, val ); |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | /** |
| | | * @fileOverview 没有图像处理的版本。 |
| | | */ |
| | | define('preset/withoutimage',[ |
| | | 'base', |
| | | |
| | | // widgets |
| | | 'widgets/filednd', |
| | | 'widgets/filepaste', |
| | | 'widgets/filepicker', |
| | | 'widgets/queue', |
| | | 'widgets/runtime', |
| | | 'widgets/upload', |
| | | 'widgets/validator', |
| | | |
| | | // runtimes |
| | | // html5 |
| | | 'runtime/html5/blob', |
| | | 'runtime/html5/dnd', |
| | | 'runtime/html5/filepaste', |
| | | 'runtime/html5/filepicker', |
| | | 'runtime/html5/transport', |
| | | |
| | | // flash |
| | | 'runtime/flash/filepicker', |
| | | 'runtime/flash/transport' |
| | | ], function( Base ) { |
| | | return Base; |
| | | }); |
| | | define('webuploader',[ |
| | | 'preset/withoutimage' |
| | | ], function( preset ) { |
| | | return preset; |
| | | }); |
| | | return require('webuploader'); |
| | | }); |
| | | /*! WebUploader 0.1.2 */
|
| | |
|
| | |
|
| | | /**
|
| | | * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。
|
| | | *
|
| | | * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。
|
| | | */
|
| | | (function( root, factory ) {
|
| | | var modules = {},
|
| | |
|
| | | // 内部require, 简单不完全实现。
|
| | | // https://github.com/amdjs/amdjs-api/wiki/require
|
| | | _require = function( deps, callback ) {
|
| | | var args, len, i;
|
| | |
|
| | | // 如果deps不是数组,则直接返回指定module
|
| | | if ( typeof deps === 'string' ) {
|
| | | return getModule( deps );
|
| | | } else {
|
| | | args = [];
|
| | | for( len = deps.length, i = 0; i < len; i++ ) {
|
| | | args.push( getModule( deps[ i ] ) );
|
| | | }
|
| | |
|
| | | return callback.apply( null, args );
|
| | | }
|
| | | },
|
| | |
|
| | | // 内部define,暂时不支持不指定id.
|
| | | _define = function( id, deps, factory ) {
|
| | | if ( arguments.length === 2 ) {
|
| | | factory = deps;
|
| | | deps = null;
|
| | | }
|
| | |
|
| | | _require( deps || [], function() {
|
| | | setModule( id, factory, arguments );
|
| | | });
|
| | | },
|
| | |
|
| | | // 设置module, 兼容CommonJs写法。
|
| | | setModule = function( id, factory, args ) {
|
| | | var module = {
|
| | | exports: factory
|
| | | },
|
| | | returned;
|
| | |
|
| | | if ( typeof factory === 'function' ) {
|
| | | args.length || (args = [ _require, module.exports, module ]);
|
| | | returned = factory.apply( null, args );
|
| | | returned !== undefined && (module.exports = returned);
|
| | | }
|
| | |
|
| | | modules[ id ] = module.exports;
|
| | | },
|
| | |
|
| | | // 根据id获取module
|
| | | getModule = function( id ) {
|
| | | var module = modules[ id ] || root[ id ];
|
| | |
|
| | | if ( !module ) {
|
| | | throw new Error( '`' + id + '` is undefined' );
|
| | | }
|
| | |
|
| | | return module;
|
| | | },
|
| | |
|
| | | // 将所有modules,将路径ids装换成对象。
|
| | | exportsTo = function( obj ) {
|
| | | var key, host, parts, part, last, ucFirst;
|
| | |
|
| | | // make the first character upper case.
|
| | | ucFirst = function( str ) {
|
| | | return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 ));
|
| | | };
|
| | |
|
| | | for ( key in modules ) {
|
| | | host = obj;
|
| | |
|
| | | if ( !modules.hasOwnProperty( key ) ) {
|
| | | continue;
|
| | | }
|
| | |
|
| | | parts = key.split('/');
|
| | | last = ucFirst( parts.pop() );
|
| | |
|
| | | while( (part = ucFirst( parts.shift() )) ) {
|
| | | host[ part ] = host[ part ] || {};
|
| | | host = host[ part ];
|
| | | }
|
| | |
|
| | | host[ last ] = modules[ key ];
|
| | | }
|
| | | },
|
| | |
|
| | | exports = factory( root, _define, _require ),
|
| | | origin;
|
| | |
|
| | | // exports every module.
|
| | | exportsTo( exports );
|
| | |
|
| | | if ( typeof module === 'object' && typeof module.exports === 'object' ) {
|
| | |
|
| | | // For CommonJS and CommonJS-like environments where a proper window is present,
|
| | | module.exports = exports;
|
| | | } else if ( typeof define === 'function' && define.amd ) {
|
| | |
|
| | | // Allow using this built library as an AMD module
|
| | | // in another project. That other project will only
|
| | | // see this AMD call, not the internal modules in
|
| | | // the closure below.
|
| | | define([], exports );
|
| | | } else {
|
| | |
|
| | | // Browser globals case. Just assign the
|
| | | // result to a property on the global.
|
| | | origin = root.WebUploader;
|
| | | root.WebUploader = exports;
|
| | | root.WebUploader.noConflict = function() {
|
| | | root.WebUploader = origin;
|
| | | };
|
| | | }
|
| | | })( this, function( window, define, require ) {
|
| | |
|
| | |
|
| | | /**
|
| | | * @fileOverview jQuery or Zepto
|
| | | */
|
| | | define('dollar-third',[],function() {
|
| | | return window.jQuery || window.Zepto;
|
| | | });
|
| | | /**
|
| | | * @fileOverview Dom 操作相关
|
| | | */
|
| | | define('dollar',[
|
| | | 'dollar-third'
|
| | | ], function( _ ) {
|
| | | return _;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 使用jQuery的Promise
|
| | | */
|
| | | define('promise-third',[
|
| | | 'dollar'
|
| | | ], function( $ ) {
|
| | | return {
|
| | | Deferred: $.Deferred,
|
| | | when: $.when,
|
| | | |
| | | isPromise: function( anything ) {
|
| | | return anything && typeof anything.then === 'function';
|
| | | }
|
| | | };
|
| | | });
|
| | | /**
|
| | | * @fileOverview Promise/A+
|
| | | */
|
| | | define('promise',[
|
| | | 'promise-third'
|
| | | ], function( _ ) {
|
| | | return _;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 基础类方法。
|
| | | */
|
| | | |
| | | /**
|
| | | * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。
|
| | | *
|
| | | * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id.
|
| | | * 默认module id该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如:
|
| | | *
|
| | | * * module `base`:WebUploader.Base
|
| | | * * module `file`: WebUploader.File
|
| | | * * module `lib/dnd`: WebUploader.Lib.Dnd
|
| | | * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd
|
| | | *
|
| | | *
|
| | | * 以下文档将可能省略`WebUploader`前缀。
|
| | | * @module WebUploader
|
| | | * @title WebUploader API文档
|
| | | */
|
| | | define('base',[
|
| | | 'dollar',
|
| | | 'promise'
|
| | | ], function( $, promise ) {
|
| | | |
| | | var noop = function() {},
|
| | | call = Function.call;
|
| | | |
| | | // http://jsperf.com/uncurrythis
|
| | | // 反科里化
|
| | | function uncurryThis( fn ) {
|
| | | return function() {
|
| | | return call.apply( fn, arguments );
|
| | | };
|
| | | }
|
| | | |
| | | function bindFn( fn, context ) {
|
| | | return function() {
|
| | | return fn.apply( context, arguments );
|
| | | };
|
| | | }
|
| | | |
| | | function createObject( proto ) {
|
| | | var f;
|
| | | |
| | | if ( Object.create ) {
|
| | | return Object.create( proto );
|
| | | } else {
|
| | | f = function() {};
|
| | | f.prototype = proto;
|
| | | return new f();
|
| | | }
|
| | | }
|
| | | |
| | | |
| | | /**
|
| | | * 基础类,提供一些简单常用的方法。
|
| | | * @class Base
|
| | | */
|
| | | return {
|
| | | |
| | | /**
|
| | | * @property {String} version 当前版本号。
|
| | | */
|
| | | version: '0.1.2',
|
| | | |
| | | /**
|
| | | * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。
|
| | | */
|
| | | $: $,
|
| | | |
| | | Deferred: promise.Deferred,
|
| | | |
| | | isPromise: promise.isPromise,
|
| | | |
| | | when: promise.when,
|
| | | |
| | | /**
|
| | | * @description 简单的浏览器检查结果。
|
| | | *
|
| | | * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。
|
| | | * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。
|
| | | * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+**
|
| | | * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。
|
| | | * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。
|
| | | * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。
|
| | | *
|
| | | * @property {Object} [browser]
|
| | | */
|
| | | browser: (function( ua ) {
|
| | | var ret = {},
|
| | | webkit = ua.match( /WebKit\/([\d.]+)/ ),
|
| | | chrome = ua.match( /Chrome\/([\d.]+)/ ) ||
|
| | | ua.match( /CriOS\/([\d.]+)/ ),
|
| | | |
| | | ie = ua.match( /MSIE\s([\d\.]+)/ ) ||
|
| | | ua.match(/(?:trident)(?:.*rv:([\w.]+))?/i),
|
| | | firefox = ua.match( /Firefox\/([\d.]+)/ ),
|
| | | safari = ua.match( /Safari\/([\d.]+)/ ),
|
| | | opera = ua.match( /OPR\/([\d.]+)/ );
|
| | | |
| | | webkit && (ret.webkit = parseFloat( webkit[ 1 ] ));
|
| | | chrome && (ret.chrome = parseFloat( chrome[ 1 ] ));
|
| | | ie && (ret.ie = parseFloat( ie[ 1 ] ));
|
| | | firefox && (ret.firefox = parseFloat( firefox[ 1 ] ));
|
| | | safari && (ret.safari = parseFloat( safari[ 1 ] ));
|
| | | opera && (ret.opera = parseFloat( opera[ 1 ] ));
|
| | | |
| | | return ret;
|
| | | })( navigator.userAgent ),
|
| | | |
| | | /**
|
| | | * @description 操作系统检查结果。
|
| | | *
|
| | | * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。
|
| | | * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。
|
| | | * @property {Object} [os]
|
| | | */
|
| | | os: (function( ua ) {
|
| | | var ret = {},
|
| | | |
| | | // osx = !!ua.match( /\(Macintosh\; Intel / ),
|
| | | android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ),
|
| | | ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ );
|
| | | |
| | | // osx && (ret.osx = true);
|
| | | android && (ret.android = parseFloat( android[ 1 ] ));
|
| | | ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) ));
|
| | | |
| | | return ret;
|
| | | })( navigator.userAgent ),
|
| | | |
| | | /**
|
| | | * 实现类与类之间的继承。
|
| | | * @method inherits
|
| | | * @grammar Base.inherits( super ) => child
|
| | | * @grammar Base.inherits( super, protos ) => child
|
| | | * @grammar Base.inherits( super, protos, statics ) => child
|
| | | * @param {Class} super 父类
|
| | | * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。
|
| | | * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。
|
| | | * @param {Object} [statics] 静态属性或方法。
|
| | | * @return {Class} 返回子类。
|
| | | * @example
|
| | | * function Person() {
|
| | | * console.log( 'Super' );
|
| | | * }
|
| | | * Person.prototype.hello = function() {
|
| | | * console.log( 'hello' );
|
| | | * };
|
| | | *
|
| | | * var Manager = Base.inherits( Person, {
|
| | | * world: function() {
|
| | | * console.log( 'World' );
|
| | | * }
|
| | | * });
|
| | | *
|
| | | * // 因为没有指定构造器,父类的构造器将会执行。
|
| | | * var instance = new Manager(); // => Super
|
| | | *
|
| | | * // 继承子父类的方法
|
| | | * instance.hello(); // => hello
|
| | | * instance.world(); // => World
|
| | | *
|
| | | * // 子类的__super__属性指向父类
|
| | | * console.log( Manager.__super__ === Person ); // => true
|
| | | */
|
| | | inherits: function( Super, protos, staticProtos ) {
|
| | | var child;
|
| | | |
| | | if ( typeof protos === 'function' ) {
|
| | | child = protos;
|
| | | protos = null;
|
| | | } else if ( protos && protos.hasOwnProperty('constructor') ) {
|
| | | child = protos.constructor;
|
| | | } else {
|
| | | child = function() {
|
| | | return Super.apply( this, arguments );
|
| | | };
|
| | | }
|
| | | |
| | | // 复制静态方法
|
| | | $.extend( true, child, Super, staticProtos || {} );
|
| | | |
| | | /* jshint camelcase: false */
|
| | | |
| | | // 让子类的__super__属性指向父类。
|
| | | child.__super__ = Super.prototype;
|
| | | |
| | | // 构建原型,添加原型方法或属性。
|
| | | // 暂时用Object.create实现。
|
| | | child.prototype = createObject( Super.prototype );
|
| | | protos && $.extend( true, child.prototype, protos );
|
| | | |
| | | return child;
|
| | | },
|
| | | |
| | | /**
|
| | | * 一个不做任何事情的方法。可以用来赋值给默认的callback.
|
| | | * @method noop
|
| | | */
|
| | | noop: noop,
|
| | | |
| | | /**
|
| | | * 返回一个新的方法,此方法将已指定的`context`来执行。
|
| | | * @grammar Base.bindFn( fn, context ) => Function
|
| | | * @method bindFn
|
| | | * @example
|
| | | * var doSomething = function() {
|
| | | * console.log( this.name );
|
| | | * },
|
| | | * obj = {
|
| | | * name: 'Object Name'
|
| | | * },
|
| | | * aliasFn = Base.bind( doSomething, obj );
|
| | | *
|
| | | * aliasFn(); // => Object Name
|
| | | *
|
| | | */
|
| | | bindFn: bindFn,
|
| | | |
| | | /**
|
| | | * 引用Console.log如果存在的话,否则引用一个[空函数loop](#WebUploader:Base.log)。
|
| | | * @grammar Base.log( args... ) => undefined
|
| | | * @method log
|
| | | */
|
| | | log: (function() {
|
| | | if ( window.console ) {
|
| | | return bindFn( console.log, console );
|
| | | }
|
| | | return noop;
|
| | | })(),
|
| | | |
| | | nextTick: (function() {
|
| | | |
| | | return function( cb ) {
|
| | | setTimeout( cb, 1 );
|
| | | };
|
| | | |
| | | // @bug 当浏览器不在当前窗口时就停了。
|
| | | // var next = window.requestAnimationFrame ||
|
| | | // window.webkitRequestAnimationFrame ||
|
| | | // window.mozRequestAnimationFrame ||
|
| | | // function( cb ) {
|
| | | // window.setTimeout( cb, 1000 / 60 );
|
| | | // };
|
| | | |
| | | // // fix: Uncaught TypeError: Illegal invocation
|
| | | // return bindFn( next, window );
|
| | | })(),
|
| | | |
| | | /**
|
| | | * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。
|
| | | * 将用来将非数组对象转化成数组对象。
|
| | | * @grammar Base.slice( target, start[, end] ) => Array
|
| | | * @method slice
|
| | | * @example
|
| | | * function doSomthing() {
|
| | | * var args = Base.slice( arguments, 1 );
|
| | | * console.log( args );
|
| | | * }
|
| | | *
|
| | | * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"]
|
| | | */
|
| | | slice: uncurryThis( [].slice ),
|
| | | |
| | | /**
|
| | | * 生成唯一的ID
|
| | | * @method guid
|
| | | * @grammar Base.guid() => String
|
| | | * @grammar Base.guid( prefx ) => String
|
| | | */
|
| | | guid: (function() {
|
| | | var counter = 0;
|
| | | |
| | | return function( prefix ) {
|
| | | var guid = (+new Date()).toString( 32 ),
|
| | | i = 0;
|
| | | |
| | | for ( ; i < 5; i++ ) {
|
| | | guid += Math.floor( Math.random() * 65535 ).toString( 32 );
|
| | | }
|
| | | |
| | | return (prefix || 'wu_') + guid + (counter++).toString( 32 );
|
| | | };
|
| | | })(),
|
| | | |
| | | /**
|
| | | * 格式化文件大小, 输出成带单位的字符串
|
| | | * @method formatSize
|
| | | * @grammar Base.formatSize( size ) => String
|
| | | * @grammar Base.formatSize( size, pointLength ) => String
|
| | | * @grammar Base.formatSize( size, pointLength, units ) => String
|
| | | * @param {Number} size 文件大小
|
| | | * @param {Number} [pointLength=2] 精确到的小数点数。
|
| | | * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K.
|
| | | * @example
|
| | | * console.log( Base.formatSize( 100 ) ); // => 100B
|
| | | * console.log( Base.formatSize( 1024 ) ); // => 1.00K
|
| | | * console.log( Base.formatSize( 1024, 0 ) ); // => 1K
|
| | | * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M
|
| | | * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G
|
| | | * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB
|
| | | */
|
| | | formatSize: function( size, pointLength, units ) {
|
| | | var unit;
|
| | | |
| | | units = units || [ 'B', 'K', 'M', 'G', 'TB' ];
|
| | | |
| | | while ( (unit = units.shift()) && size > 1024 ) {
|
| | | size = size / 1024;
|
| | | }
|
| | | |
| | | return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) +
|
| | | unit;
|
| | | }
|
| | | };
|
| | | });
|
| | | /**
|
| | | * 事件处理类,可以独立使用,也可以扩展给对象使用。
|
| | | * @fileOverview Mediator
|
| | | */
|
| | | define('mediator',[
|
| | | 'base'
|
| | | ], function( Base ) {
|
| | | var $ = Base.$,
|
| | | slice = [].slice,
|
| | | separator = /\s+/,
|
| | | protos;
|
| | | |
| | | // 根据条件过滤出事件handlers.
|
| | | function findHandlers( arr, name, callback, context ) {
|
| | | return $.grep( arr, function( handler ) {
|
| | | return handler &&
|
| | | (!name || handler.e === name) &&
|
| | | (!callback || handler.cb === callback ||
|
| | | handler.cb._cb === callback) &&
|
| | | (!context || handler.ctx === context);
|
| | | });
|
| | | }
|
| | | |
| | | function eachEvent( events, callback, iterator ) {
|
| | | // 不支持对象,只支持多个event用空格隔开
|
| | | $.each( (events || '').split( separator ), function( _, key ) {
|
| | | iterator( key, callback );
|
| | | });
|
| | | }
|
| | | |
| | | function triggerHanders( events, args ) {
|
| | | var stoped = false,
|
| | | i = -1,
|
| | | len = events.length,
|
| | | handler;
|
| | | |
| | | while ( ++i < len ) {
|
| | | handler = events[ i ];
|
| | | |
| | | if ( handler.cb.apply( handler.ctx2, args ) === false ) {
|
| | | stoped = true;
|
| | | break;
|
| | | }
|
| | | }
|
| | | |
| | | return !stoped;
|
| | | }
|
| | | |
| | | protos = {
|
| | | |
| | | /**
|
| | | * 绑定事件。
|
| | | *
|
| | | * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如
|
| | | * ```javascript
|
| | | * var obj = {};
|
| | | *
|
| | | * // 使得obj有事件行为
|
| | | * Mediator.installTo( obj );
|
| | | *
|
| | | * obj.on( 'testa', function( arg1, arg2 ) {
|
| | | * console.log( arg1, arg2 ); // => 'arg1', 'arg2'
|
| | | * });
|
| | | *
|
| | | * obj.trigger( 'testa', 'arg1', 'arg2' );
|
| | | * ```
|
| | | *
|
| | | * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。
|
| | | * 切会影响到`trigger`方法的返回值,为`false`。
|
| | | *
|
| | | * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处,
|
| | | * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。
|
| | | * ```javascript
|
| | | * obj.on( 'all', function( type, arg1, arg2 ) {
|
| | | * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2'
|
| | | * });
|
| | | * ```
|
| | | *
|
| | | * @method on
|
| | | * @grammar on( name, callback[, context] ) => self
|
| | | * @param {String} name 事件名,支持多个事件用空格隔开
|
| | | * @param {Function} callback 事件处理器
|
| | | * @param {Object} [context] 事件处理器的上下文。
|
| | | * @return {self} 返回自身,方便链式
|
| | | * @chainable
|
| | | * @class Mediator
|
| | | */
|
| | | on: function( name, callback, context ) {
|
| | | var me = this,
|
| | | set;
|
| | | |
| | | if ( !callback ) {
|
| | | return this;
|
| | | }
|
| | | |
| | | set = this._events || (this._events = []);
|
| | | |
| | | eachEvent( name, callback, function( name, callback ) {
|
| | | var handler = { e: name };
|
| | | |
| | | handler.cb = callback;
|
| | | handler.ctx = context;
|
| | | handler.ctx2 = context || me;
|
| | | handler.id = set.length;
|
| | | |
| | | set.push( handler );
|
| | | });
|
| | | |
| | | return this;
|
| | | },
|
| | | |
| | | /**
|
| | | * 绑定事件,且当handler执行完后,自动解除绑定。
|
| | | * @method once
|
| | | * @grammar once( name, callback[, context] ) => self
|
| | | * @param {String} name 事件名
|
| | | * @param {Function} callback 事件处理器
|
| | | * @param {Object} [context] 事件处理器的上下文。
|
| | | * @return {self} 返回自身,方便链式
|
| | | * @chainable
|
| | | */
|
| | | once: function( name, callback, context ) {
|
| | | var me = this;
|
| | | |
| | | if ( !callback ) {
|
| | | return me;
|
| | | }
|
| | | |
| | | eachEvent( name, callback, function( name, callback ) {
|
| | | var once = function() {
|
| | | me.off( name, once );
|
| | | return callback.apply( context || me, arguments );
|
| | | };
|
| | | |
| | | once._cb = callback;
|
| | | me.on( name, once, context );
|
| | | });
|
| | | |
| | | return me;
|
| | | },
|
| | | |
| | | /**
|
| | | * 解除事件绑定
|
| | | * @method off
|
| | | * @grammar off( [name[, callback[, context] ] ] ) => self
|
| | | * @param {String} [name] 事件名
|
| | | * @param {Function} [callback] 事件处理器
|
| | | * @param {Object} [context] 事件处理器的上下文。
|
| | | * @return {self} 返回自身,方便链式
|
| | | * @chainable
|
| | | */
|
| | | off: function( name, cb, ctx ) {
|
| | | var events = this._events;
|
| | | |
| | | if ( !events ) {
|
| | | return this;
|
| | | }
|
| | | |
| | | if ( !name && !cb && !ctx ) {
|
| | | this._events = [];
|
| | | return this;
|
| | | }
|
| | | |
| | | eachEvent( name, cb, function( name, cb ) {
|
| | | $.each( findHandlers( events, name, cb, ctx ), function() {
|
| | | delete events[ this.id ];
|
| | | });
|
| | | });
|
| | | |
| | | return this;
|
| | | },
|
| | | |
| | | /**
|
| | | * 触发事件
|
| | | * @method trigger
|
| | | * @grammar trigger( name[, args...] ) => self
|
| | | * @param {String} type 事件名
|
| | | * @param {*} [...] 任意参数
|
| | | * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true
|
| | | */
|
| | | trigger: function( type ) {
|
| | | var args, events, allEvents;
|
| | | |
| | | if ( !this._events || !type ) {
|
| | | return this;
|
| | | }
|
| | | |
| | | args = slice.call( arguments, 1 );
|
| | | events = findHandlers( this._events, type );
|
| | | allEvents = findHandlers( this._events, 'all' );
|
| | | |
| | | return triggerHanders( events, args ) &&
|
| | | triggerHanders( allEvents, arguments );
|
| | | }
|
| | | };
|
| | | |
| | | /**
|
| | | * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。
|
| | | * 主要目的是负责模块与模块之间的合作,降低耦合度。
|
| | | *
|
| | | * @class Mediator
|
| | | */
|
| | | return $.extend({
|
| | | |
| | | /**
|
| | | * 可以通过这个接口,使任何对象具备事件功能。
|
| | | * @method installTo
|
| | | * @param {Object} obj 需要具备事件行为的对象。
|
| | | * @return {Object} 返回obj.
|
| | | */
|
| | | installTo: function( obj ) {
|
| | | return $.extend( obj, protos );
|
| | | }
|
| | | |
| | | }, protos );
|
| | | });
|
| | | /**
|
| | | * @fileOverview Uploader上传类
|
| | | */
|
| | | define('uploader',[
|
| | | 'base',
|
| | | 'mediator'
|
| | | ], function( Base, Mediator ) {
|
| | | |
| | | var $ = Base.$;
|
| | | |
| | | /**
|
| | | * 上传入口类。
|
| | | * @class Uploader
|
| | | * @constructor
|
| | | * @grammar new Uploader( opts ) => Uploader
|
| | | * @example
|
| | | * var uploader = WebUploader.Uploader({
|
| | | * swf: 'path_of_swf/Uploader.swf',
|
| | | *
|
| | | * // 开起分片上传。
|
| | | * chunked: true
|
| | | * });
|
| | | */
|
| | | function Uploader( opts ) {
|
| | | this.options = $.extend( true, {}, Uploader.options, opts );
|
| | | this._init( this.options );
|
| | | }
|
| | | |
| | | // default Options
|
| | | // widgets中有相应扩展
|
| | | Uploader.options = {};
|
| | | Mediator.installTo( Uploader.prototype );
|
| | | |
| | | // 批量添加纯命令式方法。
|
| | | $.each({
|
| | | upload: 'start-upload',
|
| | | stop: 'stop-upload',
|
| | | getFile: 'get-file',
|
| | | getFiles: 'get-files',
|
| | | addFile: 'add-file',
|
| | | addFiles: 'add-file',
|
| | | sort: 'sort-files',
|
| | | removeFile: 'remove-file',
|
| | | skipFile: 'skip-file',
|
| | | retry: 'retry',
|
| | | isInProgress: 'is-in-progress',
|
| | | makeThumb: 'make-thumb',
|
| | | getDimension: 'get-dimension',
|
| | | addButton: 'add-btn',
|
| | | getRuntimeType: 'get-runtime-type',
|
| | | refresh: 'refresh',
|
| | | disable: 'disable',
|
| | | enable: 'enable',
|
| | | reset: 'reset'
|
| | | }, function( fn, command ) {
|
| | | Uploader.prototype[ fn ] = function() {
|
| | | return this.request( command, arguments );
|
| | | };
|
| | | });
|
| | | |
| | | $.extend( Uploader.prototype, {
|
| | | state: 'pending',
|
| | | |
| | | _init: function( opts ) {
|
| | | var me = this;
|
| | | |
| | | me.request( 'init', opts, function() {
|
| | | me.state = 'ready';
|
| | | me.trigger('ready');
|
| | | });
|
| | | },
|
| | | |
| | | /**
|
| | | * 获取或者设置Uploader配置项。
|
| | | * @method option
|
| | | * @grammar option( key ) => *
|
| | | * @grammar option( key, val ) => self
|
| | | * @example
|
| | | *
|
| | | * // 初始状态图片上传前不会压缩
|
| | | * var uploader = new WebUploader.Uploader({
|
| | | * resize: null;
|
| | | * });
|
| | | *
|
| | | * // 修改后图片上传前,尝试将图片压缩到1600 * 1600
|
| | | * uploader.options( 'resize', {
|
| | | * width: 1600,
|
| | | * height: 1600
|
| | | * });
|
| | | */
|
| | | option: function( key, val ) {
|
| | | var opts = this.options;
|
| | | |
| | | // setter
|
| | | if ( arguments.length > 1 ) {
|
| | | |
| | | if ( $.isPlainObject( val ) &&
|
| | | $.isPlainObject( opts[ key ] ) ) {
|
| | | $.extend( opts[ key ], val );
|
| | | } else {
|
| | | opts[ key ] = val;
|
| | | }
|
| | | |
| | | } else { // getter
|
| | | return key ? opts[ key ] : opts;
|
| | | }
|
| | | },
|
| | | |
| | | /**
|
| | | * 获取文件统计信息。返回一个包含一下信息的对象。
|
| | | * * `successNum` 上传成功的文件数
|
| | | * * `uploadFailNum` 上传失败的文件数
|
| | | * * `cancelNum` 被删除的文件数
|
| | | * * `invalidNum` 无效的文件数
|
| | | * * `queueNum` 还在队列中的文件数
|
| | | * @method getStats
|
| | | * @grammar getStats() => Object
|
| | | */
|
| | | getStats: function() {
|
| | | // return this._mgr.getStats.apply( this._mgr, arguments );
|
| | | var stats = this.request('get-stats');
|
| | | |
| | | return {
|
| | | successNum: stats.numOfSuccess,
|
| | | |
| | | // who care?
|
| | | // queueFailNum: 0,
|
| | | cancelNum: stats.numOfCancel,
|
| | | invalidNum: stats.numOfInvalid,
|
| | | uploadFailNum: stats.numOfUploadFailed,
|
| | | queueNum: stats.numOfQueue
|
| | | };
|
| | | },
|
| | | |
| | | // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器
|
| | | trigger: function( type/*, args...*/ ) {
|
| | | var args = [].slice.call( arguments, 1 ),
|
| | | opts = this.options,
|
| | | name = 'on' + type.substring( 0, 1 ).toUpperCase() +
|
| | | type.substring( 1 );
|
| | | |
| | | if (
|
| | | // 调用通过on方法注册的handler.
|
| | | Mediator.trigger.apply( this, arguments ) === false ||
|
| | | |
| | | // 调用opts.onEvent
|
| | | $.isFunction( opts[ name ] ) &&
|
| | | opts[ name ].apply( this, args ) === false ||
|
| | | |
| | | // 调用this.onEvent
|
| | | $.isFunction( this[ name ] ) &&
|
| | | this[ name ].apply( this, args ) === false ||
|
| | | |
| | | // 广播所有uploader的事件。
|
| | | Mediator.trigger.apply( Mediator,
|
| | | [ this, type ].concat( args ) ) === false ) {
|
| | | |
| | | return false;
|
| | | }
|
| | | |
| | | return true;
|
| | | },
|
| | | |
| | | // widgets/widget.js将补充此方法的详细文档。
|
| | | request: Base.noop
|
| | | });
|
| | | |
| | | /**
|
| | | * 创建Uploader实例,等同于new Uploader( opts );
|
| | | * @method create
|
| | | * @class Base
|
| | | * @static
|
| | | * @grammar Base.create( opts ) => Uploader
|
| | | */
|
| | | Base.create = Uploader.create = function( opts ) {
|
| | | return new Uploader( opts );
|
| | | };
|
| | | |
| | | // 暴露Uploader,可以通过它来扩展业务逻辑。
|
| | | Base.Uploader = Uploader;
|
| | | |
| | | return Uploader;
|
| | | });
|
| | | /**
|
| | | * @fileOverview Runtime管理器,负责Runtime的选择, 连接
|
| | | */
|
| | | define('runtime/runtime',[
|
| | | 'base',
|
| | | 'mediator'
|
| | | ], function( Base, Mediator ) {
|
| | | |
| | | var $ = Base.$,
|
| | | factories = {},
|
| | | |
| | | // 获取对象的第一个key
|
| | | getFirstKey = function( obj ) {
|
| | | for ( var key in obj ) {
|
| | | if ( obj.hasOwnProperty( key ) ) {
|
| | | return key;
|
| | | }
|
| | | }
|
| | | return null;
|
| | | };
|
| | | |
| | | // 接口类。
|
| | | function Runtime( options ) {
|
| | | this.options = $.extend({
|
| | | container: document.body
|
| | | }, options );
|
| | | this.uid = Base.guid('rt_');
|
| | | }
|
| | | |
| | | $.extend( Runtime.prototype, {
|
| | | |
| | | getContainer: function() {
|
| | | var opts = this.options,
|
| | | parent, container;
|
| | | |
| | | if ( this._container ) {
|
| | | return this._container;
|
| | | }
|
| | | |
| | | parent = $( opts.container || document.body );
|
| | | container = $( document.createElement('div') );
|
| | | |
| | | container.attr( 'id', 'rt_' + this.uid );
|
| | | container.css({
|
| | | position: 'absolute',
|
| | | top: '0px',
|
| | | left: '0px',
|
| | | width: '1px',
|
| | | height: '1px',
|
| | | overflow: 'hidden'
|
| | | });
|
| | | |
| | | parent.append( container );
|
| | | parent.addClass('webuploader-container');
|
| | | this._container = container;
|
| | | return container;
|
| | | },
|
| | | |
| | | init: Base.noop,
|
| | | exec: Base.noop,
|
| | | |
| | | destroy: function() {
|
| | | if ( this._container ) {
|
| | | this._container.parentNode.removeChild( this.__container );
|
| | | }
|
| | | |
| | | this.off();
|
| | | }
|
| | | });
|
| | | |
| | | Runtime.orders = 'html5,flash';
|
| | | |
| | | |
| | | /**
|
| | | * 添加Runtime实现。
|
| | | * @param {String} type 类型
|
| | | * @param {Runtime} factory 具体Runtime实现。
|
| | | */
|
| | | Runtime.addRuntime = function( type, factory ) {
|
| | | factories[ type ] = factory;
|
| | | };
|
| | | |
| | | Runtime.hasRuntime = function( type ) {
|
| | | return !!(type ? factories[ type ] : getFirstKey( factories ));
|
| | | };
|
| | | |
| | | Runtime.create = function( opts, orders ) {
|
| | | var type, runtime;
|
| | | |
| | | orders = orders || Runtime.orders;
|
| | | $.each( orders.split( /\s*,\s*/g ), function() {
|
| | | if ( factories[ this ] ) {
|
| | | type = this;
|
| | | return false;
|
| | | }
|
| | | });
|
| | | |
| | | type = type || getFirstKey( factories );
|
| | | |
| | | if ( !type ) {
|
| | | throw new Error('Runtime Error');
|
| | | }
|
| | | |
| | | runtime = new factories[ type ]( opts );
|
| | | return runtime;
|
| | | };
|
| | | |
| | | Mediator.installTo( Runtime.prototype );
|
| | | return Runtime;
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview Runtime管理器,负责Runtime的选择, 连接
|
| | | */
|
| | | define('runtime/client',[
|
| | | 'base',
|
| | | 'mediator',
|
| | | 'runtime/runtime'
|
| | | ], function( Base, Mediator, Runtime ) {
|
| | | |
| | | var cache;
|
| | | |
| | | cache = (function() {
|
| | | var obj = {};
|
| | | |
| | | return {
|
| | | add: function( runtime ) {
|
| | | obj[ runtime.uid ] = runtime;
|
| | | },
|
| | | |
| | | get: function( ruid, standalone ) {
|
| | | var i;
|
| | | |
| | | if ( ruid ) {
|
| | | return obj[ ruid ];
|
| | | }
|
| | | |
| | | for ( i in obj ) {
|
| | | // 有些类型不能重用,比如filepicker.
|
| | | if ( standalone && obj[ i ].__standalone ) {
|
| | | continue;
|
| | | }
|
| | | |
| | | return obj[ i ];
|
| | | }
|
| | | |
| | | return null;
|
| | | },
|
| | | |
| | | remove: function( runtime ) {
|
| | | delete obj[ runtime.uid ];
|
| | | }
|
| | | };
|
| | | })();
|
| | | |
| | | function RuntimeClient( component, standalone ) {
|
| | | var deferred = Base.Deferred(),
|
| | | runtime;
|
| | | |
| | | this.uid = Base.guid('client_');
|
| | | |
| | | // 允许runtime没有初始化之前,注册一些方法在初始化后执行。
|
| | | this.runtimeReady = function( cb ) {
|
| | | return deferred.done( cb );
|
| | | };
|
| | | |
| | | this.connectRuntime = function( opts, cb ) {
|
| | | |
| | | // already connected.
|
| | | if ( runtime ) {
|
| | | throw new Error('already connected!');
|
| | | }
|
| | | |
| | | deferred.done( cb );
|
| | | |
| | | if ( typeof opts === 'string' && cache.get( opts ) ) {
|
| | | runtime = cache.get( opts );
|
| | | }
|
| | | |
| | | // 像filePicker只能独立存在,不能公用。
|
| | | runtime = runtime || cache.get( null, standalone );
|
| | | |
| | | // 需要创建
|
| | | if ( !runtime ) {
|
| | | runtime = Runtime.create( opts, opts.runtimeOrder );
|
| | | runtime.__promise = deferred.promise();
|
| | | runtime.once( 'ready', deferred.resolve );
|
| | | runtime.init();
|
| | | cache.add( runtime );
|
| | | runtime.__client = 1;
|
| | | } else {
|
| | | // 来自cache
|
| | | Base.$.extend( runtime.options, opts );
|
| | | runtime.__promise.then( deferred.resolve );
|
| | | runtime.__client++;
|
| | | }
|
| | | |
| | | standalone && (runtime.__standalone = standalone);
|
| | | return runtime;
|
| | | };
|
| | | |
| | | this.getRuntime = function() {
|
| | | return runtime;
|
| | | };
|
| | | |
| | | this.disconnectRuntime = function() {
|
| | | if ( !runtime ) {
|
| | | return;
|
| | | }
|
| | | |
| | | runtime.__client--;
|
| | | |
| | | if ( runtime.__client <= 0 ) {
|
| | | cache.remove( runtime );
|
| | | delete runtime.__promise;
|
| | | runtime.destroy();
|
| | | }
|
| | | |
| | | runtime = null;
|
| | | };
|
| | | |
| | | this.exec = function() {
|
| | | if ( !runtime ) {
|
| | | return;
|
| | | }
|
| | | |
| | | var args = Base.slice( arguments );
|
| | | component && args.unshift( component );
|
| | | |
| | | return runtime.exec.apply( this, args );
|
| | | };
|
| | | |
| | | this.getRuid = function() {
|
| | | return runtime && runtime.uid;
|
| | | };
|
| | | |
| | | this.destroy = (function( destroy ) {
|
| | | return function() {
|
| | | destroy && destroy.apply( this, arguments );
|
| | | this.trigger('destroy');
|
| | | this.off();
|
| | | this.exec('destroy');
|
| | | this.disconnectRuntime();
|
| | | };
|
| | | })( this.destroy );
|
| | | }
|
| | | |
| | | Mediator.installTo( RuntimeClient.prototype );
|
| | | return RuntimeClient;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 错误信息
|
| | | */
|
| | | define('lib/dnd',[
|
| | | 'base',
|
| | | 'mediator',
|
| | | 'runtime/client'
|
| | | ], function( Base, Mediator, RuntimeClent ) {
|
| | | |
| | | var $ = Base.$;
|
| | | |
| | | function DragAndDrop( opts ) {
|
| | | opts = this.options = $.extend({}, DragAndDrop.options, opts );
|
| | | |
| | | opts.container = $( opts.container );
|
| | | |
| | | if ( !opts.container.length ) {
|
| | | return;
|
| | | }
|
| | | |
| | | RuntimeClent.call( this, 'DragAndDrop' );
|
| | | }
|
| | | |
| | | DragAndDrop.options = {
|
| | | accept: null,
|
| | | disableGlobalDnd: false
|
| | | };
|
| | | |
| | | Base.inherits( RuntimeClent, {
|
| | | constructor: DragAndDrop,
|
| | | |
| | | init: function() {
|
| | | var me = this;
|
| | | |
| | | me.connectRuntime( me.options, function() {
|
| | | me.exec('init');
|
| | | me.trigger('ready');
|
| | | });
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | this.disconnectRuntime();
|
| | | }
|
| | | });
|
| | | |
| | | Mediator.installTo( DragAndDrop.prototype );
|
| | | |
| | | return DragAndDrop;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 组件基类。
|
| | | */
|
| | | define('widgets/widget',[
|
| | | 'base',
|
| | | 'uploader'
|
| | | ], function( Base, Uploader ) {
|
| | | |
| | | var $ = Base.$,
|
| | | _init = Uploader.prototype._init,
|
| | | IGNORE = {},
|
| | | widgetClass = [];
|
| | | |
| | | function isArrayLike( obj ) {
|
| | | if ( !obj ) {
|
| | | return false;
|
| | | }
|
| | | |
| | | var length = obj.length,
|
| | | type = $.type( obj );
|
| | | |
| | | if ( obj.nodeType === 1 && length ) {
|
| | | return true;
|
| | | }
|
| | | |
| | | return type === 'array' || type !== 'function' && type !== 'string' &&
|
| | | (length === 0 || typeof length === 'number' && length > 0 &&
|
| | | (length - 1) in obj);
|
| | | }
|
| | | |
| | | function Widget( uploader ) {
|
| | | this.owner = uploader;
|
| | | this.options = uploader.options;
|
| | | }
|
| | | |
| | | $.extend( Widget.prototype, {
|
| | | |
| | | init: Base.noop,
|
| | | |
| | | // 类Backbone的事件监听声明,监听uploader实例上的事件
|
| | | // widget直接无法监听事件,事件只能通过uploader来传递
|
| | | invoke: function( apiName, args ) {
|
| | | |
| | | /*
|
| | | {
|
| | | 'make-thumb': 'makeThumb'
|
| | | }
|
| | | */
|
| | | var map = this.responseMap;
|
| | | |
| | | // 如果无API响应声明则忽略
|
| | | if ( !map || !(apiName in map) || !(map[ apiName ] in this) ||
|
| | | !$.isFunction( this[ map[ apiName ] ] ) ) {
|
| | | |
| | | return IGNORE;
|
| | | }
|
| | | |
| | | return this[ map[ apiName ] ].apply( this, args );
|
| | | |
| | | },
|
| | | |
| | | /**
|
| | | * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。
|
| | | * @method request
|
| | | * @grammar request( command, args ) => * | Promise
|
| | | * @grammar request( command, args, callback ) => Promise
|
| | | * @for Uploader
|
| | | */
|
| | | request: function() {
|
| | | return this.owner.request.apply( this.owner, arguments );
|
| | | }
|
| | | });
|
| | | |
| | | // 扩展Uploader.
|
| | | $.extend( Uploader.prototype, {
|
| | | |
| | | // 覆写_init用来初始化widgets
|
| | | _init: function() {
|
| | | var me = this,
|
| | | widgets = me._widgets = [];
|
| | | |
| | | $.each( widgetClass, function( _, klass ) {
|
| | | widgets.push( new klass( me ) );
|
| | | });
|
| | | |
| | | return _init.apply( me, arguments );
|
| | | },
|
| | | |
| | | request: function( apiName, args, callback ) {
|
| | | var i = 0,
|
| | | widgets = this._widgets,
|
| | | len = widgets.length,
|
| | | rlts = [],
|
| | | dfds = [],
|
| | | widget, rlt, promise, key;
|
| | | |
| | | args = isArrayLike( args ) ? args : [ args ];
|
| | | |
| | | for ( ; i < len; i++ ) {
|
| | | widget = widgets[ i ];
|
| | | rlt = widget.invoke( apiName, args );
|
| | | |
| | | if ( rlt !== IGNORE ) {
|
| | | |
| | | // Deferred对象
|
| | | if ( Base.isPromise( rlt ) ) {
|
| | | dfds.push( rlt );
|
| | | } else {
|
| | | rlts.push( rlt );
|
| | | }
|
| | | }
|
| | | }
|
| | | |
| | | // 如果有callback,则用异步方式。
|
| | | if ( callback || dfds.length ) {
|
| | | promise = Base.when.apply( Base, dfds );
|
| | | key = promise.pipe ? 'pipe' : 'then';
|
| | | |
| | | // 很重要不能删除。删除了会死循环。
|
| | | // 保证执行顺序。让callback总是在下一个tick中执行。
|
| | | return promise[ key ](function() {
|
| | | var deferred = Base.Deferred(),
|
| | | args = arguments;
|
| | | |
| | | setTimeout(function() {
|
| | | deferred.resolve.apply( deferred, args );
|
| | | }, 1 );
|
| | | |
| | | return deferred.promise();
|
| | | })[ key ]( callback || Base.noop );
|
| | | } else {
|
| | | return rlts[ 0 ];
|
| | | }
|
| | | }
|
| | | });
|
| | | |
| | | /**
|
| | | * 添加组件
|
| | | * @param {object} widgetProto 组件原型,构造函数通过constructor属性定义
|
| | | * @param {object} responseMap API名称与函数实现的映射
|
| | | * @example
|
| | | * Uploader.register( {
|
| | | * init: function( options ) {},
|
| | | * makeThumb: function() {}
|
| | | * }, {
|
| | | * 'make-thumb': 'makeThumb'
|
| | | * } );
|
| | | */
|
| | | Uploader.register = Widget.register = function( responseMap, widgetProto ) {
|
| | | var map = { init: 'init' },
|
| | | klass;
|
| | | |
| | | if ( arguments.length === 1 ) {
|
| | | widgetProto = responseMap;
|
| | | widgetProto.responseMap = map;
|
| | | } else {
|
| | | widgetProto.responseMap = $.extend( map, responseMap );
|
| | | }
|
| | | |
| | | klass = Base.inherits( Widget, widgetProto );
|
| | | widgetClass.push( klass );
|
| | | |
| | | return klass;
|
| | | };
|
| | | |
| | | return Widget;
|
| | | });
|
| | | /**
|
| | | * @fileOverview DragAndDrop Widget。
|
| | | */
|
| | | define('widgets/filednd',[
|
| | | 'base',
|
| | | 'uploader',
|
| | | 'lib/dnd',
|
| | | 'widgets/widget'
|
| | | ], function( Base, Uploader, Dnd ) {
|
| | | var $ = Base.$;
|
| | | |
| | | Uploader.options.dnd = '';
|
| | | |
| | | /**
|
| | | * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @event dndAccept
|
| | | * @param {DataTransferItemList} items DataTransferItem
|
| | | * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。
|
| | | * @for Uploader
|
| | | */
|
| | | return Uploader.register({
|
| | | init: function( opts ) {
|
| | | |
| | | if ( !opts.dnd ||
|
| | | this.request('predict-runtime-type') !== 'html5' ) {
|
| | | return;
|
| | | }
|
| | | |
| | | var me = this,
|
| | | deferred = Base.Deferred(),
|
| | | options = $.extend({}, {
|
| | | disableGlobalDnd: opts.disableGlobalDnd,
|
| | | container: opts.dnd,
|
| | | accept: opts.accept
|
| | | }),
|
| | | dnd;
|
| | | |
| | | dnd = new Dnd( options );
|
| | | |
| | | dnd.once( 'ready', deferred.resolve );
|
| | | dnd.on( 'drop', function( files ) {
|
| | | me.request( 'add-file', [ files ]);
|
| | | });
|
| | | |
| | | // 检测文件是否全部允许添加。
|
| | | dnd.on( 'accept', function( items ) {
|
| | | return me.owner.trigger( 'dndAccept', items );
|
| | | });
|
| | | |
| | | dnd.init();
|
| | | |
| | | return deferred.promise();
|
| | | }
|
| | | });
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview 错误信息
|
| | | */
|
| | | define('lib/filepaste',[
|
| | | 'base',
|
| | | 'mediator',
|
| | | 'runtime/client'
|
| | | ], function( Base, Mediator, RuntimeClent ) {
|
| | | |
| | | var $ = Base.$;
|
| | | |
| | | function FilePaste( opts ) {
|
| | | opts = this.options = $.extend({}, opts );
|
| | | opts.container = $( opts.container || document.body );
|
| | | RuntimeClent.call( this, 'FilePaste' );
|
| | | }
|
| | | |
| | | Base.inherits( RuntimeClent, {
|
| | | constructor: FilePaste,
|
| | | |
| | | init: function() {
|
| | | var me = this;
|
| | | |
| | | me.connectRuntime( me.options, function() {
|
| | | me.exec('init');
|
| | | me.trigger('ready');
|
| | | });
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | this.exec('destroy');
|
| | | this.disconnectRuntime();
|
| | | this.off();
|
| | | }
|
| | | });
|
| | | |
| | | Mediator.installTo( FilePaste.prototype );
|
| | | |
| | | return FilePaste;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 组件基类。
|
| | | */
|
| | | define('widgets/filepaste',[
|
| | | 'base',
|
| | | 'uploader',
|
| | | 'lib/filepaste',
|
| | | 'widgets/widget'
|
| | | ], function( Base, Uploader, FilePaste ) {
|
| | | var $ = Base.$;
|
| | | |
| | | /**
|
| | | * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`.
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | */
|
| | | return Uploader.register({
|
| | | init: function( opts ) {
|
| | | |
| | | if ( !opts.paste ||
|
| | | this.request('predict-runtime-type') !== 'html5' ) {
|
| | | return;
|
| | | }
|
| | | |
| | | var me = this,
|
| | | deferred = Base.Deferred(),
|
| | | options = $.extend({}, {
|
| | | container: opts.paste,
|
| | | accept: opts.accept
|
| | | }),
|
| | | paste;
|
| | | |
| | | paste = new FilePaste( options );
|
| | | |
| | | paste.once( 'ready', deferred.resolve );
|
| | | paste.on( 'paste', function( files ) {
|
| | | me.owner.request( 'add-file', [ files ]);
|
| | | });
|
| | | paste.init();
|
| | | |
| | | return deferred.promise();
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview Blob
|
| | | */
|
| | | define('lib/blob',[
|
| | | 'base',
|
| | | 'runtime/client'
|
| | | ], function( Base, RuntimeClient ) {
|
| | | |
| | | function Blob( ruid, source ) {
|
| | | var me = this;
|
| | | |
| | | me.source = source;
|
| | | me.ruid = ruid;
|
| | | |
| | | RuntimeClient.call( me, 'Blob' );
|
| | | |
| | | this.uid = source.uid || this.uid;
|
| | | this.type = source.type || '';
|
| | | this.size = source.size || 0;
|
| | | |
| | | if ( ruid ) {
|
| | | me.connectRuntime( ruid );
|
| | | }
|
| | | }
|
| | | |
| | | Base.inherits( RuntimeClient, {
|
| | | constructor: Blob,
|
| | | |
| | | slice: function( start, end ) {
|
| | | return this.exec( 'slice', start, end );
|
| | | },
|
| | | |
| | | getSource: function() {
|
| | | return this.source;
|
| | | }
|
| | | });
|
| | | |
| | | return Blob;
|
| | | });
|
| | | /**
|
| | | * 为了统一化Flash的File和HTML5的File而存在。
|
| | | * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。
|
| | | * @fileOverview File
|
| | | */
|
| | | define('lib/file',[
|
| | | 'base',
|
| | | 'lib/blob'
|
| | | ], function( Base, Blob ) {
|
| | | |
| | | var uid = 1,
|
| | | rExt = /\.([^.]+)$/;
|
| | | |
| | | function File( ruid, file ) {
|
| | | var ext;
|
| | | |
| | | Blob.apply( this, arguments );
|
| | | this.name = file.name || ('untitled' + uid++);
|
| | | ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : '';
|
| | | |
| | | // todo 支持其他类型文件的转换。
|
| | | |
| | | // 如果有mimetype, 但是文件名里面没有找出后缀规律
|
| | | if ( !ext && this.type ) {
|
| | | ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( this.type ) ?
|
| | | RegExp.$1.toLowerCase() : '';
|
| | | this.name += '.' + ext;
|
| | | }
|
| | | |
| | | // 如果没有指定mimetype, 但是知道文件后缀。
|
| | | if ( !this.type && ~'jpg,jpeg,png,gif,bmp'.indexOf( ext ) ) {
|
| | | this.type = 'image/' + (ext === 'jpg' ? 'jpeg' : ext);
|
| | | }
|
| | | |
| | | this.ext = ext;
|
| | | this.lastModifiedDate = file.lastModifiedDate ||
|
| | | (new Date()).toLocaleString();
|
| | | }
|
| | | |
| | | return Base.inherits( Blob, File );
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview 错误信息
|
| | | */
|
| | | define('lib/filepicker',[
|
| | | 'base',
|
| | | 'runtime/client',
|
| | | 'lib/file'
|
| | | ], function( Base, RuntimeClent, File ) {
|
| | | |
| | | var $ = Base.$;
|
| | | |
| | | function FilePicker( opts ) {
|
| | | opts = this.options = $.extend({}, FilePicker.options, opts );
|
| | | |
| | | opts.container = $( opts.id );
|
| | | |
| | | if ( !opts.container.length ) {
|
| | | throw new Error('按钮指定错误');
|
| | | }
|
| | | |
| | | opts.innerHTML = opts.innerHTML || opts.label ||
|
| | | opts.container.html() || '';
|
| | | |
| | | opts.button = $( opts.button || document.createElement('div') );
|
| | | opts.button.html( opts.innerHTML );
|
| | | opts.container.html( opts.button );
|
| | | |
| | | RuntimeClent.call( this, 'FilePicker', true );
|
| | | }
|
| | | |
| | | FilePicker.options = {
|
| | | button: null,
|
| | | container: null,
|
| | | label: null,
|
| | | innerHTML: null,
|
| | | multiple: true,
|
| | | accept: null,
|
| | | name: 'file'
|
| | | };
|
| | | |
| | | Base.inherits( RuntimeClent, {
|
| | | constructor: FilePicker,
|
| | | |
| | | init: function() {
|
| | | var me = this,
|
| | | opts = me.options,
|
| | | button = opts.button;
|
| | | |
| | | button.addClass('webuploader-pick');
|
| | | |
| | | me.on( 'all', function( type ) {
|
| | | var files;
|
| | | |
| | | switch ( type ) {
|
| | | case 'mouseenter':
|
| | | button.addClass('webuploader-pick-hover');
|
| | | break;
|
| | | |
| | | case 'mouseleave':
|
| | | button.removeClass('webuploader-pick-hover');
|
| | | break;
|
| | | |
| | | case 'change':
|
| | | files = me.exec('getFiles');
|
| | | me.trigger( 'select', $.map( files, function( file ) {
|
| | | file = new File( me.getRuid(), file );
|
| | | |
| | | // 记录来源。
|
| | | file._refer = opts.container;
|
| | | return file;
|
| | | }), opts.container );
|
| | | break;
|
| | | }
|
| | | });
|
| | | |
| | | me.connectRuntime( opts, function() {
|
| | | me.refresh();
|
| | | me.exec( 'init', opts );
|
| | | me.trigger('ready');
|
| | | });
|
| | | |
| | | $( window ).on( 'resize', function() {
|
| | | me.refresh();
|
| | | });
|
| | | },
|
| | | |
| | | refresh: function() {
|
| | | var shimContainer = this.getRuntime().getContainer(),
|
| | | button = this.options.button,
|
| | | width = button.outerWidth ?
|
| | | button.outerWidth() : button.width(),
|
| | | |
| | | height = button.outerHeight ?
|
| | | button.outerHeight() : button.height(),
|
| | | |
| | | pos = button.offset();
|
| | | |
| | | width && height && shimContainer.css({
|
| | | bottom: 'auto',
|
| | | right: 'auto',
|
| | | width: width + 'px',
|
| | | height: height + 'px'
|
| | | }).offset( pos );
|
| | | },
|
| | | |
| | | enable: function() {
|
| | | var btn = this.options.button;
|
| | | |
| | | btn.removeClass('webuploader-pick-disable');
|
| | | this.refresh();
|
| | | },
|
| | | |
| | | disable: function() {
|
| | | var btn = this.options.button;
|
| | | |
| | | this.getRuntime().getContainer().css({
|
| | | top: '-99999px'
|
| | | });
|
| | | |
| | | btn.addClass('webuploader-pick-disable');
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | if ( this.runtime ) {
|
| | | this.exec('destroy');
|
| | | this.disconnectRuntime();
|
| | | }
|
| | | }
|
| | | });
|
| | | |
| | | return FilePicker;
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview 文件选择相关
|
| | | */
|
| | | define('widgets/filepicker',[
|
| | | 'base',
|
| | | 'uploader',
|
| | | 'lib/filepicker',
|
| | | 'widgets/widget'
|
| | | ], function( Base, Uploader, FilePicker ) {
|
| | | var $ = Base.$;
|
| | | |
| | | $.extend( Uploader.options, {
|
| | | |
| | | /**
|
| | | * @property {Selector | Object} [pick=undefined]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 指定选择文件的按钮容器,不指定则不创建按钮。
|
| | | *
|
| | | * * `id` {Seletor} 指定选择文件的按钮容器,不指定则不创建按钮。
|
| | | * * `label` {String} 请采用 `innerHTML` 代替
|
| | | * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。
|
| | | * * `multiple` {Boolean} 是否开起同时选择多个文件能力。
|
| | | */
|
| | | pick: null,
|
| | | |
| | | /**
|
| | | * @property {Arroy} [accept=null]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。
|
| | | *
|
| | | * * `title` {String} 文字描述
|
| | | * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。
|
| | | * * `mimeTypes` {String} 多个用逗号分割。
|
| | | *
|
| | | * 如:
|
| | | *
|
| | | * ```
|
| | | * {
|
| | | * title: 'Images',
|
| | | * extensions: 'gif,jpg,jpeg,bmp,png',
|
| | | * mimeTypes: 'image/*'
|
| | | * }
|
| | | * ```
|
| | | */
|
| | | accept: null/*{
|
| | | title: 'Images',
|
| | | extensions: 'gif,jpg,jpeg,bmp,png',
|
| | | mimeTypes: 'image/*'
|
| | | }*/
|
| | | });
|
| | | |
| | | return Uploader.register({
|
| | | 'add-btn': 'addButton',
|
| | | refresh: 'refresh',
|
| | | disable: 'disable',
|
| | | enable: 'enable'
|
| | | }, {
|
| | | |
| | | init: function( opts ) {
|
| | | this.pickers = [];
|
| | | return opts.pick && this.addButton( opts.pick );
|
| | | },
|
| | | |
| | | refresh: function() {
|
| | | $.each( this.pickers, function() {
|
| | | this.refresh();
|
| | | });
|
| | | },
|
| | | |
| | | /**
|
| | | * @method addButton
|
| | | * @for Uploader
|
| | | * @grammar addButton( pick ) => Promise
|
| | | * @description
|
| | | * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。
|
| | | * @example
|
| | | * uploader.addButton({
|
| | | * id: '#btnContainer',
|
| | | * innerHTML: '选择文件'
|
| | | * });
|
| | | */
|
| | | addButton: function( pick ) {
|
| | | var me = this,
|
| | | opts = me.options,
|
| | | accept = opts.accept,
|
| | | options, picker, deferred;
|
| | | |
| | | if ( !pick ) {
|
| | | return;
|
| | | }
|
| | | |
| | | deferred = Base.Deferred();
|
| | | $.isPlainObject( pick ) || (pick = {
|
| | | id: pick
|
| | | });
|
| | | |
| | | options = $.extend({}, pick, {
|
| | | accept: $.isPlainObject( accept ) ? [ accept ] : accept,
|
| | | swf: opts.swf,
|
| | | runtimeOrder: opts.runtimeOrder
|
| | | });
|
| | | |
| | | picker = new FilePicker( options );
|
| | | |
| | | picker.once( 'ready', deferred.resolve );
|
| | | picker.on( 'select', function( files ) {
|
| | | me.owner.request( 'add-file', [ files ]);
|
| | | });
|
| | | picker.init();
|
| | | |
| | | this.pickers.push( picker );
|
| | | |
| | | return deferred.promise();
|
| | | },
|
| | | |
| | | disable: function() {
|
| | | $.each( this.pickers, function() {
|
| | | this.disable();
|
| | | });
|
| | | },
|
| | | |
| | | enable: function() {
|
| | | $.each( this.pickers, function() {
|
| | | this.enable();
|
| | | });
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview 文件属性封装
|
| | | */
|
| | | define('file',[
|
| | | 'base',
|
| | | 'mediator'
|
| | | ], function( Base, Mediator ) {
|
| | | |
| | | var $ = Base.$,
|
| | | idPrefix = 'WU_FILE_',
|
| | | idSuffix = 0,
|
| | | rExt = /\.([^.]+)$/,
|
| | | statusMap = {};
|
| | | |
| | | function gid() {
|
| | | return idPrefix + idSuffix++;
|
| | | }
|
| | | |
| | | /**
|
| | | * 文件类
|
| | | * @class File
|
| | | * @constructor 构造函数
|
| | | * @grammar new File( source ) => File
|
| | | * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。
|
| | | */
|
| | | function WUFile( source ) {
|
| | | |
| | | /**
|
| | | * 文件名,包括扩展名(后缀)
|
| | | * @property name
|
| | | * @type {string}
|
| | | */
|
| | | this.name = source.name || 'Untitled';
|
| | | |
| | | /**
|
| | | * 文件体积(字节)
|
| | | * @property size
|
| | | * @type {uint}
|
| | | * @default 0
|
| | | */
|
| | | this.size = source.size || 0;
|
| | | |
| | | /**
|
| | | * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny)
|
| | | * @property type
|
| | | * @type {string}
|
| | | * @default 'application'
|
| | | */
|
| | | this.type = source.type || 'application';
|
| | | |
| | | /**
|
| | | * 文件最后修改日期
|
| | | * @property lastModifiedDate
|
| | | * @type {int}
|
| | | * @default 当前时间戳
|
| | | */
|
| | | this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1);
|
| | | |
| | | /**
|
| | | * 文件ID,每个对象具有唯一ID,与文件名无关
|
| | | * @property id
|
| | | * @type {string}
|
| | | */
|
| | | this.id = gid();
|
| | | |
| | | /**
|
| | | * 文件扩展名,通过文件名获取,例如test.png的扩展名为png
|
| | | * @property ext
|
| | | * @type {string}
|
| | | */
|
| | | this.ext = rExt.exec( this.name ) ? RegExp.$1 : '';
|
| | | |
| | | |
| | | /**
|
| | | * 状态文字说明。在不同的status语境下有不同的用途。
|
| | | * @property statusText
|
| | | * @type {string}
|
| | | */
|
| | | this.statusText = '';
|
| | | |
| | | // 存储文件状态,防止通过属性直接修改
|
| | | statusMap[ this.id ] = WUFile.Status.INITED;
|
| | | |
| | | this.source = source;
|
| | | this.loaded = 0;
|
| | | |
| | | this.on( 'error', function( msg ) {
|
| | | this.setStatus( WUFile.Status.ERROR, msg );
|
| | | });
|
| | | }
|
| | | |
| | | $.extend( WUFile.prototype, {
|
| | | |
| | | /**
|
| | | * 设置状态,状态变化时会触发`change`事件。
|
| | | * @method setStatus
|
| | | * @grammar setStatus( status[, statusText] );
|
| | | * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status)
|
| | | * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。
|
| | | */
|
| | | setStatus: function( status, text ) {
|
| | | |
| | | var prevStatus = statusMap[ this.id ];
|
| | | |
| | | typeof text !== 'undefined' && (this.statusText = text);
|
| | | |
| | | if ( status !== prevStatus ) {
|
| | | statusMap[ this.id ] = status;
|
| | | /**
|
| | | * 文件状态变化
|
| | | * @event statuschange
|
| | | */
|
| | | this.trigger( 'statuschange', status, prevStatus );
|
| | | }
|
| | | |
| | | },
|
| | | |
| | | /**
|
| | | * 获取文件状态
|
| | | * @return {File.Status}
|
| | | * @example
|
| | | 文件状态具体包括以下几种类型:
|
| | | {
|
| | | // 初始化
|
| | | INITED: 0,
|
| | | // 已入队列
|
| | | QUEUED: 1,
|
| | | // 正在上传
|
| | | PROGRESS: 2,
|
| | | // 上传出错
|
| | | ERROR: 3,
|
| | | // 上传成功
|
| | | COMPLETE: 4,
|
| | | // 上传取消
|
| | | CANCELLED: 5
|
| | | }
|
| | | */
|
| | | getStatus: function() {
|
| | | return statusMap[ this.id ];
|
| | | },
|
| | | |
| | | /**
|
| | | * 获取文件原始信息。
|
| | | * @return {*}
|
| | | */
|
| | | getSource: function() {
|
| | | return this.source;
|
| | | },
|
| | | |
| | | destory: function() {
|
| | | delete statusMap[ this.id ];
|
| | | }
|
| | | });
|
| | | |
| | | Mediator.installTo( WUFile.prototype );
|
| | | |
| | | /**
|
| | | * 文件状态值,具体包括以下几种类型:
|
| | | * * `inited` 初始状态
|
| | | * * `queued` 已经进入队列, 等待上传
|
| | | * * `progress` 上传中
|
| | | * * `complete` 上传完成。
|
| | | * * `error` 上传出错,可重试
|
| | | * * `interrupt` 上传中断,可续传。
|
| | | * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。
|
| | | * * `cancelled` 文件被移除。
|
| | | * @property {Object} Status
|
| | | * @namespace File
|
| | | * @class File
|
| | | * @static
|
| | | */
|
| | | WUFile.Status = {
|
| | | INITED: 'inited', // 初始状态
|
| | | QUEUED: 'queued', // 已经进入队列, 等待上传
|
| | | PROGRESS: 'progress', // 上传中
|
| | | ERROR: 'error', // 上传出错,可重试
|
| | | COMPLETE: 'complete', // 上传完成。
|
| | | CANCELLED: 'cancelled', // 上传取消。
|
| | | INTERRUPT: 'interrupt', // 上传中断,可续传。
|
| | | INVALID: 'invalid' // 文件不合格,不能重试上传。
|
| | | };
|
| | | |
| | | return WUFile;
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview 文件队列
|
| | | */
|
| | | define('queue',[
|
| | | 'base',
|
| | | 'mediator',
|
| | | 'file'
|
| | | ], function( Base, Mediator, WUFile ) {
|
| | | |
| | | var $ = Base.$,
|
| | | STATUS = WUFile.Status;
|
| | | |
| | | /**
|
| | | * 文件队列, 用来存储各个状态中的文件。
|
| | | * @class Queue
|
| | | * @extends Mediator
|
| | | */
|
| | | function Queue() {
|
| | | |
| | | /**
|
| | | * 统计文件数。
|
| | | * * `numOfQueue` 队列中的文件数。
|
| | | * * `numOfSuccess` 上传成功的文件数
|
| | | * * `numOfCancel` 被移除的文件数
|
| | | * * `numOfProgress` 正在上传中的文件数
|
| | | * * `numOfUploadFailed` 上传错误的文件数。
|
| | | * * `numOfInvalid` 无效的文件数。
|
| | | * @property {Object} stats
|
| | | */
|
| | | this.stats = {
|
| | | numOfQueue: 0,
|
| | | numOfSuccess: 0,
|
| | | numOfCancel: 0,
|
| | | numOfProgress: 0,
|
| | | numOfUploadFailed: 0,
|
| | | numOfInvalid: 0
|
| | | };
|
| | | |
| | | // 上传队列,仅包括等待上传的文件
|
| | | this._queue = [];
|
| | | |
| | | // 存储所有文件
|
| | | this._map = {};
|
| | | }
|
| | | |
| | | $.extend( Queue.prototype, {
|
| | | |
| | | /**
|
| | | * 将新文件加入对队列尾部
|
| | | *
|
| | | * @method append
|
| | | * @param {File} file 文件对象
|
| | | */
|
| | | append: function( file ) {
|
| | | this._queue.push( file );
|
| | | this._fileAdded( file );
|
| | | return this;
|
| | | },
|
| | | |
| | | /**
|
| | | * 将新文件加入对队列头部
|
| | | *
|
| | | * @method prepend
|
| | | * @param {File} file 文件对象
|
| | | */
|
| | | prepend: function( file ) {
|
| | | this._queue.unshift( file );
|
| | | this._fileAdded( file );
|
| | | return this;
|
| | | },
|
| | | |
| | | /**
|
| | | * 获取文件对象
|
| | | *
|
| | | * @method getFile
|
| | | * @param {String} fileId 文件ID
|
| | | * @return {File}
|
| | | */
|
| | | getFile: function( fileId ) {
|
| | | if ( typeof fileId !== 'string' ) {
|
| | | return fileId;
|
| | | }
|
| | | return this._map[ fileId ];
|
| | | },
|
| | | |
| | | /**
|
| | | * 从队列中取出一个指定状态的文件。
|
| | | * @grammar fetch( status ) => File
|
| | | * @method fetch
|
| | | * @param {String} status [文件状态值](#WebUploader:File:File.Status)
|
| | | * @return {File} [File](#WebUploader:File)
|
| | | */
|
| | | fetch: function( status ) {
|
| | | var len = this._queue.length,
|
| | | i, file;
|
| | | |
| | | status = status || STATUS.QUEUED;
|
| | | |
| | | for ( i = 0; i < len; i++ ) {
|
| | | file = this._queue[ i ];
|
| | | |
| | | if ( status === file.getStatus() ) {
|
| | | return file;
|
| | | }
|
| | | }
|
| | | |
| | | return null;
|
| | | },
|
| | | |
| | | /**
|
| | | * 对队列进行排序,能够控制文件上传顺序。
|
| | | * @grammar sort( fn ) => undefined
|
| | | * @method sort
|
| | | * @param {Function} fn 排序方法
|
| | | */
|
| | | sort: function( fn ) {
|
| | | if ( typeof fn === 'function' ) {
|
| | | this._queue.sort( fn );
|
| | | }
|
| | | },
|
| | | |
| | | /**
|
| | | * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。
|
| | | * @grammar getFiles( [status1[, status2 ...]] ) => Array
|
| | | * @method getFiles
|
| | | * @param {String} [status] [文件状态值](#WebUploader:File:File.Status)
|
| | | */
|
| | | getFiles: function() {
|
| | | var sts = [].slice.call( arguments, 0 ),
|
| | | ret = [],
|
| | | i = 0,
|
| | | len = this._queue.length,
|
| | | file;
|
| | | |
| | | for ( ; i < len; i++ ) {
|
| | | file = this._queue[ i ];
|
| | | |
| | | if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) {
|
| | | continue;
|
| | | }
|
| | | |
| | | ret.push( file );
|
| | | }
|
| | | |
| | | return ret;
|
| | | },
|
| | | |
| | | _fileAdded: function( file ) {
|
| | | var me = this,
|
| | | existing = this._map[ file.id ];
|
| | | |
| | | if ( !existing ) {
|
| | | this._map[ file.id ] = file;
|
| | | |
| | | file.on( 'statuschange', function( cur, pre ) {
|
| | | me._onFileStatusChange( cur, pre );
|
| | | });
|
| | | }
|
| | | |
| | | file.setStatus( STATUS.QUEUED );
|
| | | },
|
| | | |
| | | _onFileStatusChange: function( curStatus, preStatus ) {
|
| | | var stats = this.stats;
|
| | | |
| | | switch ( preStatus ) {
|
| | | case STATUS.PROGRESS:
|
| | | stats.numOfProgress--;
|
| | | break;
|
| | | |
| | | case STATUS.QUEUED:
|
| | | stats.numOfQueue --;
|
| | | break;
|
| | | |
| | | case STATUS.ERROR:
|
| | | stats.numOfUploadFailed--;
|
| | | break;
|
| | | |
| | | case STATUS.INVALID:
|
| | | stats.numOfInvalid--;
|
| | | break;
|
| | | }
|
| | | |
| | | switch ( curStatus ) {
|
| | | case STATUS.QUEUED:
|
| | | stats.numOfQueue++;
|
| | | break;
|
| | | |
| | | case STATUS.PROGRESS:
|
| | | stats.numOfProgress++;
|
| | | break;
|
| | | |
| | | case STATUS.ERROR:
|
| | | stats.numOfUploadFailed++;
|
| | | break;
|
| | | |
| | | case STATUS.COMPLETE:
|
| | | stats.numOfSuccess++;
|
| | | break;
|
| | | |
| | | case STATUS.CANCELLED:
|
| | | stats.numOfCancel++;
|
| | | break;
|
| | | |
| | | case STATUS.INVALID:
|
| | | stats.numOfInvalid++;
|
| | | break;
|
| | | }
|
| | | }
|
| | | |
| | | });
|
| | | |
| | | Mediator.installTo( Queue.prototype );
|
| | | |
| | | return Queue;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 队列
|
| | | */
|
| | | define('widgets/queue',[
|
| | | 'base',
|
| | | 'uploader',
|
| | | 'queue',
|
| | | 'file',
|
| | | 'lib/file',
|
| | | 'runtime/client',
|
| | | 'widgets/widget'
|
| | | ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) {
|
| | | |
| | | var $ = Base.$,
|
| | | rExt = /\.\w+$/,
|
| | | Status = WUFile.Status;
|
| | | |
| | | return Uploader.register({
|
| | | 'sort-files': 'sortFiles',
|
| | | 'add-file': 'addFiles',
|
| | | 'get-file': 'getFile',
|
| | | 'fetch-file': 'fetchFile',
|
| | | 'get-stats': 'getStats',
|
| | | 'get-files': 'getFiles',
|
| | | 'remove-file': 'removeFile',
|
| | | 'retry': 'retry',
|
| | | 'reset': 'reset',
|
| | | 'accept-file': 'acceptFile'
|
| | | }, {
|
| | | |
| | | init: function( opts ) {
|
| | | var me = this,
|
| | | deferred, len, i, item, arr, accept, runtime;
|
| | | |
| | | if ( $.isPlainObject( opts.accept ) ) {
|
| | | opts.accept = [ opts.accept ];
|
| | | }
|
| | | |
| | | // accept中的中生成匹配正则。
|
| | | if ( opts.accept ) {
|
| | | arr = [];
|
| | | |
| | | for ( i = 0, len = opts.accept.length; i < len; i++ ) {
|
| | | item = opts.accept[ i ].extensions;
|
| | | item && arr.push( item );
|
| | | }
|
| | | |
| | | if ( arr.length ) {
|
| | | accept = '\\.' + arr.join(',')
|
| | | .replace( /,/g, '$|\\.' )
|
| | | .replace( /\*/g, '.*' ) + '$';
|
| | | }
|
| | | |
| | | me.accept = new RegExp( accept, 'i' );
|
| | | }
|
| | | |
| | | me.queue = new Queue();
|
| | | me.stats = me.queue.stats;
|
| | | |
| | | // 如果当前不是html5运行时,那就算了。
|
| | | // 不执行后续操作
|
| | | if ( this.request('predict-runtime-type') !== 'html5' ) {
|
| | | return;
|
| | | }
|
| | | |
| | | // 创建一个 html5 运行时的 placeholder
|
| | | // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。
|
| | | deferred = Base.Deferred();
|
| | | runtime = new RuntimeClient('Placeholder');
|
| | | runtime.connectRuntime({
|
| | | runtimeOrder: 'html5'
|
| | | }, function() {
|
| | | me._ruid = runtime.getRuid();
|
| | | deferred.resolve();
|
| | | });
|
| | | return deferred.promise();
|
| | | },
|
| | | |
| | | |
| | | // 为了支持外部直接添加一个原生File对象。
|
| | | _wrapFile: function( file ) {
|
| | | if ( !(file instanceof WUFile) ) {
|
| | | |
| | | if ( !(file instanceof File) ) {
|
| | | if ( !this._ruid ) {
|
| | | throw new Error('Can\'t add external files.');
|
| | | }
|
| | | file = new File( this._ruid, file );
|
| | | }
|
| | | |
| | | file = new WUFile( file );
|
| | | }
|
| | | |
| | | return file;
|
| | | },
|
| | | |
| | | // 判断文件是否可以被加入队列
|
| | | acceptFile: function( file ) {
|
| | | var invalid = !file || file.size < 6 || this.accept &&
|
| | | |
| | | // 如果名字中有后缀,才做后缀白名单处理。
|
| | | rExt.exec( file.name ) && !this.accept.test( file.name );
|
| | | |
| | | return !invalid;
|
| | | },
|
| | | |
| | | |
| | | /**
|
| | | * @event beforeFileQueued
|
| | | * @param {File} file File对象
|
| | | * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @event fileQueued
|
| | | * @param {File} file File对象
|
| | | * @description 当文件被加入队列以后触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | _addFile: function( file ) {
|
| | | var me = this;
|
| | | |
| | | file = me._wrapFile( file );
|
| | | |
| | | // 不过类型判断允许不允许,先派送 `beforeFileQueued`
|
| | | if ( !me.owner.trigger( 'beforeFileQueued', file ) ) {
|
| | | return;
|
| | | }
|
| | | |
| | | // 类型不匹配,则派送错误事件,并返回。
|
| | | if ( !me.acceptFile( file ) ) {
|
| | | me.owner.trigger( 'error', 'Q_TYPE_DENIED', file );
|
| | | return;
|
| | | }
|
| | | |
| | | me.queue.append( file );
|
| | | me.owner.trigger( 'fileQueued', file );
|
| | | return file;
|
| | | },
|
| | | |
| | | getFile: function( fileId ) {
|
| | | return this.queue.getFile( fileId );
|
| | | },
|
| | | |
| | | /**
|
| | | * @event filesQueued
|
| | | * @param {File} files 数组,内容为原始File(lib/File)对象。
|
| | | * @description 当一批文件添加进队列以后触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @method addFiles
|
| | | * @grammar addFiles( file ) => undefined
|
| | | * @grammar addFiles( [file1, file2 ...] ) => undefined
|
| | | * @param {Array of File or File} [files] Files 对象 数组
|
| | | * @description 添加文件到队列
|
| | | * @for Uploader
|
| | | */
|
| | | addFiles: function( files ) {
|
| | | var me = this;
|
| | | |
| | | if ( !files.length ) {
|
| | | files = [ files ];
|
| | | }
|
| | | |
| | | files = $.map( files, function( file ) {
|
| | | return me._addFile( file );
|
| | | });
|
| | | |
| | | me.owner.trigger( 'filesQueued', files );
|
| | | |
| | | if ( me.options.auto ) {
|
| | | me.request('start-upload');
|
| | | }
|
| | | },
|
| | | |
| | | getStats: function() {
|
| | | return this.stats;
|
| | | },
|
| | | |
| | | /**
|
| | | * @event fileDequeued
|
| | | * @param {File} file File对象
|
| | | * @description 当文件被移除队列后触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @method removeFile
|
| | | * @grammar removeFile( file ) => undefined
|
| | | * @grammar removeFile( id ) => undefined
|
| | | * @param {File|id} file File对象或这File对象的id
|
| | | * @description 移除某一文件。
|
| | | * @for Uploader
|
| | | * @example
|
| | | *
|
| | | * $li.on('click', '.remove-this', function() {
|
| | | * uploader.removeFile( file );
|
| | | * })
|
| | | */
|
| | | removeFile: function( file ) {
|
| | | var me = this;
|
| | | |
| | | file = file.id ? file : me.queue.getFile( file );
|
| | | |
| | | file.setStatus( Status.CANCELLED );
|
| | | me.owner.trigger( 'fileDequeued', file );
|
| | | },
|
| | | |
| | | /**
|
| | | * @method getFiles
|
| | | * @grammar getFiles() => Array
|
| | | * @grammar getFiles( status1, status2, status... ) => Array
|
| | | * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。
|
| | | * @for Uploader
|
| | | * @example
|
| | | * console.log( uploader.getFiles() ); // => all files
|
| | | * console.log( uploader.getFiles('error') ) // => all error files.
|
| | | */
|
| | | getFiles: function() {
|
| | | return this.queue.getFiles.apply( this.queue, arguments );
|
| | | },
|
| | | |
| | | fetchFile: function() {
|
| | | return this.queue.fetch.apply( this.queue, arguments );
|
| | | },
|
| | | |
| | | /**
|
| | | * @method retry
|
| | | * @grammar retry() => undefined
|
| | | * @grammar retry( file ) => undefined
|
| | | * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。
|
| | | * @for Uploader
|
| | | * @example
|
| | | * function retry() {
|
| | | * uploader.retry();
|
| | | * }
|
| | | */
|
| | | retry: function( file, noForceStart ) {
|
| | | var me = this,
|
| | | files, i, len;
|
| | | |
| | | if ( file ) {
|
| | | file = file.id ? file : me.queue.getFile( file );
|
| | | file.setStatus( Status.QUEUED );
|
| | | noForceStart || me.request('start-upload');
|
| | | return;
|
| | | }
|
| | | |
| | | files = me.queue.getFiles( Status.ERROR );
|
| | | i = 0;
|
| | | len = files.length;
|
| | | |
| | | for ( ; i < len; i++ ) {
|
| | | file = files[ i ];
|
| | | file.setStatus( Status.QUEUED );
|
| | | }
|
| | | |
| | | me.request('start-upload');
|
| | | },
|
| | | |
| | | /**
|
| | | * @method sort
|
| | | * @grammar sort( fn ) => undefined
|
| | | * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。
|
| | | * @for Uploader
|
| | | */
|
| | | sortFiles: function() {
|
| | | return this.queue.sort.apply( this.queue, arguments );
|
| | | },
|
| | | |
| | | /**
|
| | | * @method reset
|
| | | * @grammar reset() => undefined
|
| | | * @description 重置uploader。目前只重置了队列。
|
| | | * @for Uploader
|
| | | * @example
|
| | | * uploader.reset();
|
| | | */
|
| | | reset: function() {
|
| | | this.queue = new Queue();
|
| | | this.stats = this.queue.stats;
|
| | | }
|
| | | });
|
| | | |
| | | });
|
| | | /**
|
| | | * @fileOverview 添加获取Runtime相关信息的方法。
|
| | | */
|
| | | define('widgets/runtime',[
|
| | | 'uploader',
|
| | | 'runtime/runtime',
|
| | | 'widgets/widget'
|
| | | ], function( Uploader, Runtime ) {
|
| | | |
| | | Uploader.support = function() {
|
| | | return Runtime.hasRuntime.apply( Runtime, arguments );
|
| | | };
|
| | | |
| | | return Uploader.register({
|
| | | 'predict-runtime-type': 'predictRuntmeType'
|
| | | }, {
|
| | | |
| | | init: function() {
|
| | | if ( !this.predictRuntmeType() ) {
|
| | | throw Error('Runtime Error');
|
| | | }
|
| | | },
|
| | | |
| | | /**
|
| | | * 预测Uploader将采用哪个`Runtime`
|
| | | * @grammar predictRuntmeType() => String
|
| | | * @method predictRuntmeType
|
| | | * @for Uploader
|
| | | */
|
| | | predictRuntmeType: function() {
|
| | | var orders = this.options.runtimeOrder || Runtime.orders,
|
| | | type = this.type,
|
| | | i, len;
|
| | | |
| | | if ( !type ) {
|
| | | orders = orders.split( /\s*,\s*/g );
|
| | | |
| | | for ( i = 0, len = orders.length; i < len; i++ ) {
|
| | | if ( Runtime.hasRuntime( orders[ i ] ) ) {
|
| | | this.type = type = orders[ i ];
|
| | | break;
|
| | | }
|
| | | }
|
| | | }
|
| | | |
| | | return type;
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview Transport
|
| | | */
|
| | | define('lib/transport',[
|
| | | 'base',
|
| | | 'runtime/client',
|
| | | 'mediator'
|
| | | ], function( Base, RuntimeClient, Mediator ) {
|
| | | |
| | | var $ = Base.$;
|
| | | |
| | | function Transport( opts ) {
|
| | | var me = this;
|
| | | |
| | | opts = me.options = $.extend( true, {}, Transport.options, opts || {} );
|
| | | RuntimeClient.call( this, 'Transport' );
|
| | | |
| | | this._blob = null;
|
| | | this._formData = opts.formData || {};
|
| | | this._headers = opts.headers || {};
|
| | | |
| | | this.on( 'progress', this._timeout );
|
| | | this.on( 'load error', function() {
|
| | | me.trigger( 'progress', 1 );
|
| | | clearTimeout( me._timer );
|
| | | });
|
| | | }
|
| | | |
| | | Transport.options = {
|
| | | server: '',
|
| | | method: 'POST',
|
| | | |
| | | // 跨域时,是否允许携带cookie, 只有html5 runtime才有效
|
| | | withCredentials: false,
|
| | | fileVal: 'file',
|
| | | timeout: 2 * 60 * 1000, // 2分钟
|
| | | formData: {},
|
| | | headers: {},
|
| | | sendAsBinary: false
|
| | | };
|
| | | |
| | | $.extend( Transport.prototype, {
|
| | | |
| | | // 添加Blob, 只能添加一次,最后一次有效。
|
| | | appendBlob: function( key, blob, filename ) {
|
| | | var me = this,
|
| | | opts = me.options;
|
| | | |
| | | if ( me.getRuid() ) {
|
| | | me.disconnectRuntime();
|
| | | }
|
| | | |
| | | // 连接到blob归属的同一个runtime.
|
| | | me.connectRuntime( blob.ruid, function() {
|
| | | me.exec('init');
|
| | | });
|
| | | |
| | | me._blob = blob;
|
| | | opts.fileVal = key || opts.fileVal;
|
| | | opts.filename = filename || opts.filename;
|
| | | },
|
| | | |
| | | // 添加其他字段
|
| | | append: function( key, value ) {
|
| | | if ( typeof key === 'object' ) {
|
| | | $.extend( this._formData, key );
|
| | | } else {
|
| | | this._formData[ key ] = value;
|
| | | }
|
| | | },
|
| | | |
| | | setRequestHeader: function( key, value ) {
|
| | | if ( typeof key === 'object' ) {
|
| | | $.extend( this._headers, key );
|
| | | } else {
|
| | | this._headers[ key ] = value;
|
| | | }
|
| | | },
|
| | | |
| | | send: function( method ) {
|
| | | this.exec( 'send', method );
|
| | | this._timeout();
|
| | | },
|
| | | |
| | | abort: function() {
|
| | | clearTimeout( this._timer );
|
| | | return this.exec('abort');
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | this.trigger('destroy');
|
| | | this.off();
|
| | | this.exec('destroy');
|
| | | this.disconnectRuntime();
|
| | | },
|
| | | |
| | | getResponse: function() {
|
| | | return this.exec('getResponse');
|
| | | },
|
| | | |
| | | getResponseAsJson: function() {
|
| | | return this.exec('getResponseAsJson');
|
| | | },
|
| | | |
| | | getStatus: function() {
|
| | | return this.exec('getStatus');
|
| | | },
|
| | | |
| | | _timeout: function() {
|
| | | var me = this,
|
| | | duration = me.options.timeout;
|
| | | |
| | | if ( !duration ) {
|
| | | return;
|
| | | }
|
| | | |
| | | clearTimeout( me._timer );
|
| | | me._timer = setTimeout(function() {
|
| | | me.abort();
|
| | | me.trigger( 'error', 'timeout' );
|
| | | }, duration );
|
| | | }
|
| | | |
| | | });
|
| | | |
| | | // 让Transport具备事件功能。
|
| | | Mediator.installTo( Transport.prototype );
|
| | | |
| | | return Transport;
|
| | | });
|
| | | /**
|
| | | * @fileOverview 负责文件上传相关。
|
| | | */
|
| | | define('widgets/upload',[
|
| | | 'base',
|
| | | 'uploader',
|
| | | 'file',
|
| | | 'lib/transport',
|
| | | 'widgets/widget'
|
| | | ], function( Base, Uploader, WUFile, Transport ) {
|
| | | |
| | | var $ = Base.$,
|
| | | isPromise = Base.isPromise,
|
| | | Status = WUFile.Status;
|
| | | |
| | | // 添加默认配置项
|
| | | $.extend( Uploader.options, {
|
| | | |
| | | |
| | | /**
|
| | | * @property {Boolean} [prepareNextFile=false]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 是否允许在文件传输时提前把下一个文件准备好。
|
| | | * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。
|
| | | * 如果能提前在当前文件传输期处理,可以节省总体耗时。
|
| | | */
|
| | | prepareNextFile: false,
|
| | | |
| | | /**
|
| | | * @property {Boolean} [chunked=false]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 是否要分片处理大文件上传。
|
| | | */
|
| | | chunked: false,
|
| | | |
| | | /**
|
| | | * @property {Boolean} [chunkSize=5242880]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 如果要分片,分多大一片? 默认大小为5M.
|
| | | */
|
| | | chunkSize: 5 * 1024 * 1024,
|
| | | |
| | | /**
|
| | | * @property {Boolean} [chunkRetry=2]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 如果某个分片由于网络问题出错,允许自动重传多少次?
|
| | | */
|
| | | chunkRetry: 2,
|
| | | |
| | | /**
|
| | | * @property {Boolean} [threads=3]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 上传并发数。允许同时最大上传进程数。
|
| | | */
|
| | | threads: 3,
|
| | | |
| | | |
| | | /**
|
| | | * @property {Object} [formData]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。
|
| | | */
|
| | | formData: null
|
| | | |
| | | /**
|
| | | * @property {Object} [fileVal='file']
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 设置文件上传域的name。
|
| | | */
|
| | | |
| | | /**
|
| | | * @property {Object} [method='POST']
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 文件上传方式,`POST`或者`GET`。
|
| | | */
|
| | | |
| | | /**
|
| | | * @property {Object} [sendAsBinary=false]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容,
|
| | | * 其他参数在$_GET数组中。
|
| | | */
|
| | | });
|
| | | |
| | | // 负责将文件切片。
|
| | | function CuteFile( file, chunkSize ) {
|
| | | var pending = [],
|
| | | blob = file.source,
|
| | | total = blob.size,
|
| | | chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1,
|
| | | start = 0,
|
| | | index = 0,
|
| | | len;
|
| | | |
| | | while ( index < chunks ) {
|
| | | len = Math.min( chunkSize, total - start );
|
| | | |
| | | pending.push({
|
| | | file: file,
|
| | | start: start,
|
| | | end: chunkSize ? (start + len) : total,
|
| | | total: total,
|
| | | chunks: chunks,
|
| | | chunk: index++
|
| | | });
|
| | | start += len;
|
| | | }
|
| | | |
| | | file.blocks = pending.concat();
|
| | | file.remaning = pending.length;
|
| | | |
| | | return {
|
| | | file: file,
|
| | | |
| | | has: function() {
|
| | | return !!pending.length;
|
| | | },
|
| | | |
| | | fetch: function() {
|
| | | return pending.shift();
|
| | | }
|
| | | };
|
| | | }
|
| | | |
| | | Uploader.register({
|
| | | 'start-upload': 'start',
|
| | | 'stop-upload': 'stop',
|
| | | 'skip-file': 'skipFile',
|
| | | 'is-in-progress': 'isInProgress'
|
| | | }, {
|
| | | |
| | | init: function() {
|
| | | var owner = this.owner;
|
| | | |
| | | this.runing = false;
|
| | | |
| | | // 记录当前正在传的数据,跟threads相关
|
| | | this.pool = [];
|
| | | |
| | | // 缓存即将上传的文件。
|
| | | this.pending = [];
|
| | | |
| | | // 跟踪还有多少分片没有完成上传。
|
| | | this.remaning = 0;
|
| | | this.__tick = Base.bindFn( this._tick, this );
|
| | | |
| | | owner.on( 'uploadComplete', function( file ) {
|
| | | // 把其他块取消了。
|
| | | file.blocks && $.each( file.blocks, function( _, v ) {
|
| | | v.transport && (v.transport.abort(), v.transport.destroy());
|
| | | delete v.transport;
|
| | | });
|
| | | |
| | | delete file.blocks;
|
| | | delete file.remaning;
|
| | | });
|
| | | },
|
| | | |
| | | /**
|
| | | * @event startUpload
|
| | | * @description 当开始上传流程时触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。
|
| | | * @grammar upload() => undefined
|
| | | * @method upload
|
| | | * @for Uploader
|
| | | */
|
| | | start: function() {
|
| | | var me = this;
|
| | | |
| | | // 移出invalid的文件
|
| | | $.each( me.request( 'get-files', Status.INVALID ), function() {
|
| | | me.request( 'remove-file', this );
|
| | | });
|
| | | |
| | | if ( me.runing ) {
|
| | | return;
|
| | | }
|
| | | |
| | | me.runing = true;
|
| | | |
| | | // 如果有暂停的,则续传
|
| | | $.each( me.pool, function( _, v ) {
|
| | | var file = v.file;
|
| | | |
| | | if ( file.getStatus() === Status.INTERRUPT ) {
|
| | | file.setStatus( Status.PROGRESS );
|
| | | me._trigged = false;
|
| | | v.transport && v.transport.send();
|
| | | }
|
| | | });
|
| | | |
| | | me._trigged = false;
|
| | | me.owner.trigger('startUpload');
|
| | | Base.nextTick( me.__tick );
|
| | | },
|
| | | |
| | | /**
|
| | | * @event stopUpload
|
| | | * @description 当开始上传流程暂停时触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。
|
| | | * @grammar stop() => undefined
|
| | | * @grammar stop( true ) => undefined
|
| | | * @method stop
|
| | | * @for Uploader
|
| | | */
|
| | | stop: function( interrupt ) {
|
| | | var me = this;
|
| | | |
| | | if ( me.runing === false ) {
|
| | | return;
|
| | | }
|
| | | |
| | | me.runing = false;
|
| | | |
| | | interrupt && $.each( me.pool, function( _, v ) {
|
| | | v.transport && v.transport.abort();
|
| | | v.file.setStatus( Status.INTERRUPT );
|
| | | });
|
| | | |
| | | me.owner.trigger('stopUpload');
|
| | | },
|
| | | |
| | | /**
|
| | | * 判断`Uplaode`r是否正在上传中。
|
| | | * @grammar isInProgress() => Boolean
|
| | | * @method isInProgress
|
| | | * @for Uploader
|
| | | */
|
| | | isInProgress: function() {
|
| | | return !!this.runing;
|
| | | },
|
| | | |
| | | getStats: function() {
|
| | | return this.request('get-stats');
|
| | | },
|
| | | |
| | | /**
|
| | | * 掉过一个文件上传,直接标记指定文件为已上传状态。
|
| | | * @grammar skipFile( file ) => undefined
|
| | | * @method skipFile
|
| | | * @for Uploader
|
| | | */
|
| | | skipFile: function( file, status ) {
|
| | | file = this.request( 'get-file', file );
|
| | | |
| | | file.setStatus( status || Status.COMPLETE );
|
| | | file.skipped = true;
|
| | | |
| | | // 如果正在上传。
|
| | | file.blocks && $.each( file.blocks, function( _, v ) {
|
| | | var _tr = v.transport;
|
| | | |
| | | if ( _tr ) {
|
| | | _tr.abort();
|
| | | _tr.destroy();
|
| | | delete v.transport;
|
| | | }
|
| | | });
|
| | | |
| | | this.owner.trigger( 'uploadSkip', file );
|
| | | },
|
| | | |
| | | /**
|
| | | * @event uploadFinished
|
| | | * @description 当所有文件上传结束时触发。
|
| | | * @for Uploader
|
| | | */
|
| | | _tick: function() {
|
| | | var me = this,
|
| | | opts = me.options,
|
| | | fn, val;
|
| | | |
| | | // 上一个promise还没有结束,则等待完成后再执行。
|
| | | if ( me._promise ) {
|
| | | return me._promise.always( me.__tick );
|
| | | }
|
| | | |
| | | // 还有位置,且还有文件要处理的话。
|
| | | if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) {
|
| | | me._trigged = false;
|
| | | |
| | | fn = function( val ) {
|
| | | me._promise = null;
|
| | | |
| | | // 有可能是reject过来的,所以要检测val的类型。
|
| | | val && val.file && me._startSend( val );
|
| | | Base.nextTick( me.__tick );
|
| | | };
|
| | | |
| | | me._promise = isPromise( val ) ? val.always( fn ) : fn( val );
|
| | | |
| | | // 没有要上传的了,且没有正在传输的了。
|
| | | } else if ( !me.remaning && !me.getStats().numOfQueue ) {
|
| | | me.runing = false;
|
| | | |
| | | me._trigged || Base.nextTick(function() {
|
| | | me.owner.trigger('uploadFinished');
|
| | | });
|
| | | me._trigged = true;
|
| | | }
|
| | | },
|
| | | |
| | | _nextBlock: function() {
|
| | | var me = this,
|
| | | act = me._act,
|
| | | opts = me.options,
|
| | | next, done;
|
| | | |
| | | // 如果当前文件还有没有需要传输的,则直接返回剩下的。
|
| | | if ( act && act.has() &&
|
| | | act.file.getStatus() === Status.PROGRESS ) {
|
| | | |
| | | // 是否提前准备下一个文件
|
| | | if ( opts.prepareNextFile && !me.pending.length ) {
|
| | | me._prepareNextFile();
|
| | | }
|
| | | |
| | | return act.fetch();
|
| | | |
| | | // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。
|
| | | } else if ( me.runing ) {
|
| | | |
| | | // 如果缓存中有,则直接在缓存中取,没有则去queue中取。
|
| | | if ( !me.pending.length && me.getStats().numOfQueue ) {
|
| | | me._prepareNextFile();
|
| | | }
|
| | | |
| | | next = me.pending.shift();
|
| | | done = function( file ) {
|
| | | if ( !file ) {
|
| | | return null;
|
| | | }
|
| | | |
| | | act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 );
|
| | | me._act = act;
|
| | | return act.fetch();
|
| | | };
|
| | | |
| | | // 文件可能还在prepare中,也有可能已经完全准备好了。
|
| | | return isPromise( next ) ?
|
| | | next[ next.pipe ? 'pipe' : 'then']( done ) :
|
| | | done( next );
|
| | | }
|
| | | },
|
| | | |
| | | |
| | | /**
|
| | | * @event uploadStart
|
| | | * @param {File} file File对象
|
| | | * @description 某个文件开始上传前触发,一个文件只会触发一次。
|
| | | * @for Uploader
|
| | | */
|
| | | _prepareNextFile: function() {
|
| | | var me = this,
|
| | | file = me.request('fetch-file'),
|
| | | pending = me.pending,
|
| | | promise;
|
| | | |
| | | if ( file ) {
|
| | | promise = me.request( 'before-send-file', file, function() {
|
| | | |
| | | // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued.
|
| | | if ( file.getStatus() === Status.QUEUED ) {
|
| | | me.owner.trigger( 'uploadStart', file );
|
| | | file.setStatus( Status.PROGRESS );
|
| | | return file;
|
| | | }
|
| | | |
| | | return me._finishFile( file );
|
| | | });
|
| | | |
| | | // 如果还在pending中,则替换成文件本身。
|
| | | promise.done(function() {
|
| | | var idx = $.inArray( promise, pending );
|
| | | |
| | | ~idx && pending.splice( idx, 1, file );
|
| | | });
|
| | | |
| | | // befeore-send-file的钩子就有错误发生。
|
| | | promise.fail(function( reason ) {
|
| | | file.setStatus( Status.ERROR, reason );
|
| | | me.owner.trigger( 'uploadError', file, reason );
|
| | | me.owner.trigger( 'uploadComplete', file );
|
| | | });
|
| | | |
| | | pending.push( promise );
|
| | | }
|
| | | },
|
| | | |
| | | // 让出位置了,可以让其他分片开始上传
|
| | | _popBlock: function( block ) {
|
| | | var idx = $.inArray( block, this.pool );
|
| | | |
| | | this.pool.splice( idx, 1 );
|
| | | block.file.remaning--;
|
| | | this.remaning--;
|
| | | },
|
| | | |
| | | // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。
|
| | | _startSend: function( block ) {
|
| | | var me = this,
|
| | | file = block.file,
|
| | | promise;
|
| | | |
| | | me.pool.push( block );
|
| | | me.remaning++;
|
| | | |
| | | // 如果没有分片,则直接使用原始的。
|
| | | // 不会丢失content-type信息。
|
| | | block.blob = block.chunks === 1 ? file.source :
|
| | | file.source.slice( block.start, block.end );
|
| | | |
| | | // hook, 每个分片发送之前可能要做些异步的事情。
|
| | | promise = me.request( 'before-send', block, function() {
|
| | | |
| | | // 有可能文件已经上传出错了,所以不需要再传输了。
|
| | | if ( file.getStatus() === Status.PROGRESS ) {
|
| | | me._doSend( block );
|
| | | } else {
|
| | | me._popBlock( block );
|
| | | Base.nextTick( me.__tick );
|
| | | }
|
| | | });
|
| | | |
| | | // 如果为fail了,则跳过此分片。
|
| | | promise.fail(function() {
|
| | | if ( file.remaning === 1 ) {
|
| | | me._finishFile( file ).always(function() {
|
| | | block.percentage = 1;
|
| | | me._popBlock( block );
|
| | | me.owner.trigger( 'uploadComplete', file );
|
| | | Base.nextTick( me.__tick );
|
| | | });
|
| | | } else {
|
| | | block.percentage = 1;
|
| | | me._popBlock( block );
|
| | | Base.nextTick( me.__tick );
|
| | | }
|
| | | });
|
| | | },
|
| | | |
| | | |
| | | /**
|
| | | * @event uploadBeforeSend
|
| | | * @param {Object} object
|
| | | * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。
|
| | | * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @event uploadAccept
|
| | | * @param {Object} object
|
| | | * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。
|
| | | * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @event uploadProgress
|
| | | * @param {File} file File对象
|
| | | * @param {Number} percentage 上传进度
|
| | | * @description 上传过程中触发,携带上传进度。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | |
| | | /**
|
| | | * @event uploadError
|
| | | * @param {File} file File对象
|
| | | * @param {String} reason 出错的code
|
| | | * @description 当文件上传出错时触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @event uploadSuccess
|
| | | * @param {File} file File对象
|
| | | * @param {Object} response 服务端返回的数据
|
| | | * @description 当文件上传成功时触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | /**
|
| | | * @event uploadComplete
|
| | | * @param {File} [file] File对象
|
| | | * @description 不管成功或者失败,文件上传完成时触发。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | // 做上传操作。
|
| | | _doSend: function( block ) {
|
| | | var me = this,
|
| | | owner = me.owner,
|
| | | opts = me.options,
|
| | | file = block.file,
|
| | | tr = new Transport( opts ),
|
| | | data = $.extend({}, opts.formData ),
|
| | | headers = $.extend({}, opts.headers ),
|
| | | requestAccept, ret;
|
| | | |
| | | block.transport = tr;
|
| | | |
| | | tr.on( 'destroy', function() {
|
| | | delete block.transport;
|
| | | me._popBlock( block );
|
| | | Base.nextTick( me.__tick );
|
| | | });
|
| | | |
| | | // 广播上传进度。以文件为单位。
|
| | | tr.on( 'progress', function( percentage ) {
|
| | | var totalPercent = 0,
|
| | | uploaded = 0;
|
| | | |
| | | // 可能没有abort掉,progress还是执行进来了。
|
| | | // if ( !file.blocks ) {
|
| | | // return;
|
| | | // }
|
| | | |
| | | totalPercent = block.percentage = percentage;
|
| | | |
| | | if ( block.chunks > 1 ) { // 计算文件的整体速度。
|
| | | $.each( file.blocks, function( _, v ) {
|
| | | uploaded += (v.percentage || 0) * (v.end - v.start);
|
| | | });
|
| | | |
| | | totalPercent = uploaded / file.size;
|
| | | }
|
| | | |
| | | owner.trigger( 'uploadProgress', file, totalPercent || 0 );
|
| | | });
|
| | | |
| | | // 用来询问,是否返回的结果是有错误的。
|
| | | requestAccept = function( reject ) {
|
| | | var fn;
|
| | | |
| | | ret = tr.getResponseAsJson() || {};
|
| | | ret._raw = tr.getResponse();
|
| | | fn = function( value ) {
|
| | | reject = value;
|
| | | };
|
| | | |
| | | // 服务端响应了,不代表成功了,询问是否响应正确。
|
| | | if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) {
|
| | | reject = reject || 'server';
|
| | | }
|
| | | |
| | | return reject;
|
| | | };
|
| | | |
| | | // 尝试重试,然后广播文件上传出错。
|
| | | tr.on( 'error', function( type, flag ) {
|
| | | block.retried = block.retried || 0;
|
| | | |
| | | // 自动重试
|
| | | if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) &&
|
| | | block.retried < opts.chunkRetry ) {
|
| | | |
| | | block.retried++;
|
| | | tr.send();
|
| | | |
| | | } else {
|
| | | |
| | | // http status 500 ~ 600
|
| | | if ( !flag && type === 'server' ) {
|
| | | type = requestAccept( type );
|
| | | }
|
| | | |
| | | file.setStatus( Status.ERROR, type );
|
| | | owner.trigger( 'uploadError', file, type );
|
| | | owner.trigger( 'uploadComplete', file );
|
| | | }
|
| | | });
|
| | | |
| | | // 上传成功
|
| | | tr.on( 'load', function() {
|
| | | var reason;
|
| | | |
| | | // 如果非预期,转向上传出错。
|
| | | if ( (reason = requestAccept()) ) {
|
| | | tr.trigger( 'error', reason, true );
|
| | | return;
|
| | | }
|
| | | |
| | | // 全部上传完成。
|
| | | if ( file.remaning === 1 ) {
|
| | | me._finishFile( file, ret );
|
| | | } else {
|
| | | tr.destroy();
|
| | | }
|
| | | });
|
| | | |
| | | // 配置默认的上传字段。
|
| | | data = $.extend( data, {
|
| | | id: file.id,
|
| | | name: file.name,
|
| | | type: file.type,
|
| | | lastModifiedDate: file.lastModifiedDate,
|
| | | size: file.size
|
| | | });
|
| | | |
| | | block.chunks > 1 && $.extend( data, {
|
| | | chunks: block.chunks,
|
| | | chunk: block.chunk
|
| | | });
|
| | | |
| | | // 在发送之间可以添加字段什么的。。。
|
| | | // 如果默认的字段不够使用,可以通过监听此事件来扩展
|
| | | owner.trigger( 'uploadBeforeSend', block, data, headers );
|
| | | |
| | | // 开始发送。
|
| | | tr.appendBlob( opts.fileVal, block.blob, file.name );
|
| | | tr.append( data );
|
| | | tr.setRequestHeader( headers );
|
| | | tr.send();
|
| | | },
|
| | | |
| | | // 完成上传。
|
| | | _finishFile: function( file, ret, hds ) {
|
| | | var owner = this.owner;
|
| | | |
| | | return owner
|
| | | .request( 'after-send-file', arguments, function() {
|
| | | file.setStatus( Status.COMPLETE );
|
| | | owner.trigger( 'uploadSuccess', file, ret, hds );
|
| | | })
|
| | | .fail(function( reason ) {
|
| | | |
| | | // 如果外部已经标记为invalid什么的,不再改状态。
|
| | | if ( file.getStatus() === Status.PROGRESS ) {
|
| | | file.setStatus( Status.ERROR, reason );
|
| | | }
|
| | | |
| | | owner.trigger( 'uploadError', file, reason );
|
| | | })
|
| | | .always(function() {
|
| | | owner.trigger( 'uploadComplete', file );
|
| | | });
|
| | | }
|
| | | |
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。
|
| | | */
|
| | | |
| | | define('widgets/validator',[
|
| | | 'base',
|
| | | 'uploader',
|
| | | 'file',
|
| | | 'widgets/widget'
|
| | | ], function( Base, Uploader, WUFile ) {
|
| | | |
| | | var $ = Base.$,
|
| | | validators = {},
|
| | | api;
|
| | | |
| | | /**
|
| | | * @event error
|
| | | * @param {String} type 错误类型。
|
| | | * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。
|
| | | *
|
| | | * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。
|
| | | * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。
|
| | | * @for Uploader
|
| | | */
|
| | | |
| | | // 暴露给外面的api
|
| | | api = {
|
| | | |
| | | // 添加验证器
|
| | | addValidator: function( type, cb ) {
|
| | | validators[ type ] = cb;
|
| | | },
|
| | | |
| | | // 移除验证器
|
| | | removeValidator: function( type ) {
|
| | | delete validators[ type ];
|
| | | }
|
| | | };
|
| | | |
| | | // 在Uploader初始化的时候启动Validators的初始化
|
| | | Uploader.register({
|
| | | init: function() {
|
| | | var me = this;
|
| | | $.each( validators, function() {
|
| | | this.call( me.owner );
|
| | | });
|
| | | }
|
| | | });
|
| | | |
| | | /**
|
| | | * @property {int} [fileNumLimit=undefined]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 验证文件总数量, 超出则不允许加入队列。
|
| | | */
|
| | | api.addValidator( 'fileNumLimit', function() {
|
| | | var uploader = this,
|
| | | opts = uploader.options,
|
| | | count = 0,
|
| | | max = opts.fileNumLimit >> 0,
|
| | | flag = true;
|
| | | |
| | | if ( !max ) {
|
| | | return;
|
| | | }
|
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) {
|
| | | |
| | | if ( count >= max && flag ) {
|
| | | flag = false;
|
| | | this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file );
|
| | | setTimeout(function() {
|
| | | flag = true;
|
| | | }, 1 );
|
| | | }
|
| | | |
| | | return count >= max ? false : true;
|
| | | });
|
| | | |
| | | uploader.on( 'fileQueued', function() {
|
| | | count++;
|
| | | });
|
| | | |
| | | uploader.on( 'fileDequeued', function() {
|
| | | count--;
|
| | | });
|
| | | |
| | | uploader.on( 'uploadFinished', function() {
|
| | | count = 0;
|
| | | });
|
| | | });
|
| | | |
| | | |
| | | /**
|
| | | * @property {int} [fileSizeLimit=undefined]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。
|
| | | */
|
| | | api.addValidator( 'fileSizeLimit', function() {
|
| | | var uploader = this,
|
| | | opts = uploader.options,
|
| | | count = 0,
|
| | | max = opts.fileSizeLimit >> 0,
|
| | | flag = true;
|
| | | |
| | | if ( !max ) {
|
| | | return;
|
| | | }
|
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) {
|
| | | var invalid = count + file.size > max;
|
| | | |
| | | if ( invalid && flag ) {
|
| | | flag = false;
|
| | | this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file );
|
| | | setTimeout(function() {
|
| | | flag = true;
|
| | | }, 1 );
|
| | | }
|
| | | |
| | | return invalid ? false : true;
|
| | | });
|
| | | |
| | | uploader.on( 'fileQueued', function( file ) {
|
| | | count += file.size;
|
| | | });
|
| | | |
| | | uploader.on( 'fileDequeued', function( file ) {
|
| | | count -= file.size;
|
| | | });
|
| | | |
| | | uploader.on( 'uploadFinished', function() {
|
| | | count = 0;
|
| | | });
|
| | | });
|
| | | |
| | | /**
|
| | | * @property {int} [fileSingleSizeLimit=undefined]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。
|
| | | */
|
| | | api.addValidator( 'fileSingleSizeLimit', function() {
|
| | | var uploader = this,
|
| | | opts = uploader.options,
|
| | | max = opts.fileSingleSizeLimit;
|
| | | |
| | | if ( !max ) {
|
| | | return;
|
| | | }
|
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) {
|
| | | |
| | | if ( file.size > max ) {
|
| | | file.setStatus( WUFile.Status.INVALID, 'exceed_size' );
|
| | | this.trigger( 'error', 'F_EXCEED_SIZE', file );
|
| | | return false;
|
| | | }
|
| | | |
| | | });
|
| | | |
| | | });
|
| | | |
| | | /**
|
| | | * @property {int} [duplicate=undefined]
|
| | | * @namespace options
|
| | | * @for Uploader
|
| | | * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key.
|
| | | */
|
| | | api.addValidator( 'duplicate', function() {
|
| | | var uploader = this,
|
| | | opts = uploader.options,
|
| | | mapping = {};
|
| | | |
| | | if ( opts.duplicate ) {
|
| | | return;
|
| | | }
|
| | | |
| | | function hashString( str ) {
|
| | | var hash = 0,
|
| | | i = 0,
|
| | | len = str.length,
|
| | | _char;
|
| | | |
| | | for ( ; i < len; i++ ) {
|
| | | _char = str.charCodeAt( i );
|
| | | hash = _char + (hash << 6) + (hash << 16) - hash;
|
| | | }
|
| | | |
| | | return hash;
|
| | | }
|
| | | |
| | | uploader.on( 'beforeFileQueued', function( file ) {
|
| | | var hash = file.__hash || (file.__hash = hashString( file.name +
|
| | | file.size + file.lastModifiedDate ));
|
| | | |
| | | // 已经重复了
|
| | | if ( mapping[ hash ] ) {
|
| | | this.trigger( 'error', 'F_DUPLICATE', file );
|
| | | return false;
|
| | | }
|
| | | });
|
| | | |
| | | uploader.on( 'fileQueued', function( file ) {
|
| | | var hash = file.__hash;
|
| | | |
| | | hash && (mapping[ hash ] = true);
|
| | | });
|
| | | |
| | | uploader.on( 'fileDequeued', function( file ) {
|
| | | var hash = file.__hash;
|
| | | |
| | | hash && (delete mapping[ hash ]);
|
| | | });
|
| | | });
|
| | | |
| | | return api;
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview Runtime管理器,负责Runtime的选择, 连接
|
| | | */
|
| | | define('runtime/compbase',[],function() {
|
| | | |
| | | function CompBase( owner, runtime ) {
|
| | | |
| | | this.owner = owner;
|
| | | this.options = owner.options;
|
| | | |
| | | this.getRuntime = function() {
|
| | | return runtime;
|
| | | };
|
| | | |
| | | this.getRuid = function() {
|
| | | return runtime.uid;
|
| | | };
|
| | | |
| | | this.trigger = function() {
|
| | | return owner.trigger.apply( owner, arguments );
|
| | | };
|
| | | }
|
| | | |
| | | return CompBase;
|
| | | });
|
| | | /**
|
| | | * @fileOverview Html5Runtime
|
| | | */
|
| | | define('runtime/html5/runtime',[
|
| | | 'base',
|
| | | 'runtime/runtime',
|
| | | 'runtime/compbase'
|
| | | ], function( Base, Runtime, CompBase ) {
|
| | | |
| | | var type = 'html5',
|
| | | components = {};
|
| | | |
| | | function Html5Runtime() {
|
| | | var pool = {},
|
| | | me = this,
|
| | | destory = this.destory;
|
| | | |
| | | Runtime.apply( me, arguments );
|
| | | me.type = type;
|
| | | |
| | | |
| | | // 这个方法的调用者,实际上是RuntimeClient
|
| | | me.exec = function( comp, fn/*, args...*/) {
|
| | | var client = this,
|
| | | uid = client.uid,
|
| | | args = Base.slice( arguments, 2 ),
|
| | | instance;
|
| | | |
| | | if ( components[ comp ] ) {
|
| | | instance = pool[ uid ] = pool[ uid ] ||
|
| | | new components[ comp ]( client, me );
|
| | | |
| | | if ( instance[ fn ] ) {
|
| | | return instance[ fn ].apply( instance, args );
|
| | | }
|
| | | }
|
| | | };
|
| | | |
| | | me.destory = function() {
|
| | | // @todo 删除池子中的所有实例
|
| | | return destory && destory.apply( this, arguments );
|
| | | };
|
| | | }
|
| | | |
| | | Base.inherits( Runtime, {
|
| | | constructor: Html5Runtime,
|
| | | |
| | | // 不需要连接其他程序,直接执行callback
|
| | | init: function() {
|
| | | var me = this;
|
| | | setTimeout(function() {
|
| | | me.trigger('ready');
|
| | | }, 1 );
|
| | | }
|
| | | |
| | | });
|
| | | |
| | | // 注册Components
|
| | | Html5Runtime.register = function( name, component ) {
|
| | | var klass = components[ name ] = Base.inherits( CompBase, component );
|
| | | return klass;
|
| | | };
|
| | | |
| | | // 注册html5运行时。
|
| | | // 只有在支持的前提下注册。
|
| | | if ( window.Blob && window.FileReader && window.DataView ) {
|
| | | Runtime.addRuntime( type, Html5Runtime );
|
| | | }
|
| | | |
| | | return Html5Runtime;
|
| | | });
|
| | | /**
|
| | | * @fileOverview Blob Html实现
|
| | | */
|
| | | define('runtime/html5/blob',[
|
| | | 'runtime/html5/runtime',
|
| | | 'lib/blob'
|
| | | ], function( Html5Runtime, Blob ) {
|
| | | |
| | | return Html5Runtime.register( 'Blob', {
|
| | | slice: function( start, end ) {
|
| | | var blob = this.owner.source,
|
| | | slice = blob.slice || blob.webkitSlice || blob.mozSlice;
|
| | | |
| | | blob = slice.call( blob, start, end );
|
| | | |
| | | return new Blob( this.getRuid(), blob );
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview FilePaste
|
| | | */
|
| | | define('runtime/html5/dnd',[
|
| | | 'base',
|
| | | 'runtime/html5/runtime',
|
| | | 'lib/file'
|
| | | ], function( Base, Html5Runtime, File ) {
|
| | | |
| | | var $ = Base.$,
|
| | | prefix = 'webuploader-dnd-';
|
| | | |
| | | return Html5Runtime.register( 'DragAndDrop', {
|
| | | init: function() {
|
| | | var elem = this.elem = this.options.container;
|
| | | |
| | | this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this );
|
| | | this.dragOverHandler = Base.bindFn( this._dragOverHandler, this );
|
| | | this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this );
|
| | | this.dropHandler = Base.bindFn( this._dropHandler, this );
|
| | | this.dndOver = false;
|
| | | |
| | | elem.on( 'dragenter', this.dragEnterHandler );
|
| | | elem.on( 'dragover', this.dragOverHandler );
|
| | | elem.on( 'dragleave', this.dragLeaveHandler );
|
| | | elem.on( 'drop', this.dropHandler );
|
| | | |
| | | if ( this.options.disableGlobalDnd ) {
|
| | | $( document ).on( 'dragover', this.dragOverHandler );
|
| | | $( document ).on( 'drop', this.dropHandler );
|
| | | }
|
| | | },
|
| | | |
| | | _dragEnterHandler: function( e ) {
|
| | | var me = this,
|
| | | denied = me._denied || false,
|
| | | items;
|
| | | |
| | | e = e.originalEvent || e;
|
| | | |
| | | if ( !me.dndOver ) {
|
| | | me.dndOver = true;
|
| | | |
| | | // 注意只有 chrome 支持。
|
| | | items = e.dataTransfer.items;
|
| | | |
| | | if ( items && items.length ) {
|
| | | me._denied = denied = !me.trigger( 'accept', items );
|
| | | }
|
| | | |
| | | me.elem.addClass( prefix + 'over' );
|
| | | me.elem[ denied ? 'addClass' :
|
| | | 'removeClass' ]( prefix + 'denied' );
|
| | | }
|
| | | |
| | | |
| | | e.dataTransfer.dropEffect = denied ? 'none' : 'copy';
|
| | | |
| | | return false;
|
| | | },
|
| | | |
| | | _dragOverHandler: function( e ) {
|
| | | // 只处理框内的。
|
| | | var parentElem = this.elem.parent().get( 0 );
|
| | | if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) {
|
| | | return false;
|
| | | }
|
| | | |
| | | clearTimeout( this._leaveTimer );
|
| | | this._dragEnterHandler.call( this, e );
|
| | | |
| | | return false;
|
| | | },
|
| | | |
| | | _dragLeaveHandler: function() {
|
| | | var me = this,
|
| | | handler;
|
| | | |
| | | handler = function() {
|
| | | me.dndOver = false;
|
| | | me.elem.removeClass( prefix + 'over ' + prefix + 'denied' );
|
| | | };
|
| | | |
| | | clearTimeout( me._leaveTimer );
|
| | | me._leaveTimer = setTimeout( handler, 100 );
|
| | | return false;
|
| | | },
|
| | | |
| | | _dropHandler: function( e ) {
|
| | | var me = this,
|
| | | ruid = me.getRuid(),
|
| | | parentElem = me.elem.parent().get( 0 );
|
| | | |
| | | // 只处理框内的。
|
| | | if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) {
|
| | | return false;
|
| | | }
|
| | | |
| | | me._getTansferFiles( e, function( results ) {
|
| | | me.trigger( 'drop', $.map( results, function( file ) {
|
| | | return new File( ruid, file );
|
| | | }) );
|
| | | });
|
| | | |
| | | me.dndOver = false;
|
| | | me.elem.removeClass( prefix + 'over' );
|
| | | return false;
|
| | | },
|
| | | |
| | | // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。
|
| | | _getTansferFiles: function( e, callback ) {
|
| | | var results = [],
|
| | | promises = [],
|
| | | items, files, dataTransfer, file, item, i, len, canAccessFolder;
|
| | | |
| | | e = e.originalEvent || e;
|
| | | |
| | | dataTransfer = e.dataTransfer;
|
| | | items = dataTransfer.items;
|
| | | files = dataTransfer.files;
|
| | | |
| | | canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry);
|
| | | |
| | | for ( i = 0, len = files.length; i < len; i++ ) {
|
| | | file = files[ i ];
|
| | | item = items && items[ i ];
|
| | | |
| | | if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) {
|
| | | |
| | | promises.push( this._traverseDirectoryTree(
|
| | | item.webkitGetAsEntry(), results ) );
|
| | | } else {
|
| | | results.push( file );
|
| | | }
|
| | | }
|
| | | |
| | | Base.when.apply( Base, promises ).done(function() {
|
| | | |
| | | if ( !results.length ) {
|
| | | return;
|
| | | }
|
| | | |
| | | callback( results );
|
| | | });
|
| | | },
|
| | | |
| | | _traverseDirectoryTree: function( entry, results ) {
|
| | | var deferred = Base.Deferred(),
|
| | | me = this;
|
| | | |
| | | if ( entry.isFile ) {
|
| | | entry.file(function( file ) {
|
| | | results.push( file );
|
| | | deferred.resolve();
|
| | | });
|
| | | } else if ( entry.isDirectory ) {
|
| | | entry.createReader().readEntries(function( entries ) {
|
| | | var len = entries.length,
|
| | | promises = [],
|
| | | arr = [], // 为了保证顺序。
|
| | | i;
|
| | | |
| | | for ( i = 0; i < len; i++ ) {
|
| | | promises.push( me._traverseDirectoryTree(
|
| | | entries[ i ], arr ) );
|
| | | }
|
| | | |
| | | Base.when.apply( Base, promises ).then(function() {
|
| | | results.push.apply( results, arr );
|
| | | deferred.resolve();
|
| | | }, deferred.reject );
|
| | | });
|
| | | }
|
| | | |
| | | return deferred.promise();
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | var elem = this.elem;
|
| | | |
| | | elem.off( 'dragenter', this.dragEnterHandler );
|
| | | elem.off( 'dragover', this.dragEnterHandler );
|
| | | elem.off( 'dragleave', this.dragLeaveHandler );
|
| | | elem.off( 'drop', this.dropHandler );
|
| | | |
| | | if ( this.options.disableGlobalDnd ) {
|
| | | $( document ).off( 'dragover', this.dragOverHandler );
|
| | | $( document ).off( 'drop', this.dropHandler );
|
| | | }
|
| | | }
|
| | | });
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview FilePaste
|
| | | */
|
| | | define('runtime/html5/filepaste',[
|
| | | 'base',
|
| | | 'runtime/html5/runtime',
|
| | | 'lib/file'
|
| | | ], function( Base, Html5Runtime, File ) {
|
| | | |
| | | return Html5Runtime.register( 'FilePaste', {
|
| | | init: function() {
|
| | | var opts = this.options,
|
| | | elem = this.elem = opts.container,
|
| | | accept = '.*',
|
| | | arr, i, len, item;
|
| | | |
| | | // accetp的mimeTypes中生成匹配正则。
|
| | | if ( opts.accept ) {
|
| | | arr = [];
|
| | | |
| | | for ( i = 0, len = opts.accept.length; i < len; i++ ) {
|
| | | item = opts.accept[ i ].mimeTypes;
|
| | | item && arr.push( item );
|
| | | }
|
| | | |
| | | if ( arr.length ) {
|
| | | accept = arr.join(',');
|
| | | accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' );
|
| | | }
|
| | | }
|
| | | this.accept = accept = new RegExp( accept, 'i' );
|
| | | this.hander = Base.bindFn( this._pasteHander, this );
|
| | | elem.on( 'paste', this.hander );
|
| | | },
|
| | | |
| | | _pasteHander: function( e ) {
|
| | | var allowed = [],
|
| | | ruid = this.getRuid(),
|
| | | items, item, blob, i, len;
|
| | | |
| | | e = e.originalEvent || e;
|
| | | items = e.clipboardData.items;
|
| | | |
| | | for ( i = 0, len = items.length; i < len; i++ ) {
|
| | | item = items[ i ];
|
| | | |
| | | if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) {
|
| | | continue;
|
| | | }
|
| | | |
| | | allowed.push( new File( ruid, blob ) );
|
| | | }
|
| | | |
| | | if ( allowed.length ) {
|
| | | // 不阻止非文件粘贴(文字粘贴)的事件冒泡
|
| | | e.preventDefault();
|
| | | e.stopPropagation();
|
| | | this.trigger( 'paste', allowed );
|
| | | }
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | this.elem.off( 'paste', this.hander );
|
| | | }
|
| | | });
|
| | | });
|
| | | |
| | | /**
|
| | | * @fileOverview FilePicker
|
| | | */
|
| | | define('runtime/html5/filepicker',[
|
| | | 'base',
|
| | | 'runtime/html5/runtime'
|
| | | ], function( Base, Html5Runtime ) {
|
| | | |
| | | var $ = Base.$;
|
| | | |
| | | return Html5Runtime.register( 'FilePicker', {
|
| | | init: function() {
|
| | | var container = this.getRuntime().getContainer(),
|
| | | me = this,
|
| | | owner = me.owner,
|
| | | opts = me.options,
|
| | | lable = $( document.createElement('label') ),
|
| | | input = $( document.createElement('input') ),
|
| | | arr, i, len, mouseHandler;
|
| | | |
| | | input.attr( 'type', 'file' );
|
| | | input.attr( 'name', opts.name );
|
| | | input.addClass('webuploader-element-invisible');
|
| | | |
| | | lable.on( 'click', function() {
|
| | | input.trigger('click');
|
| | | });
|
| | | |
| | | lable.css({
|
| | | opacity: 0,
|
| | | width: '100%',
|
| | | height: '100%',
|
| | | display: 'block',
|
| | | cursor: 'pointer',
|
| | | background: '#ffffff'
|
| | | });
|
| | | |
| | | if ( opts.multiple ) {
|
| | | input.attr( 'multiple', 'multiple' );
|
| | | }
|
| | | |
| | | // @todo Firefox不支持单独指定后缀
|
| | | if ( opts.accept && opts.accept.length > 0 ) {
|
| | | arr = [];
|
| | | |
| | | for ( i = 0, len = opts.accept.length; i < len; i++ ) {
|
| | | arr.push( opts.accept[ i ].mimeTypes );
|
| | | }
|
| | | |
| | | input.attr( 'accept', arr.join(',') );
|
| | | }
|
| | | |
| | | container.append( input );
|
| | | container.append( lable );
|
| | | |
| | | mouseHandler = function( e ) {
|
| | | owner.trigger( e.type );
|
| | | };
|
| | | |
| | | input.on( 'change', function( e ) {
|
| | | var fn = arguments.callee,
|
| | | clone;
|
| | | |
| | | me.files = e.target.files;
|
| | | |
| | | // reset input
|
| | | clone = this.cloneNode( true );
|
| | | this.parentNode.replaceChild( clone, this );
|
| | | |
| | | input.off();
|
| | | input = $( clone ).on( 'change', fn )
|
| | | .on( 'mouseenter mouseleave', mouseHandler );
|
| | | |
| | | owner.trigger('change');
|
| | | });
|
| | | |
| | | lable.on( 'mouseenter mouseleave', mouseHandler );
|
| | | |
| | | },
|
| | | |
| | | |
| | | getFiles: function() {
|
| | | return this.files;
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | // todo
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview Transport
|
| | | * @todo 支持chunked传输,优势:
|
| | | * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分,
|
| | | * 而不需要重头再传一次。另外断点续传也需要用chunked方式。
|
| | | */
|
| | | define('runtime/html5/transport',[
|
| | | 'base',
|
| | | 'runtime/html5/runtime'
|
| | | ], function( Base, Html5Runtime ) {
|
| | | |
| | | var noop = Base.noop,
|
| | | $ = Base.$;
|
| | | |
| | | return Html5Runtime.register( 'Transport', {
|
| | | init: function() {
|
| | | this._status = 0;
|
| | | this._response = null;
|
| | | },
|
| | | |
| | | send: function() {
|
| | | var owner = this.owner,
|
| | | opts = this.options,
|
| | | xhr = this._initAjax(),
|
| | | blob = owner._blob,
|
| | | server = opts.server,
|
| | | formData, binary, fr;
|
| | | |
| | | if ( opts.sendAsBinary ) {
|
| | | server += (/\?/.test( server ) ? '&' : '?') +
|
| | | $.param( owner._formData );
|
| | | |
| | | binary = blob.getSource();
|
| | | } else {
|
| | | formData = new FormData();
|
| | | $.each( owner._formData, function( k, v ) {
|
| | | formData.append( k, v );
|
| | | });
|
| | | |
| | | formData.append( opts.fileVal, blob.getSource(),
|
| | | opts.filename || owner._formData.name || '' );
|
| | | }
|
| | | |
| | | if ( opts.withCredentials && 'withCredentials' in xhr ) {
|
| | | xhr.open( opts.method, server, true );
|
| | | xhr.withCredentials = true;
|
| | | } else {
|
| | | xhr.open( opts.method, server );
|
| | | }
|
| | | |
| | | this._setRequestHeader( xhr, opts.headers );
|
| | | |
| | | if ( binary ) {
|
| | | xhr.overrideMimeType('application/octet-stream');
|
| | | |
| | | // android直接发送blob会导致服务端接收到的是空文件。
|
| | | // bug详情。
|
| | | // https://code.google.com/p/android/issues/detail?id=39882
|
| | | // 所以先用fileReader读取出来再通过arraybuffer的方式发送。
|
| | | if ( Base.os.android ) {
|
| | | fr = new FileReader();
|
| | | |
| | | fr.onload = function() {
|
| | | xhr.send( this.result );
|
| | | fr = fr.onload = null;
|
| | | };
|
| | | |
| | | fr.readAsArrayBuffer( binary );
|
| | | } else {
|
| | | xhr.send( binary );
|
| | | }
|
| | | } else {
|
| | | xhr.send( formData );
|
| | | }
|
| | | },
|
| | | |
| | | getResponse: function() {
|
| | | return this._response;
|
| | | },
|
| | | |
| | | getResponseAsJson: function() {
|
| | | return this._parseJson( this._response );
|
| | | },
|
| | | |
| | | getStatus: function() {
|
| | | return this._status;
|
| | | },
|
| | | |
| | | abort: function() {
|
| | | var xhr = this._xhr;
|
| | | |
| | | if ( xhr ) {
|
| | | xhr.upload.onprogress = noop;
|
| | | xhr.onreadystatechange = noop;
|
| | | xhr.abort();
|
| | | |
| | | this._xhr = xhr = null;
|
| | | }
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | this.abort();
|
| | | },
|
| | | |
| | | _initAjax: function() {
|
| | | var me = this,
|
| | | xhr = new XMLHttpRequest(),
|
| | | opts = this.options;
|
| | | |
| | | if ( opts.withCredentials && !('withCredentials' in xhr) &&
|
| | | typeof XDomainRequest !== 'undefined' ) {
|
| | | xhr = new XDomainRequest();
|
| | | }
|
| | | |
| | | xhr.upload.onprogress = function( e ) {
|
| | | var percentage = 0;
|
| | | |
| | | if ( e.lengthComputable ) {
|
| | | percentage = e.loaded / e.total;
|
| | | }
|
| | | |
| | | return me.trigger( 'progress', percentage );
|
| | | };
|
| | | |
| | | xhr.onreadystatechange = function() {
|
| | | |
| | | if ( xhr.readyState !== 4 ) {
|
| | | return;
|
| | | }
|
| | | |
| | | xhr.upload.onprogress = noop;
|
| | | xhr.onreadystatechange = noop;
|
| | | me._xhr = null;
|
| | | me._status = xhr.status;
|
| | | |
| | | if ( xhr.status >= 200 && xhr.status < 300 ) {
|
| | | me._response = xhr.responseText;
|
| | | return me.trigger('load');
|
| | | } else if ( xhr.status >= 500 && xhr.status < 600 ) {
|
| | | me._response = xhr.responseText;
|
| | | return me.trigger( 'error', 'server' );
|
| | | }
|
| | | |
| | | |
| | | return me.trigger( 'error', me._status ? 'http' : 'abort' );
|
| | | };
|
| | | |
| | | me._xhr = xhr;
|
| | | return xhr;
|
| | | },
|
| | | |
| | | _setRequestHeader: function( xhr, headers ) {
|
| | | $.each( headers, function( key, val ) {
|
| | | xhr.setRequestHeader( key, val );
|
| | | });
|
| | | },
|
| | | |
| | | _parseJson: function( str ) {
|
| | | var json;
|
| | | |
| | | try {
|
| | | json = JSON.parse( str );
|
| | | } catch ( ex ) {
|
| | | json = {};
|
| | | }
|
| | | |
| | | return json;
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview FlashRuntime
|
| | | */
|
| | | define('runtime/flash/runtime',[
|
| | | 'base',
|
| | | 'runtime/runtime',
|
| | | 'runtime/compbase'
|
| | | ], function( Base, Runtime, CompBase ) {
|
| | | |
| | | var $ = Base.$,
|
| | | type = 'flash',
|
| | | components = {};
|
| | | |
| | | |
| | | function getFlashVersion() {
|
| | | var version;
|
| | | |
| | | try {
|
| | | version = navigator.plugins[ 'Shockwave Flash' ];
|
| | | version = version.description;
|
| | | } catch ( ex ) {
|
| | | try {
|
| | | version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash')
|
| | | .GetVariable('$version');
|
| | | } catch ( ex2 ) {
|
| | | version = '0.0';
|
| | | }
|
| | | }
|
| | | version = version.match( /\d+/g );
|
| | | return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 );
|
| | | }
|
| | | |
| | | function FlashRuntime() {
|
| | | var pool = {},
|
| | | clients = {},
|
| | | destory = this.destory,
|
| | | me = this,
|
| | | jsreciver = Base.guid('webuploader_');
|
| | | |
| | | Runtime.apply( me, arguments );
|
| | | me.type = type;
|
| | | |
| | | |
| | | // 这个方法的调用者,实际上是RuntimeClient
|
| | | me.exec = function( comp, fn/*, args...*/ ) {
|
| | | var client = this,
|
| | | uid = client.uid,
|
| | | args = Base.slice( arguments, 2 ),
|
| | | instance;
|
| | | |
| | | clients[ uid ] = client;
|
| | | |
| | | if ( components[ comp ] ) {
|
| | | if ( !pool[ uid ] ) {
|
| | | pool[ uid ] = new components[ comp ]( client, me );
|
| | | }
|
| | | |
| | | instance = pool[ uid ];
|
| | | |
| | | if ( instance[ fn ] ) {
|
| | | return instance[ fn ].apply( instance, args );
|
| | | }
|
| | | }
|
| | | |
| | | return me.flashExec.apply( client, arguments );
|
| | | };
|
| | | |
| | | function handler( evt, obj ) {
|
| | | var type = evt.type || evt,
|
| | | parts, uid;
|
| | | |
| | | parts = type.split('::');
|
| | | uid = parts[ 0 ];
|
| | | type = parts[ 1 ];
|
| | | |
| | | // console.log.apply( console, arguments );
|
| | | |
| | | if ( type === 'Ready' && uid === me.uid ) {
|
| | | me.trigger('ready');
|
| | | } else if ( clients[ uid ] ) {
|
| | | clients[ uid ].trigger( type.toLowerCase(), evt, obj );
|
| | | }
|
| | | |
| | | // Base.log( evt, obj );
|
| | | }
|
| | | |
| | | // flash的接受器。
|
| | | window[ jsreciver ] = function() {
|
| | | var args = arguments;
|
| | | |
| | | // 为了能捕获得到。
|
| | | setTimeout(function() {
|
| | | handler.apply( null, args );
|
| | | }, 1 );
|
| | | };
|
| | | |
| | | this.jsreciver = jsreciver;
|
| | | |
| | | this.destory = function() {
|
| | | // @todo 删除池子中的所有实例
|
| | | return destory && destory.apply( this, arguments );
|
| | | };
|
| | | |
| | | this.flashExec = function( comp, fn ) {
|
| | | var flash = me.getFlash(),
|
| | | args = Base.slice( arguments, 2 );
|
| | | |
| | | return flash.exec( this.uid, comp, fn, args );
|
| | | };
|
| | | |
| | | // @todo
|
| | | }
|
| | | |
| | | Base.inherits( Runtime, {
|
| | | constructor: FlashRuntime,
|
| | | |
| | | init: function() {
|
| | | var container = this.getContainer(),
|
| | | opts = this.options,
|
| | | html;
|
| | | |
| | | // if not the minimal height, shims are not initialized
|
| | | // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc)
|
| | | container.css({
|
| | | position: 'absolute',
|
| | | top: '-8px',
|
| | | left: '-8px',
|
| | | width: '9px',
|
| | | height: '9px',
|
| | | overflow: 'hidden'
|
| | | });
|
| | | |
| | | // insert flash object
|
| | | html = '<object id="' + this.uid + '" type="application/' +
|
| | | 'x-shockwave-flash" data="' + opts.swf + '" ';
|
| | | |
| | | if ( Base.browser.ie ) {
|
| | | html += 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" ';
|
| | | }
|
| | | |
| | | html += 'width="100%" height="100%" style="outline:0">' +
|
| | | '<param name="movie" value="' + opts.swf + '" />' +
|
| | | '<param name="flashvars" value="uid=' + this.uid +
|
| | | '&jsreciver=' + this.jsreciver + '" />' +
|
| | | '<param name="wmode" value="transparent" />' +
|
| | | '<param name="allowscriptaccess" value="always" />' +
|
| | | '</object>';
|
| | | |
| | | container.html( html );
|
| | | },
|
| | | |
| | | getFlash: function() {
|
| | | if ( this._flash ) {
|
| | | return this._flash;
|
| | | }
|
| | | |
| | | this._flash = $( '#' + this.uid ).get( 0 );
|
| | | return this._flash;
|
| | | }
|
| | | |
| | | });
|
| | | |
| | | FlashRuntime.register = function( name, component ) {
|
| | | component = components[ name ] = Base.inherits( CompBase, $.extend({
|
| | | |
| | | // @todo fix this later
|
| | | flashExec: function() {
|
| | | var owner = this.owner,
|
| | | runtime = this.getRuntime();
|
| | | |
| | | return runtime.flashExec.apply( owner, arguments );
|
| | | }
|
| | | }, component ) );
|
| | | |
| | | return component;
|
| | | };
|
| | | |
| | | if ( getFlashVersion() >= 11.4 ) {
|
| | | Runtime.addRuntime( type, FlashRuntime );
|
| | | }
|
| | | |
| | | return FlashRuntime;
|
| | | });
|
| | | /**
|
| | | * @fileOverview FilePicker
|
| | | */
|
| | | define('runtime/flash/filepicker',[
|
| | | 'base',
|
| | | 'runtime/flash/runtime'
|
| | | ], function( Base, FlashRuntime ) {
|
| | | var $ = Base.$;
|
| | | |
| | | return FlashRuntime.register( 'FilePicker', {
|
| | | init: function( opts ) {
|
| | | var copy = $.extend({}, opts ),
|
| | | len, i;
|
| | | |
| | | // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug.
|
| | | len = copy.accept && copy.accept.length;
|
| | | for ( i = 0; i < len; i++ ) {
|
| | | if ( !copy.accept[ i ].title ) {
|
| | | copy.accept[ i ].title = 'Files';
|
| | | }
|
| | | }
|
| | | |
| | | delete copy.button;
|
| | | delete copy.container;
|
| | | |
| | | this.flashExec( 'FilePicker', 'init', copy );
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | // todo
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview Transport flash实现
|
| | | */
|
| | | define('runtime/flash/transport',[
|
| | | 'base',
|
| | | 'runtime/flash/runtime',
|
| | | 'runtime/client'
|
| | | ], function( Base, FlashRuntime, RuntimeClient ) {
|
| | | var $ = Base.$;
|
| | | |
| | | return FlashRuntime.register( 'Transport', {
|
| | | init: function() {
|
| | | this._status = 0;
|
| | | this._response = null;
|
| | | this._responseJson = null;
|
| | | },
|
| | | |
| | | send: function() {
|
| | | var owner = this.owner,
|
| | | opts = this.options,
|
| | | xhr = this._initAjax(),
|
| | | blob = owner._blob,
|
| | | server = opts.server,
|
| | | binary;
|
| | | |
| | | xhr.connectRuntime( blob.ruid );
|
| | | |
| | | if ( opts.sendAsBinary ) {
|
| | | server += (/\?/.test( server ) ? '&' : '?') +
|
| | | $.param( owner._formData );
|
| | | |
| | | binary = blob.uid;
|
| | | } else {
|
| | | $.each( owner._formData, function( k, v ) {
|
| | | xhr.exec( 'append', k, v );
|
| | | });
|
| | | |
| | | xhr.exec( 'appendBlob', opts.fileVal, blob.uid,
|
| | | opts.filename || owner._formData.name || '' );
|
| | | }
|
| | | |
| | | this._setRequestHeader( xhr, opts.headers );
|
| | | xhr.exec( 'send', {
|
| | | method: opts.method,
|
| | | url: server
|
| | | }, binary );
|
| | | },
|
| | | |
| | | getStatus: function() {
|
| | | return this._status;
|
| | | },
|
| | | |
| | | getResponse: function() {
|
| | | return this._response;
|
| | | },
|
| | | |
| | | getResponseAsJson: function() {
|
| | | return this._responseJson;
|
| | | },
|
| | | |
| | | abort: function() {
|
| | | var xhr = this._xhr;
|
| | | |
| | | if ( xhr ) {
|
| | | xhr.exec('abort');
|
| | | xhr.destroy();
|
| | | this._xhr = xhr = null;
|
| | | }
|
| | | },
|
| | | |
| | | destroy: function() {
|
| | | this.abort();
|
| | | },
|
| | | |
| | | _initAjax: function() {
|
| | | var me = this,
|
| | | xhr = new RuntimeClient('XMLHttpRequest');
|
| | | |
| | | xhr.on( 'uploadprogress progress', function( e ) {
|
| | | return me.trigger( 'progress', e.loaded / e.total );
|
| | | });
|
| | | |
| | | xhr.on( 'load', function() {
|
| | | var status = xhr.exec('getStatus'),
|
| | | err = '';
|
| | | |
| | | xhr.off();
|
| | | me._xhr = null;
|
| | | |
| | | if ( status >= 200 && status < 300 ) {
|
| | | me._response = xhr.exec('getResponse');
|
| | | me._responseJson = xhr.exec('getResponseAsJson');
|
| | | } else if ( status >= 500 && status < 600 ) {
|
| | | me._response = xhr.exec('getResponse');
|
| | | me._responseJson = xhr.exec('getResponseAsJson');
|
| | | err = 'server';
|
| | | } else {
|
| | | err = 'http';
|
| | | }
|
| | | |
| | | xhr.destroy();
|
| | | xhr = null;
|
| | | |
| | | return err ? me.trigger( 'error', err ) : me.trigger('load');
|
| | | });
|
| | | |
| | | xhr.on( 'error', function() {
|
| | | xhr.off();
|
| | | me._xhr = null;
|
| | | me.trigger( 'error', 'http' );
|
| | | });
|
| | | |
| | | me._xhr = xhr;
|
| | | return xhr;
|
| | | },
|
| | | |
| | | _setRequestHeader: function( xhr, headers ) {
|
| | | $.each( headers, function( key, val ) {
|
| | | xhr.exec( 'setRequestHeader', key, val );
|
| | | });
|
| | | }
|
| | | });
|
| | | });
|
| | | /**
|
| | | * @fileOverview 没有图像处理的版本。
|
| | | */
|
| | | define('preset/withoutimage',[
|
| | | 'base',
|
| | | |
| | | // widgets
|
| | | 'widgets/filednd',
|
| | | 'widgets/filepaste',
|
| | | 'widgets/filepicker',
|
| | | 'widgets/queue',
|
| | | 'widgets/runtime',
|
| | | 'widgets/upload',
|
| | | 'widgets/validator',
|
| | | |
| | | // runtimes
|
| | | // html5
|
| | | 'runtime/html5/blob',
|
| | | 'runtime/html5/dnd',
|
| | | 'runtime/html5/filepaste',
|
| | | 'runtime/html5/filepicker',
|
| | | 'runtime/html5/transport',
|
| | | |
| | | // flash
|
| | | 'runtime/flash/filepicker',
|
| | | 'runtime/flash/transport'
|
| | | ], function( Base ) {
|
| | | return Base;
|
| | | });
|
| | | define('webuploader',[
|
| | | 'preset/withoutimage'
|
| | | ], function( preset ) {
|
| | | return preset;
|
| | | });
|
| | | return require('webuploader');
|
| | | });
|