jsdk-offical
Version:
JSDK is the most comprehensive TypeScript framework, like JDK.
639 lines (565 loc) • 27 kB
text/typescript
/**
* @project JSDK
* @license MIT
* @website https://github.com/fengboyue/jsdk
*
* @version 2.0.0
* @author Frank.Feng
*/
/// <reference path="../../libs/webuploader/0.1.5/webuploader.d.ts" />
/// <reference path="../util/Files.ts"/>
/// <reference path="FormWidget.ts"/>
/// <reference path="Toast.ts"/>
/// <reference path="UploaderFiles.ts"/>
module JS {
export namespace fx {
let E = Check.isEmpty, A = Arrays;
export enum UploaderFaceMode {
list = 'list',
image = 'image',
square = 'square',
round = 'round',
shadow = 'shadow'
}
export class UploaderConfig extends FormWidgetConfig<Uploader> {
readonly?: boolean = false;
server?: string;
dnd?: boolean = false;
paste?: boolean | 'body' = false;
accept?: FileAccepts;
/**
* 配置生成缩略图的选项。
*/
thumb?: { width?: number, height?: number } = { width: 1, height: 1 };
/**
* 配置压缩的图片的选项。如果此选项为undefined, 则图片在上传前不进行压缩。
*/
compress?: { width?: number, height?: number };
maxNumbers?: number;
maxTotalSize?: number;
maxSingleSize?: number;
duplicate?: boolean = true;
needProgerss?: boolean;
multiple?: boolean = false;
/**
* 文件上传域的name
*/
fieldName?: string = 'file';
uploadData?: JsonObject;
faceMode?: UploaderFaceMode | UploaderFaceMode[] = [UploaderFaceMode.square, UploaderFaceMode.list];
i18n?: URLString | UploaderResource;
iniValue?: MimeFile[] = null;
data?: MimeFile[] = null;
dataFormat?: ResultSetFormat | ((this: Uploader, rawData: any) => ResultSet<MimeFile[]>);
listeners?: UploaderListeners;
}
export interface UploaderListeners extends FormWidgetListeners<Uploader> {
adding?: EventHandler1<Uploader, MimeFile>
added?: EventHandler1<Uploader, MimeFile>
removed?: EventHandler1<Uploader, MimeFile>
uploading?: EventHandler1<Uploader, MimeFile>
uploadprogress?: EventHandler2<Uploader, MimeFile, number>//file, percentage
uploadsuccess?: EventHandler2<Uploader, MimeFile, any>//file, response
uploadfailure?: EventHandler2<Uploader, MimeFile, string>//file, reason
uploaded?: EventHandler1<Uploader, MimeFile>
beginupload?: EventHandler<Uploader>
endupload?: EventHandler<Uploader>
}
export type UploaderEvents = FormWidgetEvents |
'adding' | 'added' | 'removed' |
'uploading' | 'uploadprogress' | 'uploadsuccess' | 'uploaderror' | 'uploaded' |
'beginupload' | 'endupload';
export type UploaderResource = {
pickTitle: string,
pickTip: string,
retryTip: string,
removeTip: string,
viewDenied: string,
exceedMaxSize: string,
wrongDuplicate: string,
wrongType: string,
exceedNumbers: string,
exceedMaxTotalSize: string
}
export class Uploader extends FormWidget implements ICRUDWidget<MimeFile[]> {
public static I18N: UploaderResource = {
pickTitle: 'Select your local files please',
pickTip: '<Accepts>\nFileExts={fileExts}\nMaxTotalSize={maxTotalSize}\nMaxNumbers={maxNumbers}\nMaxSingleSize={maxSingleSize}',
retryTip: 'Retry',
removeTip: 'Remove',
viewDenied: 'The file can\'t be viewed in local mode',
exceedMaxSize: 'Exceed the max size of single file',
wrongDuplicate: 'Can\'t upload duplicate file',
wrongType: 'Wrong file type',
exceedNumbers: 'Exceed the max numbers of file',
exceedMaxTotalSize: 'Exceed the max size of total files'
}
private _uploader: WebUploader.Uploader;
constructor(cfg: UploaderConfig) {
super(cfg);
}
protected _initUploader(cfg: UploaderConfig) {
if (this._uploader) return;
let me = this;
$('#' + this.id).find('.classic-pick').on('click', function () {
$('#' + me.id).find('.webuploader-element-invisible').click();
});
let url = JS.config('libs')['webuploader.swf'];
url = url.startsWith('~') ? (JS.config('libRoot') || '') + url.slice(1) : url;
let cf = {
pick: {
id: `#${this.id} .pick`,
multiple: cfg.multiple
},
paste: cfg.paste == true ? `#${this.id}` : (cfg.paste == 'body' ? document.body : undefined),
dnd: cfg.dnd ? `#${this.id}` : undefined,
swf: url,
auto: true,
accept: cfg.accept || null,
fileNumLimit: cfg.maxNumbers || undefined,
fileSizeLimit: cfg.maxTotalSize || undefined,
fileSingleSizeLimit: cfg.maxSingleSize || undefined,
disableGlobalDnd: false,
duplicate: cfg.duplicate,
fileVal: cfg.fieldName,
formData: cfg.uploadData || {},
thumb: {
width: cfg.thumb && cfg.thumb.width,
height: cfg.thumb && cfg.thumb.height,
// 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false.
allowMagnify: false,
// 是否允许裁剪。
crop: false,
// 为空的话则保留原有图片格式。
type: ''
},
compress: cfg.compress && cfg.compress.width && cfg.compress.height ? {
width: cfg.compress.width,
height: cfg.compress.height,
// 图片质量,只有type为`image/jpeg`的时候才有效。
quality: 90,
// 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false.
allowMagnify: false,
// 是否允许裁剪。
crop: false,
// 是否保留头部meta信息。
preserveHeaders: true,
// 如果发现压缩后文件大小比原来还大,则使用原来图片
noCompressIfLarger: true,
// 单位字节,如果图片大小小于此值,不会采用压缩。
compressSize: 0
} : false
};
this._uploader = WebUploader.Uploader.create(<WebUploader.UploaderOptions>cf);
let eMap = {
'adding': 'beforeFileQueued',
'added': 'filesQueued',
'removed': 'fileDequeued',
'uploading': 'uploadStart',
'uploadprogress': 'uploadProgress',
'uploadsuccess': 'uploadSuccess',
'uploaderror': 'uploadError',
'uploaded': 'uploadComplete',
'beginupload': 'startUpload',
'endupload': 'uploadFinished'
};
this._uploader.on(eMap['adding'], function (file) {
return me._fire('adding', [me._toMimeFile(file)]);
});
this._uploader.on(eMap['added'], function (files: Array<WebUploader.File>) {
files.forEach((file) => {
me._onFileQueued(file);
})
me._fire('added', [me._toMimeFiles(files)]);
});
this._uploader.on(eMap['removed'], function (file) {
me._onFileDequeued(file);
me._fire('removed', [me._toMimeFile(file)]);
});
this._uploader.on(eMap['uploading'], function (file, percentage) {
me._fire('uploading', [me._toMimeFile(file), percentage]);
});
this._uploader.on(eMap['uploaderror'], function (file, reason) {
me._onUploadFail(file);
me._fire('uploaderror', [me._toMimeFile(file), reason]);
});
this._uploader.on(eMap['uploadsuccess'], function (file, response) {
me._onUploadSuccess(file, response);
me._fire('uploadsuccess', [me._toMimeFile(file), response]);
});
this._uploader.on(eMap['uploaded'], function (file) {
me._fire('uploaded', [me._toMimeFile(file)]);
});
this._uploader.on(eMap['beginupload'], function () {
me._fire('beginupload');
});
this._uploader.on(eMap['endupload'], function () {
me._fire('endupload');
});
let errors = {
'F_EXCEED_SIZE': 'exceedMaxSize',
'F_DUPLICATE': 'wrongDuplicate',
'Q_TYPE_DENIED': 'wrongType',
'Q_EXCEED_NUM_LIMIT': 'exceedNumbers',
'Q_EXCEED_SIZE_LIMIT': 'exceedMaxTotalSize'
}
this._uploader.on('error', (type: string) => {
Toast.show({ type: 'error', message: me._i18n<string>(errors[type]), place: 'cb' })
});
}
protected _showError(msg: string) {
super._showError(msg);
this.widgetEl.find('.body').addClass('jsfx-input-error');
}
protected _hideError() {
super._hideError();
this.widgetEl.find('.body').removeClass('jsfx-input-error');
}
/**
* 在父类构造函数中的Widget初始化之前,由子类重载实现
* @param config
*/
protected _onAfterRender() {
this._initUploader(<UploaderConfig>this._config);
super._onAfterRender();
}
private _createShadow(id: string, ctor: JQuery): JQuery {
return $(`<div id="${id}"></div>`).css({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: "#808080",
opacity: 0.1,
zIndex: (Number(ctor.css('z-index')) || 0) + 1
})
}
public readonly(): boolean
public readonly(is: boolean): this
public readonly(is?: boolean): any {
let cfg = <UploaderConfig>this._config;
if (arguments.length == 0) return cfg.readonly;
if (cfg.readonly == is) return this;
cfg.readonly = is;
$(`#${this.id} .body`)[is ? 'addClass' : 'removeClass']('readonly');
let p = $(`#${this.id} .pick`);
is ? p.hide() : p.show();
return this;
}
public disable() {
if (!this.isEnabled()) return this;
(<UploaderConfig>this._config).disabled = true;
let ctor = $(`#${this.id} .body`).addClass('disabled');
this._createShadow(this.id + '_shadow', ctor).appendTo(ctor);
return this;
}
public enable() {
if (this.isEnabled()) return this;
(<UploaderConfig>this._config).disabled = false;
$(`#${this.id} .body`).removeClass('disabled');
$('#' + this.id + '_shadow').remove();
return this;
}
private _pickText(key: string) {
let cfg = <UploaderConfig>this._config,
fileExts = (cfg.accept && cfg.accept.title) || '*',
maxTotalSize = cfg.maxTotalSize ? Files.toSizeString(cfg.maxTotalSize) : '*',
maxNumbers = cfg.maxNumbers || '*',
maxSingleSize = cfg.maxSingleSize ? Files.toSizeString(cfg.maxSingleSize) : '*';
return Strings.merge(this._i18n<string>(key) || '', {
fileExts: fileExts,
maxTotalSize: maxTotalSize,
maxNumbers: maxNumbers,
maxSingleSize: maxSingleSize
})
}
protected _bodyFragment() {
let cfg = <UploaderConfig>this._config,
title = this._pickText('pickTitle'),
tip = this._pickText('pickTip').replace(/\n/g, ' '),
fag = !this._hasFaceMode(UploaderFaceMode.image) ?
`<ul class="files-area list"></ul>` :
`<div class="files-area image"></div>`,
cls = '';
if (this._hasFaceMode(UploaderFaceMode.shadow)) cls += ' border-shadow';
if (this._hasFaceMode(UploaderFaceMode.round)) cls += ' border-round';
return `
<div class="body font-${cfg.sizeMode || 'md'}${cls}">
<div class="pick" title="${tip}">
<i class="la la-cloud-upload"></i>
<span class="pick-title">${title}</span>
</div>
${fag}
</div>`
}
isCrud(): boolean {
return true
}
public crudValue(): MimeFile[] {
let val = this.value()||[],
iniVal = this.iniValue()||[],
arr = [];
iniVal.forEach(v => {
if (val.findIndex(it => {
return it.id == v.id
}) < 0) {
arr[arr.length] = Jsons.union(v ,{ _crud: 'D' })
}
});
val.forEach(v => {
if (iniVal.findIndex(it => {
return it.id == v.id
}) < 0) {
if(!v.id.startsWith('WU_FILE_') && v.id!=v['_wuid']) arr[arr.length] = Jsons.union(v ,{ _crud: 'C' })
}
});
return arr;
}
public iniValue(): MimeFile[]
public iniValue(v: MimeFile[], render?:boolean): this
public iniValue(v?: MimeFile[], render?:boolean): any {
if (arguments.length == 0) return super.iniValue();
return super.iniValue(v, render)
}
public value(): MimeFile[]
public value(file: MimeFile | MimeFile[]): this
public value(file?: MimeFile | MimeFile[]): any {
if (arguments.length == 0) return super.value();
if (E(file)) {
this._uploader.reset();
$(`#${this.id} .files-area`).children().remove();
this._setValue(null);
return this;
}
return this.add(<any>file)
}
protected _equalValues(newVal: MimeFile[], oldVal: MimeFile[]): boolean {
return A.equal(oldVal, newVal, (file1, file2) => {
return file1.id == file2.id
})
}
public add(file: MimeFile | MimeFile[]) {
if (E(file)) return this;
this._addFiles(A.toArray(file));
return this
}
//id可以是临时id,也可以是上传成功后的正式id
public remove(id: string | string[]) {
if (E(id)) return this;
let rms: string[] = A.toArray(id);
rms.forEach(i=>{
let el = this.widgetEl.find(`[file-id="${i}"]`);
if(el.length==1) this._removeFile(el.attr('wu-id'));
})
return this
}
public data(): MimeFile[]
public data(data: MimeFile[]): this
public data(data?: MimeFile[]): any {
if (arguments.length == 0) return this.value();
return this.value(data)
}
private _onUploadSuccess(wuFile: WebUploader.File, res: any): void {
let cfg = <UploaderConfig>this._config, fmt = cfg.dataFormat,
result: ResultSet<MimeFile> = Types.isFunction(fmt) ? (<Function>fmt).apply(this, res) : ResultSet.parseJSON(res, <any>fmt);
if (result.success()) {
//update id + uri
let file = <MimeFile>result.data(),
val = this.value() || [],
index = val.findIndex(item => {
return wuFile.id == item.id;
})
if (index >= 0) {
let oFile = val[index];
oFile.id = file.id;//改写id成服务器端id
oFile.uri = file.uri;
}
} else {
this._onUploadFail(wuFile);
}
}
private _onUploadFail(file: WebUploader.File): void {
this.widgetEl.find(`[file-id="${file.id}"]`).addClass('fail');
}
private _onFileDequeued(file: WebUploader.File) {
this.widgetEl.find(`[wu-id="${file.id}"]`).remove();
let newVal = Jsons.clone(this.value()).remove((mFile)=>{return mFile['_wuid']==file.id});
this._valueModel.set(this.name(), newVal);
}
private _fileIcon(path: string) {
let icon = 'alt';
if (Files.isFileType(path, 'pdf')) {
icon = 'pdf'
} else if (Files.isFileType(path, 'doc,docx')) {
icon = 'word'
} else if (Files.isFileType(path, 'xls,xlsx')) {
icon = 'excel'
} else if (Files.isFileType(path, 'ppt,pptx')) {
icon = 'powerpoint'
} else if (Files.isFileType(path, FileTypes.AUDIOS)) {
icon = 'audio'
} else if (Files.isFileType(path, FileTypes.VIDEOS)) {
icon = 'video'
} else if (Files.isFileType(path, FileTypes.ZIPS)) {
icon = 'archive'
} else if (Files.isFileType(path, FileTypes.CODES)) {
icon = 'code'
} else if (Files.isFileType(path, FileTypes.IMAGES)) {
icon = 'image'
}
return '<span><i class="far fa-file-' + icon + '"></i></span>'
}
private _onFileQueued(wuFile: WebUploader.File) {
let file = this._toMimeFile(wuFile);
this._renderFile(file);
if (this._hasFaceMode(UploaderFaceMode.image)) {
let isImage = Files.isFileType(file.name, FileTypes.IMAGES);
if (!file.uri && isImage) {
this._makeThumb(wuFile);//生成50%缩略图
} else if (!isImage) this.widgetEl.find(`[file-id=${file.id}] img`).replaceWith(this._fileIcon('.' + file.ext));
}
//已存在的文件则忽略不上传
if (file.uri) this._uploader.skipFile(wuFile.id);
//save wuid
file['_wuid'] = wuFile.id;
this.widgetEl.find('[file-id]:last-child').attr('wu-id', wuFile.id);
//需要同步添加至value
this._setValue((this.value() || []).concat(file));
}
private _renderFile(file: MimeFile) {
//file-id可能是source.id也可能是wu.id
let url = file.uri || '', fId = file.id || '',
fileLink = `<a id="${this.id}-${fId}" src="${url}" href="javascript:void(0);">${file.name}</a>`,
retryTip = this._i18n<string>('retryTip') || 'Retry', removeTip = this._i18n<string>('removeTip') || 'Remove',
html = !this._hasFaceMode(UploaderFaceMode.image) ?
$(`<li file-id="${fId}">
<div class="text-truncate file-name" title="${Strings.escapeHTML(file.name)}">
${this._fileIcon('.' + file.ext)}
${fileLink}
</div>
<div class="file-actions">
<span class="action remove text-center" title="${removeTip}"><i class="fa fa-times"></i></span>
<span class="action retry text-center" title="${retryTip}"><i class="fa fa-upload"></i></span>
</div>
</li>`)
: $(`
<div file-id="${fId}">
<div class="file-image-area">
<div class="file-image items-center items-middle"><img id="${this.id}-${fId}" src="${url}"/></div>
<div class="file-actions">
<span class="action remove text-center" title="${removeTip}"><i class="fa fa-times"></i></span>
<span class="action retry text-center" title="${retryTip}"><i class="fa fa-upload"></i></span>
</div>
</div>
<div class="text-truncate file-name" title="${Strings.escapeHTML(file.name)}">
${fileLink}
</div>
</div>
`);
this.widgetEl.find(`.files-area`).append(html);
this._bindActions(fId);
}
private _makeThumb(file: WebUploader.File) {
this._uploader.makeThumb(file, (error, src) => {
let el = this.widgetEl.find(`[file-id=${file.id}]`);
if (error) {
el.find('img').replaceWith(this._fileIcon('.' + file.ext));
return
}
el.find(`#${this.id}-${file.id}`).attr('src', src);
});
}
private _bindActions(fileId: string): void {
//enlarge
let fEl = this.widgetEl.find(`[file-id="${fileId}"]`);
fEl.on('click', !this._hasFaceMode(UploaderFaceMode.image) ? 'a' : 'a,.file-image', (e: JQuery.Event) => {
let src = this.widgetEl.find(`#${this.id}-${fileId}`).attr('src');
if (src) {
(Files.isFileType(src, FileTypes.IMAGES) || (<string>src).indexOf('data:image/') == 0) ? window.open().document.body.innerHTML = `<img src="${src}" >` : window.open(src);
} else {
Toast.show({
type: 'error',
message: this._i18n<string>('viewDenied')
})
}
return false
});
//remove
fEl.on('click', '.action.remove', (e: JQuery.Event) => {
this._removeFile(fEl.attr('wu-id'));
fEl.remove();
return false
})
//retry
fEl.on('click', '.action.retry', (e: JQuery.Event) => {
this._retryFile(fEl.attr('wu-id'));
return false
})
}
private _toMimeFiles(wfs: WebUploader.File[]): MimeFile[] {
if (E(wfs)) return [];
let fs = [];
wfs.forEach(file => {
fs.push(this._toMimeFile(file));
})
return fs;
}
private _toMimeFile(wf: WebUploader.File): MimeFile {
if (!wf) return null;
return {
id: wf.source.id || wf.id,
mime: wf.type,
name: wf.name,
ext: wf.ext,
size: wf.size,
uri: wf.source.uri
}
}
private _toWUFile(cf: MimeFile): WebUploader.File {
if (!cf) return null;
if (!cf.uri) throw new URIError(`The file<${cf.name}> has not URI.`);
let file = {
id: cf.id,
type: cf.mime,
name: cf.name,
ext: cf.ext || Files.getFileType(cf.name),
size: cf.size || 1,
getRuid: () => { return '' },
getSource: () => { return null }
}
file['uri'] = cf.uri;
return file;
}
protected _removeFile(wuFileId: string) {
let f = this._uploader.getFile(wuFileId);
if (f) this._uploader.removeFile(f, true);
return this
}
protected _retryFile(wuFileId: string) {
let f = this._uploader.getFile(wuFileId);
if (f) this._uploader.retry(f);
return this
}
protected _addFiles(files: MimeFile[]) {
if (E(files)) return this;
let wuFiles = [], value = this.value()||[];
files.forEach(f => {
//不可重复添加
if(value.findIndex((v)=>{return v.id==f.id})<0) wuFiles.push(new WebUploader.File(this._toWUFile(f)));
})
if(wuFiles.length>0) this._uploader.addFiles(wuFiles);
return this
}
public inProgress(): boolean {
return this._uploader.isInProgress();
}
}
}
}
import Uploader = JS.fx.Uploader;
import UploaderEvents = JS.fx.UploaderEvents;
import UploaderConfig = JS.fx.UploaderConfig;
import UploaderFaceMode = JS.fx.UploaderFaceMode;
import UploaderResource = JS.fx.UploaderResource;