refactoring(modules): the listeners module now a util (#1572)

* listeners module goes util

* fix listeners 'off' bug

* improve garbage collecting
This commit is contained in:
Peter Savchenko 2021-03-02 16:25:52 +03:00 committed by GitHub
parent 3bf30f0c1e
commit 2759b25d35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 38 additions and 49 deletions

View file

@ -13,6 +13,7 @@
- `Fix` - Fix an unstable block cut process [#1489](https://github.com/codex-team/editor.js/issues/1489). - `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` - 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 unexpected behavior on an empty link pasting [#1348](https://github.com/codex-team/editor.js/issues/1348).
- `Refactoring` - The Listeners module now is a util.
- `Fix` - Editor Config now immutable [#1552](https://github.com/codex-team/editor.js/issues/1552). - `Fix` - Editor Config now immutable [#1552](https://github.com/codex-team/editor.js/issues/1552).
### 2.19.1 ### 2.19.1

View file

@ -87,6 +87,7 @@ export default class EditorJS {
if (_.isFunction(moduleInstance.destroy)) { if (_.isFunction(moduleInstance.destroy)) {
moduleInstance.destroy(); moduleInstance.destroy();
} }
moduleInstance.listeners.removeAll();
}); });
editor = null; editor = null;

View file

@ -1,6 +1,7 @@
import { EditorModules } from '../types-internal/editor-modules'; import { EditorModules } from '../types-internal/editor-modules';
import { EditorConfig } from '../../types'; import { EditorConfig } from '../../types';
import { ModuleConfig } from '../types-internal/module-config'; import { ModuleConfig } from '../types-internal/module-config';
import Listeners from './utils/listeners';
/** /**
* The type <T> of the Module generic. * The type <T> of the Module generic.
@ -38,6 +39,11 @@ export default class Module<T extends ModuleNodes = {}> {
*/ */
protected config: EditorConfig; protected config: EditorConfig;
/**
* 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 * This object provides methods to push into set of listeners that being dropped when read-only mode is enabled
*/ */
@ -56,10 +62,8 @@ export default class Module<T extends ModuleNodes = {}> {
handler: (event: Event) => void, handler: (event: Event) => void,
options: boolean | AddEventListenerOptions = false options: boolean | AddEventListenerOptions = false
): void => { ): void => {
const { Listeners } = this.Editor;
this.mutableListenerIds.push( this.mutableListenerIds.push(
Listeners.on(element, eventType, handler, options) this.listeners.on(element, eventType, handler, options)
); );
}, },
@ -67,10 +71,8 @@ export default class Module<T extends ModuleNodes = {}> {
* Clears all mutable listeners * Clears all mutable listeners
*/ */
clearAll: (): void => { clearAll: (): void => {
const { Listeners } = this.Editor;
for (const id of this.mutableListenerIds) { for (const id of this.mutableListenerIds) {
Listeners.offById(id); this.listeners.offById(id);
} }
this.mutableListenerIds = []; this.mutableListenerIds = [];

View file

@ -27,7 +27,7 @@ export default class ListenersAPI extends Module {
* @param {boolean} useCapture - capture event or not * @param {boolean} useCapture - capture event or not
*/ */
public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void { 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 * @param {boolean} useCapture - capture event or not
*/ */
public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void { 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);
} }
} }

View file

@ -184,7 +184,7 @@ export default class BlockManager extends Module {
}); });
/** Copy event */ /** Copy event */
this.Editor.Listeners.on( this.listeners.on(
document, document,
'copy', 'copy',
(e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandC(e) (e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandC(e)

View file

@ -23,9 +23,7 @@ export default class CrossBlockSelection extends Module {
* @returns {Promise} * @returns {Promise}
*/ */
public async prepare(): Promise<void> { public async prepare(): Promise<void> {
const { Listeners } = this.Editor; this.listeners.on(document, 'mousedown', (event: MouseEvent) => {
Listeners.on(document, 'mousedown', (event: MouseEvent) => {
this.enableCrossBlockSelection(event); this.enableCrossBlockSelection(event);
}); });
} }
@ -40,13 +38,13 @@ export default class CrossBlockSelection extends Module {
return; return;
} }
const { BlockManager, Listeners } = this.Editor; const { BlockManager } = this.Editor;
this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement); this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement);
this.lastSelectedBlock = this.firstSelectedBlock; this.lastSelectedBlock = this.firstSelectedBlock;
Listeners.on(document, 'mouseover', this.onMouseOver); this.listeners.on(document, 'mouseover', this.onMouseOver);
Listeners.on(document, 'mouseup', this.onMouseUp); this.listeners.on(document, 'mouseup', this.onMouseUp);
} }
/** /**
@ -176,10 +174,8 @@ export default class CrossBlockSelection extends Module {
* Removes the listeners * Removes the listeners
*/ */
private onMouseUp = (): void => { private onMouseUp = (): void => {
const { Listeners } = this.Editor; this.listeners.off(document, 'mouseover', this.onMouseOver);
this.listeners.off(document, 'mouseup', this.onMouseUp);
Listeners.off(document, 'mouseover', this.onMouseOver);
Listeners.off(document, 'mouseup', this.onMouseUp);
} }
/** /**

View file

@ -58,7 +58,7 @@ export default class ModificationsObserver extends Module {
this.observer.disconnect(); this.observer.disconnect();
} }
this.observer = null; 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; this.mutationDebouncer = null;
} }
@ -163,13 +163,13 @@ export default class ModificationsObserver extends Module {
private updateNativeInputs(): void { private updateNativeInputs(): void {
if (this.nativeInputs) { if (this.nativeInputs) {
this.nativeInputs.forEach((input) => { 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 = 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));
} }
/** /**

View file

@ -263,18 +263,14 @@ export default class Paste extends Module {
* Set onPaste callback handler * Set onPaste callback handler
*/ */
private setCallback(): void { private setCallback(): void {
const { Listeners } = this.Editor; this.listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
Listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
} }
/** /**
* Unset onPaste callback handler * Unset onPaste callback handler
*/ */
private unsetCallback(): void { private unsetCallback(): void {
const { Listeners } = this.Editor; this.listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
Listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
} }
/** /**

View file

@ -179,26 +179,25 @@ export default class RectangleSelection extends Module {
* Sets Module necessary event handlers * Sets Module necessary event handlers
*/ */
private enableModuleBindings(): void { private enableModuleBindings(): void {
const { Listeners } = this.Editor;
const { container } = this.genHTML(); const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => { this.listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => {
this.processMouseDown(mouseEvent); this.processMouseDown(mouseEvent);
}, false); }, false);
Listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => { this.listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => {
this.processMouseMove(mouseEvent); this.processMouseMove(mouseEvent);
}, false); }, false);
Listeners.on(document.body, 'mouseleave', () => { this.listeners.on(document.body, 'mouseleave', () => {
this.processMouseLeave(); this.processMouseLeave();
}); });
Listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => { this.listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => {
this.processScroll(mouseEvent); this.processScroll(mouseEvent);
}, false); }, false);
Listeners.on(document.body, 'mouseup', () => { this.listeners.on(document.body, 'mouseup', () => {
this.processMouseUp(); this.processMouseUp();
}, false); }, false);
} }

