From b7b00fd0606330ae41876bd481b2c57fe4e99640 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Fri, 12 May 2023 20:50:48 +0300 Subject: [PATCH] chore(onChange): bugfix, batching, improvements of ModificationsObserver (#2349) * block onchange stash * improve block filtration * update tool root * chore(block): remove willSelect und willUnselect * onchange events batching * get rid of CustomEvent extension, create custom event map instead * improve types of EventsDispatcher * fix tests * custom sinon + chai matchers * improve tests, add mutex for fake cursor * add test for fake-cursor mutex * test for batch filtering * fix caret setting by enter press at the end of the block * test for detectToolRootChange * remove resolved todos * changelog added * fix tests * Update CHANGELOG.md * rename FakeCursorAboutToBeSet -> FakeCursorAboutToBeToggled * update didMutated statements * move inputs cache clearing to a separate method * rm Record inheritance from Event maps * add type alisases * rename isElementContainsFakeCursor -> isFakeCursorInsideContainer * improve code style --- docs/CHANGELOG.md | 12 +- index.html | 2 +- package.json | 1 + src/components/__module.ts | 3 +- src/components/block/index.ts | 320 +++++++++------- src/components/core.ts | 11 +- src/components/dom.ts | 2 +- src/components/events/BlockChanged.ts | 16 + src/components/events/BlockHovered.ts | 16 + .../events/FakeCursorAboutToBeToggled.ts | 17 + .../events/FakeCursorHaveBeenSet.ts | 17 + src/components/events/RedactorDomChanged.ts | 14 + src/components/events/index.ts | 26 ++ src/components/modules/blockEvents.ts | 7 + src/components/modules/blockManager.ts | 44 ++- .../modules/modificationsObserver.ts | 112 +++++- src/components/modules/toolbar/index.ts | 6 +- src/components/modules/ui.ts | 12 +- src/components/selection.ts | 43 ++- src/components/ui/toolbox.ts | 17 +- src/components/utils/events.ts | 54 +-- src/components/utils/mutations.ts | 26 ++ src/components/utils/popover/index.ts | 21 +- src/types-internal/module-config.d.ts | 3 +- test/cypress/support/index.d.ts | 12 + test/cypress/support/index.ts | 7 + test/cypress/tests/api/block.cy.ts | 48 +-- test/cypress/tests/modules/BlockEvents.cy.ts | 35 ++ test/cypress/tests/modules/Saver.cy.ts | 49 +++ test/cypress/tests/onchange.cy.ts | 348 +++++++++++++----- types/configs/editor-config.d.ts | 9 +- types/data-formats/block-data.d.ts | 5 +- types/data-formats/block-id.ts | 4 + types/data-formats/output-data.d.ts | 3 +- types/events/block/Base.ts | 11 + types/events/block/BlockAdded.ts | 21 ++ types/events/block/BlockChanged.ts | 21 ++ types/events/block/BlockMoved.ts | 26 ++ types/events/block/BlockRemoved.ts | 21 ++ types/events/block/index.ts | 44 +++ types/events/block/mutation-type.ts | 24 -- types/index.d.ts | 19 +- yarn.lock | 5 + 43 files changed, 1151 insertions(+), 363 deletions(-) create mode 100644 src/components/events/BlockChanged.ts create mode 100644 src/components/events/BlockHovered.ts create mode 100644 src/components/events/FakeCursorAboutToBeToggled.ts create mode 100644 src/components/events/FakeCursorHaveBeenSet.ts create mode 100644 src/components/events/RedactorDomChanged.ts create mode 100644 src/components/events/index.ts create mode 100644 src/components/utils/mutations.ts create mode 100644 types/data-formats/block-id.ts create mode 100644 types/events/block/Base.ts create mode 100644 types/events/block/BlockAdded.ts create mode 100644 types/events/block/BlockChanged.ts create mode 100644 types/events/block/BlockMoved.ts create mode 100644 types/events/block/BlockRemoved.ts create mode 100644 types/events/block/index.ts delete mode 100644 types/events/block/mutation-type.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7ec3ddd9..e3333c06 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,10 +6,20 @@ - `Refactoring` — Popover class refactored. - `Improvement` — *Toolbox* — Number of `close()` method calls optimized. - `Improvement` — The `onChange` callback won't be triggered only if all mutations contain nodes with the `data-mutation-free` attributes. +- `Improvement` — **Breaking Change** — Batching added to the `onChange` callback. Now the second argument can contain an array of CustomEvents as well as a single one. If several changes will be made in short period of time, they will be batched under the single `onChange` call +- `Improvement` — Pressing "Enter" at the end of a Block won't lead to redundant `block-changed` event triggering. Only `block-added` event will be dispatched. +- `Fix` — The `onChange` won't be triggered with Block Tunes opening and closing. +- `Refactoring` — `EventDispatcher` types improved. Now we can pass `EventsMap` via generic to specify a map of event names and their payloads that can be used in a particular EventDispatcher instance. +- `Refactoring` — All events in common editor Event Bus now have own type declarations. +- `Refactoring` — Now Blocks don't have own Mutation Observer. The single observer attached to the editor's blocks wrapper element. `ModificationsObserver` listens blocks wrapper changes, then emits an event into the common editor Event Bus. Each Block subscribes to such events and handle those who related to a particular Block. +- `Improvement` — Block's mutation handler now will be called on every block change (including background changes). Previously, it have worked only if a particular Block have been focused. +- `Refactoring` — Debounce have been removed from Block's mutation handler. The batching in `ModificationObserver` used instead. +- `Improvement` — Number of caret saving method calls optimized for Block Tunes opening/closing. +- `New` — Types for Block mutation events added. - `Fix` — Resolve compiler error from importing the BlockToolData type. - `Fix` — Resolved a problem when document was being scrolled to the beginning after moving up a Block above the viewport. - `Improvement` — Package size reduced by removing redundant files. -- `Fix`- Several bugs caused by random browser extensions. +- `Fix`- Several bugs caused by random browser extensions. Now we are not searching for a block's container in the DOM on saving. Instead, we keep it in memory and update it when the tool changes a container element. - `Improvement` — *Dependencies* — Upgrade TypeScript to v5. - `Fix` — *ToolsAPI* — `pasteConfig` getter with `false` value could be used to disable paste handling by Editor.js core. Could be useful if your tool has its own paste handler. - `Improvement` — *Dependencies* — Upgrade Cypress to v12, upgrade related libraries to latest versions. diff --git a/index.html b/index.html index db380636..43c8b4ae 100644 --- a/index.html +++ b/index.html @@ -406,4 +406,4 @@ }) - \ No newline at end of file + diff --git a/package.json b/package.json index be1396a7..c1e1362d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@editorjs/paragraph": "^2.9.0", "@editorjs/simple-image": "^1.4.1", "@types/node": "^18.15.11", + "chai-subset": "^1.6.0", "codex-notifier": "^1.1.2", "codex-tooltip": "^1.0.5", "core-js": "3.30.0", diff --git a/src/components/__module.ts b/src/components/__module.ts index c5391422..4ad521ad 100644 --- a/src/components/__module.ts +++ b/src/components/__module.ts @@ -3,6 +3,7 @@ import { EditorConfig } from '../../types'; import { ModuleConfig } from '../types-internal/module-config'; import Listeners from './utils/listeners'; import EventsDispatcher from './utils/events'; +import { EditorEventMap } from './events'; /** * The type of the Module generic. @@ -42,7 +43,7 @@ export default class Module> /** * Editor event dispatcher class */ - protected eventsDispatcher: EventsDispatcher; + protected eventsDispatcher: EventsDispatcher; /** * Util for bind/unbind DOM event listeners diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 642ed483..e7e25c49 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -22,6 +22,9 @@ import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import ToolsCollection from '../tools/collection'; import EventsDispatcher from '../utils/events'; import { TunesMenuConfigItem } from '../../../types/tools'; +import { isMutationBelongsToElement } from '../utils/mutations'; +import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events'; +import { RedactorDomChangedPayload } from '../events/RedactorDomChanged'; /** * Interface describes Block class constructor argument @@ -84,9 +87,11 @@ export enum BlockToolAPI { } /** - * Names of events supported by Block class + * Names of events used in Block */ -type BlockEvents = 'didMutated'; +interface BlockEvents { + 'didMutated': Block, +} /** * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance @@ -192,89 +197,14 @@ export default class Block extends EventsDispatcher { private inputIndex = 0; /** - * Mutation observer to handle DOM mutations - * - * @type {MutationObserver} + * Common editor event bus */ - private mutationObserver: MutationObserver; + private readonly editorEventBus: EventsDispatcher | null = null; /** - * Debounce Timer - * - * @type {number} + * Link to editor dom change callback. Used to remove listener on remove */ - private readonly modificationDebounceTimer = 450; - - /** - * Is fired when DOM mutation has been happened - * - * mutationsOrInputEvent — actual changes - * - MutationRecord[] - any DOM change - * - InputEvent — change - * - undefined — manual triggering of block.dispatchChange() - */ - private didMutated = _.debounce((mutationsOrInputEvent: MutationRecord[] | InputEvent = undefined): void => { - /** - * We won't fire a Block mutation event if mutation contain only nodes marked with 'data-mutation-free' attributes - */ - let shouldFireUpdate; - - if (mutationsOrInputEvent === undefined) { - shouldFireUpdate = true; - } else if (mutationsOrInputEvent instanceof InputEvent) { - shouldFireUpdate = true; - } else { - /** - * Update from 2023, Feb 17: - * Changed mutationsOrInputEvent.some() to mutationsOrInputEvent.every() - * since there could be a real mutations same-time with mutation-free changes, - * for example when Block Tune change: block is changing along with FakeCursor (mutation-free) removing - * — we should fire 'didMutated' event in that case - */ - const everyRecordIsMutationFree = mutationsOrInputEvent.length > 0 && mutationsOrInputEvent.every((record) => { - const { addedNodes, removedNodes } = record; - const changedNodes = [ - ...Array.from(addedNodes), - ...Array.from(removedNodes), - ]; - - return changedNodes.some((node) => { - if ($.isElement(node) === false) { - return false; - } - - return (node as HTMLElement).dataset.mutationFree === 'true'; - }); - }); - - if (everyRecordIsMutationFree) { - shouldFireUpdate = false; - } else { - shouldFireUpdate = true; - } - } - - /** - * In case some mutation free elements are added or removed, do not trigger didMutated event - */ - if (!shouldFireUpdate) { - return; - } - - /** - * Drop cache - */ - this.cachedInputs = []; - - /** - * Update current input - */ - this.updateCurrentInput(); - - this.call(BlockToolAPI.UPDATED); - - this.emit('didMutated', this); - }, this.modificationDebounceTimer); + private redactorDomChangedCallback: (payload: RedactorDomChangedPayload) => void; /** * Current block API interface @@ -282,12 +212,13 @@ export default class Block extends EventsDispatcher { private readonly blockAPI: BlockAPIInterface; /** - * @param {object} options - block constructor options - * @param {string} [options.id] - block's id. Will be generated if omitted. - * @param {BlockToolData} options.data - Tool's initial data - * @param {BlockTool} options.tool — block's tool + * @param options - block constructor options + * @param [options.id] - block's id. Will be generated if omitted. + * @param options.data - Tool's initial data + * @param options.tool — block's tool * @param options.api - Editor API module for pass it to the Block Tunes - * @param {boolean} options.readOnly - Read-Only flag + * @param options.readOnly - Read-Only flag + * @param [eventBus] - Editor common event bus. Allows to subscribe on some Editor events. Could be omitted when "virtual" Block is created. See BlocksAPI@composeBlockData. */ constructor({ id = _.generateBlockId(), @@ -296,7 +227,7 @@ export default class Block extends EventsDispatcher { api, readOnly, tunesData, - }: BlockConstructorOptions) { + }: BlockConstructorOptions, eventBus?: EventsDispatcher) { super(); this.name = tool.name; @@ -304,10 +235,9 @@ export default class Block extends EventsDispatcher { this.settings = tool.settings; this.config = tool.settings.config || {}; this.api = api; + this.editorEventBus = eventBus || null; this.blockAPI = new BlockAPI(this); - this.mutationObserver = new MutationObserver(this.didMutated); - this.tool = tool; this.toolInstance = tool.create(data, this.blockAPI, readOnly); @@ -319,6 +249,17 @@ export default class Block extends EventsDispatcher { this.composeTunes(tunesData); this.holder = this.compose(); + + /** + * Start watching block mutations + */ + this.watchBlockMutations(); + + /** + * Mutation observer doesn't track changes in "" and "