diff --git a/CHANGELOG.md b/CHANGELOG.md index 1085cf146..c52df61a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ #### :bug: Bug Fix - [Wrong new empty paragraph location when cursor is set after a table and key is pressed #953](https://github.com/xdan/jodit/issues/953) +- The PluginSystem module has been refactored: now asynchronous plugins do not block the initialization of the editor and it is ready to work without them. ## 3.24.4 @@ -1983,11 +1984,11 @@ Related with https://github.com/xdan/jodit/issues/574. In some cases need to lim - @property {IUIOption[]} link.selectOptionsClassName=[] The list of the option for the select (to use with modeClassName="select") - ex: [ -- { value: "", text: "" }, -- { value: "val1", text: "text1" }, -- { value: "val2", text: "text2" }, -- { value: "val3", text: "text3" } -- ] +- { value: "", text: "" }, +- { value: "val1", text: "text1" }, +- { value: "val2", text: "text2" }, +- { value: "val3", text: "text3" } +- ] PR: https://github.com/xdan/jodit/pull/577 Thanks @s-renier-taonix-fr ##### New option `statusbar: boolean = true` diff --git a/src/core/dom/dom.ts b/src/core/dom/dom.ts index 4bab42a0d..488a21f8e 100644 --- a/src/core/dom/dom.ts +++ b/src/core/dom/dom.ts @@ -1006,6 +1006,8 @@ export class Dom { static safeInsertNode(range: Range, node: Node): void { range.collapsed || range.deleteContents(); range.insertNode(node); + range.setStartBefore(node); + range.collapse(true); // https://developer.mozilla.org/en-US/docs/Web/API/Range/insertNode // if the new node is to be added to a text Node, that Node is split at the diff --git a/src/core/plugin/helpers/init-instance.ts b/src/core/plugin/helpers/init-instance.ts new file mode 100644 index 000000000..17bb121b5 --- /dev/null +++ b/src/core/plugin/helpers/init-instance.ts @@ -0,0 +1,80 @@ +/*! + * Jodit Editor (https://xdsoft.net/jodit/) + * Released under MIT see LICENSE.txt in the project root for license information. + * Copyright (c) 2013-2023 Valeriy Chupurnov. All rights reserved. https://xdsoft.net + */ + +/** + * @module plugin + */ + +import type { IDictionary, IJodit, IPlugin, PluginInstance } from 'jodit/types'; +import { isInitable } from 'jodit/core/helpers/checker'; +import { loadStyle } from './load' + +/** + * Init plugin and try init waiting list + * @private + */ +export function initInstance( + jodit: IJodit, + pluginName: string, + instance: PluginInstance, + doneList: Set, + waitingList: IDictionary +): void { + if (init(jodit, pluginName, instance, doneList, waitingList)) { + Object.keys(waitingList).forEach(name => { + const plugin = waitingList[name]; + init(jodit, name, plugin, doneList, waitingList); + }); + } +} + +/** + * Init plugin if it has not dependencies in another case wait requires plugins will be init + * @private + */ +export function init( + jodit: IJodit, + pluginName: string, + instance: PluginInstance, + doneList: Set, + waitingList: IDictionary +): boolean { + const req = (instance as IPlugin).requires; + + if (req?.length && !req.every(name => doneList.has(name))) { + // @ts-ignore + if (!isProd && !isTest && !waitingList[pluginName]) { + console.log('Await plugin: ', pluginName); + } + + waitingList[pluginName] = instance; + return false; + } + + if (isInitable(instance)) { + try { + instance.init(jodit); + } catch (e) { + console.error(e); + + // @ts-ignore + if (!isProd) { + throw e; + } + } + } + + doneList.add(pluginName); + delete waitingList[pluginName]; + + if ((instance as IPlugin).hasStyle) { + loadStyle(jodit, pluginName).catch(e => { + !isProd && console.log(e); + }); + } + + return true; +} diff --git a/src/core/plugin/helpers/load.ts b/src/core/plugin/helpers/load.ts new file mode 100644 index 000000000..611574684 --- /dev/null +++ b/src/core/plugin/helpers/load.ts @@ -0,0 +1,98 @@ +/*! + * Jodit Editor (https://xdsoft.net/jodit/) + * Released under MIT see LICENSE.txt in the project root for license information. + * Copyright (c) 2013-2023 Valeriy Chupurnov. All rights reserved. https://xdsoft.net + */ + +/** + * @module plugin + */ + +import type { IExtraPlugin, IJodit, PluginType } from 'jodit/types'; +import { + appendScriptAsync, + appendStyleAsync +} from 'jodit/core/helpers/utils/append-script'; +import { kebabCase } from 'jodit/core/helpers/string/kebab-case'; +import { normalizeName } from 'jodit/core/plugin/helpers/utils'; + +const styles: Set = new Set(); + +/** + * @private + */ +export async function loadStyle( + jodit: IJodit, + pluginName: string +): Promise { + const url = getFullUrl(jodit, pluginName, false); + + if (styles.has(url)) { + return; + } + + styles.add(url); + + return appendStyleAsync(jodit, url); +} + +/** + * Call full url to the script or style file + * @private + */ +function getFullUrl(jodit: IJodit, name: string, js: boolean): string { + name = kebabCase(name); + + return ( + jodit.basePath + + 'plugins/' + + name + + '/' + + name + + '.' + + (js ? 'js' : 'css') + ); +} + +/** + * @private + */ +export function loadExtras( + items: Map, + jodit: IJodit, + extrasList: IExtraPlugin[], + callback: () => void +): void { + try { + const needLoadExtras = extrasList.filter( + extra => !items.has(normalizeName(extra.name)) + ); + + if (needLoadExtras.length) { + load(jodit, needLoadExtras, callback); + } + } catch (e) { + // @ts-ignore + if (!isProd) { + throw e; + } + } +} + +/** + * Download plugins + * @private + */ +function load( + jodit: IJodit, + pluginList: IExtraPlugin[], + callback: () => void +): void { + pluginList.map(extra => { + const url = extra.url || getFullUrl(jodit, extra.name, true); + + return appendScriptAsync(jodit, url) + .then(callback) + .catch(() => null); + }); +} diff --git a/src/core/plugin/helpers/make-instance.ts b/src/core/plugin/helpers/make-instance.ts new file mode 100644 index 000000000..dc696a15c --- /dev/null +++ b/src/core/plugin/helpers/make-instance.ts @@ -0,0 +1,40 @@ +/*! + * Jodit Editor (https://xdsoft.net/jodit/) + * Released under MIT see LICENSE.txt in the project root for license information. + * Copyright (c) 2013-2023 Valeriy Chupurnov. All rights reserved. https://xdsoft.net + */ + +/** + * @module plugin + */ + +import type { IJodit, Nullable, PluginInstance, PluginType } from 'jodit/types'; +import { isFunction } from 'jodit/core/helpers/checker'; + +/** + * Create instance of plugin + * @private + */ +export function makeInstance( + jodit: IJodit, + plugin: PluginType +): Nullable { + try { + try { + // @ts-ignore + return isFunction(plugin) ? new plugin(jodit) : plugin; + } catch (e) { + if (isFunction(plugin) && !plugin.prototype) { + return (plugin as Function)(jodit); + } + } + } catch (e) { + console.error(e); + // @ts-ignore + if (!isProd) { + throw e; + } + } + + return null; +} diff --git a/src/core/plugin/helpers/utils.ts b/src/core/plugin/helpers/utils.ts new file mode 100644 index 000000000..25c87f2d6 --- /dev/null +++ b/src/core/plugin/helpers/utils.ts @@ -0,0 +1,18 @@ +/*! + * Jodit Editor (https://xdsoft.net/jodit/) + * Released under MIT see LICENSE.txt in the project root for license information. + * Copyright (c) 2013-2023 Valeriy Chupurnov. All rights reserved. https://xdsoft.net + */ + +/** + * @module plugin + */ + +import { kebabCase } from 'jodit/core/helpers/string/kebab-case'; + +/** + * @private + */ +export function normalizeName(name: string): string { + return kebabCase(name).toLowerCase(); +} diff --git a/src/core/plugin/interface.ts b/src/core/plugin/interface.ts new file mode 100644 index 000000000..ef1134d66 --- /dev/null +++ b/src/core/plugin/interface.ts @@ -0,0 +1,15 @@ +/*! + * Jodit Editor (https://xdsoft.net/jodit/) + * Released under MIT see LICENSE.txt in the project root for license information. + * Copyright (c) 2013-2023 Valeriy Chupurnov. All rights reserved. https://xdsoft.net + */ + +declare module 'jodit/types/events' { + interface IEventEmitter { + /** + * Emitted every time after the plugins have been initialized + * or a deferred plugin has been loaded and also initialized + */ + on(event: 'updatePlugins', callback: () => void): this; + } +} diff --git a/src/core/plugin/plugin-system.ts b/src/core/plugin/plugin-system.ts index d9408a6ee..59e4a903d 100644 --- a/src/core/plugin/plugin-system.ts +++ b/src/core/plugin/plugin-system.ts @@ -12,32 +12,23 @@ import type { IExtraPlugin, IDictionary, IJodit, - IPlugin, IPluginSystem, PluginInstance, PluginType, - CanPromise, CanUndef, Nullable } from 'jodit/types'; -import { - isInitable, - isDestructable, - isFunction, - isString, - isArray -} from 'jodit/core/helpers/checker'; +import './interface'; -import { - appendScriptAsync, - appendStyleAsync -} from 'jodit/core/helpers/utils/append-script'; +import { isDestructable, isString, isArray } from 'jodit/core/helpers/checker'; import { splitArray } from 'jodit/core/helpers/array'; -import { kebabCase } from 'jodit/core/helpers/string'; -import { callPromise } from 'jodit/core/helpers/utils/utils'; import { eventEmitter } from 'jodit/core/global'; +import { loadExtras } from 'jodit/core/plugin/helpers/load'; +import { normalizeName } from 'jodit/core/plugin/helpers/utils'; +import { makeInstance } from 'jodit/core/plugin/helpers/make-instance'; +import { initInstance } from 'jodit/core/plugin/helpers/init-instance'; /** * Jodit plugin system @@ -52,27 +43,11 @@ import { eventEmitter } from 'jodit/core/global'; * ``` */ export class PluginSystem implements IPluginSystem { - private normalizeName(name: string): string { - return kebabCase(name).toLowerCase(); - } - - private _items = new Map(); - - private items(filter: Nullable): Array<[string, PluginType]> { - const results: Array<[string, PluginType]> = []; - - this._items.forEach((plugin, name) => { - results.push([name, plugin]); - }); - - return results.filter(([name]) => !filter || filter.includes(name)); - } - /** * Add plugin in store */ add(name: string, plugin: PluginType): void { - this._items.set(this.normalizeName(name), plugin); + this.__items.set(normalizeName(name), plugin); eventEmitter.fire(`plugin:${name}:ready`); } @@ -80,42 +55,53 @@ export class PluginSystem implements IPluginSystem { * Get plugin from store */ get(name: string): PluginType | void { - return this._items.get(this.normalizeName(name)); + return this.__items.get(normalizeName(name)); } /** * Remove plugin from store */ remove(name: string): void { - this._items.delete(this.normalizeName(name)); + this.__items.delete(normalizeName(name)); + } + + private __items = new Map(); + + private __filter( + filter: Nullable> + ): Array<[string, PluginType]> { + const results: Array<[string, PluginType]> = []; + + this.__items.forEach((plugin, name) => { + results.push([name, plugin]); + }); + + return results.filter(([name]) => !filter || filter.has(name)); } /** * Public method for async init all plugins */ - init(jodit: IJodit): CanPromise { - const extrasList: IExtraPlugin[] = jodit.o.extraPlugins.map(s => - isString(s) ? { name: s } : s - ), - disableList = splitArray(jodit.o.disablePlugins).map(s => { - const name = this.normalizeName(s); - - // @ts-ignore - if (!isProd && !this._items.has(name)) { - console.error(TypeError(`Unknown plugin disabled:${name}`)); - } + __init(jodit: IJodit): void { + const { extrasList, disableList, filter } = getSpecialLists(jodit); + + const doneList: Set = new Set(); + const waitingList: IDictionary = {}; + const pluginsMap: IDictionary = {}; + + (jodit as any).__plugins = pluginsMap; - return name; - }), - doneList: string[] = [], - promiseList: IDictionary = {}, - plugins: PluginInstance[] = [], - pluginsMap: IDictionary = {}, - makeAndInit = ([name, plugin]: [string, PluginType]): void => { + const initPlugins = (): void => { + if (jodit.isInDestruct) { + return; + } + + let commit: boolean = false; + this.__filter(filter).forEach(([name, plugin]) => { if ( - disableList.includes(name) || - doneList.includes(name) || - promiseList[name] + disableList.has(name) || + doneList.has(name) || + waitingList[name] ) { return; } @@ -127,53 +113,41 @@ export class PluginSystem implements IPluginSystem { if ( requires && isArray(requires) && - this.hasDisabledRequires(disableList, requires) + Boolean(requires.some(req => disableList.has(req))) ) { return; } - const instance = PluginSystem.makePluginInstance(jodit, plugin); - - if (instance) { - this.initOrWait( - jodit, - name, - instance, - doneList, - promiseList - ); + commit = true; + const instance = makeInstance(jodit, plugin); - plugins.push(instance); - pluginsMap[name] = instance; + if (!instance) { + doneList.add(name); + delete waitingList[name]; + return; } - }; - const resultLoadExtras = this.loadExtras(jodit, extrasList); + initInstance(jodit, name, instance, doneList, waitingList); - return callPromise(resultLoadExtras, () => { - if (jodit.isInDestruct) { - return; - } + pluginsMap[name] = instance; + }); - this.items( - jodit.o.safeMode - ? jodit.o.safePluginsList.concat( - extrasList.map(s => s.name) - ) - : null - ).forEach(makeAndInit); + commit && jodit.e.fire('updatePlugins'); + }; - this.addListenerOnBeforeDestruct(jodit, plugins); + if (!extrasList || !extrasList.length) { + loadExtras(this.__items, jodit, extrasList, initPlugins); + } - (jodit as any).__plugins = pluginsMap; - }); + initPlugins(); + bindOnBeforeDestruct(jodit, pluginsMap); } /** * Returns the promise to wait for the plugin to load. */ wait(name: string): Promise { - return new Promise(resolve => { + return new Promise((resolve): void => { if (this.get(name)) { return resolve(); } @@ -186,211 +160,46 @@ export class PluginSystem implements IPluginSystem { eventEmitter.on(`plugin:${name}:ready`, onReady); }); } +} - /** - * Plugin type has disabled requires - */ - private hasDisabledRequires( - disableList: string[], - requires: string[] - ): boolean { - return Boolean( - requires?.length && - disableList.some(disabled => requires.includes(disabled)) - ); - } - - /** - * Create instance of plugin - */ - static makePluginInstance( - jodit: IJodit, - plugin: PluginType - ): Nullable { - try { - try { - // @ts-ignore - return isFunction(plugin) ? new plugin(jodit) : plugin; - } catch (e) { - if (isFunction(plugin) && !plugin.prototype) { - return (plugin as Function)(jodit); - } - } - } catch (e) { - console.error(e); - // @ts-ignore - if (!isProd) { - throw e; - } - } - - return null; - } - - /** - * Init plugin if it has not dependencies in another case wait requires plugins will be init - */ - private initOrWait( - jodit: IJodit, - pluginName: string, - instance: PluginInstance, - doneList: string[], - promiseList: IDictionary - ): void { - const initPlugin = (name: string, plugin: PluginInstance): boolean => { - if (isInitable(plugin)) { - const req = (plugin as IPlugin).requires; - - if ( - !req?.length || - req.every(name => doneList.includes(name)) - ) { - try { - plugin.init(jodit); - } catch (e) { - console.error(e); - - // @ts-ignore - if (!isProd) { - throw e; - } - } - - doneList.push(name); - } else { - // @ts-ignore - if (!isProd && !isTest && !promiseList[name]) { - console.log('Await plugin: ', name); - } - - promiseList[name] = plugin; - return false; - } - } else { - doneList.push(name); - } - - if ((plugin as IPlugin).hasStyle) { - PluginSystem.loadStyle(jodit, name); - } - - return true; - }; - - initPlugin(pluginName, instance); - - Object.keys(promiseList).forEach(name => { - const plugin = promiseList[name]; - - if (!plugin) { - return; +/** + * Destroy all plugins before - Jodit will be destroyed + */ +function bindOnBeforeDestruct( + jodit: IJodit, + plugins: IDictionary +): void { + jodit.e.on('beforeDestruct', () => { + Object.keys(plugins).forEach(name => { + const instance = plugins[name]; + + if (isDestructable(instance)) { + instance.destruct(jodit); } - if (initPlugin(name, plugin)) { - promiseList[name] = undefined; - delete promiseList[name]; - } + delete plugins[name]; }); - } - /** - * Destroy all plugins before - Jodit will be destroyed - */ - private addListenerOnBeforeDestruct( - jodit: IJodit, - plugins: PluginInstance[] - ): void { - jodit.e.on('beforeDestruct', () => { - plugins.forEach(instance => { - if (isDestructable(instance)) { - instance.destruct(jodit); - } - }); - - plugins.length = 0; - - delete (jodit as any).__plugins; - }); - } - - /** - * Download plugins - */ - private load(jodit: IJodit, pluginList: IExtraPlugin[]): Promise { - const reflect = (p: Promise): Promise => - p.then( - (v: any) => ({ v, status: 'fulfilled' }), - (e: any) => ({ e, status: 'rejected' }) - ); - - return Promise.all( - pluginList.map(extra => { - const url = - extra.url || - PluginSystem.getFullUrl(jodit, extra.name, true); - - return reflect(appendScriptAsync(jodit, url)); - }) - ); - } - - private static async loadStyle( - jodit: IJodit, - pluginName: string - ): Promise { - const url = PluginSystem.getFullUrl(jodit, pluginName, false); - - if (this.styles.has(url)) { - return; - } - - this.styles.add(url); + delete (jodit as any).__plugins; + }); +} - return appendStyleAsync(jodit, url); - } +function getSpecialLists(jodit: IJodit): { + extrasList: IExtraPlugin[]; + disableList: Set; + filter: Set | null; +} { + const extrasList: IExtraPlugin[] = jodit.o.extraPlugins.map(s => + isString(s) ? { name: s } : s + ); - private static styles: Set = new Set(); + const disableList = new Set( + splitArray(jodit.o.disablePlugins).map(normalizeName) + ); - /** - * Call full url to the script or style file - */ - private static getFullUrl( - jodit: IJodit, - name: string, - js: boolean - ): string { - name = kebabCase(name); - - return ( - jodit.basePath + - 'plugins/' + - name + - '/' + - name + - '.' + - (js ? 'js' : 'css') - ); - } + const filter = jodit.o.safeMode + ? new Set(jodit.o.safePluginsList.concat(extrasList.map(s => s.name))) + : null; - private loadExtras( - jodit: IJodit, - extrasList: IExtraPlugin[] - ): CanPromise { - if (extrasList && extrasList.length) { - try { - const needLoadExtras = extrasList.filter( - extra => !this._items.has(this.normalizeName(extra.name)) - ); - - if (needLoadExtras.length) { - return this.load(jodit, needLoadExtras); - } - } catch (e) { - // @ts-ignore - if (!isProd) { - throw e; - } - } - } - } + return { extrasList, disableList, filter }; } diff --git a/src/core/selection/select.ts b/src/core/selection/select.ts index d71db8987..3206610d8 100644 --- a/src/core/selection/select.ts +++ b/src/core/selection/select.ts @@ -897,22 +897,21 @@ export class Select implements ISelect { * * @param start - true - check whether the cursor is at the start block * @param parentBlock - Find in this + * @param fake - Node for cursor position * * @returns true - the cursor is at the end(start) block, null - cursor somewhere outside */ cursorInTheEdge( start: boolean, - parentBlock: HTMLElement + parentBlock: HTMLElement, + fake: Node | null = null ): Nullable { const end = !start, - range = this.sel?.getRangeAt(0), - current = this.current(false); + range = this.sel?.getRangeAt(0); - if ( - !range || - !current || - !Dom.isOrContains(parentBlock, current, true) - ) { + fake ??= this.current(false); + + if (!range || !fake || !Dom.isOrContains(parentBlock, fake, true)) { return null; } @@ -961,7 +960,7 @@ export class Select implements ISelect { } } - let next: Nullable = current; + let next: Nullable = fake; while (next && next !== parentBlock) { const nextOne = Dom.sibling(next, start); @@ -982,15 +981,21 @@ export class Select implements ISelect { /** * Wrapper for cursorInTheEdge */ - cursorOnTheLeft(parentBlock: HTMLElement): Nullable { - return this.cursorInTheEdge(true, parentBlock); + cursorOnTheLeft( + parentBlock: HTMLElement, + fake?: Node | null + ): Nullable { + return this.cursorInTheEdge(true, parentBlock, fake); } /** * Wrapper for cursorInTheEdge */ - cursorOnTheRight(parentBlock: HTMLElement): Nullable { - return this.cursorInTheEdge(false, parentBlock); + cursorOnTheRight( + parentBlock: HTMLElement, + fake?: Node | null + ): Nullable { + return this.cursorInTheEdge(false, parentBlock, fake); } /** @@ -1360,7 +1365,7 @@ export class Select implements ISelect { /** * Split selection on two parts: left and right */ - splitSelection(currentBox: HTMLElement): Nullable { + splitSelection(currentBox: HTMLElement, edge?: Node): Nullable { if (!this.isCollapsed()) { return null; } @@ -1370,16 +1375,20 @@ export class Select implements ISelect { leftRange.setStartBefore(currentBox); - const cursorOnTheRight = this.cursorOnTheRight(currentBox); - const cursorOnTheLeft = this.cursorOnTheLeft(currentBox); + const cursorOnTheRight = this.cursorOnTheRight(currentBox, edge); + const cursorOnTheLeft = this.cursorOnTheLeft(currentBox, edge); const br = this.j.createInside.element('br'), - prevFake = this.j.createInside.text(INVISIBLE_SPACE), + prevFake = this.j.createInside.fake(), nextFake = prevFake.cloneNode(); try { if (cursorOnTheRight || cursorOnTheLeft) { - Dom.safeInsertNode(range, br); + if (edge) { + Dom.before(edge, br); + } else { + Dom.safeInsertNode(range, br); + } const clearBR = ( start: Node, @@ -1428,21 +1437,21 @@ export class Select implements ISelect { node => Dom.isEmptyTextNode(node) && Dom.safeRemove(node) ); - if (currentBox.parentNode) { - try { - clearEmpties(fragment); - clearEmpties(currentBox); - currentBox.parentNode.insertBefore(fragment, currentBox); - - if (cursorOnTheRight && br?.parentNode) { - const range = this.createRange(); - range.setStartBefore(br); - this.selectRange(range); - } - } catch (e) { - if (!isProd) { - throw e; - } + assert(currentBox.parentNode, 'Splitting fails'); + + try { + clearEmpties(fragment); + clearEmpties(currentBox); + currentBox.parentNode.insertBefore(fragment, currentBox); + + if (!edge && cursorOnTheRight && br?.parentNode) { + const range = this.createRange(); + range.setStartBefore(br); + this.selectRange(range); + } + } catch (e) { + if (!isProd) { + throw e; } } diff --git a/src/jodit.ts b/src/jodit.ts index eb4460832..465c30f2b 100644 --- a/src/jodit.ts +++ b/src/jodit.ts @@ -1232,35 +1232,33 @@ export class Jodit extends ViewWithToolbar implements IJodit, Dlgs { callPromise(beforeInitHookResult, (): void => { this.e.fire('beforeInit', this); - const initPluginsResult = pluginSystem.init(this); + pluginSystem.__init(this); - callPromise(initPluginsResult, () => { - this.e.fire('afterPluginSystemInit', this); + this.e.fire('afterPluginSystemInit', this); - this.e.on('changePlace', () => { - this.setReadOnly(this.o.readonly); - this.setDisabled(this.o.disabled); - }); + this.e.on('changePlace', () => { + this.setReadOnly(this.o.readonly); + this.setDisabled(this.o.disabled); + }); - this.places.length = 0; - const addPlaceResult = this.addPlace(element, options); + this.places.length = 0; + const addPlaceResult = this.addPlace(element, options); - instances[this.id] = this; + instances[this.id] = this; - const init = (): void => { - if (this.e) { - this.e.fire('afterInit', this); - } + const init = (): void => { + if (this.e) { + this.e.fire('afterInit', this); + } - this.afterInitHook(); + this.afterInitHook(); - this.setStatus(STATUSES.ready); + this.setStatus(STATUSES.ready); - this.e.fire('afterConstructor', this); - }; + this.e.fire('afterConstructor', this); + }; - callPromise(addPlaceResult, init); - }); + callPromise(addPlaceResult, init); }); } diff --git a/src/modules/file-browser/file-browser.ts b/src/modules/file-browser/file-browser.ts index a0e4762a9..4697625ec 100644 --- a/src/modules/file-browser/file-browser.ts +++ b/src/modules/file-browser/file-browser.ts @@ -261,11 +261,15 @@ export class FileBrowser extends ViewWithToolbar implements IFileBrowser, Dlgs { } switch (btn) { + case 'filebrowser.upload': + return this.dataProvider.canI('FileUpload'); + case 'filebrowser.edit': return ( this.dataProvider.canI('ImageResize') || this.dataProvider.canI('ImageCrop') ); + case 'filebrowser.remove': return this.dataProvider.canI('FileRemove'); } diff --git a/src/modules/toolbar/collection/collection.ts b/src/modules/toolbar/collection/collection.ts index de748e02d..b828ea10a 100644 --- a/src/modules/toolbar/collection/collection.ts +++ b/src/modules/toolbar/collection/collection.ts @@ -41,8 +41,8 @@ export class ToolbarCollection return 'ToolbarCollection'; } - readonly listenEvents = - 'updateToolbar changeStack mousedown mouseup keydown change afterInit readonly afterResize ' + + private readonly __listenEvents = + 'updatePlugins updateToolbar changeStack mousedown mouseup keydown change afterInit readonly afterResize ' + 'selectionchange changeSelection focus afterSetMode touchstart focus blur'; /** @@ -82,7 +82,7 @@ export class ToolbarCollection } @autobind - immediateUpdate(): void { + private __immediateUpdate(): void { if (this.isDestructed || this.j.isLocked) { return; } @@ -93,7 +93,7 @@ export class ToolbarCollection } override update = this.j.async.debounce( - this.immediateUpdate, + this.__immediateUpdate, () => this.j.defaultTimeout ); @@ -109,14 +109,14 @@ export class ToolbarCollection constructor(jodit: IViewBased) { super(jodit as T); - this.initEvents(); + this.__initEvents(); this.__tooltip = UITooltip.make(jodit); } - private initEvents(): void { + private __initEvents(): void { this.j.e - .on(this.listenEvents, this.update) - .on('afterSetMode focus', this.immediateUpdate); + .on(this.__listenEvents, this.update) + .on('afterSetMode focus', this.__immediateUpdate); } hide(): void { @@ -158,8 +158,8 @@ export class ToolbarCollection this.__tooltip?.destruct(); this.j.e - .off(this.listenEvents, this.update) - .off('afterSetMode focus', this.immediateUpdate); + .off(this.__listenEvents, this.update) + .off('afterSetMode focus', this.__immediateUpdate); super.destruct(); } diff --git a/src/plugins/enter/enter.test.js b/src/plugins/enter/enter.test.js index 6c03541a0..7c777cbe3 100644 --- a/src/plugins/enter/enter.test.js +++ b/src/plugins/enter/enter.test.js @@ -527,14 +527,12 @@ describe('Enter behavior Tests', function () { describe('In the start', function () { it('should add new P element before blockquote', function () { const editor = getJodit({ - disablePlugins: ['paste-code'] + disablePlugins: ['paste-code'] // For PRO version }); - editor.value = '
test
'; + editor.value = '
|test
'; - editor.s - .createRange(true) - .setStart(editor.editor.firstChild.firstChild, 0); + setCursorToChar(editor); simulateEvent( 'keydown', @@ -547,8 +545,10 @@ describe('Enter behavior Tests', function () { editor.s.insertNode(editor.createInside.text('split ')); + replaceCursorToChar(editor); + expect(editor.value).equals( - '