View file

@ -325,7 +325,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
$.append(this.nodes.tools, tool); $.append(this.nodes.tools, tool);
this.tools[toolName] = tool; this.tools[toolName] = tool;
this.Editor.Listeners.on(tool, 'click', async () => { this.listeners.on(tool, 'click', async () => {
await this.replaceWithBlock(toolName); await this.replaceWithBlock(toolName);
}); });
} }

View file

@ -357,7 +357,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.nodes.actions = $.make('div', this.CSS.actionsWrapper); this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
// To prevent reset of a selection when click on the wrapper // 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}`); const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
// If click is on actions wrapper, // If click is on actions wrapper,
@ -479,7 +479,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.nodes.togglerAndButtonsWrapper.appendChild(this.nodes.conversionToggler); 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) => { this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => {
/** /**
* When ConversionToolbar is opening on activated InlineToolbar flipper * When ConversionToolbar is opening on activated InlineToolbar flipper
@ -594,7 +594,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/ */
private addTool(toolName: string, tool: InlineTool): void { private addTool(toolName: string, tool: InlineTool): void {
const { const {
Listeners,
Tools, Tools,
Tooltip, Tooltip,
} = this.Editor; } = this.Editor;
@ -617,7 +616,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.nodes.actions.appendChild(actions); this.nodes.actions.appendChild(actions);
} }
Listeners.on(button, 'click', (event) => { this.listeners.on(button, 'click', (event) => {
this.toolClicked(tool); this.toolClicked(tool);
event.preventDefault(); event.preventDefault();
}); });

View file

@ -225,7 +225,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
/** /**
* Add click listener * Add click listener
*/ */
this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { this.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName); this.toolButtonActivate(event, toolName);
}); });

View file

@ -1,4 +1,3 @@
import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
/** /**
@ -36,11 +35,9 @@ export interface ListenerData {
} }
/** /**
* Editor.js Listeners module * Editor.js Listeners helper
* *
* @module Listeners * Decorator for event listeners assignment
*
* Module-decorator for event listeners assignment
* *
* @author Codex Team * @author Codex Team
* @version 2.0.0 * @version 2.0.0
@ -50,7 +47,7 @@ export interface ListenerData {
* @typedef {Listeners} Listeners * @typedef {Listeners} Listeners
* @property {ListenerData[]} allListeners - listeners store * @property {ListenerData[]} allListeners - listeners store
*/ */
export default class Listeners extends Module { export default class Listeners {
/** /**
* Stores all listeners data to find/remove/process it * Stores all listeners data to find/remove/process it
* *
@ -114,7 +111,7 @@ export default class Listeners extends Module {
existingListeners.forEach((listener, i) => { existingListeners.forEach((listener, i) => {
const index = this.allListeners.indexOf(existingListeners[i]); const index = this.allListeners.indexOf(existingListeners[i]);
if (index > 0) { if (index > -1) {
this.allListeners.splice(index, 1); this.allListeners.splice(index, 1);
listener.element.removeEventListener(listener.eventType, listener.handler, listener.options); listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);

View file

@ -1,6 +1,5 @@
import UI from '../components/modules/ui'; import UI from '../components/modules/ui';
import BlockEvents from '../components/modules/blockEvents'; import BlockEvents from '../components/modules/blockEvents';
import Listeners from '../components/modules/listeners';
import Toolbar from '../components/modules/toolbar/index'; import Toolbar from '../components/modules/toolbar/index';
import InlineToolbar from '../components/modules/toolbar/inline'; import InlineToolbar from '../components/modules/toolbar/inline';
import Toolbox from '../components/modules/toolbar/toolbox'; import Toolbox from '../components/modules/toolbar/toolbox';
@ -44,7 +43,6 @@ export interface EditorModules {
BlockEvents: BlockEvents; BlockEvents: BlockEvents;
BlockSelection: BlockSelection; BlockSelection: BlockSelection;
RectangleSelection: RectangleSelection; RectangleSelection: RectangleSelection;
Listeners: Listeners;
Toolbar: Toolbar; Toolbar: Toolbar;
InlineToolbar: InlineToolbar; InlineToolbar: InlineToolbar;
Toolbox: Toolbox; Toolbox: Toolbox;