jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
786 lines (681 loc) • 18.8 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Licensed under GNU General Public License version 2 or later or a commercial license or MIT;
* For GPL see LICENSE-GPL.txt in the project root for license information.
* For MIT see LICENSE-MIT.txt in the project root for license information.
* For commercial licenses see https://xdsoft.net/jodit/commercial/
* Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Config } from '../Config';
import { IS_IE, TEXT_PLAIN } from '../constants';
import {
BuildDataResult,
HandlerError,
HandlerSuccess,
IDictionary,
IJodit,
IUploader,
IUploaderAnswer,
IUploaderData,
IUploaderOptions,
IViewBased
} from '../types/';
import { Ajax } from './Ajax';
import { browser, extend, isPlainObject } from './helpers/';
import { Dom } from './Dom';
import { isJoditObject } from './helpers/checker/isJoditObject';
import { Component } from './Component';
declare module '../Config' {
interface Config {
enableDragAndDropFileToEditor: boolean;
uploader: IUploaderOptions<Uploader>;
}
}
/**
* Module for processing download documents and images by Drag and Drop
*
* @tutorial {@link http://xdsoft.net/jodit/doc/tutorial-uploader-settings.html|Uploader options and
* Drag and Drop files}
* @module Uploader
* @params {Object} parent Jodit main object
*/
/**
* @property {boolean} enableDragAndDropFileToEditor=true Enable drag and drop file toWYSIWYG editor
*/
Config.prototype.enableDragAndDropFileToEditor = true;
Config.prototype.uploader = {
url: '',
insertImageAsBase64URI: false,
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif'],
headers: null,
data: null,
filesVariableName(i: number): string {
return `files[${i}]`;
},
withCredentials: false,
pathVariableName: 'path',
format: 'json',
method: 'POST',
prepareData(this: Uploader, formData: FormData) {
return formData;
},
isSuccess(this: Uploader, resp: IUploaderAnswer): boolean {
return resp.success;
},
getMessage(this: Uploader, resp: IUploaderAnswer) {
return resp.data.messages !== undefined &&
Array.isArray(resp.data.messages)
? resp.data.messages.join(' ')
: '';
},
process(this: Uploader, resp: IUploaderAnswer): IUploaderData {
return resp.data;
},
error(this: Uploader, e: Error) {
this.jodit.events.fire('errorMessage', e.message, 'error', 4000);
},
defaultHandlerSuccess(this: Uploader, resp: IUploaderData) {
if (resp.files && resp.files.length) {
resp.files.forEach((filename, index: number) => {
const [tagName, attr]: string[] =
resp.isImages && resp.isImages[index]
? ['img', 'src']
: ['a', 'href'];
const elm: HTMLElement = this.jodit.create.inside.element(<
'img' | 'a'
>tagName);
elm.setAttribute(attr, resp.baseurl + filename);
if (tagName === 'a') {
elm.innerText = resp.baseurl + filename;
}
if (isJoditObject(this.jodit)) {
if (tagName === 'img') {
this.jodit.selection.insertImage(
elm as HTMLImageElement,
null,
this.jodit.options.imageDefaultWidth
);
} else {
this.jodit.selection.insertNode(elm);
}
}
});
}
},
defaultHandlerError(this: Uploader, e: Error) {
this.jodit.events.fire('errorMessage', e.message);
},
contentType(this: Uploader, requestData: any) {
return (this.jodit.ownerWindow as any).FormData !== undefined &&
typeof requestData !== 'string'
? false
: 'application/x-www-form-urlencoded; charset=UTF-8';
}
} as IUploaderOptions<Uploader>;
export class Uploader extends Component implements IUploader {
/**
* Convert dataURI to Blob
*
* @param {string} dataURI
* @return {Blob}
*/
public static dataURItoBlob(dataURI: string): Blob {
// convert base64 toWYSIWYG raw binary data held in a string
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
const byteString: string = atob(dataURI.split(',')[1]),
// separate out the mime component
mimeString: string = dataURI
.split(',')[0]
.split(':')[1]
.split(';')[0],
// write the bytes of the string toWYSIWYG an ArrayBuffer
ab: ArrayBuffer = new ArrayBuffer(byteString.length),
ia: Uint8Array = new Uint8Array(ab);
for (let i: number = 0; i < byteString.length; i += 1) {
ia[i] = byteString.charCodeAt(i);
}
// write the ArrayBuffer toWYSIWYG a blob, and you're done
return new Blob([ia], { type: mimeString });
}
private path: string = '';
private source: string = 'default';
private options: IUploaderOptions<Uploader>;
jodit: IViewBased;
buildData(data: FormData | IDictionary<string> | string): BuildDataResult {
if (
this.options.buildData &&
typeof this.options.buildData === 'function'
) {
return this.options.buildData.call(this, data);
}
const FD: typeof FormData = (this.jodit.ownerWindow as any).FormData;
if (FD !== undefined) {
if (data instanceof FD) {
return data;
}
if (typeof data === 'string') {
return data;
}
const newdata: FormData = new FD();
Object.keys(data).forEach(key => {
newdata.append(key, data[key]);
});
return newdata;
}
return data;
}
private ajaxInstances: Ajax[] = [];
send(
data: FormData | IDictionary<string>,
success: (resp: IUploaderAnswer) => void
): Promise<any> {
const requestData: BuildDataResult = this.buildData(data),
sendData = (
request: FormData | IDictionary<string> | string
): Promise<any> => {
const ajax: Ajax = new Ajax(this.jodit || this, {
xhr: () => {
const xhr: XMLHttpRequest = new XMLHttpRequest();
if (
(this.jodit.ownerWindow as any).FormData !==
undefined &&
xhr.upload
) {
xhr.upload.addEventListener(
'progress',
evt => {
if (evt.lengthComputable) {
let percentComplete =
evt.loaded / evt.total;
percentComplete *= 100;
this.jodit.progress_bar.style.display =
'block';
this.jodit.progress_bar.style.width =
percentComplete + '%';
if (percentComplete === 100) {
this.jodit.progress_bar.style.display =
'none';
}
}
},
false
);
} else {
this.jodit.progress_bar.style.display = 'none';
}
return xhr;
},
method: this.options.method || 'POST',
data: request,
url: this.options.url,
headers: this.options.headers,
queryBuild: this.options.queryBuild,
contentType: this.options.contentType.call(this, request),
dataType: this.options.format || 'json',
withCredentials: this.options.withCredentials || false
});
this.ajaxInstances.push(ajax);
const removeAjaxInstanceFromList = () => {
const index = this.ajaxInstances.indexOf(ajax);
if (index !== -1) {
this.ajaxInstances.splice(index, 1);
}
};
return ajax
.send()
.then(resp => {
removeAjaxInstanceFromList();
success.call(this, resp);
})
.catch(error => {
removeAjaxInstanceFromList();
this.options.error.call(this, error);
});
};
if (requestData instanceof Promise) {
return requestData.then(sendData).catch(error => {
this.options.error.call(this, error);
});
} else {
return sendData(requestData);
}
}
/**
* Send files to server
*
* @param files
* @param handlerSuccess
* @param handlerError
* @param process
*/
sendFiles(
files: FileList | File[] | null,
handlerSuccess?: HandlerSuccess,
handlerError?: HandlerError,
process?: (form: FormData) => void
): Promise<any> {
if (!files) {
return Promise.reject(new Error('Need files'));
}
const uploader: Uploader = this;
let fileList: File[] = Array.from(files);
if (!fileList.length) {
return Promise.reject(new Error('Need files'));
}
const promises: Array<Promise<any>> = [];
if (this.options.insertImageAsBase64URI) {
let file: File, i: number;
for (i = 0; i < fileList.length; i += 1) {
file = fileList[i];
if (file && file.type) {
const mime: string[] = file.type.match(
/\/([a-z0-9]+)/i
) as string[];
const extension: string = mime[1]
? mime[1].toLowerCase()
: '';
if (this.options.imagesExtensions.includes(extension)) {
const
reader: FileReader = new FileReader();
promises.push(
new Promise<any>((resolve, reject) => {
reader.onerror = reject;
reader.onloadend = () => {
const resp: IUploaderData = {
baseurl: '',
files: [reader.result],
isImages: [true]
} as IUploaderData;
if (
typeof (
handlerSuccess ||
uploader.options
.defaultHandlerSuccess
) === 'function'
) {
((handlerSuccess ||
uploader.options
.defaultHandlerSuccess) as HandlerSuccess).call(
uploader,
resp
);
}
resolve(resp);
};
reader.readAsDataURL(file);
})
);
(fileList[i] as any) = null;
}
}
}
}
fileList = fileList.filter(a => a);
if (fileList.length) {
const form: FormData = new FormData();
form.append(this.options.pathVariableName, uploader.path);
form.append('source', uploader.source);
let file: File;
for (let i = 0; i < fileList.length; i += 1) {
file = fileList[i];
if (file) {
const mime: string[] = file.type.match(
/\/([a-z0-9]+)/i
) as string[];
const extension: string =
mime && mime[1] ? mime[1].toLowerCase() : '';
let newName =
fileList[i].name ||
Math.random()
.toString()
.replace('.', '');
if (extension) {
let extForReg = extension;
if (['jpeg', 'jpg'].includes(extForReg)) {
extForReg = 'jpeg|jpg';
}
const reEnd = new RegExp('.(' + extForReg + ')$', 'i');
if (!reEnd.test(newName)) {
newName += '.' + extension;
}
}
form.append(this.options.filesVariableName(i), fileList[i], newName);
}
}
if (process) {
process(form);
}
if (uploader.options.data && isPlainObject(uploader.options.data)) {
Object.keys(uploader.options.data).forEach((key: string) => {
form.append(key, (uploader.options.data as any)[key]);
});
}
uploader.options.prepareData.call(this, form);
promises.push(
uploader
.send(form, (resp: IUploaderAnswer) => {
if (this.options.isSuccess.call(uploader, resp)) {
if (
typeof (
handlerSuccess ||
uploader.options.defaultHandlerSuccess
) === 'function'
) {
((handlerSuccess ||
uploader.options
.defaultHandlerSuccess) as HandlerSuccess).call(
uploader,
uploader.options.process.call(
uploader,
resp
) as IUploaderData
);
}
} else {
if (
typeof (
handlerError ||
uploader.options.defaultHandlerError
)
) {
((handlerError ||
uploader.options
.defaultHandlerError) as HandlerError).call(
uploader,
new Error(
uploader.options.getMessage.call(
uploader,
resp
)
)
);
return;
}
}
})
.then(() => {
this.jodit.events &&
this.jodit.events.fire('filesWereUploaded');
})
);
}
return Promise.all(promises);
}
/**
* It sets the path for uploading files
* @method setPath
* @param {string} path
*/
setPath(path: string) {
this.path = path;
}
/**
* It sets the source for connector
*
* @method setSource
* @param {string} source
*/
setSource(source: string) {
this.source = source;
}
/**
* Set the handlers Drag and Drop toWYSIWYG `$form`
*
* @method bind
* @param {HTMLElement} form Form or any Node on which you can drag and drop the file. In addition will be processed
* <code><input type="file" ></code>
* @param {function} [handlerSuccess] The function toWYSIWYG be called when a successful uploading files
* toWYSIWYG the server
* @param {function} [handlerError] The function that will be called during a failed download files
* toWYSIWYG a server
* @example
* ```javascript
* var $form = jQuery('<form><input type="text" typpe="file"></form>');
* jQuery('body').append($form);
* Jodit.editors.someidfoeditor.uploader.bind($form[0], function (files) {
* var i;
* for (i = 0; i < data.files.length; i += 1) {
* parent.selection.insertImage(data.files[i])
* }
* });
* ```
*/
bind(
form: HTMLElement,
handlerSuccess?: HandlerSuccess,
handlerError?: HandlerError
) {
const self: Uploader = this,
onPaste = (e: ClipboardEvent): false | void => {
let i: number, file: File | null, extension: string;
const process = (formdata: FormData) => {
if (file) {
formdata.append('extension', extension);
formdata.append('mimetype', file.type);
}
};
// send data on server
if (
e.clipboardData &&
e.clipboardData.files &&
e.clipboardData.files.length
) {
this.sendFiles(
e.clipboardData.files,
handlerSuccess,
handlerError
);
return false;
}
if (browser('ff') || IS_IE) {
if (
e.clipboardData &&
(!e.clipboardData.types.length &&
e.clipboardData.types[0] !== TEXT_PLAIN)
) {
const div = this.jodit.create.div('', {
'tabindex': -1,
'style': 'left: -9999px; top: 0; width: 0; height: 100%;line-height: 140%; ' +
'overflow: hidden; position: fixed; z-index: 2147483647; word-break: break-all;',
'contenteditable': true
});
this.jodit.ownerDocument.body.appendChild(div);
const
selection = this.jodit && isJoditObject(this.jodit) ? this.jodit.selection.save() : null,
restore = () =>
selection &&
this.jodit &&
isJoditObject(this.jodit) &&
this.jodit.selection.restore(selection);
div.focus();
setTimeout(() => {
const child: HTMLDivElement | null = div.firstChild as HTMLDivElement;
Dom.safeRemove(div);
if (child && child.hasAttribute('src')) {
const src: string =
child.getAttribute('src') || '';
restore();
self.sendFiles(
[Uploader.dataURItoBlob(src) as File],
handlerSuccess,
handlerError
);
}
}, 200);
}
return;
}
if (
e.clipboardData &&
e.clipboardData.items &&
e.clipboardData.items.length
) {
const items = e.clipboardData.items;
for (i = 0; i < items.length; i += 1) {
if (
items[i].kind === 'file' &&
items[i].type === 'image/png'
) {
file = items[i].getAsFile();
if (file) {
const mime: string[] = file.type.match(
/\/([a-z0-9]+)/i
) as string[];
extension = mime[1]
? mime[1].toLowerCase()
: '';
this.sendFiles(
[file],
handlerSuccess,
handlerError,
process
);
}
e.preventDefault();
break;
}
}
}
};
if (this.jodit && (<IJodit>this.jodit).editor !== form) {
self.jodit.events.on(form, 'paste', onPaste);
} else {
self.jodit.events.on('beforePaste', onPaste);
}
const hasFiles = (event: DragEvent): boolean =>
Boolean(
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length !== 0
);
self.jodit.events
.on(
form,
'dragend dragover dragenter dragleave drop',
(e: DragEvent) => {
e.preventDefault();
}
)
.on(form, 'dragover', (event: DragEvent) => {
if (hasFiles(event)) {
form.classList.contains('jodit_draghover') ||
form.classList.add('jodit_draghover');
event.preventDefault();
}
})
.on(form, 'dragend', (event: DragEvent) => {
if (hasFiles(event)) {
form.classList.contains('jodit_draghover') &&
form.classList.remove('jodit_draghover');
event.preventDefault();
}
})
.on(
form,
'drop',
(event: DragEvent): false | void => {
form.classList.remove('jodit_draghover');
if (
hasFiles(event) &&
event.dataTransfer &&
event.dataTransfer.files
) {
event.preventDefault();
event.stopImmediatePropagation();
this.sendFiles(
event.dataTransfer.files,
handlerSuccess,
handlerError
);
}
}
);
const inputFile: HTMLInputElement | null = form.querySelector(
'input[type=file]'
);
if (inputFile) {
self.jodit.events.on(inputFile, 'change', function(
this: HTMLInputElement
) {
self.sendFiles(this.files, handlerSuccess, handlerError).then(
() => {
inputFile.value = '';
if (!/safari/i.test(navigator.userAgent)) {
inputFile.type = '';
inputFile.type = 'file';
}
}
);
});
}
}
/**
* Upload images toWYSIWYG a server by its URL, making it through the connector server.
*
* @param {string} url
* @param {HandlerSuccess} [handlerSuccess]
* @param {HandlerError} [handlerError]
*/
uploadRemoteImage(
url: string,
handlerSuccess?: HandlerSuccess,
handlerError?: HandlerError
) {
const uploader = this;
uploader.send(
{
action: 'fileUploadRemote',
url
},
(resp: IUploaderAnswer) => {
if (uploader.options.isSuccess.call(uploader, resp)) {
if (typeof handlerSuccess === 'function') {
handlerSuccess.call(
uploader,
this.options.process.call(this, resp)
);
} else {
this.options.defaultHandlerSuccess.call(
uploader,
this.options.process.call(this, resp)
);
}
} else {
if (
typeof (
handlerError || uploader.options.defaultHandlerError
) === 'function'
) {
(handlerError || this.options.defaultHandlerError).call(
uploader,
new Error(
uploader.options.getMessage.call(this, resp)
)
);
return;
}
}
}
);
}
constructor(editor: IViewBased, options?: IUploaderOptions<Uploader>) {
super(editor);
this.options = extend(
true,
{},
Config.defaultOptions.uploader,
isJoditObject(editor) ? editor.options.uploader : null,
options
) as IUploaderOptions<Uploader>;
}
destruct(): any {
this.ajaxInstances.forEach(ajax => {
try {
ajax.abort();
} catch {
}
});
delete this.options;
super.destruct();
}
}