split test
' + '


split |test
' ); }); }); @@ -935,8 +935,8 @@ describe('Enter behavior Tests', function () { simulateEvent('keydown', Jodit.KEY_ENTER, editor.editor); editor.s.insertNode(editor.createInside.text(' a ')); - - expect(editor.value).equals('Some text


a
'); + replaceCursorToChar(editor); + expect(editor.value).equals('Some text


a |
'); }); }); }); @@ -980,7 +980,7 @@ describe('Enter behavior Tests', function () { }); }); - describe.only('After table', function () { + describe('After table', function () { it('Should add P directly after table', function () { const editor = getJodit(); @@ -1007,12 +1007,17 @@ describe('Enter behavior Tests', function () { describe('Cases', () => { [ - ['test|', 'test
|
', { enter: 'br' }], + ['
|test
', '

|test
'], [ - '

test

table
|

pop

tost

', - '

test

table

|

pop

tost

', - { disablePlugins: ['WrapNodes'] } + '
|test
', + '


|test
', + undefined, + opt => { + opt.shiftKey = true; + } ], + ['
test

|
', '
test

|

'], + ['test|', 'test
|
', { enter: 'br' }], ['

test|

', '

test

|

'], [ '

test|

', @@ -1063,7 +1068,7 @@ describe('Enter behavior Tests', function () { '
  • 1
    • 2
  • |
