UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

786 lines (681 loc) 18.8 kB
/*! * 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>&lt;input type="file" &gt;</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(); } }