1282 lines
41 KiB
JavaScript
1282 lines
41 KiB
JavaScript
/*!
|
|
* Jodit Editor (https://xdsoft.net/jodit/)
|
|
* Released under MIT see LICENSE.txt in the project root for license information.
|
|
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
|
|
*/
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
r = Reflect.decorate(decorators, target, key, desc);
|
|
else
|
|
for (var i = decorators.length - 1; i >= 0; i--)
|
|
if (d = decorators[i])
|
|
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var Jodit_1;
|
|
import * as constants from "./core/constants.js";
|
|
import { FAT_MODE, IS_PROD, lang } from "./core/constants.js";
|
|
import { autobind, cache, cached, derive, throttle, watch } from "./core/decorators/index.js";
|
|
import { eventEmitter, instances, modules, pluginSystem } from "./core/global.js";
|
|
import { asArray, attr, callPromise, ConfigProto, css, error, isFunction, isJoditObject, isNumber, isPromise, isString, isVoid, kebabCase, markAsAtomic, normalizeKeyAliases, resolveElement, toArray, ucfirst } from "./core/helpers/index.js";
|
|
import { Ajax } from "./core/request/index.js";
|
|
import { Dlgs } from "./core/traits/dlgs.js";
|
|
import { Config } from "./config.js";
|
|
import { Create, Dom, History, Plugin, Selection, StatusBar, STATUSES, ViewWithToolbar } from "./modules/index.js";
|
|
const __defaultStyleDisplayKey = 'data-jodit-default-style-display';
|
|
const __defaultClassesKey = 'data-jodit-default-classes';
|
|
let Jodit = Jodit_1 = class Jodit extends ViewWithToolbar {
|
|
/** @override */
|
|
className() {
|
|
return 'Jodit';
|
|
}
|
|
/**
|
|
* Return promise for ready actions
|
|
* @example
|
|
* ```js
|
|
* const jodit = Jodit.make('#editor');
|
|
* await jodit.waitForReady();
|
|
* jodit.e.fire('someAsyncLoadedPluginEvent', (test) => {
|
|
* alert(test);
|
|
* });
|
|
* ```
|
|
*/
|
|
waitForReady() {
|
|
if (this.isReady) {
|
|
return Promise.resolve(this);
|
|
}
|
|
return this.async.promise(resolve => {
|
|
this.hookStatus('ready', () => resolve(this));
|
|
});
|
|
}
|
|
/**
|
|
* @deprecated I don't know why I wrote itp
|
|
*/
|
|
static get ready() {
|
|
return new Promise(resolve => {
|
|
eventEmitter.on('joditready', resolve);
|
|
});
|
|
}
|
|
/**
|
|
* Plain text editor's value
|
|
*/
|
|
get text() {
|
|
if (this.editor) {
|
|
return this.editor.innerText || '';
|
|
}
|
|
const div = this.createInside.div();
|
|
div.innerHTML = this.getElementValue();
|
|
return div.innerText || '';
|
|
}
|
|
/**
|
|
* Return a default timeout period in milliseconds for some debounce or throttle functions.
|
|
* By default, `{history.timeout}` options
|
|
*/
|
|
get defaultTimeout() {
|
|
return isNumber(this.o.defaultTimeout)
|
|
? this.o.defaultTimeout
|
|
: Config.defaultOptions.defaultTimeout;
|
|
}
|
|
/**
|
|
* Method wrap usual object in Object helper for prevent deep object merging in options*
|
|
* ```js
|
|
* const editor = Jodit.make('#editor', {
|
|
* controls: {
|
|
* fontsize: {
|
|
* list: Jodit.atom([8, 9, 10])
|
|
* }
|
|
* }
|
|
* });
|
|
* ```
|
|
* In this case, the array [8, 9, 10] will not be combined with other arrays, but will replace them
|
|
*/
|
|
static atom(object) {
|
|
return markAsAtomic(object);
|
|
}
|
|
/**
|
|
* Factory for creating Jodit instance
|
|
*/
|
|
static make(element, options) {
|
|
return new this(element, options);
|
|
}
|
|
/**
|
|
* Checks if the element has already been initialized when for Jodit
|
|
*/
|
|
static isJoditAssigned(element) {
|
|
return (element &&
|
|
isJoditObject(element.component) &&
|
|
!element.component.isInDestruct);
|
|
}
|
|
/**
|
|
* Default settings
|
|
*/
|
|
static get defaultOptions() {
|
|
return Config.defaultOptions;
|
|
}
|
|
get createInside() {
|
|
return new Create(() => this.ed, this.o.createAttributes);
|
|
}
|
|
__setPlaceField(field, value) {
|
|
if (!this.currentPlace) {
|
|
this.currentPlace = {};
|
|
this.places = [this.currentPlace];
|
|
}
|
|
this.currentPlace[field] = value;
|
|
}
|
|
/**
|
|
* element It contains source element
|
|
*/
|
|
get element() {
|
|
return this.currentPlace.element;
|
|
}
|
|
/**
|
|
* editor It contains the root element editor
|
|
*/
|
|
get editor() {
|
|
return this.currentPlace.editor;
|
|
}
|
|
set editor(editor) {
|
|
this.__setPlaceField('editor', editor);
|
|
}
|
|
/**
|
|
* Container for all staff
|
|
*/
|
|
get container() {
|
|
return this.currentPlace.container;
|
|
}
|
|
set container(container) {
|
|
this.__setPlaceField('container', container);
|
|
}
|
|
/**
|
|
* workplace It contains source and wysiwyg editors
|
|
*/
|
|
get workplace() {
|
|
return this.currentPlace.workplace;
|
|
}
|
|
get message() {
|
|
return this.getMessageModule(this.workplace);
|
|
}
|
|
/**
|
|
* Statusbar module
|
|
*/
|
|
get statusbar() {
|
|
return this.currentPlace.statusbar;
|
|
}
|
|
/**
|
|
* iframe Iframe for iframe mode
|
|
*/
|
|
get iframe() {
|
|
return this.currentPlace.iframe;
|
|
}
|
|
set iframe(iframe) {
|
|
this.__setPlaceField('iframe', iframe);
|
|
}
|
|
get history() {
|
|
return this.currentPlace.history;
|
|
}
|
|
/**
|
|
* In iframe mode editor's window can be different by owner
|
|
*/
|
|
get editorWindow() {
|
|
return this.currentPlace.editorWindow;
|
|
}
|
|
set editorWindow(win) {
|
|
this.__setPlaceField('editorWindow', win);
|
|
}
|
|
/**
|
|
* Alias for this.ew
|
|
*/
|
|
get ew() {
|
|
return this.editorWindow;
|
|
}
|
|
/**
|
|
* In iframe mode editor's window can be different by owner
|
|
*/
|
|
get editorDocument() {
|
|
return this.currentPlace.editorWindow.document;
|
|
}
|
|
/**
|
|
* Alias for this.ew
|
|
*/
|
|
get ed() {
|
|
return this.editorDocument;
|
|
}
|
|
/**
|
|
* options All Jodit settings default + second arguments of constructor
|
|
*/
|
|
get options() {
|
|
return this.currentPlace.options;
|
|
}
|
|
set options(opt) {
|
|
this.__options = opt;
|
|
this.__setPlaceField('options', opt);
|
|
}
|
|
/**
|
|
* Alias for this.selection
|
|
*/
|
|
get s() {
|
|
return this.selection;
|
|
}
|
|
get uploader() {
|
|
return this.getInstance('Uploader', this.o.uploader);
|
|
}
|
|
get filebrowser() {
|
|
const jodit = this;
|
|
const options = ConfigProto({
|
|
defaultTimeout: jodit.defaultTimeout,
|
|
uploader: jodit.o.uploader,
|
|
language: jodit.o.language,
|
|
license: jodit.o.license,
|
|
theme: jodit.o.theme,
|
|
shadowRoot: jodit.o.shadowRoot,
|
|
defaultCallback(data) {
|
|
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', options);
|
|
}
|
|
/**
|
|
* Editor's mode
|
|
*/
|
|
get mode() {
|
|
return this.__mode;
|
|
}
|
|
set mode(mode) {
|
|
this.setMode(mode);
|
|
}
|
|
/**
|
|
* Return real HTML value from WYSIWYG editor.
|
|
* @internal
|
|
*/
|
|
getNativeEditorValue() {
|
|
const value = this.e.fire('beforeGetNativeEditorValue');
|
|
if (isString(value)) {
|
|
return value;
|
|
}
|
|
if (this.editor) {
|
|
return this.editor.innerHTML;
|
|
}
|
|
return this.getElementValue();
|
|
}
|
|
/**
|
|
* Set value to native editor
|
|
*/
|
|
setNativeEditorValue(value) {
|
|
const data = {
|
|
value
|
|
};
|
|
if (this.e.fire('beforeSetNativeEditorValue', data)) {
|
|
return;
|
|
}
|
|
if (this.editor) {
|
|
this.editor.innerHTML = data.value;
|
|
}
|
|
}
|
|
/**
|
|
* HTML value
|
|
*/
|
|
get value() {
|
|
return this.getEditorValue();
|
|
}
|
|
set value(html) {
|
|
this.setEditorValue(html);
|
|
// @ts-ignore Internal method
|
|
this.history.__processChanges();
|
|
}
|
|
synchronizeValues() {
|
|
this.__imdSynchronizeValues();
|
|
}
|
|
/**
|
|
* This is an internal method, do not use it in your applications.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
__imdSynchronizeValues() {
|
|
this.setEditorValue();
|
|
}
|
|
/**
|
|
* Return editor value
|
|
*/
|
|
getEditorValue(removeSelectionMarkers = true, consumer) {
|
|
/**
|
|
* Triggered before getEditorValue executed.
|
|
* If returned not undefined, getEditorValue will return this value
|
|
* @example
|
|
* ```javascript
|
|
* var editor = Jodit.make("#redactor");
|
|
* editor.e.on('beforeGetValueFromEditor', function () {
|
|
* return editor.editor.innerHTML.replace(/a/g, 'b');
|
|
* });
|
|
* ```
|
|
*/
|
|
let value;
|
|
value = this.e.fire('beforeGetValueFromEditor', consumer);
|
|
if (value !== undefined) {
|
|
return value;
|
|
}
|
|
value = this.getNativeEditorValue().replace(constants.INVISIBLE_SPACE_REG_EXP(), '');
|
|
if (removeSelectionMarkers) {
|
|
value = value.replace(/<span[^>]+id="jodit-selection_marker_[^>]+><\/span>/g, '');
|
|
}
|
|
if (value === '<br>') {
|
|
value = '';
|
|
}
|
|
/**
|
|
* Triggered after getEditorValue got value from wysiwyg.
|
|
* It can change new_value.value
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* var editor = Jodit.make("#redactor");
|
|
* editor.e.on('afterGetValueFromEditor', function (new_value) {
|
|
* new_value.value = new_value.value.replace('a', 'b');
|
|
* });
|
|
* ```
|
|
*/
|
|
const new_value = { value };
|
|
this.e.fire('afterGetValueFromEditor', new_value, consumer);
|
|
return new_value.value;
|
|
}
|
|
/**
|
|
* Set editor html value and if set sync fill source element value
|
|
* When method was called without arguments - it is a simple way to synchronize editor to element
|
|
*/
|
|
setEditorValue(value) {
|
|
/**
|
|
* Triggered before getEditorValue set value to wysiwyg.
|
|
* @example
|
|
* ```javascript
|
|
* var editor = Jodit.make("#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 = 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 (!isVoid(value) && this.getNativeEditorValue() !== value) {
|
|
this.setNativeEditorValue(value);
|
|
}
|
|
this.e.fire('postProcessSetEditorValue');
|
|
const old_value = this.getElementValue(), new_value = this.getEditorValue();
|
|
if (!this.__isSilentChange &&
|
|
old_value !== new_value &&
|
|
this.__callChangeCount < constants.SAFE_COUNT_CHANGE_CALL) {
|
|
this.__setElementValue(new_value);
|
|
this.__callChangeCount += 1;
|
|
if (!IS_PROD && this.__callChangeCount > 4) {
|
|
console.warn('Too many recursive changes', new_value, old_value);
|
|
}
|
|
try {
|
|
// @ts-ignore Internal method
|
|
this.history.__upTick();
|
|
this.e.fire('change', new_value, old_value);
|
|
this.e.fire(this.history, 'change', new_value, old_value);
|
|
}
|
|
finally {
|
|
this.__callChangeCount = 0;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* If some plugin changes the DOM directly, then you need to update the content of the original element
|
|
*/
|
|
updateElementValue() {
|
|
this.__setElementValue(this.getEditorValue());
|
|
}
|
|
/**
|
|
* Return source element value
|
|
*/
|
|
getElementValue() {
|
|
return this.element.value !== undefined
|
|
? this.element.value
|
|
: this.element.innerHTML;
|
|
}
|
|
__setElementValue(value) {
|
|
if (!isString(value)) {
|
|
throw error('value must be string');
|
|
}
|
|
if (this.element !== this.container &&
|
|
value !== this.getElementValue()) {
|
|
const data = { value };
|
|
const res = this.e.fire('beforeSetElementValue', data);
|
|
callPromise(res, () => {
|
|
if (this.element.value !== undefined) {
|
|
this.element.value = data.value;
|
|
}
|
|
else {
|
|
this.element.innerHTML = data.value;
|
|
}
|
|
this.e.fire('afterSetElementValue', data);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Register custom handler for command
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* var jodit = Jodit.make('#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
|
|
*
|
|
* // 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));
|
|
* }
|
|
* });
|
|
*
|
|
* ```
|
|
*/
|
|
registerCommand(commandNameOriginal, command, options) {
|
|
const commandName = commandNameOriginal.toLowerCase();
|
|
let commands = this.commands.get(commandName);
|
|
if (commands === undefined) {
|
|
commands = [];
|
|
this.commands.set(commandName, commands);
|
|
}
|
|
commands.push(command);
|
|
if (!isFunction(command)) {
|
|
const hotkeys = this.o.commandToHotkeys[commandName] ||
|
|
this.o.commandToHotkeys[commandNameOriginal] ||
|
|
command.hotkeys;
|
|
if (hotkeys) {
|
|
this.registerHotkeyToCommand(hotkeys, commandName, options === null || options === void 0 ? void 0 : options.stopPropagation);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Register hotkey for command
|
|
*/
|
|
registerHotkeyToCommand(hotkeys, commandName, shouldStop = true) {
|
|
const shortcuts = asArray(hotkeys)
|
|
.map(normalizeKeyAliases)
|
|
.map(hotkey => hotkey + '.hotkey')
|
|
.join(' ');
|
|
this.e
|
|
.off(shortcuts)
|
|
.on(shortcuts, (type, stop) => {
|
|
if (stop) {
|
|
stop.shouldStop = shouldStop !== null && shouldStop !== void 0 ? shouldStop : true;
|
|
}
|
|
return this.execCommand(commandName); // because need `beforeCommand`
|
|
});
|
|
}
|
|
/**
|
|
* Execute command editor
|
|
*
|
|
* @param command - command. It supports all the
|
|
* @see 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
|
|
* @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, showUI, value, ...args) {
|
|
if (!this.s.isFocused()) {
|
|
this.s.focus();
|
|
}
|
|
if (this.o.readonly &&
|
|
!this.o.allowCommandsInReadOnly.includes(command)) {
|
|
return;
|
|
}
|
|
let result;
|
|
command = command.toLowerCase();
|
|
/**
|
|
* Called before any command
|
|
* @param command - Command name in lowercase
|
|
* @param second - The second parameter for the command
|
|
* @param 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 executes native command
|
|
* }
|
|
* })
|
|
* ```
|
|
*/
|
|
result = this.e.fire(`beforeCommand${ucfirst(command)}`, showUI, value, ...args);
|
|
if (result !== false) {
|
|
result = this.e.fire('beforeCommand', command, showUI, value, ...args);
|
|
}
|
|
if (result !== false) {
|
|
result = this.__execCustomCommands(command, showUI, value, ...args);
|
|
}
|
|
if (result !== false) {
|
|
this.s.focus();
|
|
try {
|
|
result = this.nativeExecCommand(command, showUI, value);
|
|
}
|
|
catch (e) {
|
|
if (!IS_PROD) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* It called after any command
|
|
* @param 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.__imdSynchronizeValues(); // synchrony
|
|
return result;
|
|
}
|
|
/**
|
|
* Exec native command
|
|
*/
|
|
nativeExecCommand(command, showUI, value) {
|
|
this.__isSilentChange = true;
|
|
try {
|
|
return this.ed.execCommand(command, showUI, value);
|
|
}
|
|
finally {
|
|
this.__isSilentChange = false;
|
|
}
|
|
}
|
|
__execCustomCommands(commandName, second, third, ...args) {
|
|
commandName = commandName.toLowerCase();
|
|
const commands = this.commands.get(commandName);
|
|
if (commands !== undefined) {
|
|
let result;
|
|
commands.forEach((command) => {
|
|
let callback;
|
|
if (isFunction(command)) {
|
|
callback = command;
|
|
}
|
|
else {
|
|
callback = command.exec;
|
|
}
|
|
const resultCurrent = callback.call(this, commandName, second, third, ...args);
|
|
if (resultCurrent !== undefined) {
|
|
result = resultCurrent;
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
/**
|
|
* Disable selecting
|
|
*/
|
|
lock(name = 'any') {
|
|
if (super.lock(name)) {
|
|
this.__selectionLocked = this.s.save();
|
|
this.s.clear();
|
|
this.editor.classList.add('jodit_lock');
|
|
this.e.fire('lock', true);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Enable selecting
|
|
*/
|
|
unlock() {
|
|
if (super.unlock()) {
|
|
this.editor.classList.remove('jodit_lock');
|
|
if (this.__selectionLocked) {
|
|
this.s.restore();
|
|
}
|
|
this.e.fire('lock', false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Return current editor mode: Jodit.MODE_WYSIWYG, Jodit.MODE_SOURCE or Jodit.MODE_SPLIT
|
|
*/
|
|
getMode() {
|
|
return this.mode;
|
|
}
|
|
isEditorMode() {
|
|
return this.getRealMode() === constants.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 = Jodit.make('#editor');
|
|
* console.log(editor.getRealMode());
|
|
* ```
|
|
*/
|
|
getRealMode() {
|
|
if (this.getMode() !== constants.MODE_SPLIT) {
|
|
return this.getMode();
|
|
}
|
|
const active = this.od.activeElement;
|
|
if (active &&
|
|
(active === this.iframe ||
|
|
Dom.isOrContains(this.editor, active) ||
|
|
Dom.isOrContains(this.toolbar.container, active))) {
|
|
return constants.MODE_WYSIWYG;
|
|
}
|
|
return constants.MODE_SOURCE;
|
|
}
|
|
/**
|
|
* Set current mode
|
|
*/
|
|
setMode(mode) {
|
|
const oldMode = this.getMode();
|
|
const data = {
|
|
mode: parseInt(mode.toString(), 10)
|
|
}, modeClasses = [
|
|
'jodit-wysiwyg_mode',
|
|
'jodit-source__mode',
|
|
'jodit_split_mode'
|
|
];
|
|
/**
|
|
* Triggered before setMode executed. If returned false method stopped
|
|
* @param data - PlainObject `{mode: {string}}` In handler you can change data.mode
|
|
* @example
|
|
* ```javascript
|
|
* var editor = Jodit.make("#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 = [
|
|
constants.MODE_SOURCE,
|
|
constants.MODE_WYSIWYG,
|
|
constants.MODE_SPLIT
|
|
].includes(data.mode)
|
|
? data.mode
|
|
: constants.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 setMode executed
|
|
* @example
|
|
* ```javascript
|
|
* var editor = Jodit.make("#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 = Jodit.make('#editor');
|
|
* editor.toggleMode();
|
|
* ```
|
|
*/
|
|
toggleMode() {
|
|
let mode = this.getMode();
|
|
if ([
|
|
constants.MODE_SOURCE,
|
|
constants.MODE_WYSIWYG,
|
|
this.o.useSplitMode ? constants.MODE_SPLIT : 9
|
|
].includes(mode + 1)) {
|
|
mode += 1;
|
|
}
|
|
else {
|
|
mode = constants.MODE_WYSIWYG;
|
|
}
|
|
this.setMode(mode);
|
|
}
|
|
/**
|
|
* Switch on/off the editor into the disabled state.
|
|
* When disabled, the user is not able to change the editor content
|
|
* This function firing the `disabled` event.
|
|
*/
|
|
setDisabled(isDisabled) {
|
|
this.o.disabled = isDisabled;
|
|
const readOnly = 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() {
|
|
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.
|
|
*/
|
|
setReadOnly(isReadOnly) {
|
|
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() {
|
|
return this.o.readonly;
|
|
}
|
|
focus() {
|
|
if (this.getMode() !== constants.MODE_SOURCE) {
|
|
this.s.focus();
|
|
}
|
|
}
|
|
get isFocused() {
|
|
return this.s.isFocused();
|
|
}
|
|
/**
|
|
* Hook before init
|
|
*/
|
|
beforeInitHook() {
|
|
// do nothing
|
|
}
|
|
/**
|
|
* Hook after init
|
|
*/
|
|
afterInitHook() {
|
|
// do nothing
|
|
}
|
|
/** @override **/
|
|
initOptions(options) {
|
|
this.options = (ConfigProto(options || {}, Config.defaultOptions));
|
|
}
|
|
/** @override **/
|
|
initOwners() {
|
|
// in iframe, it can be changed
|
|
this.editorWindow = this.o.ownerWindow;
|
|
this.ownerWindow = this.o.ownerWindow;
|
|
}
|
|
/**
|
|
* Create instance of Jodit
|
|
*
|
|
* @param element - Selector or HTMLElement
|
|
* @param options - Editor's options
|
|
*/
|
|
constructor(element, options) {
|
|
super(options, true);
|
|
/**
|
|
* Define if object is Jodit
|
|
*/
|
|
this.isJodit = true;
|
|
this.commands = new Map();
|
|
this.__selectionLocked = null;
|
|
this.__wasReadOnly = false;
|
|
/**
|
|
* Editor has focus in this time
|
|
*/
|
|
this.editorIsActive = false;
|
|
this.__mode = constants.MODE_WYSIWYG;
|
|
this.__callChangeCount = 0;
|
|
/**
|
|
* Don't raise a change event
|
|
*/
|
|
this.__isSilentChange = false;
|
|
this.currentPlace = {
|
|
options: this.__options,
|
|
container: this.__container
|
|
};
|
|
this.places = [];
|
|
this.__elementToPlace = new Map();
|
|
try {
|
|
const elementSource = resolveElement(element, this.options.shadowRoot || this.od);
|
|
if (Jodit_1.isJoditAssigned(elementSource)) {
|
|
// @ts-ignore
|
|
return elementSource.component;
|
|
}
|
|
}
|
|
catch (e) {
|
|
if (!IS_PROD) {
|
|
throw 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.attachEvents(options);
|
|
this.e.on(this.ow, 'resize', () => {
|
|
if (this.e) {
|
|
this.e.fire('resize');
|
|
}
|
|
});
|
|
this.e.on('prepareWYSIWYGEditor', this.__prepareWYSIWYGEditor);
|
|
this.selection = new Selection(this);
|
|
const beforeInitHookResult = this.beforeInitHook();
|
|
callPromise(beforeInitHookResult, () => {
|
|
if (this.isInDestruct) {
|
|
return;
|
|
}
|
|
this.e.fire('beforeInit', this);
|
|
pluginSystem.__init(this);
|
|
this.e.fire('afterPluginSystemInit', this);
|
|
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.isInDestruct) {
|
|
return;
|
|
}
|
|
if (this.e) {
|
|
this.e.fire('afterInit', this);
|
|
}
|
|
callPromise(this.afterInitHook());
|
|
this.setStatus(STATUSES.ready);
|
|
this.e.fire('afterConstructor', this);
|
|
};
|
|
callPromise(addPlaceResult, init);
|
|
});
|
|
}
|
|
/**
|
|
* Create and init current editable place
|
|
*/
|
|
addPlace(source, options) {
|
|
const element = resolveElement(source, this.o.shadowRoot || this.od);
|
|
this.attachEvents(options);
|
|
if (element.attributes) {
|
|
toArray(element.attributes).forEach((attr) => {
|
|
const name = attr.name;
|
|
let value = attr.value;
|
|
if (Config.defaultOptions[name] !== undefined &&
|
|
(!options || options[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[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'}`);
|
|
addClassNames(this.o.className, container);
|
|
if (this.o.containerStyle) {
|
|
css(container, this.o.containerStyle);
|
|
}
|
|
const { styleValues } = this.o;
|
|
Object.keys(styleValues).forEach(key => {
|
|
const property = kebabCase(key);
|
|
container.style.setProperty(`--jd-${property}`, styleValues[key]);
|
|
});
|
|
container.setAttribute('contenteditable', 'false');
|
|
let buffer = null;
|
|
if (this.o.inline) {
|
|
if (['TEXTAREA', 'INPUT'].indexOf(element.nodeName) === -1) {
|
|
container = element;
|
|
element.setAttribute(__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(__defaultStyleDisplayKey, element.style.display);
|
|
}
|
|
element.style.display = 'none';
|
|
}
|
|
const workplace = this.c.div('jodit-workplace', {
|
|
contenteditable: false
|
|
});
|
|
container.appendChild(workplace);
|
|
if (element.parentNode && element !== container) {
|
|
element.parentNode.insertBefore(container, element);
|
|
}
|
|
Object.defineProperty(element, 'component', {
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: this
|
|
});
|
|
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: new StatusBar(this, container),
|
|
options: this.isReady
|
|
? ConfigProto(options || {}, Config.defaultOptions)
|
|
: this.options,
|
|
history: new History(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);
|
|
};
|
|
return callPromise(initResult, init);
|
|
}
|
|
addDisclaimer(elm) {
|
|
this.workplace.appendChild(elm);
|
|
}
|
|
/**
|
|
* Set current place object
|
|
*/
|
|
setCurrentPlace(place) {
|
|
if (this.currentPlace === place) {
|
|
return;
|
|
}
|
|
if (!this.isEditorMode()) {
|
|
this.setMode(constants.MODE_WYSIWYG);
|
|
}
|
|
this.currentPlace = place;
|
|
this.buildToolbar();
|
|
if (this.isReady) {
|
|
this.e.fire('changePlace', place);
|
|
}
|
|
}
|
|
__initEditor(buffer) {
|
|
const result = this.__createEditor();
|
|
return callPromise(result, () => {
|
|
if (this.isInDestruct) {
|
|
return;
|
|
}
|
|
// syncro
|
|
if (this.element !== this.container) {
|
|
const value = this.getElementValue();
|
|
if (value !== this.getEditorValue()) {
|
|
this.setEditorValue(value);
|
|
}
|
|
}
|
|
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 isn't installed
|
|
try {
|
|
this.ed.execCommand('defaultParagraphSeparator', false, this.o.enter.toLowerCase());
|
|
}
|
|
catch (_a) { }
|
|
});
|
|
}
|
|
/**
|
|
* Create main DIV element and replace source textarea
|
|
*/
|
|
__createEditor() {
|
|
const defaultEditorArea = this.editor;
|
|
const stayDefault = this.e.fire('createEditor', this);
|
|
return callPromise(stayDefault, () => {
|
|
if (this.isInDestruct) {
|
|
return;
|
|
}
|
|
if (stayDefault === false || isPromise(stayDefault)) {
|
|
Dom.safeRemove(defaultEditorArea);
|
|
}
|
|
addClassNames(this.o.editorClassName, this.editor);
|
|
if (this.o.style) {
|
|
css(this.editor, this.o.style);
|
|
}
|
|
this.e
|
|
.on('synchro', () => {
|
|
this.setEditorValue();
|
|
})
|
|
.on('focus', () => {
|
|
this.editorIsActive = true;
|
|
})
|
|
.on('blur', () => (this.editorIsActive = false));
|
|
this.__prepareWYSIWYGEditor();
|
|
if (this.o.triggerChangeEvent) {
|
|
this.e.on('change', this.async.debounce(() => {
|
|
this.e && this.e.fire(this.element, 'change');
|
|
}, this.defaultTimeout));
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Attach some native event listeners
|
|
*/
|
|
__prepareWYSIWYGEditor() {
|
|
const { editor } = this;
|
|
// direction
|
|
if (this.o.direction) {
|
|
const direction = this.o.direction.toLowerCase() === 'rtl' ? 'rtl' : 'ltr';
|
|
this.editor.style.direction = direction;
|
|
this.editor.setAttribute('dir', direction);
|
|
this.container.style.direction = direction;
|
|
this.container.setAttribute('dir', direction);
|
|
this.toolbar.setDirection(direction);
|
|
}
|
|
// proxy events
|
|
this.e
|
|
.on(editor, 'mousedown touchstart focus', () => {
|
|
const place = this.__elementToPlace.get(editor);
|
|
if (place) {
|
|
this.setCurrentPlace(place);
|
|
}
|
|
})
|
|
.on(editor, 'compositionend', this.synchronizeValues)
|
|
.on(editor, 'selectionchange selectionstart keydown keyup input keypress dblclick mousedown mouseup ' +
|
|
'click copy cut dragstart drop dragover paste resize touchstart touchend focus blur', (event) => {
|
|
if (this.o.readonly || this.__isSilentChange) {
|
|
return;
|
|
}
|
|
const w = this.ew;
|
|
if (event instanceof w.KeyboardEvent &&
|
|
event.isComposing) {
|
|
return;
|
|
}
|
|
if (this.e && this.e.fire) {
|
|
if (this.e.fire(event.type, event) === false) {
|
|
return false;
|
|
}
|
|
this.synchronizeValues();
|
|
}
|
|
});
|
|
}
|
|
fetch(url, options) {
|
|
const ajax = new Ajax({
|
|
url,
|
|
...options
|
|
}, this.o.defaultAjaxOptions);
|
|
const destroy = () => {
|
|
this.e.off('beforeDestruct', destroy);
|
|
this.progressbar.progress(100).hide();
|
|
ajax.destruct();
|
|
};
|
|
this.e.one('beforeDestruct', destroy);
|
|
this.progressbar.show().progress(30);
|
|
const promise = ajax.send();
|
|
promise.finally(destroy).catch(() => null);
|
|
return promise;
|
|
}
|
|
/**
|
|
* Jodit's Destructor. Remove editor, and return source input
|
|
*/
|
|
destruct() {
|
|
var _a, _b;
|
|
if (this.isInDestruct) {
|
|
return;
|
|
}
|
|
this.setStatus(STATUSES.beforeDestruct);
|
|
this.__elementToPlace.clear();
|
|
(_a = cached(this, 'storage')) === null || _a === void 0 ? void 0 : _a.clear();
|
|
(_b = cached(this, 'buffer')) === null || _b === void 0 ? void 0 : _b.clear();
|
|
this.commands.clear();
|
|
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);
|
|
const tmpValue = this.editor ? this.getEditorValue() : '';
|
|
this.places.forEach(({ container, workplace, statusbar, element, iframe, editor, history }) => {
|
|
if (!element) {
|
|
return;
|
|
}
|
|
if (element !== container) {
|
|
if (element.hasAttribute(__defaultStyleDisplayKey)) {
|
|
const display = attr(element, __defaultStyleDisplayKey);
|
|
if (display) {
|
|
element.style.display = display;
|
|
element.removeAttribute(__defaultStyleDisplayKey);
|
|
}
|
|
}
|
|
else {
|
|
element.style.display = '';
|
|
}
|
|
}
|
|
else {
|
|
if (element.hasAttribute(__defaultClassesKey)) {
|
|
element.className =
|
|
attr(element, __defaultClassesKey) || '';
|
|
element.removeAttribute(__defaultClassesKey);
|
|
}
|
|
}
|
|
if (element.hasAttribute('style') && !attr(element, 'style')) {
|
|
element.removeAttribute('style');
|
|
}
|
|
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);
|
|
}
|
|
Object.defineProperty(element, 'component', {
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: null
|
|
});
|
|
Dom.safeRemove(iframe);
|
|
// inline mode
|
|
if (container === element) {
|
|
element.innerHTML = tmpValue;
|
|
}
|
|
history.destruct();
|
|
});
|
|
this.places.length = 0;
|
|
this.currentPlace = {};
|
|
delete instances[this.id];
|
|
super.destruct();
|
|
}
|
|
};
|
|
Jodit.fatMode = FAT_MODE;
|
|
Jodit.plugins = pluginSystem;
|
|
Jodit.modules = modules;
|
|
Jodit.ns = modules;
|
|
Jodit.decorators = {};
|
|
Jodit.constants = constants;
|
|
Jodit.instances = instances;
|
|
Jodit.lang = lang;
|
|
Jodit.core = {
|
|
Plugin
|
|
};
|
|
__decorate([
|
|
cache
|
|
], Jodit.prototype, "createInside", null);
|
|
__decorate([
|
|
cache
|
|
], Jodit.prototype, "message", null);
|
|
__decorate([
|
|
cache
|
|
], Jodit.prototype, "s", null);
|
|
__decorate([
|
|
cache
|
|
], Jodit.prototype, "uploader", null);
|
|
__decorate([
|
|
cache
|
|
], Jodit.prototype, "filebrowser", null);
|
|
__decorate([
|
|
throttle()
|
|
], Jodit.prototype, "synchronizeValues", null);
|
|
__decorate([
|
|
watch(':internalChange')
|
|
], Jodit.prototype, "updateElementValue", null);
|
|
__decorate([
|
|
autobind
|
|
], Jodit.prototype, "__prepareWYSIWYGEditor", null);
|
|
Jodit = Jodit_1 = __decorate([
|
|
derive(Dlgs)
|
|
], Jodit);
|
|
export { Jodit };
|
|
function addClassNames(className, elm) {
|
|
if (className) {
|
|
className.split(/\s+/).forEach(cn => elm.classList.add(cn));
|
|
}
|
|
}
|