UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

1,385 lines (1,167 loc) 33 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, OptionsDefault } from './Config'; import * as consts from './constants'; import { Component } from './modules/Component'; import { Dom } from './modules/Dom'; import { asArray, css, debounce, defaultLanguage, inArray, normalizeKeyAliases, splitArray, sprintf } from './modules/helpers/'; import { JoditArray } from './modules/helpers/JoditArray'; import { JoditObject } from './modules/helpers/JoditObject'; import { Observer } from './modules/observer/observer'; import { Select } from './modules/Selection'; import { StatusBar } from './modules/StatusBar'; import { LocalStorageProvider } from './modules/storage/localStorageProvider'; import { Storage } from './modules/storage/storage'; import { CustomCommand, ExecCommandCallback, IDictionary, IPlugin, markerInfo, Modes } from './types/types'; import { ViewWithToolbar } from './modules/view/viewWithToolbar'; import { IJodit } from './types/jodit'; import { IFileBrowser, IUploader } from './types'; import { ucfirst } from './modules/helpers/string/ucfirst'; const SAFE_COUNT_CHANGE_CALL = 10; /** * Class Jodit. Main class */ export class Jodit extends ViewWithToolbar implements IJodit { get value(): string { return this.getEditorValue(); } set value(html: string) { this.setEditorValue(html); } /** * Return default timeout period in milliseconds for some debounce or throttle functions. * By default return {observer.timeout} options * * @return {number} */ get defaultTimeout(): number { return this.options && this.options.observer ? this.options.observer.timeout : Jodit.defaultOptions.observer.timeout; } static Array(array: any[]): JoditArray { return new JoditArray(array); } static Object(object: any): JoditObject { return new JoditObject(object); } static fireEach(events: string, ...args: any[]) { Object.keys(Jodit.instances).forEach((key: string) => { const editor: Jodit = Jodit.instances[key]; if (!editor.isDestructed && editor.events) { editor.events.fire(events, ...args); } }); } static defaultOptions: Config; static plugins: any = {}; static modules: any = {}; static instances: IDictionary<Jodit> = {}; static lang: any = {}; private __defaultStyleDisplayKey = 'data-jodit-default-style-display'; private __defaultClassesKey = 'data-jodit-default-classes'; private commands: IDictionary<Array<CustomCommand<IJodit>>> = {}; private __selectionLocked: markerInfo[] | null = null; private __wasReadOnly: boolean = false; /** * @property {HTMLDocument} editorDocument */ editorDocument: HTMLDocument; /** * @property {Window} editorWindow */ editorWindow: Window; /** * Container for set/get value * @type {Storage} */ storage: Storage = new Storage(new LocalStorageProvider()); /** * workplace It contains source and wysiwyg editors */ workplace: HTMLDivElement; statusbar: StatusBar; observer: Observer; /** * element It contains source element */ element: HTMLElement; /** * editor It contains the root element editor */ editor: HTMLDivElement | HTMLBodyElement; /** * iframe Iframe for iframe mode */ iframe: HTMLIFrameElement | null = null; /** * options All Jodit settings default + second arguments of constructor */ options: Config; /** * @property {Select} selection */ selection: Select; /** * @property {Uploader} uploader */ get uploader(): IUploader { return this.getInstance('Uploader'); } /** * @property {FileBrowser} filebrowser */ get filebrowser(): IFileBrowser { return this.getInstance('FileBrowser'); } helper: any; __plugins: IDictionary<IPlugin> = {}; mode: Modes = consts.MODE_WYSIWYG; /** * Return source element value */ getElementValue() { return (this.element as HTMLInputElement).value !== undefined ? (this.element as HTMLInputElement).value : this.element.innerHTML; } /** * Return real HTML value from WYSIWYG editor. * * @return {string} */ getNativeEditorValue(): string { if (this.editor) { return this.editor.innerHTML; } return this.getElementValue(); } /** * Set value to native editor * @param value */ setNativeEditorValue(value: string) { if (this.editor) { this.editor.innerHTML = value; } } /** * Return editor value */ getEditorValue(removeSelectionMarkers: boolean = true): string { /** * Triggered before {@link Jodit~getEditorValue|getEditorValue} executed. * If returned not undefined getEditorValue will return this value * * @event beforeGetValueFromEditor * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('beforeGetValueFromEditor', function () { * return editor.editor.innerHTML.replace(/a/g, 'b'); * }); * ``` */ let value: string; value = this.events.fire('beforeGetValueFromEditor'); if (value !== undefined) { return value; } value = this.getNativeEditorValue().replace( consts.INVISIBLE_SPACE_REG_EXP, '' ); if (removeSelectionMarkers) { value = value.replace( /<span[^>]+id="jodit_selection_marker_[^>]+><\/span>/g, '' ); } if (value === '<br>') { value = ''; } /** * Triggered after {@link Jodit~getEditorValue|getEditorValue} got value from wysiwyg. * It can change new_value.value * * @event afterGetValueFromEditor * @param string new_value * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('afterGetValueFromEditor', function (new_value) { * new_value.value = new_value.value.replace('a', 'b'); * }); * ``` */ const new_value: { value: string } = { value }; this.events.fire('afterGetValueFromEditor', new_value); return new_value.value; } getEditorText(): string { if (this.editor) { return this.editor.innerText; } const div: HTMLDivElement = this.create.inside.div(); div.innerHTML = this.getElementValue(); return div.innerText; } /** * Set source element value and if set sync fill editor value * When method was called without arguments - it is simple way to synchronize element to editor * * @param {string} [value] */ setElementValue(value?: string) { if (typeof value !== 'string' && value !== undefined) { throw new Error('value must be string'); } if (value !== undefined) { if (this.element !== this.container) { if ((this.element as HTMLInputElement).value !== undefined) { (this.element as HTMLInputElement).value = value; } else { this.element.innerHTML = value; } } } else { value = this.getElementValue(); } if (value !== this.getEditorValue()) { this.setEditorValue(value); } } private __callChangeCount = 0; /** * Set editor html value and if set sync fill source element value * When method was called without arguments - it is simple way to synchronize editor to element * @event beforeSetValueToEditor * @param {string} [value] */ setEditorValue(value?: string) { /** * Triggered before {@link Jodit~getEditorValue|setEditorValue} set value to wysiwyg. * * @event beforeSetValueToEditor * @param string old_value * @returns string | undefined | false * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('beforeSetValueToEditor', function (old_value) { * return old_value.value.replace('a', 'b'); * }); * editor.events.on('beforeSetValueToEditor', function () { * return false; // disable setEditorValue method * }); * ``` */ const newValue: string | undefined | false = this.events.fire( 'beforeSetValueToEditor', value ); if (newValue === false) { return; } if (typeof newValue === 'string') { value = newValue; } if (!this.editor) { if (value !== undefined) { this.setElementValue(value); } return; // try change value before init or after destruct } if (typeof value !== 'string' && value !== undefined) { throw new Error('value must be string'); } if (value !== undefined && this.editor.innerHTML !== value) { this.setNativeEditorValue(value); } const old_value = this.getElementValue(), new_value = this.getEditorValue(); if ( old_value !== new_value && this.__callChangeCount < SAFE_COUNT_CHANGE_CALL ) { this.setElementValue(new_value); this.__callChangeCount += 1; try { this.events.fire('change', new_value, old_value); } finally { this.__callChangeCount = 0; } } } /** * Register custom handler for command * * @example * ```javascript * var jodit = new Jodit('#editor); * * jodit.setEditorValue('test test test'); * * jodit.registerCommand('replaceString', function (command, needle, replace) { * var value = this.getEditorValue(); * this.setEditorValue(value.replace(needle, replace)); * return false; // stop execute native command * }); * * jodit.execCommand('replaceString', 'test', 'stop'); * * console.log(jodit.value); // stop test test * * // and you can add hotkeys for command * jodit.registerCommand('replaceString', { * hotkeys: 'ctrl+r', * exec: function (command, needle, replace) { * var value = this.getEditorValue(); * this.setEditorValue(value.replace(needle, replace)); * } * }); * * ``` * * @param {string} commandNameOriginal * @param {ICommandType | Function} command */ registerCommand( commandNameOriginal: string, command: CustomCommand<IJodit> ): IJodit { const commandName: string = commandNameOriginal.toLowerCase(); if (this.commands[commandName] === undefined) { this.commands[commandName] = []; } this.commands[commandName].push(command); if (typeof command !== 'function') { const hotkeys: string | string[] | void = this.options.commandToHotkeys[commandName] || this.options.commandToHotkeys[commandNameOriginal] || command.hotkeys; if (hotkeys) { this.registerHotkeyToCommand(hotkeys, commandName); } } return this; } /** * Register hotkey for command * * @param hotkeys * @param commandName */ registerHotkeyToCommand(hotkeys: string | string[], commandName: string) { const shortcuts: string = asArray(hotkeys) .map(normalizeKeyAliases) .map(hotkey => hotkey + '.hotkey') .join(' '); this.events.off(shortcuts).on(shortcuts, () => { return this.execCommand(commandName); // because need `beforeCommand` }); } /** * Execute command editor * * @param {string} command command. It supports all the * {@link https://developer.mozilla.org/ru/docs/Web/API/Document/execCommand#commands} and a number of its own * for example applyCSSProperty. Comand fontSize receives the second parameter px, * formatBlock and can take several options * @param {boolean|string|int} showUI * @param {boolean|string|int} value * @fires beforeCommand * @fires afterCommand * @example * ```javascript * this.execCommand('fontSize', 12); // sets the size of 12 px * this.execCommand('underline'); * this.execCommand('formatBlock', 'p'); // will be inserted paragraph * ``` */ execCommand( command: string, showUI: any = false, value: null | any = null ) { if (this.options.readonly && command !== 'selectall') { return; } let result: any; command = command.toLowerCase(); /** * Called before any command * @event beforeCommand * @param {string} command Command name in lowercase * @param {string} second The second parameter for the command * @param {string} third The third option is for the team * @example * ```javascript * parent.events.on('beforeCommand', function (command) { * if (command === 'justifyCenter') { * var p = parent.getDocument().createElement('p') * parent.selection.insertNode(p) * parent.selection.setCursorIn(p); * p.style.textAlign = 'justyfy'; * return false; // break execute native command * } * }) * ``` */ result = this.events.fire('beforeCommand', command, showUI, value); if (result !== false) { result = this.execCustomCommands(command, showUI, value); } if (result !== false) { this.selection.focus(); if (command === 'selectall') { this.selection.select(this.editor, true); } else { try { result = this.editorDocument.execCommand( command, showUI, value ); } catch {} } } /** * It called after any command * @event afterCommand * @param {string} command name command * @param {*} second The second parameter for the command * @param {*} third The third option is for the team */ this.events.fire('afterCommand', command, showUI, value); this.setEditorValue(); // synchrony return result; } private execCustomCommands( commandName: string, second: any = false, third: null | any = null ): false | void { commandName = commandName.toLowerCase(); if (this.commands[commandName] !== undefined) { let result: any; const exec = (command: CustomCommand<Jodit>) => { let callback: ExecCommandCallback<Jodit>; if (typeof command === 'function') { callback = command; } else { callback = command.exec; } const resultCurrent: any = (callback as any).call( this, commandName, second, third ); if (resultCurrent !== undefined) { result = resultCurrent; } }; for (let i = 0; i < this.commands[commandName].length; i += 1) { exec(this.commands[commandName][i]); } return result; } } /** * Disable selecting */ lock(name: string = 'any') { if (super.lock(name)) { this.__selectionLocked = this.selection.save(); this.editor.classList.add('jodit_disabled'); return true; } return false; } /** * Enable selecting */ unlock() { if (super.unlock()) { this.editor.classList.remove('jodit_disabled'); if (this.__selectionLocked) { this.selection.restore(this.__selectionLocked); } return true; } return false; } /** * Return current editor mode: Jodit.MODE_WYSIWYG, Jodit.MODE_SOURCE or Jodit.MODE_SPLIT * @return {number} */ getMode(): Modes { return this.mode; } isEditorMode(): boolean { return this.getRealMode() === consts.MODE_WYSIWYG; } /** * Return current real work mode. When editor in MODE_SOURCE or MODE_WYSIWYG it will * return them, but then editor in MODE_SPLIT it will return MODE_SOURCE if * Textarea(CodeMirror) focused or MODE_WYSIWYG otherwise * * @example * ```javascript * var editor = new Jodit('#editor'); * console.log(editor.getRealMode()); * ``` */ getRealMode(): Modes { if (this.getMode() !== consts.MODE_SPLIT) { return this.getMode(); } const active: Element | null = this.ownerDocument.activeElement; if ( active && (Dom.isOrContains(this.editor, active) || Dom.isOrContains(this.toolbar.container, active)) ) { return consts.MODE_WYSIWYG; } return consts.MODE_SOURCE; } /** * Set current mode * * @fired beforeSetMode * @fired afterSetMode */ setMode(mode: number | string) { const oldmode: Modes = this.getMode(); const data = { mode: <Modes>parseInt(mode.toString(), 10) }, modeClasses = [ 'jodit_wysiwyg_mode', 'jodit_source_mode', 'jodit_split_mode' ]; /** * Triggered before {@link Jodit~setMode|setMode} executed. If returned false method stopped * @event beforeSetMode * @param {Object} data PlainObject {mode: {string}} In handler you can change data.mode * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('beforeSetMode', function (data) { * data.mode = Jodit.MODE_SOURCE; // not respond to the mode change. Always make the source code mode * }); * ``` */ if (this.events.fire('beforeSetMode', data) === false) { return; } this.mode = inArray(data.mode, [ consts.MODE_SOURCE, consts.MODE_WYSIWYG, consts.MODE_SPLIT ]) ? data.mode : consts.MODE_WYSIWYG; if (this.options.saveModeInStorage) { this.storage.set('jodit_default_mode', this.mode); } modeClasses.forEach(className => { this.container.classList.remove(className); }); this.container.classList.add(modeClasses[this.mode - 1]); /** * Triggered after {@link Jodit~setMode|setMode} executed * @event afterSetMode * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('afterSetMode', function () { * editor.setEditorValue(''); // clear editor's value after change mode * }); * ``` */ if (oldmode !== this.getMode()) { this.events.fire('afterSetMode'); } } /** * Toggle editor mode WYSIWYG to TEXTAREA(CodeMirror) to SPLIT(WYSIWYG and TEXTAREA) to again WYSIWYG * * @example * ```javascript * var editor = new Jodit('#editor'); * editor.toggleMode(); * ``` */ toggleMode() { let mode: number = this.getMode(); if ( inArray(mode + 1, [ consts.MODE_SOURCE, consts.MODE_WYSIWYG, this.options.useSplitMode ? consts.MODE_SPLIT : 9 ]) ) { mode += 1; } else { mode = consts.MODE_WYSIWYG; } this.setMode(mode); } /** * Internationalization method. Uses Jodit.lang object * * @param {string} key Some text * @param {string[]} params Some text * @return {string} * @example * ```javascript * var editor = new Jodit("#redactor", { * langusage: 'ru' * }); * console.log(editor.i18n('Cancel')) //Отмена; * * Jodit.defaultOptions.language = 'ru'; * console.log(Jodit.prototype.i18n('Cancel')) //Отмена * * Jodit.lang.cs = { * Cancel: 'Zrušit' * }; * Jodit.defaultOptions.language = 'cs'; * console.log(Jodit.prototype.i18n('Cancel')) //Zrušit * * Jodit.lang.cs = { * 'Hello world': 'Hello \s Good \s' * }; * Jodit.defaultOptions.language = 'cs'; * console.log(Jodit.prototype.i18n('Hello world', 'mr.Perkins', 'day')) //Hello mr.Perkins Good day * ``` */ i18n(key: string, ...params: Array<string | number>): string { const debug: boolean = this.options !== undefined && this.options.debugLanguage; let store: IDictionary; const parse = (value: string): string => params.length ? sprintf.apply(this, [value].concat(params as string[])) : value, default_language: string = Config.defaultOptions.language === 'auto' ? defaultLanguage(Config.defaultOptions.language) : Config.defaultOptions.language, language: string = defaultLanguage( this.options ? this.options.language : default_language ); if (this.options !== undefined && Jodit.lang[language] !== undefined) { store = Jodit.lang[language]; } else { if (Jodit.lang[default_language] !== undefined) { store = Jodit.lang[default_language]; } else { store = Jodit.lang.en; } } if ( this.options !== undefined && (this.options.i18n as any)[language] !== undefined && (this.options.i18n as any)[language][key] ) { return parse((this.options.i18n as any)[language][key]); } if (typeof store[key] === 'string' && store[key]) { return parse(store[key]); } const lckey = key.toLowerCase(); if (typeof store[lckey] === 'string' && store[lckey]) { return parse(store[lckey]); } const ucfkey = ucfirst(key); if (typeof store[ucfkey] === 'string' && store[ucfkey]) { return parse(store[ucfkey]); } if (debug) { return '{' + key + '}'; } if (typeof Jodit.lang.en[key] === 'string' && Jodit.lang.en[key]) { return parse(Jodit.lang.en[key]); } if (process.env.NODE_ENV !== 'production' && language !== 'en') { throw new Error(`i18n need "${key}" in "${language}"`); } return parse(key); } /** * Switch on/off the editor into the disabled state. * When in disabled, the user is not able to change the editor content * This function firing the `disabled` event. * * @param {boolean} isDisabled */ setDisabled(isDisabled: boolean) { this.options.disabled = isDisabled; const readOnly: boolean = this.__wasReadOnly; this.setReadOnly(isDisabled || readOnly); this.__wasReadOnly = readOnly; if (this.editor) { this.editor.setAttribute('aria-disabled', isDisabled.toString()); this.container.classList.toggle('jodit_disabled', isDisabled); this.events.fire('disabled', isDisabled); } } /** * Return true if editor in disabled mode */ getDisabled(): boolean { return this.options.disabled; } /** * Switch on/off the editor into the read-only state. * When in readonly, the user is not able to change the editor content, but can still * use some editor functions (show source code, print content, or seach). * This function firing the `readonly` event. * * @param {boolean} isReadOnly */ setReadOnly(isReadOnly: boolean) { if (this.__wasReadOnly === isReadOnly) { return; } this.__wasReadOnly = isReadOnly; this.options.readonly = isReadOnly; if (isReadOnly) { this.editor && this.editor.removeAttribute('contenteditable'); } else { this.editor && this.editor.setAttribute('contenteditable', 'true'); } this.events && this.events.fire('readonly', isReadOnly); } /** * Return true if editor in read-only mode */ getReadOnly(): boolean { return this.options.readonly; } /** * Create instance of Jodit * @constructor * * @param {HTMLInputElement | string} element Selector or HTMLElement * @param {object} options Editor's options */ constructor(element: HTMLInputElement | string, options?: object) { super(); this.options = new OptionsDefault(options) as Config; // in iframe it can be changed this.editorDocument = this.options.ownerDocument; this.editorWindow = this.options.ownerWindow; this.ownerDocument = this.options.ownerDocument; this.ownerWindow = this.options.ownerWindow; if (typeof element === 'string') { try { this.element = this.ownerDocument.querySelector( element ) as HTMLInputElement; } catch { throw new Error( 'String "' + element + '" should be valid HTML selector' ); } } else { this.element = element; } // Duck checking if ( !this.element || typeof this.element !== 'object' || this.element.nodeType !== Node.ELEMENT_NODE || !this.element.cloneNode ) { throw new Error( 'Element "' + element + '" should be string or HTMLElement instance' ); } if (this.element.attributes) { Array.from(this.element.attributes).forEach((attr: Attr) => { const name: string = attr.name; let value: string | boolean | number = attr.value; if ( (Jodit.defaultOptions as any)[name] !== undefined && (!options || (options as any)[name] === undefined) ) { if (['readonly', 'disabled'].indexOf(name) !== -1) { value = value === '' || value === 'true'; } if (/^[0-9]+(\.)?([0-9]+)?$/.test(value.toString())) { value = Number(value); } (this.options as any)[name] = value; } }); } if (this.options.events) { Object.keys(this.options.events).forEach((key: string) => { this.events.on(key, this.options.events[key]); }); } this.container.classList.add('jodit_container'); this.container.setAttribute('contenteditable', 'false'); this.selection = new Select(this); this.events.on('removeMarkers', () => { if (this.selection) { this.selection.removeMarkers(); } }); this.observer = new Observer(this); let buffer: null | string = null; if (this.options.inline) { if (['TEXTAREA', 'INPUT'].indexOf(this.element.nodeName) === -1) { this.container = this.element as HTMLDivElement; this.element.setAttribute( this.__defaultClassesKey, this.element.className.toString() ); buffer = this.container.innerHTML; this.container.innerHTML = ''; } this.container.classList.add('jodit_inline'); this.container.classList.add('jodit_container'); } // actual for inline mode if (this.element !== this.container) { // hide source element if (this.element.style.display) { this.element.setAttribute( this.__defaultStyleDisplayKey, this.element.style.display ); } this.element.style.display = 'none'; } this.container.classList.add( 'jodit_' + (this.options.theme || 'default') + '_theme' ); if (this.options.zIndex) { this.container.style.zIndex = parseInt( this.options.zIndex.toString(), 10 ).toString(); } this.workplace = this.create.div('jodit_workplace', { contenteditable: false }); if (this.options.toolbar) { this.toolbar.build( splitArray(this.options.buttons).concat( this.options.extraButtons ), this.container ); } const bs = this.options.toolbarButtonSize.toLowerCase(); this.container.classList.add( 'jodit_toolbar_size-' + (['middle', 'large', 'small'].indexOf(bs) !== -1 ? bs : 'middle') ); if (this.options.textIcons) { this.container.classList.add('jodit_text_icons'); } this.events.on(this.ownerWindow, 'resize', () => { if (this.events) { this.events.fire('resize'); } }); this.container.appendChild(this.workplace); this.statusbar = new StatusBar(this, this.container); this.workplace.appendChild(this.progress_bar); if (this.element.parentNode && this.element !== this.container) { this.element.parentNode.insertBefore(this.container, this.element); } this.id = this.element.getAttribute('id') || new Date().getTime().toString(); this.editor = this.create.div('jodit_wysiwyg', { contenteditable: true, 'aria-disabled': false, tabindex: this.options.tabIndex }); this.workplace.appendChild(this.editor); this.setNativeEditorValue(this.getElementValue()); // Init value (async () => { await this.events.fire('beforeInit', this); this.__initPlugines(); await this.__initEditor(buffer); if (this.isDestructed) { return; } if ( this.options.enableDragAndDropFileToEditor && this.options.uploader && (this.options.uploader.url || this.options.uploader.insertImageAsBase64URI) ) { this.uploader.bind(this.editor); } this.isInited = true; await this.events.fire('afterInit', this); this.events.fire('afterConstructor', this); })(); } isInited: boolean = false; private __initPlugines() { const dp = this.options.disablePlugins; const disable = Array.isArray(dp) ? dp.map((name) => name.toLowerCase()) : dp.toLowerCase().split(/[\s,]+/); Object.keys(Jodit.plugins).forEach((key: string) => { if (disable.indexOf(key.toLowerCase()) === -1) { this.__plugins[key] = new Jodit.plugins[key](this); } }); } private async __initEditor(buffer: null | string) { await this.__createEditor(); if (this.isDestructed) { return; } // syncro if (this.element !== this.container) { this.setElementValue(); } else { buffer !== null && this.setEditorValue(buffer); // inline mode } Jodit.instances[this.id] = this; let mode: number = this.options.defaultMode; if (this.options.saveModeInStorage) { const localMode: string | null = this.storage.get( 'jodit_default_mode' ); if (localMode !== null) { mode = parseInt(localMode, 10); } } this.setMode(mode); if (this.options.readonly) { this.setReadOnly(true); } if (this.options.disabled) { this.setDisabled(true); } // if enter plugin not installed try { this.editorDocument.execCommand( 'defaultParagraphSeparator', false, this.options.enter.toLowerCase() ); } catch { } // fix for native resizing try { this.editorDocument.execCommand( 'enableObjectResizing', false, 'false' ); } catch {} try { this.editorDocument.execCommand( 'enableInlineTableEditing', false, 'false' ); } catch {} } /** * Create main DIV element and replace source textarea * * @private */ private async __createEditor() { const defaultEditorAreae: HTMLElement = this.editor; const stayDefault: boolean | undefined = await this.events.fire( 'createEditor', this ); if (this.isDestructed) { return; } if (stayDefault === false) { Dom.safeRemove(defaultEditorAreae); } if (this.options.editorCssClass) { this.editor.classList.add(this.options.editorCssClass); } if (this.options.style) { css(this.editor, this.options.style); } // proxy events this.events .on('synchro', () => { this.setEditorValue(); }) .on( this.editor, 'selectionchange selectionstart keydown keyup keypress mousedown mouseup mousepress ' + 'click copy cut dragstart drop dragover paste resize touchstart touchend focus blur', (event: Event): false | void => { if (this.options.readonly) { return; } if (this.events && this.events.fire) { if (this.events.fire(event.type, event) === false) { return false; } this.setEditorValue(); } } ); if (this.options.spellcheck) { this.editor.setAttribute('spellcheck', 'true'); } // direction if (this.options.direction) { const direction = this.options.direction.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'; this.editor.style.direction = direction; this.container.style.direction = direction; this.editor.setAttribute('dir', direction); this.container.setAttribute('dir', direction); this.toolbar.setDirection(direction); } if (this.options.triggerChangeEvent) { this.events.on( 'change', debounce(() => { this.events && this.events.fire(this.element, 'change'); }, this.defaultTimeout) ); } } /** * Jodit's Destructor. Remove editor, and return source input */ destruct() { if (this.isDestructed) { return; } /** * Triggered before {@link events:beforeDestruct|beforeDestruct} executed. If returned false method stopped * * @event beforeDestruct * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('beforeDestruct', function (data) { * return false; * }); * ``` */ if (this.events.fire('beforeDestruct') === false) { return; } if (!this.editor) { return; } const buffer: string = this.getEditorValue(); if (this.element !== this.container) { if (this.element.hasAttribute(this.__defaultStyleDisplayKey)) { this.element.style.display = this.element.getAttribute( this.__defaultStyleDisplayKey ); this.element.removeAttribute(this.__defaultStyleDisplayKey); } else { this.element.style.display = ''; } } else { if (this.element.hasAttribute(this.__defaultClassesKey)) { this.element.className = this.element.getAttribute(this.__defaultClassesKey) || ''; this.element.removeAttribute(this.__defaultClassesKey); } } if ( this.element.hasAttribute('style') && !this.element.getAttribute('style') ) { this.element.removeAttribute('style'); } Object.keys(this.__plugins).forEach((pluginName: string) => { const plugin = this.__plugins[pluginName]; if ( plugin !== undefined && plugin.destruct !== undefined && typeof plugin.destruct === 'function' ) { plugin.destruct(); } delete this.__plugins[pluginName]; }); this.observer.destruct(); this.statusbar.destruct(); delete this.observer; delete this.statusbar; delete this.storage; this.components.forEach((component: Component) => { if ( component.destruct !== undefined && typeof component.destruct === 'function' && !component.isDestructed ) { component.destruct(); } }); this.components.length = 0; this.commands = {}; delete this.selection; this.__selectionLocked = null; this.events.off(this.ownerWindow); this.events.off(this.ownerDocument); this.events.off(this.ownerDocument.body); this.events.off(this.element); this.events.off(this.editor); Dom.safeRemove(this.workplace); Dom.safeRemove(this.editor); Dom.safeRemove(this.progress_bar); Dom.safeRemove(this.iframe); if (this.container !== this.element) { Dom.safeRemove(this.container); } delete this.workplace; delete this.editor; delete this.progress_bar; delete this.iframe; // inline mode if (this.container === this.element) { this.element.innerHTML = buffer; } delete Jodit.instances[this.id]; super.destruct(); delete this.container; } }