diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e64ecb83..7ee68258 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 2.19.4 + +- `Improvements` - Vertical caret moving with UP or DOWN key [#857](https://github.com/codex-team/editor.js/issues/857). + +### 2.19.3 + +- `Fix` — Ignore error raised by Shortcut module + ### 2.19.2 - `New` - `toolbar.toggleBlockSettings()` API method added [#1442](https://github.com/codex-team/editor.js/issues/1421). @@ -8,13 +16,17 @@ - `Improvements` - Remove bundles from the repo [#1541](https://github.com/codex-team/editor.js/pull/1541). - `Improvements` - Document will be scrolled when blocks are selected with `SHIFT+UP` or `SHIFT+DOWN` [#1447](https://github.com/codex-team/editor.js/issues/1447) - `Improvements` - The caret will be set on editor copy/paste [#1470](https://github.com/codex-team/editor.js/pull/1470) -- `Improvements` - Vertical caret moving with UP or DOWN key [#857](https://github.com/codex-team/editor.js/issues/857). - `Improvements` - Added generic types to OutputBlockData [#1551](https://github.com/codex-team/editor.js/issues/1551). - `Fix` - Fix BlockManager.setCurrentBlockByChildNode() with multiple Editor.js instances [#1503](https://github.com/codex-team/editor.js/issues/1503). - `Fix` - Fix an unstable block cut process [#1489](https://github.com/codex-team/editor.js/issues/1489). - `Fix` - Type definition of the Sanitizer config: the sanitize function now contains param definition [#1491](https://github.com/codex-team/editor.js/pull/1491). - `Fix` - Fix unexpected behavior on an empty link pasting [#1348](https://github.com/codex-team/editor.js/issues/1348). +- `Fix` - Fix SanitizerConfig type definition [#1513](https://github.com/codex-team/editor.js/issues/1513) +- `Refactoring` - The Listeners module now is a util. +- `Refactoring` - The Events module now is a util. - `Fix` - Editor Config now immutable [#1552](https://github.com/codex-team/editor.js/issues/1552). +- `Refactoring` - Shortcuts module is util now. +- `Fix` - Fix bubbling on BlockManagers' listener [#1433](https://github.com/codex-team/editor.js/issues/1433). ### 2.19.1 diff --git a/example/example-multiple.html b/example/example-multiple.html index 3cdb64c1..15470420 100644 --- a/example/example-multiple.html +++ b/example/example-multiple.html @@ -42,6 +42,7 @@ + diff --git a/package.json b/package.json index cd523694..19aff379 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.19.2-rc.1", + "version": "2.19.3", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", diff --git a/src/codex.ts b/src/codex.ts index a258edf2..29dc4b8f 100644 --- a/src/codex.ts +++ b/src/codex.ts @@ -87,6 +87,7 @@ export default class EditorJS { if (_.isFunction(moduleInstance.destroy)) { moduleInstance.destroy(); } + moduleInstance.listeners.removeAll(); }); editor = null; diff --git a/src/components/__module.ts b/src/components/__module.ts index d55b5760..877ecd88 100644 --- a/src/components/__module.ts +++ b/src/components/__module.ts @@ -1,6 +1,8 @@ import { EditorModules } from '../types-internal/editor-modules'; import { EditorConfig } from '../../types'; import { ModuleConfig } from '../types-internal/module-config'; +import Listeners from './utils/listeners'; +import EventsDispatcher from './utils/events'; /** * The type of the Module generic. @@ -38,6 +40,16 @@ export default class Module { */ protected config: EditorConfig; + /** + * Editor event dispatcher class + */ + protected eventsDispatcher: EventsDispatcher; + + /** + * Util for bind/unbind DOM event listeners + */ + protected listeners: Listeners = new Listeners(); + /** * This object provides methods to push into set of listeners that being dropped when read-only mode is enabled */ @@ -56,10 +68,8 @@ export default class Module { handler: (event: Event) => void, options: boolean | AddEventListenerOptions = false ): void => { - const { Listeners } = this.Editor; - this.mutableListenerIds.push( - Listeners.on(element, eventType, handler, options) + this.listeners.on(element, eventType, handler, options) ); }, @@ -67,10 +77,8 @@ export default class Module { * Clears all mutable listeners */ clearAll: (): void => { - const { Listeners } = this.Editor; - for (const id of this.mutableListenerIds) { - Listeners.offById(id); + this.listeners.offById(id); } this.mutableListenerIds = []; @@ -84,14 +92,17 @@ export default class Module { /** * @class + * * @param {EditorConfig} config - Editor's config + * @param {EventsDispatcher} eventsDispatcher - Editor's event dispatcher */ - constructor({ config }: ModuleConfig) { + constructor({ config, eventsDispatcher }: ModuleConfig) { if (new.target === Module) { throw new TypeError('Constructors for abstract class Module are not allowed.'); } this.config = config; + this.eventsDispatcher = eventsDispatcher; } /** diff --git a/src/components/core.ts b/src/components/core.ts index ff9c4e71..8ecd12c0 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -4,6 +4,7 @@ import { EditorConfig, SanitizerConfig } from '../../types'; import { EditorModules } from '../types-internal/editor-modules'; import I18n from './i18n'; import { CriticalError } from './errors/critical'; +import EventsDispatcher from './utils/events'; /** * @typedef {Core} Core - editor core class @@ -53,6 +54,11 @@ export default class Core { */ public isReady: Promise; + /** + * Event Dispatcher util + */ + private eventsDispatcher: EventsDispatcher = new EventsDispatcher(); + /** * @param {EditorConfig} config - user configuration * @@ -338,6 +344,7 @@ export default class Core { */ this.moduleInstances[Module.displayName] = new Module({ config: this.configuration, + eventsDispatcher: this.eventsDispatcher, }); } catch (e) { _.log(`Module ${Module.displayName} skipped because`, 'warn', e); diff --git a/src/components/modules/api/events.ts b/src/components/modules/api/events.ts index 2cbf88c1..865b913e 100644 --- a/src/components/modules/api/events.ts +++ b/src/components/modules/api/events.ts @@ -1,5 +1,5 @@ -import { Events } from '../../../../types/api'; import Module from '../../__module'; +import { Events } from '../../../../types/api'; /** * @class EventsAPI @@ -26,7 +26,7 @@ export default class EventsAPI extends Module { * @param {Function} callback - event handler */ public on(eventName, callback): void { - this.Editor.Events.on(eventName, callback); + this.eventsDispatcher.on(eventName, callback); } /** @@ -36,7 +36,7 @@ export default class EventsAPI extends Module { * @param {object} data - event's data */ public emit(eventName, data): void { - this.Editor.Events.emit(eventName, data); + this.eventsDispatcher.emit(eventName, data); } /** @@ -46,6 +46,6 @@ export default class EventsAPI extends Module { * @param {Function} callback - event handler */ public off(eventName, callback): void { - this.Editor.Events.off(eventName, callback); + this.eventsDispatcher.off(eventName, callback); } } diff --git a/src/components/modules/api/listeners.ts b/src/components/modules/api/listeners.ts index 0ec283bc..a3b403a3 100644 --- a/src/components/modules/api/listeners.ts +++ b/src/components/modules/api/listeners.ts @@ -27,7 +27,7 @@ export default class ListenersAPI extends Module { * @param {boolean} useCapture - capture event or not */ public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void { - this.Editor.Listeners.on(element, eventType, handler, useCapture); + this.listeners.on(element, eventType, handler, useCapture); } /** @@ -39,6 +39,6 @@ export default class ListenersAPI extends Module { * @param {boolean} useCapture - capture event or not */ public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void { - this.Editor.Listeners.off(element, eventType, handler, useCapture); + this.listeners.off(element, eventType, handler, useCapture); } } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 30d392b2..77c1447f 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -184,7 +184,7 @@ export default class BlockManager extends Module { }); /** Copy event */ - this.Editor.Listeners.on( + this.listeners.on( document, 'copy', (e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandC(e) @@ -719,7 +719,7 @@ export default class BlockManager extends Module { this.readOnlyMutableListeners.on(block.holder, 'keydown', (event: KeyboardEvent) => { BlockEvents.keydown(event); - }, true); + }); this.readOnlyMutableListeners.on(block.holder, 'keyup', (event: KeyboardEvent) => { BlockEvents.keyup(event); diff --git a/src/components/modules/blockSelection.ts b/src/components/modules/blockSelection.ts index 8f437785..1c05d239 100644 --- a/src/components/modules/blockSelection.ts +++ b/src/components/modules/blockSelection.ts @@ -9,6 +9,7 @@ import Module from '../__module'; import Block from '../block'; import * as _ from '../utils'; import $ from '../dom'; +import Shortcuts from '../utils/shortcuts'; import SelectionUtils from '../selection'; import { SanitizerConfig } from '../../../types/configs'; @@ -145,8 +146,6 @@ export default class BlockSelection extends Module { * to select all and copy them */ public prepare(): void { - const { Shortcuts } = this.Editor; - this.selection = new SelectionUtils(); /** @@ -181,6 +180,7 @@ export default class BlockSelection extends Module { this.handleCommandA(event); }, + on: this.Editor.UI.nodes.redactor, }); } @@ -361,10 +361,8 @@ export default class BlockSelection extends Module { * De-registers Shortcut CMD+A */ public destroy(): void { - const { Shortcuts } = this.Editor; - /** Selection shortcut */ - Shortcuts.remove('CMD+A'); + Shortcuts.remove(this.Editor.UI.nodes.redactor, 'CMD+A'); } /** diff --git a/src/components/modules/crossBlockSelection.ts b/src/components/modules/crossBlockSelection.ts index 76d31c67..fa8c4962 100644 --- a/src/components/modules/crossBlockSelection.ts +++ b/src/components/modules/crossBlockSelection.ts @@ -23,9 +23,7 @@ export default class CrossBlockSelection extends Module { * @returns {Promise} */ public async prepare(): Promise { - const { Listeners } = this.Editor; - - Listeners.on(document, 'mousedown', (event: MouseEvent) => { + this.listeners.on(document, 'mousedown', (event: MouseEvent) => { this.enableCrossBlockSelection(event); }); } @@ -40,13 +38,13 @@ export default class CrossBlockSelection extends Module { return; } - const { BlockManager, Listeners } = this.Editor; + const { BlockManager } = this.Editor; this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement); this.lastSelectedBlock = this.firstSelectedBlock; - Listeners.on(document, 'mouseover', this.onMouseOver); - Listeners.on(document, 'mouseup', this.onMouseUp); + this.listeners.on(document, 'mouseover', this.onMouseOver); + this.listeners.on(document, 'mouseup', this.onMouseUp); } /** @@ -176,10 +174,8 @@ export default class CrossBlockSelection extends Module { * Removes the listeners */ private onMouseUp = (): void => { - const { Listeners } = this.Editor; - - Listeners.off(document, 'mouseover', this.onMouseOver); - Listeners.off(document, 'mouseup', this.onMouseUp); + this.listeners.off(document, 'mouseover', this.onMouseOver); + this.listeners.off(document, 'mouseup', this.onMouseUp); } /** diff --git a/src/components/modules/modificationsObserver.ts b/src/components/modules/modificationsObserver.ts index c22f0b81..30f5797c 100644 --- a/src/components/modules/modificationsObserver.ts +++ b/src/components/modules/modificationsObserver.ts @@ -58,7 +58,7 @@ export default class ModificationsObserver extends Module { this.observer.disconnect(); } this.observer = null; - this.nativeInputs.forEach((input) => this.Editor.Listeners.off(input, 'input', this.mutationDebouncer)); + this.nativeInputs.forEach((input) => this.listeners.off(input, 'input', this.mutationDebouncer)); this.mutationDebouncer = null; } @@ -163,13 +163,13 @@ export default class ModificationsObserver extends Module { private updateNativeInputs(): void { if (this.nativeInputs) { this.nativeInputs.forEach((input) => { - this.Editor.Listeners.off(input, 'input'); + this.listeners.off(input, 'input'); }); } this.nativeInputs = Array.from(this.Editor.UI.nodes.redactor.querySelectorAll('textarea, input, select')); - this.nativeInputs.forEach((input) => this.Editor.Listeners.on(input, 'input', this.mutationDebouncer)); + this.nativeInputs.forEach((input) => this.listeners.on(input, 'input', this.mutationDebouncer)); } /** diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index 98f4530f..884cbdba 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -263,18 +263,14 @@ export default class Paste extends Module { * Set onPaste callback handler */ private setCallback(): void { - const { Listeners } = this.Editor; - - Listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent); + this.listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent); } /** * Unset onPaste callback handler */ private unsetCallback(): void { - const { Listeners } = this.Editor; - - Listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent); + this.listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent); } /** diff --git a/src/components/modules/rectangleSelection.ts b/src/components/modules/rectangleSelection.ts index 7f0e11d6..8b2ea151 100644 --- a/src/components/modules/rectangleSelection.ts +++ b/src/components/modules/rectangleSelection.ts @@ -179,26 +179,25 @@ export default class RectangleSelection extends Module { * Sets Module necessary event handlers */ private enableModuleBindings(): void { - const { Listeners } = this.Editor; const { container } = this.genHTML(); - Listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => { + this.listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => { this.processMouseDown(mouseEvent); }, false); - Listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => { + this.listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => { this.processMouseMove(mouseEvent); }, false); - Listeners.on(document.body, 'mouseleave', () => { + this.listeners.on(document.body, 'mouseleave', () => { this.processMouseLeave(); }); - Listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => { + this.listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => { this.processScroll(mouseEvent); }, false); - Listeners.on(document.body, 'mouseup', () => { + this.listeners.on(document.body, 'mouseup', () => { this.processMouseUp(); }, false); } diff --git a/src/components/modules/shortcuts.ts b/src/components/modules/shortcuts.ts deleted file mode 100644 index d790ca57..00000000 --- a/src/components/modules/shortcuts.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Shortcut from '@codexteam/shortcuts'; - -/** - * Contains keyboard and mouse events binded on each Block by Block Manager - */ -import Module from '../__module'; - -/** - * ShortcutData interface - * Each shortcut must have name and handler - * `name` is a shortcut, like 'CMD+K', 'CMD+B' etc - * `handler` is a callback - * - * @interface ShortcutData - */ -export interface ShortcutData { - - /** - * Shortcut name - * Ex. CMD+I, CMD+B .... - */ - name: string; - - /** - * Shortcut handler - */ - handler(event): void; -} - -/** - * @class Shortcut - * @classdesc Allows to register new shortcut - * - * Internal Shortcuts Module - */ -export default class Shortcuts extends Module { - /** - * All registered shortcuts - * - * @type {Shortcut[]} - */ - private registeredShortcuts: Shortcut[] = []; - - /** - * Register shortcut - * - * @param {ShortcutData} shortcut - shortcut options - */ - public add(shortcut: ShortcutData): void { - const newShortcut = new Shortcut({ - name: shortcut.name, - on: this.Editor.UI.nodes.redactor, - callback: shortcut.handler, - }); - - this.registeredShortcuts.push(newShortcut); - } - - /** - * Remove shortcut - * - * @param {string} shortcut - shortcut name - */ - public remove(shortcut: string): void { - const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut); - - if (index === -1 || !this.registeredShortcuts[index]) { - return; - } - - this.registeredShortcuts[index].remove(); - this.registeredShortcuts.splice(index, 1); - } -} diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 203a9233..a3a61c54 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -147,7 +147,7 @@ export default class BlockSettings extends Module { this.addDefaultSettings(); /** Tell to subscribers that block settings is opened */ - this.Editor.Events.emit(this.events.opened); + this.eventsDispatcher.emit(this.events.opened); this.flipper.activate(this.blockTunesButtons); } @@ -183,7 +183,7 @@ export default class BlockSettings extends Module { this.nodes.defaultSettings.innerHTML = ''; /** Tell to subscribers that block settings is closed */ - this.Editor.Events.emit(this.events.closed); + this.eventsDispatcher.emit(this.events.closed); /** Clear cached buttons */ this.buttons = []; diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 49cf0ea6..a258bf2d 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -325,7 +325,7 @@ export default class ConversionToolbar extends Module { $.append(this.nodes.tools, tool); this.tools[toolName] = tool; - this.Editor.Listeners.on(tool, 'click', async () => { + this.listeners.on(tool, 'click', async () => { await this.replaceWithBlock(toolName); }); } diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index cfd9018c..79630309 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -6,6 +6,8 @@ import { InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings } import Flipper from '../../flipper'; import I18n from '../../i18n'; import { I18nInternalNS } from '../../i18n/namespace-internal'; +import Shortcuts from '../../utils/shortcuts'; +import { EditorModules } from '../../../types-internal/editor-modules'; /** * Inline Toolbar elements @@ -87,6 +89,38 @@ export default class InlineToolbar extends Module { */ private flipper: Flipper = null; + /** + * Internal inline tools: Link, Bold, Italic + */ + private internalTools: {[name: string]: InlineToolConstructable} = {}; + + /** + * Editor modules setter + * + * @param {EditorModules} Editor - Editor's Modules + */ + public set state(Editor: EditorModules) { + this.Editor = Editor; + + const { Tools } = Editor; + + /** + * Set internal inline tools + */ + Object + .entries(Tools.internalTools) + .filter(([, toolClass]: [string, ToolConstructable | ToolSettings]) => { + if (_.isFunction(toolClass)) { + return toolClass[Tools.INTERNAL_SETTINGS.IS_INLINE]; + } + + return (toolClass as ToolSettings).class[Tools.INTERNAL_SETTINGS.IS_INLINE]; + }) + .map(([name, toolClass]: [string, InlineToolConstructable | ToolSettings]) => { + this.internalTools[name] = _.isFunction(toolClass) ? toolClass : (toolClass as ToolSettings).class; + }); + } + /** * Toggles read-only mode * @@ -185,7 +219,13 @@ export default class InlineToolbar extends Module { } this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed); - this.toolsInstances.forEach((toolInstance) => { + Array.from(this.toolsInstances.entries()).forEach(([name, toolInstance]) => { + const shortcut = this.getToolShortcut(name); + + if (shortcut) { + Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut); + } + /** * @todo replace 'clear' with 'destroy' */ @@ -357,7 +397,7 @@ export default class InlineToolbar extends Module { this.nodes.actions = $.make('div', this.CSS.actionsWrapper); // To prevent reset of a selection when click on the wrapper - this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => { + this.listeners.on(this.nodes.wrapper, 'mousedown', (event) => { const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`); // If click is on actions wrapper, @@ -479,7 +519,7 @@ export default class InlineToolbar extends Module { this.nodes.togglerAndButtonsWrapper.appendChild(this.nodes.conversionToggler); - this.Editor.Listeners.on(this.nodes.conversionToggler, 'click', () => { + this.listeners.on(this.nodes.conversionToggler, 'click', () => { this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => { /** * When ConversionToolbar is opening on activated InlineToolbar flipper @@ -594,7 +634,6 @@ export default class InlineToolbar extends Module { */ private addTool(toolName: string, tool: InlineTool): void { const { - Listeners, Tools, Tooltip, } = this.Editor; @@ -617,48 +656,17 @@ export default class InlineToolbar extends Module { this.nodes.actions.appendChild(actions); } - Listeners.on(button, 'click', (event) => { + this.listeners.on(button, 'click', (event) => { this.toolClicked(tool); event.preventDefault(); }); - /** - * Enable shortcuts - * Ignore tool that doesn't have shortcut or empty string - */ - const toolSettings = Tools.getToolSettings(toolName); - - let shortcut = null; - - /** - * Get internal inline tools - */ - const internalTools: string[] = Object - .entries(Tools.internalTools) - .filter(([, toolClass]: [string, ToolConstructable | ToolSettings]) => { - if (_.isFunction(toolClass)) { - return toolClass[Tools.INTERNAL_SETTINGS.IS_INLINE]; - } - - return (toolClass as ToolSettings).class[Tools.INTERNAL_SETTINGS.IS_INLINE]; - }) - .map(([ name ]: [string, InlineToolConstructable | ToolSettings]) => name); - - /** - * 1) For internal tools, check public getter 'shortcut' - * 2) For external tools, check tool's settings - * 3) If shortcut is not set in settings, check Tool's public property - */ - if (internalTools.includes(toolName)) { - shortcut = this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT]; - } else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) { - shortcut = toolSettings[Tools.USER_SETTINGS.SHORTCUT]; - } else if (tool.shortcut) { - shortcut = tool.shortcut; - } + const shortcut = this.getToolShortcut(toolName); if (shortcut) { - this.enableShortcuts(tool, shortcut); + try { + this.enableShortcuts(tool, shortcut); + } catch (e) {} } /** @@ -684,6 +692,35 @@ export default class InlineToolbar extends Module { }); } + /** + * Get shortcut name for tool + * + * @param toolName — Tool name + */ + private getToolShortcut(toolName): string | void { + const { Tools } = this.Editor; + + /** + * Enable shortcuts + * Ignore tool that doesn't have shortcut or empty string + */ + const toolSettings = Tools.getToolSettings(toolName); + const tool = this.toolsInstances.get(toolName); + + /** + * 1) For internal tools, check public getter 'shortcut' + * 2) For external tools, check tool's settings + * 3) If shortcut is not set in settings, check Tool's public property + */ + if (Object.keys(this.internalTools).includes(toolName)) { + return this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT]; + } else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) { + return toolSettings[Tools.USER_SETTINGS.SHORTCUT]; + } else if (tool.shortcut) { + return tool.shortcut; + } + } + /** * Enable Tool shortcut with Editor Shortcuts Module * @@ -691,7 +728,7 @@ export default class InlineToolbar extends Module { * @param {string} shortcut - shortcut according to the ShortcutData Module format */ private enableShortcuts(tool: InlineTool, shortcut: string): void { - this.Editor.Shortcuts.add({ + Shortcuts.add({ name: shortcut, handler: (event) => { const { currentBlock } = this.Editor.BlockManager; @@ -719,6 +756,7 @@ export default class InlineToolbar extends Module { event.preventDefault(); this.toolClicked(tool); }, + on: this.Editor.UI.nodes.redactor, }); } diff --git a/src/components/modules/toolbar/toolbox.ts b/src/components/modules/toolbar/toolbox.ts index 54ed807a..add3e9e7 100644 --- a/src/components/modules/toolbar/toolbox.ts +++ b/src/components/modules/toolbar/toolbox.ts @@ -6,6 +6,7 @@ import Flipper from '../../flipper'; import { BlockToolAPI } from '../../block'; import I18n from '../../i18n'; import { I18nInternalNS } from '../../i18n/namespace-internal'; +import Shortcuts from '../../utils/shortcuts'; /** * HTMLElements used for Toolbox UI @@ -225,7 +226,7 @@ export default class Toolbox extends Module { /** * Add click listener */ - this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { + this.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { this.toolButtonActivate(event, toolName); }); @@ -306,12 +307,13 @@ export default class Toolbox extends Module { * @param {string} shortcut - shortcut according to the ShortcutData Module format */ private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string): void { - this.Editor.Shortcuts.add({ + Shortcuts.add({ name: shortcut, handler: (event: KeyboardEvent) => { event.preventDefault(); this.insertNewBlock(tool, toolName); }, + on: this.Editor.UI.nodes.redactor, }); } @@ -327,7 +329,7 @@ export default class Toolbox extends Module { const shortcut = this.getToolShortcut(toolName, tools[toolName]); if (shortcut) { - this.Editor.Shortcuts.remove(shortcut); + Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut); } } } diff --git a/src/components/modules/tools.ts b/src/components/modules/tools.ts index b2531c79..ddcf66b1 100644 --- a/src/components/modules/tools.ts +++ b/src/components/modules/tools.ts @@ -14,6 +14,8 @@ import BoldInlineTool from '../inline-tools/inline-tool-bold'; import ItalicInlineTool from '../inline-tools/inline-tool-italic'; import LinkInlineTool from '../inline-tools/inline-tool-link'; import Stub from '../tools/stub'; +import { ModuleConfig } from '../../types-internal/module-config'; +import EventsDispatcher from '../utils/events'; /** * @module Editor.js Tools Submodule @@ -198,9 +200,13 @@ export default class Tools extends Module { * @class * * @param {EditorConfig} config - Editor's configuration + * @param {EventsDispatcher} eventsDispatcher - Editor's event dispatcher */ - constructor({ config }) { - super({ config }); + constructor({ config, eventsDispatcher }: ModuleConfig) { + super({ + config, + eventsDispatcher, + }); this.toolsClasses = {}; diff --git a/src/components/modules/tooltip.ts b/src/components/modules/tooltip.ts index cc50740a..f3ff6711 100644 --- a/src/components/modules/tooltip.ts +++ b/src/components/modules/tooltip.ts @@ -5,7 +5,6 @@ import Module from '../__module'; * Use external module CodeX Tooltip */ import CodeXTooltips, { TooltipContent, TooltipOptions } from 'codex-tooltip'; -import { ModuleConfig } from '../../types-internal/module-config'; /** * Tooltip @@ -20,14 +19,6 @@ export default class Tooltip extends Module { */ private lib: CodeXTooltips = new CodeXTooltips(); - /** - * @class - * @param {EditorConfig} - Editor's config - */ - constructor({ config }: ModuleConfig) { - super({ config }); - } - /** * Shows tooltip on element with passed HTML content * diff --git a/src/components/selection.ts b/src/components/selection.ts index 4a7357de..32c12ee0 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -1,5 +1,5 @@ /** - * TextRange interface fot IE9- + * TextRange interface for IE9- */ import * as _ from './utils'; import $ from './dom'; diff --git a/src/components/modules/events.ts b/src/components/utils/events.ts similarity index 94% rename from src/components/modules/events.ts rename to src/components/utils/events.ts index 36f4185f..10d501ec 100644 --- a/src/components/modules/events.ts +++ b/src/components/utils/events.ts @@ -1,19 +1,17 @@ -import Module from '../__module'; - /** - * @module eventDispatcher + * @class EventDispatcher * * Has two important methods: * - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one * - {Function} emit - fires all subscribers with data - * - {Function off - unsubsribes callback + * - {Function off - unsubscribes callback * * @version 1.0.0 * * @typedef {Events} Events * @property {object} subscribers - all subscribers grouped by event name */ -export default class Events extends Module { +export default class EventsDispatcher { /** * Object with events` names as key and array of callback functions as value * diff --git a/src/components/modules/listeners.ts b/src/components/utils/listeners.ts similarity index 96% rename from src/components/modules/listeners.ts rename to src/components/utils/listeners.ts index 663268da..e0e35609 100644 --- a/src/components/modules/listeners.ts +++ b/src/components/utils/listeners.ts @@ -1,4 +1,3 @@ -import Module from '../__module'; import * as _ from '../utils'; /** @@ -36,11 +35,9 @@ export interface ListenerData { } /** - * Editor.js Listeners module + * Editor.js Listeners helper * - * @module Listeners - * - * Module-decorator for event listeners assignment + * Decorator for event listeners assignment * * @author Codex Team * @version 2.0.0 @@ -50,7 +47,7 @@ export interface ListenerData { * @typedef {Listeners} Listeners * @property {ListenerData[]} allListeners - listeners store */ -export default class Listeners extends Module { +export default class Listeners { /** * Stores all listeners data to find/remove/process it * @@ -114,7 +111,7 @@ export default class Listeners extends Module { existingListeners.forEach((listener, i) => { const index = this.allListeners.indexOf(existingListeners[i]); - if (index > 0) { + if (index > -1) { this.allListeners.splice(index, 1); listener.element.removeEventListener(listener.eventType, listener.handler, listener.options); diff --git a/src/components/utils/shortcuts.ts b/src/components/utils/shortcuts.ts new file mode 100644 index 00000000..3e45210f --- /dev/null +++ b/src/components/utils/shortcuts.ts @@ -0,0 +1,107 @@ +import Shortcut from '@codexteam/shortcuts'; + +/** + * Contains keyboard and mouse events binded on each Block by Block Manager + */ + +/** + * ShortcutData interface + * Each shortcut must have name and handler + * `name` is a shortcut, like 'CMD+K', 'CMD+B' etc + * `handler` is a callback + * + * @interface ShortcutData + */ +export interface ShortcutData { + + /** + * Shortcut name + * Ex. CMD+I, CMD+B .... + */ + name: string; + + /** + * Shortcut handler + */ + handler(event): void; + + /** + * Element handler should be added for + */ + on: HTMLElement; +} + +/** + * @class Shortcut + * @classdesc Allows to register new shortcut + * + * Internal Shortcuts Module + */ +class Shortcuts { + /** + * All registered shortcuts + * + * @type {Map} + */ + private registeredShortcuts: Map = new Map(); + + /** + * Register shortcut + * + * @param shortcut - shortcut options + */ + public add(shortcut: ShortcutData): void { + const foundShortcut = this.findShortcut(shortcut.on, shortcut.name); + + if (foundShortcut) { + throw Error( + `Shortcut ${shortcut.name} is already registered for ${shortcut.on}. Please remove it before add a new handler.` + ); + } + + const newShortcut = new Shortcut({ + name: shortcut.name, + on: shortcut.on, + callback: shortcut.handler, + }); + const shortcuts = this.registeredShortcuts.get(shortcut.on) || []; + + this.registeredShortcuts.set(shortcut.on, [...shortcuts, newShortcut]); + } + + /** + * Remove shortcut + * + * @param element - Element shortcut is set for + * @param name - shortcut name + */ + public remove(element: Element, name: string): void { + const shortcut = this.findShortcut(element, name); + + if (!shortcut) { + return; + } + + shortcut.remove(); + + const shortcuts = this.registeredShortcuts.get(element); + + this.registeredShortcuts.set(element, shortcuts.filter(el => el !== shortcut)); + } + + /** + * Get Shortcut instance if exist + * + * @param element - Element shorcut is set for + * @param shortcut - shortcut name + * + * @returns {number} index - shortcut index if exist + */ + private findShortcut(element: Element, shortcut: string): Shortcut | void { + const shortcuts = this.registeredShortcuts.get(element) || []; + + return shortcuts.find(({ name }) => name === shortcut); + } +} + +export default new Shortcuts(); diff --git a/src/types-internal/editor-modules.d.ts b/src/types-internal/editor-modules.d.ts index 09b98015..14ad7e30 100644 --- a/src/types-internal/editor-modules.d.ts +++ b/src/types-internal/editor-modules.d.ts @@ -1,12 +1,9 @@ import UI from '../components/modules/ui'; import BlockEvents from '../components/modules/blockEvents'; -import Listeners from '../components/modules/listeners'; import Toolbar from '../components/modules/toolbar/index'; import InlineToolbar from '../components/modules/toolbar/inline'; import Toolbox from '../components/modules/toolbar/toolbox'; import BlockSettings from '../components/modules/toolbar/blockSettings'; -import Events from '../components/modules/events'; -import Shortcuts from '../components/modules/shortcuts'; import Paste from '../components/modules/paste'; import Notifier from '../components/modules/notifier'; import Tooltip from '../components/modules/tooltip'; @@ -44,14 +41,11 @@ export interface EditorModules { BlockEvents: BlockEvents; BlockSelection: BlockSelection; RectangleSelection: RectangleSelection; - Listeners: Listeners; Toolbar: Toolbar; InlineToolbar: InlineToolbar; Toolbox: Toolbox; BlockSettings: BlockSettings; ConversionToolbar: ConversionToolbar; - Events: Events; - Shortcuts: Shortcuts; Paste: Paste; DragNDrop: DragNDrop; ModificationsObserver: ModificationsObserver; diff --git a/src/types-internal/html-janitor.d.ts b/src/types-internal/html-janitor.d.ts index 6f9cc035..66f1c6a3 100644 --- a/src/types-internal/html-janitor.d.ts +++ b/src/types-internal/html-janitor.d.ts @@ -3,9 +3,15 @@ * After that we can use it at the TS modules */ declare module 'html-janitor' { + /** + * Sanitizer config of each HTML element + * @see {@link https://github.com/guardian/html-janitor#options} + */ + type TagConfig = boolean | { [attr: string]: boolean | string }; + interface Config { tags: { - [key: string]: boolean|{[attr: string]: boolean|string}|(() => any) + [key: string]: TagConfig | ((el: Element) => TagConfig) }; } diff --git a/src/types-internal/module-config.d.ts b/src/types-internal/module-config.d.ts index f0e224c6..19539585 100644 --- a/src/types-internal/module-config.d.ts +++ b/src/types-internal/module-config.d.ts @@ -1,8 +1,10 @@ -import {EditorConfig} from '../../types/index'; +import { EditorConfig } from '../../types/index'; +import EventsDispatcher from '../components/utils/events'; /** * Describes object passed to Editor modules constructor */ export interface ModuleConfig { config: EditorConfig; + eventsDispatcher: EventsDispatcher; } diff --git a/types/configs/sanitizer-config.d.ts b/types/configs/sanitizer-config.d.ts index dad68927..e15df7f3 100644 --- a/types/configs/sanitizer-config.d.ts +++ b/types/configs/sanitizer-config.d.ts @@ -1,3 +1,9 @@ +/** + * Sanitizer config of each HTML element + * @see {@link https://github.com/guardian/html-janitor#options} + */ +type TagConfig = boolean | { [attr: string]: boolean | string }; + export interface SanitizerConfig { /** * Tag name and params not to be stripped off @@ -31,5 +37,5 @@ export interface SanitizerConfig { * } * } */ - [key: string]: boolean|{[attr: string]: boolean|string}|((el?: Element) => any); + [key: string]: TagConfig | ((el: Element) => TagConfig); } diff --git a/types/index.d.ts b/types/index.d.ts index aa094730..07fe5e0d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -104,8 +104,6 @@ declare class EditorJS { public blocks: Blocks; public caret: Caret; - public events: Events; - public listeners: Listeners; public sanitizer: Sanitizer; public saver: Saver; public selection: Selection;