/** * @module ModificationsObserver * * Handles any mutations * and gives opportunity to handle outside */ import Module from '../__module'; import _ from '../utils'; import Block from '../block'; export default class ModificationsObserver extends Module { /** * Debounce Timer * @type {number} */ public static readonly DebounceTimer = 450; /** * MutationObserver instance */ private observer: MutationObserver; /** * Allows to temporary disable mutations handling */ private disabled: boolean; /** * Used to prevent several mutation callback execution * @type {Function} */ private mutationDebouncer = _.debounce( () => { this.config.onChange(); }, ModificationsObserver.DebounceTimer); /** * Clear timeout and set null to mutationDebouncer property */ public destroy() { this.mutationDebouncer = null; this.observer.disconnect(); this.observer = null; } /** * Preparation method * @return {Promise} */ public async prepare(): Promise { /** * wait till Browser render Editor's Blocks */ window.setTimeout( () => { this.setObserver(); }, 1000); } /** * Allows to disable observer, * for example when Editor wants to stealthy mutate DOM */ public disable() { this.disabled = true; } /** * Enables mutation handling * Should be called after .disable() */ public enable() { this.disabled = false; } /** * setObserver * * sets 'DOMSubtreeModified' listener on Editor's UI.nodes.redactor * so that User can handle outside from API */ private setObserver(): void { const {UI} = this.Editor; const observerOptions = { childList: true, attributes: true, subtree: true, characterData: true, characterDataOldValue: true, }; this.observer = new MutationObserver((mutationList, observer) => { this.mutationHandler(mutationList, observer); }); this.observer.observe(UI.nodes.redactor, observerOptions); } /** * MutationObserver events handler * @param mutationList * @param observer */ private mutationHandler(mutationList, observer) { /** * Skip mutations in stealth mode */ if (this.disabled) { return; } /** * We divide two Mutation types: * 1) mutations that concerns client changes: settings changes, symbol added, deletion, insertions and so on * 2) functional changes. On each client actions we set functional identifiers to interact with user */ let contentMutated = false; mutationList.forEach((mutation) => { switch (mutation.type) { case 'childList': case 'subtree': case 'characterData': case 'characterDataOldValue': contentMutated = true; break; case 'attributes': const mutatedTarget = mutation.target as Element; /** * Changes on Element.ce-block usually is functional */ if (!mutatedTarget.classList.contains(Block.CSS.wrapper)) { contentMutated = true; return; } break; } }); /** call once */ if (contentMutated) { this.mutationDebouncer(); } } }