', '
  • 1
    • 2

|

' ] - ].forEach(([source, result, options]) => { + ].forEach(([source, result, options, mod]) => { describe('For source: ' + source, () => { it('Should be result: ' + result, () => { const editor = getJodit(options); @@ -1072,7 +1077,8 @@ describe('Enter behavior Tests', function () { simulateEvent( 'keydown', Jodit.KEY_ENTER, - editor.editor + editor.editor, + mod ); replaceCursorToChar(editor); expect(editor.value).eq(result); diff --git a/src/plugins/enter/enter.ts b/src/plugins/enter/enter.ts index fbca14968..89f2a68f7 100644 --- a/src/plugins/enter/enter.ts +++ b/src/plugins/enter/enter.ts @@ -13,12 +13,7 @@ import type { IJodit } from 'jodit/types'; import { Dom } from 'jodit/core/dom/dom'; import { Plugin } from 'jodit/core/plugin/plugin'; -import { - INVISIBLE_SPACE, - BR, - PARAGRAPH, - KEY_ENTER -} from 'jodit/core/constants'; +import { BR, PARAGRAPH, KEY_ENTER } from 'jodit/core/constants'; import { watch } from 'jodit/core/decorators'; import { isBoolean } from 'jodit/core/helpers/checker/is-boolean'; @@ -86,44 +81,51 @@ export class enter extends Plugin { } private onEnter(event?: KeyboardEvent): false | void { - const jodit = this.j; + const { jodit } = this; - const current = this.getCurrentOrFillEmpty(jodit); + const fake = jodit.createInside.fake(); - moveCursorOutFromSpecialTags(jodit, current, ['a']); + try { + Dom.safeInsertNode(jodit.s.range, fake); - let currentBox = getBlockWrapper(jodit, current); + moveCursorOutFromSpecialTags(jodit, fake, ['a']); - const isLi = Dom.isTag(currentBox, 'li'); + let block = getBlockWrapper(fake, jodit); - // if use
defaultTag for break line or when was entered SHIFt key or in or or
- if ( - (!isLi || event?.shiftKey) && - !checkBR(jodit, current, event?.shiftKey) - ) { - return false; - } + const isLi = Dom.isTag(block, 'li'); - // wrap no wrapped element - if (!currentBox && !hasPreviousBlock(jodit, current)) { - currentBox = wrapText(jodit, current); - } + // if use
defaultTag for break line or when was entered SHIFt key or in or or
+ if ( + (!isLi || event?.shiftKey) && + checkBR(fake, jodit, event?.shiftKey) + ) { + return false; + } - if (!currentBox || currentBox === current) { - insertParagraph(jodit, null, isLi ? 'li' : jodit.o.enter); - return false; - } + // wrap no wrapped element + if (!block && !hasPreviousBlock(fake, jodit)) { + block = wrapText(fake, jodit); + } - if (!checkUnsplittableBox(jodit, currentBox)) { - return false; - } + if (!block) { + insertParagraph(fake, jodit, isLi ? 'li' : jodit.o.enter); + return false; + } - if (isLi && this.__isEmptyListLeaf(currentBox)) { - processEmptyLILeaf(jodit, currentBox); - return false; - } + if (!checkUnsplittableBox(fake, jodit, block)) { + return false; + } - splitFragment(jodit, currentBox); + if (isLi && this.__isEmptyListLeaf(block)) { + processEmptyLILeaf(fake, jodit, block); + return false; + } + + splitFragment(fake, jodit, block); + } finally { + fake.isConnected && jodit.s.setCursorBefore(fake); + Dom.safeRemove(fake); + } } private __isEmptyListLeaf(li: HTMLElement): boolean { @@ -131,19 +133,6 @@ export class enter extends Plugin { return isBoolean(result) ? result : Dom.isEmpty(li); } - private getCurrentOrFillEmpty(editor: IJodit): Node { - const { s } = editor; - let current = s.current(false); - - if (!current || current === editor.editor) { - current = editor.createInside.text(INVISIBLE_SPACE); - s.insertNode(current, false, false); - s.select(current); - } - - return current; - } - /** @override */ beforeDestruct(editor: IJodit): void { editor.e.off('keydown.enter'); diff --git a/src/plugins/enter/helpers/check-br.ts b/src/plugins/enter/helpers/check-br.ts index dbbb390c6..5241eb23e 100644 --- a/src/plugins/enter/helpers/check-br.ts +++ b/src/plugins/enter/helpers/check-br.ts @@ -18,14 +18,12 @@ import { BR } from 'jodit/core/constants'; * @private */ export function checkBR( + fake: Text, jodit: IJodit, - current: Node, shiftKeyPressed?: boolean ): boolean { - const isMultiLineBlock = Dom.closest( - current, - ['pre', 'blockquote'], - jodit.editor + const isMultiLineBlock = Boolean( + Dom.closest(fake, ['pre', 'blockquote'], jodit.editor) ); const isBRMode = jodit.o.enter.toLowerCase() === BR.toLowerCase(); @@ -36,22 +34,49 @@ export function checkBR( (shiftKeyPressed && !isMultiLineBlock) || (!shiftKeyPressed && isMultiLineBlock) ) { - const br = jodit.createInside.element('br'); + // 2 BR before + if (isMultiLineBlock && checkSeveralBR(fake)) { + return false; + } - jodit.s.insertNode(br, false, false); + const br = jodit.createInside.element('br'); + Dom.before(fake, br); if (!Dom.findNotEmptySibling(br, false)) { - Dom.after(br, br.cloneNode()); + const clone = br.cloneNode(); + Dom.after(br, clone); + Dom.before(clone, fake); } - const range = jodit.s.range; - range.setStartAfter(br); - range.collapse(true); - jodit.s.selectRange(range); scrollIntoViewIfNeeded(br, jodit.editor, jodit.ed); + return true; + } + + return false; +} + +function checkSeveralBR(fake: Text): boolean { + // 2 BR before + const preBr = brBefore(brBefore(fake)); + if (preBr) { + Dom.safeRemove(brBefore(fake)); + Dom.safeRemove(preBr); + return true; + } + + return false; +} + +function brBefore(start: Node | false): Node | false { + if (!start) { + return false; + } + + const prev = Dom.findSibling(start, true); + if (!prev || !Dom.isTag(prev, 'br')) { return false; } - return true; + return prev; } diff --git a/src/plugins/enter/helpers/check-unsplittable-box.ts b/src/plugins/enter/helpers/check-unsplittable-box.ts index 76e5e15aa..d843ae7f7 100644 --- a/src/plugins/enter/helpers/check-unsplittable-box.ts +++ b/src/plugins/enter/helpers/check-unsplittable-box.ts @@ -16,17 +16,12 @@ import { Dom } from 'jodit/core/dom/dom'; * @private */ export function checkUnsplittableBox( + fake: Text, jodit: IJodit, currentBox: HTMLElement ): boolean { - const sel = jodit.s; - if (!Dom.canSplitBlock(currentBox)) { - const br = jodit.createInside.element('br'); - - sel.insertNode(br, false, false); - sel.setCursorAfter(br); - + Dom.before(fake, jodit.createInside.element('br')); return false; } diff --git a/src/plugins/enter/helpers/get-block-wrapper.ts b/src/plugins/enter/helpers/get-block-wrapper.ts index e2f222049..02c1e15d9 100644 --- a/src/plugins/enter/helpers/get-block-wrapper.ts +++ b/src/plugins/enter/helpers/get-block-wrapper.ts @@ -17,11 +17,11 @@ import { Dom } from 'jodit/core/dom/dom'; * @private */ export function getBlockWrapper( + fake: Node | null, jodit: IJodit, - current: Node | null, tagReg = consts.IS_BLOCK ): Nullable { - let node = current; + let node: Node | null = fake; const root = jodit.editor; do { @@ -35,7 +35,7 @@ export function getBlockWrapper( } return ( - getBlockWrapper(jodit, node.parentNode, /^li$/i) || + getBlockWrapper(node.parentNode, jodit, /^li$/i) || (node as HTMLElement) ); } diff --git a/src/plugins/enter/helpers/has-previous-block.ts b/src/plugins/enter/helpers/has-previous-block.ts index 5744dba53..47cd22d16 100644 --- a/src/plugins/enter/helpers/has-previous-block.ts +++ b/src/plugins/enter/helpers/has-previous-block.ts @@ -14,10 +14,10 @@ import { Dom } from 'jodit/core/dom/dom'; /** * @private */ -export function hasPreviousBlock(jodit: IJodit, current: Node): boolean { +export function hasPreviousBlock(fake: Text, jodit: IJodit): boolean { return Boolean( Dom.prev( - current, + fake, elm => Dom.isBlock(elm) || Dom.isImage(elm), jodit.editor ) diff --git a/src/plugins/enter/helpers/insert-paragraph.ts b/src/plugins/enter/helpers/insert-paragraph.ts index fd7d44d68..da848cee6 100644 --- a/src/plugins/enter/helpers/insert-paragraph.ts +++ b/src/plugins/enter/helpers/insert-paragraph.ts @@ -8,7 +8,7 @@ * @module plugins/enter */ -import type { HTMLTagNames, IJodit, Nullable } from 'jodit/types'; +import type { HTMLTagNames, IJodit } from 'jodit/types'; import { Dom } from 'jodit/core/dom/dom'; import { scrollIntoViewIfNeeded } from 'jodit/core/helpers/utils/scroll-into-view'; @@ -17,33 +17,26 @@ import { scrollIntoViewIfNeeded } from 'jodit/core/helpers/utils/scroll-into-vie * @private */ export function insertParagraph( + fake: Text, editor: IJodit, - fake: Nullable, wrapperTag: HTMLTagNames, style?: CSSStyleDeclaration ): HTMLElement { - const { s, createInside } = editor, + const isBR = wrapperTag.toLowerCase() === 'br', + { createInside } = editor, p = createInside.element(wrapperTag), - helper_node = createInside.element('br'); + br = createInside.element('br'); - p.appendChild(helper_node); + if (!isBR) { + p.appendChild(br); + } if (style && style.cssText) { p.setAttribute('style', style.cssText); } - if (fake && fake.isConnected) { - Dom.before(fake, p); - Dom.safeRemove(fake); - } else { - s.insertNode(p, false, false); - } - - const range = s.createRange(); - range.setStartBefore(wrapperTag.toLowerCase() !== 'br' ? helper_node : p); - range.collapse(true); - s.sel?.removeAllRanges(); - s.sel?.addRange(range); + Dom.after(fake, p); + Dom.before(isBR ? p : br, fake); scrollIntoViewIfNeeded(p, editor.editor, editor.ed); diff --git a/src/plugins/enter/helpers/move-cursor-out-from-specal-tags.ts b/src/plugins/enter/helpers/move-cursor-out-from-specal-tags.ts index e382085d7..b14436969 100644 --- a/src/plugins/enter/helpers/move-cursor-out-from-specal-tags.ts +++ b/src/plugins/enter/helpers/move-cursor-out-from-specal-tags.ts @@ -8,7 +8,7 @@ * @module plugins/enter */ -import type { IJodit, Nullable, HTMLTagNames } from 'jodit/types'; +import type { IJodit, HTMLTagNames } from 'jodit/types'; import { Dom } from 'jodit/core/dom/dom'; /** @@ -16,17 +16,18 @@ import { Dom } from 'jodit/core/dom/dom'; */ export function moveCursorOutFromSpecialTags( jodit: IJodit, - current: Nullable, + fake: Text, tags: HTMLTagNames[] ): void { const { s } = jodit; - const link = Dom.closest(current, tags, jodit.editor); + const link = Dom.closest(fake, tags, jodit.editor); + if (link) { - if (s.cursorOnTheRight(link)) { - s.setCursorAfter(link); - } else if (s.cursorOnTheLeft(link)) { - s.setCursorBefore(link); + if (s.cursorOnTheRight(link, fake)) { + Dom.after(link, fake); + } else if (s.cursorOnTheLeft(link, fake)) { + Dom.before(link, fake); } } } diff --git a/src/plugins/enter/helpers/process-empty-li-leaf.ts b/src/plugins/enter/helpers/process-empty-li-leaf.ts index 96331aa36..ec2ab80ff 100644 --- a/src/plugins/enter/helpers/process-empty-li-leaf.ts +++ b/src/plugins/enter/helpers/process-empty-li-leaf.ts @@ -18,7 +18,11 @@ import { insertParagraph } from './insert-paragraph'; * Handles pressing the Enter key inside an empty LI inside a list * @private */ -export function processEmptyLILeaf(jodit: IJodit, li: HTMLElement): void { +export function processEmptyLILeaf( + fake: Text, + jodit: IJodit, + li: HTMLElement +): void { const list: Nullable = Dom.closest( li, ['ol', 'ul'], @@ -40,8 +44,7 @@ export function processEmptyLILeaf(jodit: IJodit, li: HTMLElement): void { leftRange.setEndAfter(list); const rightPart = leftRange.extractContents(); - const fakeTextNode = jodit.createInside.fake(); - Dom.after(container, fakeTextNode); + Dom.after(container, fake); Dom.safeRemove(li); @@ -50,8 +53,8 @@ export function processEmptyLILeaf(jodit: IJodit, li: HTMLElement): void { } const newLi = insertParagraph( + fake, jodit, - fakeTextNode, listInsideLeaf ? 'li' : jodit.o.enter ); diff --git a/src/plugins/enter/helpers/split-fragment.ts b/src/plugins/enter/helpers/split-fragment.ts index 5bac5c971..c76f47ed4 100644 --- a/src/plugins/enter/helpers/split-fragment.ts +++ b/src/plugins/enter/helpers/split-fragment.ts @@ -8,7 +8,7 @@ * @module plugins/enter */ -import type { IJodit, Nullable } from 'jodit/types'; +import type { IJodit } from 'jodit/types'; import { scrollIntoViewIfNeeded } from 'jodit/core/helpers/utils/scroll-into-view'; import { Dom } from 'jodit/core/dom/dom'; @@ -19,36 +19,38 @@ import { insertParagraph } from './insert-paragraph'; * and adds a new default block in the middle/start/end * @private */ -export function splitFragment(jodit: IJodit, currentBox: HTMLElement): void { +export function splitFragment( + fake: Text, + jodit: IJodit, + block: HTMLElement +): void { const sel = jodit.s, { enter } = jodit.o; const defaultTag = enter.toLowerCase() as typeof enter; - const isLi = Dom.isTag(currentBox, 'li'); - const canSplit = currentBox.tagName.toLowerCase() === defaultTag || isLi; + const isLi = Dom.isTag(block, 'li'); + const canSplit = block.tagName.toLowerCase() === defaultTag || isLi; - const cursorOnTheRight = sel.cursorOnTheRight(currentBox); - const cursorOnTheLeft = sel.cursorOnTheLeft(currentBox); + const cursorOnTheRight = sel.cursorOnTheRight(block, fake); + const cursorOnTheLeft = sel.cursorOnTheLeft(block, fake); if (!canSplit && (cursorOnTheRight || cursorOnTheLeft)) { - let fake: Nullable = null; - if (cursorOnTheRight) { - fake = sel.setCursorAfter(currentBox); + Dom.after(block, fake); } else { - fake = sel.setCursorBefore(currentBox); + Dom.before(block, fake); } - insertParagraph(jodit, fake, defaultTag); + insertParagraph(fake, jodit, defaultTag); if (cursorOnTheLeft && !cursorOnTheRight) { - sel.setCursorIn(currentBox, true); + Dom.prepend(block, fake); } return; } - const newP = sel.splitSelection(currentBox); + const newP = sel.splitSelection(block, fake); scrollIntoViewIfNeeded(newP, jodit.editor, jodit.ed); } diff --git a/src/plugins/enter/helpers/wrap-text.ts b/src/plugins/enter/helpers/wrap-text.ts index b6c12a199..875591f8f 100644 --- a/src/plugins/enter/helpers/wrap-text.ts +++ b/src/plugins/enter/helpers/wrap-text.ts @@ -16,8 +16,8 @@ import { Dom } from 'jodit/core/dom/dom'; * then we wrap all the nearest inline nodes in a container * @private */ -export function wrapText(jodit: IJodit, current: Node): HTMLElement { - let needWrap = current; +export function wrapText(fake: Text, jodit: IJodit): HTMLElement { + let needWrap: Node = fake; Dom.up( needWrap, @@ -32,10 +32,9 @@ export function wrapText(jodit: IJodit, current: Node): HTMLElement { const currentBox = Dom.wrapInline(needWrap, jodit.o.enter, jodit); if (Dom.isEmpty(currentBox)) { - const helper_node = jodit.createInside.element('br'); - - currentBox.appendChild(helper_node); - jodit.s.setCursorBefore(helper_node); + const br = jodit.createInside.element('br'); + currentBox.appendChild(br); + Dom.before(br, fake); } return currentBox; diff --git a/src/types/plugin.d.ts b/src/types/plugin.d.ts index 86a917b45..e748ae96c 100644 --- a/src/types/plugin.d.ts +++ b/src/types/plugin.d.ts @@ -57,5 +57,4 @@ export interface IPluginSystem { wait(name: string): Promise; get(name: string): PluginType | void; remove(name: string): void; - init(jodit: IJodit): CanPromise; } diff --git a/src/types/select.d.ts b/src/types/select.d.ts index b468f365d..35fa590ba 100644 --- a/src/types/select.d.ts +++ b/src/types/select.d.ts @@ -59,10 +59,17 @@ export interface ISelect { isCollapsed(): boolean; cursorInTheEdge( start: boolean, - parentBlock: HTMLElement + parentBlock: HTMLElement, + fake?: Node | null + ): Nullable; + cursorOnTheLeft( + parentBlock: HTMLElement, + fake?: Node | null + ): Nullable; + cursorOnTheRight( + parentBlock: HTMLElement, + fake?: Node | null ): Nullable; - cursorOnTheLeft(parentBlock: HTMLElement): Nullable; - cursorOnTheRight(parentBlock: HTMLElement): Nullable; expandSelection(): ISelect; insertCursorAtPoint(x: number, y: number): boolean; @@ -95,5 +102,5 @@ export interface ISelect { commitStyle(options: IStyleOptions): void; eachSelection(callback: (current: Node) => void): void; - splitSelection(currentBox: HTMLElement): Nullable; + splitSelection(currentBox: HTMLElement, edge?: Node): Nullable; } diff --git a/test/test.html b/test/test.html index d63ff14f0..f5a0a8f1d 100644 --- a/test/test.html +++ b/test/test.html @@ -33,7 +33,7 @@