jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
1,557 lines (1,294 loc) • 33.7 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2020 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Config, configFactory } from './config';
import * as consts from './core/constants';
import {
Create,
Dom,
FileBrowser,
Observer,
Select,
StatusBar,
STATUSES
} from './modules/';
import {
asArray,
css,
isPromise,
normalizeKeyAliases,
error,
isString,
attr,
isFunction,
resolveElement,
isVoid,
JoditArray,
JoditObject
} from './core/helpers/';
import { Storage } from './core/storage/';
import {
CustomCommand,
ExecCommandCallback,
IDictionary,
IPluginSystem,
IStatusBar,
IViewOptions,
IWorkPlace,
markerInfo,
Modes,
IFileBrowser,
IJodit,
IUploader,
ICreate,
IFileBrowserCallBackData,
IStorage
} from './types';
import { ViewWithToolbar } from './core/view/view-with-toolbar';
import { instances, pluginSystem, modules, lang } from './core/global';
import { cache } from './core/decorators';
/**
* Class Jodit. Main class
*/
export class Jodit extends ViewWithToolbar implements IJodit {
/**
* Define if object is Jodit
*/
isJodit: true = true;
/**
* Plain text editor's value
*/
get text(): string {
if (this.editor) {
return this.editor.innerText || '';
}
const div = this.createInside.div();
div.innerHTML = this.getElementValue();
return div.innerText || '';
}
/**
* HTML value
*/
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.o.observer
? this.o.observer.timeout
: Config.defaultOptions.observer.timeout;
}
/**
* Method wrap usual Array in Object helper for prevent deep array merging in options
*
* @param array
* @constructor
*/
static Array(array: never[]): JoditArray {
return new JoditArray(array);
}
/**
* Method wrap usual Has Object in Object helper for prevent deep object merging in options*
*
* @param object
* @constructor
*/
static Object(object: never): JoditObject {
return new JoditObject(object);
}
/**
* Fabric for creating Jodit instance
*
* @param element
* @param options
*/
static make(element: HTMLElement | string, options?: object): Jodit {
return new Jodit(element, options);
}
static defaultOptions: Config;
static plugins: IPluginSystem = pluginSystem;
static modules: IDictionary<Function> = modules;
static ns: IDictionary<Function> = modules;
static decorators: IDictionary<Function> = {};
static instances: IDictionary<IJodit> = instances;
static lang: any = lang;
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 = false;
/**
* Container for set/get value
* @type {Storage}
*/
readonly storage!: IStorage;
readonly createInside: ICreate = new Create(
() => this.ed,
this.o.createAttributes
);
/**
* Editor has focus in this time
*/
editorIsActive = false;
private setPlaceField(field: keyof IWorkPlace, value: any): void {
if (!this.currentPlace) {
this.currentPlace = {} as any;
this.places = [this.currentPlace];
}
this.currentPlace[field] = value;
}
/**
* element It contains source element
*/
get element(): HTMLElement {
return this.currentPlace.element;
}
/**
* editor It contains the root element editor
*/
get editor(): HTMLDivElement | HTMLBodyElement {
return this.currentPlace.editor;
}
set editor(editor: HTMLDivElement | HTMLBodyElement) {
this.setPlaceField('editor', editor);
}
/**
* Container for all staff
*/
get container(): HTMLDivElement {
return this.currentPlace.container;
}
set container(container: HTMLDivElement) {
this.setPlaceField('container', container);
}
/**
* workplace It contains source and wysiwyg editors
*/
get workplace(): HTMLDivElement {
return this.currentPlace.workplace;
}
/**
* Statusbar module
*/
get statusbar(): IStatusBar {
return this.currentPlace.statusbar;
}
/**
* iframe Iframe for iframe mode
*/
get iframe(): HTMLIFrameElement | void {
return this.currentPlace.iframe;
}
set iframe(iframe: HTMLIFrameElement | void) {
this.setPlaceField('iframe', iframe);
}
get observer(): Observer {
return this.currentPlace.observer;
}
/**
* In iframe mode editor's window can be different by owner
*/
get editorWindow(): Window {
return this.currentPlace.editorWindow;
}
set editorWindow(win: Window) {
this.setPlaceField('editorWindow', win);
}
/**
* Alias for this.ew
*/
get ew(): this['editorWindow'] {
return this.editorWindow;
}
/**
* In iframe mode editor's window can be different by owner
*/
get editorDocument(): Document {
return this.currentPlace.editorWindow.document;
}
/**
* Alias for this.ew
*/
get ed(): this['editorDocument'] {
return this.editorDocument;
}
/**
* options All Jodit settings default + second arguments of constructor
*/
get options(): Config {
return this.currentPlace.options as Config;
}
set options(opt: Config) {
this.setPlaceField('options', opt);
}
/**
* @property {Select} selection
*/
selection: Select;
/**
* Alias for this.selection
*/
get s(): this['selection'] {
return this.selection;
}
/**
* @property {Uploader} uploader
*/
get uploader(): IUploader {
return this.getInstance('Uploader', this.o.uploader);
}
/**
* @property {FileBrowser} filebrowser
*/
get filebrowser(): IFileBrowser {
const jodit = this;
const options = {
defaultTimeout: jodit.defaultTimeout,
uploader: jodit.o.uploader,
language: jodit.o.language,
theme: jodit.o.theme,
defaultCallback(data: IFileBrowserCallBackData): void {
if (data.files && data.files.length) {
data.files.forEach((file, i) => {
const url = data.baseurl + file;
const isImage = data.isImages
? data.isImages[i]
: false;
if (isImage) {
jodit.s.insertImage(
url,
null,
jodit.o.imageDefaultWidth
);
} else {
jodit.s.insertNode(
jodit.createInside.fromHTML(
`<a href="${url}" title="${url}">${url}</a>`
)
);
}
});
}
},
...this.o.filebrowser
};
return jodit.getInstance<FileBrowser>('FileBrowser', options);
}
private __mode: Modes = consts.MODE_WYSIWYG;
/**
* Editor's mode
*/
get mode(): Modes {
return this.__mode;
}
set mode(mode: Modes) {
this.setMode(mode);
}
/**
* Return real HTML value from WYSIWYG editor.
*
* @return {string}
*/
getNativeEditorValue(): string {
const value: string = this.e.fire('beforeGetNativeEditorValue');
if (isString(value)) {
return value;
}
if (this.editor) {
return this.editor.innerHTML;
}
return this.getElementValue();
}
/**
* Set value to native editor
* @param value
*/
setNativeEditorValue(value: string): void {
if (this.e.fire('beforeSetNativeEditorValue', value)) {
return;
}
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.e.on('beforeGetValueFromEditor', function () {
* return editor.editor.innerHTML.replace(/a/g, 'b');
* });
* ```
*/
let value: string;
value = this.e.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.e.on('afterGetValueFromEditor', function (new_value) {
* new_value.value = new_value.value.replace('a', 'b');
* });
* ```
*/
const new_value: { value: string } = { value };
this.e.fire('afterGetValueFromEditor', new_value);
return new_value.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 [value]
* @param [notChangeStack]
*/
setEditorValue(value?: string): void {
/**
* 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.e.on('beforeSetValueToEditor', function (old_value) {
* return old_value.value.replace('a', 'b');
* });
* editor.e.on('beforeSetValueToEditor', function () {
* return false; // disable setEditorValue method
* });
* ```
*/
const newValue: string | undefined | false = this.e.fire(
'beforeSetValueToEditor',
value
);
if (newValue === false) {
return;
}
if (isString(newValue)) {
value = newValue;
}
if (!this.editor) {
if (value !== undefined) {
this.setElementValue(value);
}
return; // try change value before init or after destruct
}
if (!isString(value) && !isVoid(value)) {
throw error('value must be string');
}
if (value !== undefined && this.getNativeEditorValue() !== value) {
this.setNativeEditorValue(value);
}
this.e.fire('postProcessSetEditorValue');
const old_value = this.getElementValue(),
new_value = this.getEditorValue();
if (
old_value !== new_value &&
this.__callChangeCount < consts.SAFE_COUNT_CHANGE_CALL
) {
this.setElementValue(new_value);
this.__callChangeCount += 1;
try {
this.observer.upTick();
this.e.fire('change', new_value, old_value);
this.e.fire(this.observer, 'change', new_value, old_value);
} finally {
this.__callChangeCount = 0;
}
}
}
/**
* Return source element value
*/
getElementValue(): string {
return (this.element as HTMLInputElement).value !== undefined
? (this.element as HTMLInputElement).value
: this.element.innerHTML;
}
/**
* 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): void {
if (!isString(value) && value !== undefined) {
throw 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);
}
}
/**
* 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.o.commandToHotkeys[commandName] ||
this.o.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
): void {
const shortcuts: string = asArray(hotkeys)
.map(normalizeKeyAliases)
.map(hotkey => hotkey + '.hotkey')
.join(' ');
this.e.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 applyStyleProperty. 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: boolean = false,
value: null | any = null
): void {
if (this.o.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.e.on('beforeCommand', function (command) {
* if (command === 'justifyCenter') {
* var p = parent.c.element('p')
* parent.s.insertNode(p)
* parent.s.setCursorIn(p);
* p.style.textAlign = 'justyfy';
* return false; // break execute native command
* }
* })
* ```
*/
result = this.e.fire('beforeCommand', command, showUI, value);
if (result !== false) {
result = this.execCustomCommands(command, showUI, value);
}
if (result !== false) {
this.s.focus();
if (command === 'selectall') {
this.s.select(this.editor, true);
} else {
try {
result = this.ed.execCommand(command, showUI, value);
} catch (e) {
if (!isProd) {
throw e;
}
}
}
}
/**
* 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.e.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 (isFunction(command)) {
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 = 'any'): boolean {
if (super.lock(name)) {
this.__selectionLocked = this.s.save();
this.s.clear();
this.editor.classList.add('jodit_disabled');
this.e.fire('lock', true);
return true;
}
return false;
}
/**
* Enable selecting
*/
unlock(): boolean {
if (super.unlock()) {
this.editor.classList.remove('jodit_disabled');
if (this.__selectionLocked) {
this.s.restore(this.__selectionLocked);
}
this.e.fire('lock', false);
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.od.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): void {
const oldmode: Modes = this.getMode();
const data = {
mode: parseInt(mode.toString(), 10) as Modes
},
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.e.on('beforeSetMode', function (data) {
* data.mode = Jodit.MODE_SOURCE; // not respond to the mode change. Always make the source code mode
* });
* ```
*/
if (this.e.fire('beforeSetMode', data) === false) {
return;
}
this.__mode = [
consts.MODE_SOURCE,
consts.MODE_WYSIWYG,
consts.MODE_SPLIT
].includes(data.mode)
? data.mode
: consts.MODE_WYSIWYG;
if (this.o.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.e.on('afterSetMode', function () {
* editor.setEditorValue(''); // clear editor's value after change mode
* });
* ```
*/
if (oldmode !== this.getMode()) {
this.e.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(): void {
let mode: number = this.getMode();
if (
[
consts.MODE_SOURCE,
consts.MODE_WYSIWYG,
this.o.useSplitMode ? consts.MODE_SPLIT : 9
].includes(mode + 1)
) {
mode += 1;
} else {
mode = consts.MODE_WYSIWYG;
}
this.setMode(mode);
}
/**
* 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): void {
this.o.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.e.fire('disabled', isDisabled);
}
}
/**
* Return true if editor in disabled mode
*/
getDisabled(): boolean {
return this.o.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): void {
if (this.__wasReadOnly === isReadOnly) {
return;
}
this.__wasReadOnly = isReadOnly;
this.o.readonly = isReadOnly;
if (isReadOnly) {
this.editor && this.editor.removeAttribute('contenteditable');
} else {
this.editor && this.editor.setAttribute('contenteditable', 'true');
}
this.e && this.e.fire('readonly', isReadOnly);
}
/**
* Return true if editor in read-only mode
*/
getReadOnly(): boolean {
return this.o.readonly;
}
/**
* Hook before init
*/
beforeInitHook(): void {
// do nothing
}
/**
* Hook after init
*/
afterInitHook(): void {
// do nothing
}
/** @override **/
protected initOptions(options?: object): void {
this.options = configFactory(options);
}
/** @override **/
protected initOwners(): void {
// in iframe it can be changed
this.editorWindow = this.o.ownerWindow;
this.ownerWindow = this.o.ownerWindow;
}
/**
* Create instance of Jodit
* @constructor
*
* @param {HTMLInputElement | string} element Selector or HTMLElement
* @param {object} options Editor's options
*/
constructor(element: HTMLElement | string, options?: object) {
super(options as IViewOptions);
try {
resolveElement(element, this.o.shadowRoot || this.od); // check element valid
} catch (e) {
this.destruct();
throw e;
}
this.setStatus(STATUSES.beforeInit);
this.id =
attr(resolveElement(element, this.o.shadowRoot || this.od), 'id') ||
new Date().getTime().toString();
instances[this.id] = this;
this.storage = Storage.makeStorage(true, this.id);
this.attachEvents(this.o);
this.e.on(this.ow, 'resize', () => {
if (this.e) {
this.e.fire('resize');
}
});
this.selection = new Select(this);
this.initPlugins();
this.e.on('changePlace', () => {
this.setReadOnly(this.o.readonly);
this.setDisabled(this.o.disabled);
});
this.places.length = 0;
const addPlaceResult = this.addPlace(element, options);
instances[this.id] = this;
const init = () => {
if (this.e) {
this.e.fire('afterInit', this);
}
this.afterInitHook();
this.setStatus(STATUSES.ready);
this.e.fire('afterConstructor', this);
};
if (isPromise(addPlaceResult)) {
addPlaceResult.finally(init);
} else {
init();
}
}
currentPlace!: IWorkPlace;
places!: IWorkPlace[];
private elementToPlace: Map<HTMLElement, IWorkPlace> = new Map();
/**
* Create and init current editable place
* @param source
* @param options
*/
addPlace(
source: HTMLElement | string,
options?: object
): void | Promise<any> {
const element = resolveElement(source, this.o.shadowRoot || this.od);
this.attachEvents(options as IViewOptions);
if (element.attributes) {
Array.from(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;
}
});
}
let container = this.c.div('jodit-container');
container.classList.add('jodit');
container.classList.add('jodit-container');
container.classList.add(`jodit_theme_${this.o.theme || 'default'}`);
container.setAttribute('contenteditable', 'false');
let buffer: null | string = null;
if (this.o.inline) {
if (['TEXTAREA', 'INPUT'].indexOf(element.nodeName) === -1) {
container = element as HTMLDivElement;
element.setAttribute(
this.__defaultClassesKey,
element.className.toString()
);
buffer = container.innerHTML;
container.innerHTML = '';
}
container.classList.add('jodit_inline');
container.classList.add('jodit-container');
}
// actual for inline mode
if (element !== container) {
// hide source element
if (element.style.display) {
element.setAttribute(
this.__defaultStyleDisplayKey,
element.style.display
);
}
element.style.display = 'none';
}
const workplace = this.c.div('jodit-workplace', {
contenteditable: false
});
container.appendChild(workplace);
const statusbar = new StatusBar(this, container);
if (element.parentNode && element !== container) {
element.parentNode.insertBefore(container, element);
}
const editor = this.c.div('jodit-wysiwyg', {
contenteditable: true,
'aria-disabled': false,
tabindex: this.o.tabIndex
});
workplace.appendChild(editor);
const currentPlace = {
editor,
element,
container,
workplace,
statusbar,
options: this.isReady ? configFactory(options) : this.options,
observer: new Observer(this),
editorWindow: this.ow
};
this.elementToPlace.set(editor, currentPlace);
this.setCurrentPlace(currentPlace);
this.places.push(currentPlace);
this.setNativeEditorValue(this.getElementValue()); // Init value
const initResult = this.initEditor(buffer);
const opt = this.options;
const init = () => {
if (
opt.enableDragAndDropFileToEditor &&
opt.uploader &&
(opt.uploader.url || opt.uploader.insertImageAsBase64URI)
) {
this.uploader.bind(this.editor);
}
// in initEditor - the editor could change
if (!this.elementToPlace.get(this.editor)) {
this.elementToPlace.set(this.editor, currentPlace);
}
this.e.fire('afterAddPlace', currentPlace);
};
if (isPromise(initResult)) {
return initResult.then(init);
}
init();
}
/**
* Set current place object
* @param place
*/
setCurrentPlace(place: IWorkPlace): void {
if (this.currentPlace === place) {
return;
}
if (!this.isEditorMode()) {
this.setMode(consts.MODE_WYSIWYG);
}
this.currentPlace = place;
this.buildToolbar();
if (this.isReady) {
this.e.fire('changePlace', place);
}
}
private initPlugins(): void {
this.beforeInitHook();
this.e.fire('beforeInit', this);
try {
pluginSystem.init(this).catch(e => {
throw e;
});
} catch (e) {
if (!isProd) {
throw e;
}
}
}
private initEditor(buffer: null | string): void | Promise<any> {
const result = this.createEditor();
const init = () => {
if (this.isInDestruct) {
return;
}
// syncro
if (this.element !== this.container) {
this.setElementValue();
} else {
buffer !== null && this.setEditorValue(buffer); // inline mode
}
let mode = this.o.defaultMode;
if (this.o.saveModeInStorage) {
const localMode = this.storage.get('jodit_default_mode');
if (typeof localMode === 'string') {
mode = parseInt(localMode, 10);
}
}
this.setMode(mode);
if (this.o.readonly) {
this.__wasReadOnly = false;
this.setReadOnly(true);
}
if (this.o.disabled) {
this.setDisabled(true);
}
// if enter plugin not installed
try {
this.ed.execCommand(
'defaultParagraphSeparator',
false,
this.o.enter.toLowerCase()
);
} catch {}
// fix for native resizing
try {
this.ed.execCommand('enableObjectResizing', false, 'false');
} catch {}
try {
this.ed.execCommand('enableInlineTableEditing', false, 'false');
} catch {}
};
if (isPromise(result)) {
return result.then(init);
}
init();
}
/**
* Create main DIV element and replace source textarea
*
* @private
*/
private createEditor(): void | Promise<any> {
const defaultEditorArea = this.editor;
const stayDefault: boolean | undefined | Promise<void> = this.e.fire(
'createEditor',
this
);
const init = () => {
if (this.isInDestruct) {
return;
}
if (stayDefault === false || isPromise(stayDefault)) {
Dom.safeRemove(defaultEditorArea);
}
if (this.o.editorCssClass) {
this.editor.classList.add(this.o.editorCssClass);
}
if (this.o.style) {
css(this.editor, this.o.style);
}
const editor = this.editor;
// proxy events
this.e
.on('synchro', () => {
this.setEditorValue();
})
.on('focus', () => {
this.editorIsActive = true;
})
.on('blur', () => (this.editorIsActive = false))
.on(editor, 'mousedown touchstart focus', () => {
const place = this.elementToPlace.get(editor);
if (place) {
this.setCurrentPlace(place);
}
})
.on(editor, 'compositionend', () => {
this.setEditorValue();
})
.on(
editor,
'selectionchange selectionstart keydown keyup keypress dblclick mousedown mouseup ' +
'click copy cut dragstart drop dragover paste resize touchstart touchend focus blur',
(event: Event): false | void => {
if (this.o.readonly) {
return;
}
if (
event instanceof KeyboardEvent &&
event.isComposing
) {
return;
}
if (this.e && this.e.fire) {
if (this.e.fire(event.type, event) === false) {
return false;
}
this.setEditorValue();
}
}
);
if (this.o.spellcheck) {
this.editor.setAttribute('spellcheck', 'true');
}
// direction
if (this.o.direction) {
const direction =
this.o.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.o.triggerChangeEvent) {
this.e.on(
'change',
this.async.debounce(() => {
this.e && this.e.fire(this.element, 'change');
}, this.defaultTimeout)
);
}
};
if (isPromise(stayDefault)) {
return stayDefault.then(init);
}
init();
}
/**
* Add option's event handlers in emitter
* @param options
*/
private attachEvents(options: IViewOptions) {
const e = options?.events;
e && Object.keys(e).forEach((key: string) => this.e.on(key, e[key]));
}
/**
* Jodit's Destructor. Remove editor, and return source input
*/
destruct(): void {
if (this.isInDestruct) {
return;
}
this.setStatus(STATUSES.beforeDestruct);
this.elementToPlace.clear();
if (!this.editor) {
return;
}
const buffer = this.getEditorValue();
this.storage.clear();
this.buffer.clear();
delete this.buffer;
this.commands = {};
delete this.selection;
this.__selectionLocked = null;
this.e.off(this.ow, 'resize');
this.e.off(this.ow);
this.e.off(this.od);
this.e.off(this.od.body);
this.places.forEach(
({
container,
workplace,
statusbar,
element,
iframe,
editor,
observer
}) => {
if (element !== container) {
if (element.hasAttribute(this.__defaultStyleDisplayKey)) {
const display = attr(
element,
this.__defaultStyleDisplayKey
);
if (display) {
element.style.display = display;
element.removeAttribute(
this.__defaultStyleDisplayKey
);
}
} else {
element.style.display = '';
}
} else {
if (element.hasAttribute(this.__defaultClassesKey)) {
element.className =
attr(element, this.__defaultClassesKey) || '';
element.removeAttribute(this.__defaultClassesKey);
}
}
if (element.hasAttribute('style') && !attr(element, 'style')) {
element.removeAttribute('style');
}
!statusbar.isInDestruct && statusbar.destruct();
this.e.off(container);
this.e.off(element);
this.e.off(editor);
Dom.safeRemove(workplace);
Dom.safeRemove(editor);
if (container !== element) {
Dom.safeRemove(container);
}
Dom.safeRemove(iframe);
// inline mode
if (container === element) {
element.innerHTML = buffer;
}
!observer.isInDestruct && observer.destruct();
}
);
this.places.length = 0;
this.currentPlace = {} as any;
delete instances[this.id];
super.destruct();
}
}