editor.js/src/components/modules/modificationsObserver.ts
Peter Savchenko b7b00fd060
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
2023-05-12 20:50:48 +03:00

148 lines
3.8 KiB
TypeScript

import { BlockId } from '../../../types';
import { BlockMutationEvent, BlockMutationType } from '../../../types/events/block';
import { ModuleConfig } from '../../types-internal/module-config';
import Module from '../__module';
import { BlockChanged, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
import * as _ from '../utils';
/**
* We use map of block mutations to filter only unique events
*/
type UniqueBlockMutationKey = `block:${BlockId}:event:${BlockMutationType}`;
/**
* Single entry point for Block mutation events
*/
export default class ModificationsObserver extends Module {
/**
* Flag shows onChange event is disabled
*/
private disabled = false;
/**
* Blocks wrapper mutation observer instance
*/
private readonly mutationObserver: MutationObserver;
/**
* Timeout used to batched several events in a single onChange call
*/
private batchingTimeout: null | ReturnType<typeof setTimeout> = null;
/**
* Array of onChange events used to batch them
*
* Map is used to filter duplicated events related to the same block
*/
private batchingOnChangeQueue = new Map<UniqueBlockMutationKey, BlockMutationEvent>();
/**
* Fired onChange events will be batched by this time
*/
private readonly batchTime = 400;
/**
* Prepare the module
*
* @param options - options used by the modification observer module
* @param options.config - Editor configuration object
* @param options.eventsDispatcher - common Editor event bus
*/
constructor({ config, eventsDispatcher }: ModuleConfig) {
super({
config,
eventsDispatcher,
});
this.mutationObserver = new MutationObserver((mutations) => {
this.redactorChanged(mutations);
});
this.eventsDispatcher.on(BlockChanged, (payload) => {
this.particularBlockChanged(payload.event);
});
/**
* Mutex for fake cursor setting/removing operation
*/
this.eventsDispatcher.on(FakeCursorAboutToBeToggled, () => {
this.disable();
});
this.eventsDispatcher.on(FakeCursorHaveBeenSet, () => {
this.enable();
});
}
/**
* Enables onChange event
*/
public enable(): void {
this.mutationObserver.observe(
this.Editor.UI.nodes.redactor,
{
childList: true,
subtree: true,
characterData: true,
attributes: true,
}
);
this.disabled = false;
}
/**
* Disables onChange event
*/
public disable(): void {
this.mutationObserver.disconnect();
this.disabled = true;
}
/**
* Call onChange event passed to Editor.js configuration
*
* @param event - some of our custom change events
*/
private particularBlockChanged(event: BlockMutationEvent): void {
if (this.disabled || !_.isFunction(this.config.onChange)) {
return;
}
this.batchingOnChangeQueue.set(`block:${event.detail.target.id}:event:${event.type as BlockMutationType}`, event);
if (this.batchingTimeout) {
clearTimeout(this.batchingTimeout);
}
this.batchingTimeout = setTimeout(() => {
let eventsToEmit;
/**
* Ih we have only 1 event in a queue, unwrap it
*/
if (this.batchingOnChangeQueue.size === 1) {
eventsToEmit = this.batchingOnChangeQueue.values().next().value;
} else {
eventsToEmit = Array.from(this.batchingOnChangeQueue.values());
}
if (this.config.onChange) {
this.config.onChange(this.Editor.API.methods, eventsToEmit);
}
this.batchingOnChangeQueue.clear();
}, this.batchTime);
}
/**
* Fired on every blocks wrapper dom change
*
* @param mutations - mutations happened
*/
private redactorChanged(mutations: MutationRecord[]): void {
this.eventsDispatcher.emit(RedactorDomChanged, {
mutations,
});
}
}