diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2eaae7b2..a60ea506 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.22.0 + +- `New` - `onChange` callback now receive Block API object of affected block + ### 2.21.0 - `New` - Blocks now have unique ids [#873](https://github.com/codex-team/editor.js/issues/873) diff --git a/docs/installation.md b/docs/installation.md index 6660ce80..29565665 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -193,7 +193,7 @@ var editor = new EditorJS({ /** * onChange callback */ - onChange: () => {console.log('Now I know that Editor\'s content changed!')} + onChange: (editorAPI, affectedBlockAPI) => {console.log('Now I know that Editor\'s content changed!')} }); ``` diff --git a/example/example-dev.html b/example/example-dev.html index cc9123f7..4801722f 100644 --- a/example/example-dev.html +++ b/example/example-dev.html @@ -319,8 +319,8 @@ onReady: function(){ saveButton.click(); }, - onChange: function() { - console.log('something changed'); + onChange: function(api, block) { + console.log('something changed', block); }, }); diff --git a/example/example.html b/example/example.html index 7de5baeb..abc6a5f6 100644 --- a/example/example.html +++ b/example/example.html @@ -281,8 +281,8 @@ onReady: function(){ saveButton.click(); }, - onChange: function() { - console.log('something changed'); + onChange: function(api, block) { + console.log('something changed', block); } }); diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 18439c9d..fcb8c412 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -18,6 +18,7 @@ import BlockTool from '../tools/block'; import BlockTune from '../tools/tune'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import ToolsCollection from '../tools/collection'; +import EventsDispatcher from '../utils/events'; /** * Interface describes Block class constructor argument @@ -79,6 +80,11 @@ export enum BlockToolAPI { ON_PASTE = 'onPaste', } +/** + * Names of events supported by Block class + */ +type BlockEvents = 'didMutated'; + /** * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance * @@ -86,7 +92,7 @@ export enum BlockToolAPI { * @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class * @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function */ -export default class Block { +export default class Block extends EventsDispatcher { /** * CSS classes for the Block * @@ -207,6 +213,8 @@ export default class Block { this.updateCurrentInput(); this.call(BlockToolAPI.UPDATED); + + this.emit('didMutated', this); }, this.modificationDebounceTimer); /** @@ -230,6 +238,8 @@ export default class Block { readOnly, tunesData, }: BlockConstructorOptions) { + super(); + this.name = tool.name; this.id = id; this.settings = tool.settings; @@ -680,6 +690,8 @@ export default class Block { * Call Tool instance destroy method */ public destroy(): void { + super.destroy(); + if (_.isFunction(this.toolInstance.destroy)) { this.toolInstance.destroy(); } @@ -777,6 +789,13 @@ export default class Block { private addInputEvents(): void { this.inputs.forEach(input => { input.addEventListener('focus', this.handleFocus); + + /** + * If input is native input add oninput listener to observe changes + */ + if ($.isNativeInput(input)) { + input.addEventListener('input', this.didMutated); + } }); } @@ -786,6 +805,10 @@ export default class Block { private removeInputEvents(): void { this.inputs.forEach(input => { input.removeEventListener('focus', this.handleFocus); + + if ($.isNativeInput(input)) { + input.removeEventListener('input', this.didMutated); + } }); } } diff --git a/src/components/modules/api/sanitizer.ts b/src/components/modules/api/sanitizer.ts index b0ce0ef0..79c91656 100644 --- a/src/components/modules/api/sanitizer.ts +++ b/src/components/modules/api/sanitizer.ts @@ -11,7 +11,7 @@ export default class SanitizerAPI extends Module { /** * Available methods * - * @returns {Sanitizer} + * @returns {SanitizerConfig} */ public get methods(): ISanitizer { return { diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index e82ba86f..cb2e1fc7 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -13,6 +13,7 @@ import * as _ from '../utils'; import Blocks from '../blocks'; import { BlockToolData, PasteEvent } from '../../../types'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; +import BlockAPI from '../block/api'; /** * @typedef {BlockManager} BlockManager @@ -290,6 +291,11 @@ export default class BlockManager extends Module { this._blocks.insert(newIndex, block, replace); + /** + * Force call of didMutated event on Block insertion + */ + this.blockDidMutated(block); + if (needToFocus) { this.currentBlockIndex = newIndex; } else if (newIndex <= this.currentBlockIndex) { @@ -361,6 +367,11 @@ export default class BlockManager extends Module { this._blocks[index] = block; + /** + * Force call of didMutated event on Block insertion + */ + this.blockDidMutated(block); + if (needToFocus) { this.currentBlockIndex = index; } else if (index <= this.currentBlockIndex) { @@ -426,8 +437,15 @@ export default class BlockManager extends Module { throw new Error('Can\'t find a Block to remove'); } + const blockToRemove = this._blocks[index]; + this._blocks.remove(index); + /** + * Force call of didMutated event on Block removal + */ + this.blockDidMutated(blockToRemove); + if (this.currentBlockIndex >= index) { this.currentBlockIndex--; } @@ -689,6 +707,11 @@ export default class BlockManager extends Module { /** Now actual block moved so that current block index changed */ this.currentBlockIndex = toIndex; + + /** + * Force call of didMutated event on Block movement + */ + this.blockDidMutated(this.currentBlock); } /** @@ -754,6 +777,8 @@ export default class BlockManager extends Module { this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => { BlockEvents.dragLeave(event); }); + + block.on('didMutated', (affectedBlock: Block) => this.blockDidMutated(affectedBlock)); } /** @@ -789,4 +814,15 @@ export default class BlockManager extends Module { private validateIndex(index: number): boolean { return !(index < 0 || index >= this._blocks.length); } + + /** + * Block mutation callback + * + * @param block - mutated block + */ + private blockDidMutated(block: Block): Block { + this.Editor.ModificationsObserver.onChange(new BlockAPI(block)); + + return block; + } } diff --git a/src/components/modules/modificationsObserver.ts b/src/components/modules/modificationsObserver.ts index 30f5797c..c1fc5123 100644 --- a/src/components/modules/modificationsObserver.ts +++ b/src/components/modules/modificationsObserver.ts @@ -1,195 +1,40 @@ -/** - * @module ModificationsObserver - * - * Handles any mutations - * and gives opportunity to handle outside - */ - import Module from '../__module'; +import { BlockAPI } from '../../../types'; import * as _ from '../utils'; -import Block from '../block'; /** - * + * Single entry point for Block mutation events */ 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 + * Flag shows onChange event is disabled */ private disabled = false; /** - * Used to prevent several mutation callback execution - * - * @type {Function} - */ - private mutationDebouncer = _.debounce(() => { - this.updateNativeInputs(); - - if (_.isFunction(this.config.onChange)) { - this.config.onChange(this.Editor.API.methods); - } - }, ModificationsObserver.DebounceTimer); - - /** - * Array of native inputs in Blocks. - * Changes in native inputs are not handled by modification observer, so we need to set change event listeners on them - */ - private nativeInputs: HTMLElement[] = []; - - /** - * Clear timeout and set null to mutationDebouncer property - */ - public destroy(): void { - this.mutationDebouncer = null; - if (this.observer) { - this.observer.disconnect(); - } - this.observer = null; - this.nativeInputs.forEach((input) => this.listeners.off(input, 'input', this.mutationDebouncer)); - this.mutationDebouncer = null; - } - - /** - * Set read-only state - * - * @param {boolean} readOnlyEnabled - read only flag value - */ - public toggleReadOnly(readOnlyEnabled: boolean): void { - if (readOnlyEnabled) { - this.disableModule(); - } else { - this.enableModule(); - } - } - - /** - * Allows to disable observer, - * for example when Editor wants to stealthy mutate DOM - */ - public disable(): void { - this.disabled = true; - } - - /** - * Enables mutation handling - * Should be called after .disable() + * Enables onChange event */ public enable(): void { this.disabled = false; } /** - * setObserver - * - * sets 'DOMSubtreeModified' listener on Editor's UI.nodes.redactor - * so that User can handle outside from API + * Disables onChange event */ - 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); + public disable(): void { + this.disabled = true; } /** - * MutationObserver events handler + * Call onChange event passed to Editor.js configuration * - * @param {MutationRecord[]} mutationList - list of mutations - * @param {MutationObserver} observer - observer instance + * @param block - changed Block */ - private mutationHandler(mutationList: MutationRecord[], observer: MutationObserver): void { - /** - * Skip mutations in stealth mode - */ - if (this.disabled) { + public onChange(block: BlockAPI): void { + if (this.disabled || !_.isFunction(this.config.onChange)) { 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 'characterData': - contentMutated = true; - break; - case 'attributes': - /** - * Changes on Element.ce-block usually is functional - */ - if (!(mutation.target as Element).classList.contains(Block.CSS.wrapper)) { - contentMutated = true; - } - break; - } - }); - - /** call once */ - if (contentMutated) { - this.mutationDebouncer(); - } - } - - /** - * Gets native inputs and set oninput event handler - */ - private updateNativeInputs(): void { - if (this.nativeInputs) { - this.nativeInputs.forEach((input) => { - this.listeners.off(input, 'input'); - }); - } - - this.nativeInputs = Array.from(this.Editor.UI.nodes.redactor.querySelectorAll('textarea, input, select')); - - this.nativeInputs.forEach((input) => this.listeners.on(input, 'input', this.mutationDebouncer)); - } - - /** - * Sets observer and enables it - */ - private enableModule(): void { - /** - * wait till Browser render Editor's Blocks - */ - window.setTimeout(() => { - this.setObserver(); - this.updateNativeInputs(); - this.enable(); - }, 1000); - } - - /** - * Disables observer - */ - private disableModule(): void { - this.disable(); + this.config.onChange(this.Editor.API.methods, block); } } diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index 61f794af..178e3cc1 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -639,7 +639,7 @@ export default class Paste extends Module { * @param {PasteData} dataToInsert - data of Block to insert */ private async processInlinePaste(dataToInsert: PasteData): Promise { - const { BlockManager, Caret, Tools } = this.Editor; + const { BlockManager, Caret } = this.Editor; const { content } = dataToInsert; const currentBlockIsDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault; diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index 0e87e751..9aad79ff 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -48,8 +48,15 @@ export default class Renderer extends Module { public async render(blocks: OutputBlockData[]): Promise { const chainData = blocks.map((block) => ({ function: (): Promise => this.insertBlock(block) })); + /** + * Disable onChange callback on render to not to spam those events + */ + this.Editor.ModificationsObserver.disable(); + const sequence = await _.sequence(chainData as _.ChainData[]); + this.Editor.ModificationsObserver.enable(); + this.Editor.UI.checkEmptiness(); return sequence; diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts index 521dbda5..59725f65 100644 --- a/src/components/modules/saver.ts +++ b/src/components/modules/saver.ts @@ -7,7 +7,7 @@ */ import Module from '../__module'; import { OutputData } from '../../../types'; -import { ValidatedData } from '../../../types/data-formats'; +import { SavedData, ValidatedData } from '../../../types/data-formats'; import Block from '../block'; import * as _ from '../utils'; import { sanitizeBlocks } from '../utils/sanitizer'; @@ -28,26 +28,28 @@ export default class Saver extends Module { * @returns {OutputData} */ public async save(): Promise { - const { BlockManager, ModificationsObserver, Tools } = this.Editor; + const { BlockManager, Tools, ModificationsObserver } = this.Editor; const blocks = BlockManager.blocks, chainData = []; - /** - * Disable modifications observe while saving - */ - ModificationsObserver.disable(); - try { + /** + * Disable onChange callback on save to not to spam those events + */ + ModificationsObserver.disable(); + blocks.forEach((block: Block) => { chainData.push(this.getSavedData(block)); }); - const extractedData = await Promise.all(chainData); + const extractedData = await Promise.all(chainData) as Array>; const sanitizedData = await sanitizeBlocks(extractedData, (name) => { return Tools.blockTools.get(name).sanitizeConfig; }); return this.makeOutput(sanitizedData); + } catch (e) { + _.logLabeled(`Saving failed due to the Error %o`, 'error', e); } finally { ModificationsObserver.enable(); } diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 4c3d29b3..9bc788ee 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -9,10 +9,8 @@ import { I18nInternalNS } from '../../i18n/namespace-internal'; import Shortcuts from '../../utils/shortcuts'; import Tooltip from '../../utils/tooltip'; import { ModuleConfig } from '../../../types-internal/module-config'; -import EventsDispatcher from '../../utils/events'; import InlineTool from '../../tools/inline'; import { CommonInternalSettings } from '../../tools/base'; -import BlockTool from '../../tools/block'; /** * Inline Toolbar elements diff --git a/src/components/utils.ts b/src/components/utils.ts index 4cc92a31..091bd54a 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -660,7 +660,7 @@ export function deprecationAssert(condition: boolean, oldProperty: string, newPr * @param propertyKey - method or accessor name * @param descriptor - property descriptor */ -export function cacheable( +export function cacheable( target: Target, propertyKey: string, descriptor: PropertyDescriptor @@ -672,7 +672,7 @@ export function cacheable( /** * Override get or value descriptor property to cache return value * - * @param args + * @param args - method args */ descriptor[propertyToOverride] = function (...args: Arguments): Value { /** @@ -688,12 +688,12 @@ export function cacheable( /** * If get accessor has been overridden, we need to override set accessor to clear cache * - * @param value + * @param value - value to set */ if (propertyToOverride === 'get' && descriptor.set) { const originalSet = descriptor.set; - descriptor.set = function (value: any): void { + descriptor.set = function (value: unknown): void { delete target[cacheKey]; originalSet.apply(this, value); diff --git a/src/components/utils/events.ts b/src/components/utils/events.ts index 10d501ec..2db6c68a 100644 --- a/src/components/utils/events.ts +++ b/src/components/utils/events.ts @@ -11,7 +11,7 @@ * @typedef {Events} Events * @property {object} subscribers - all subscribers grouped by event name */ -export default class EventsDispatcher { +export default class EventsDispatcher { /** * Object with events` names as key and array of callback functions as value * @@ -25,7 +25,7 @@ export default class EventsDispatcher { * @param {string} eventName - event name * @param {Function} callback - subscriber */ - public on(eventName: string, callback: (data: object) => object): void { + public on(eventName: Events, callback: (data: object) => object): void { if (!(eventName in this.subscribers)) { this.subscribers[eventName] = []; } @@ -40,7 +40,7 @@ export default class EventsDispatcher { * @param {string} eventName - event name * @param {Function} callback - subscriber */ - public once(eventName: string, callback: (data: object) => object): void { + public once(eventName: Events, callback: (data: object) => object): void { if (!(eventName in this.subscribers)) { this.subscribers[eventName] = []; } @@ -67,7 +67,7 @@ export default class EventsDispatcher { * @param {string} eventName - event name * @param {object} data - subscribers get this data when they were fired */ - public emit(eventName: string, data?: object): void { + public emit(eventName: Events, data?: object): void { if (!this.subscribers[eventName]) { return; } @@ -85,7 +85,7 @@ export default class EventsDispatcher { * @param {string} eventName - event name * @param {Function} callback - event handler */ - public off(eventName: string, callback: (data: object) => object): void { + public off(eventName: Events, callback: (data: object) => object): void { for (let i = 0; i < this.subscribers[eventName].length; i++) { if (this.subscribers[eventName][i] === callback) { delete this.subscribers[eventName][i]; diff --git a/src/components/utils/sanitizer.ts b/src/components/utils/sanitizer.ts index 692c05b5..5b4bc8a2 100644 --- a/src/components/utils/sanitizer.ts +++ b/src/components/utils/sanitizer.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ /** * CodeX Sanitizer * diff --git a/src/types-internal/editor-modules.d.ts b/src/types-internal/editor-modules.d.ts index 8d882f4e..95a255cb 100644 --- a/src/types-internal/editor-modules.d.ts +++ b/src/types-internal/editor-modules.d.ts @@ -6,7 +6,6 @@ import Toolbox from '../components/modules/toolbar/toolbox'; import BlockSettings from '../components/modules/toolbar/blockSettings'; import Paste from '../components/modules/paste'; import DragNDrop from '../components/modules/dragNDrop'; -import ModificationsObserver from '../components/modules/modificationsObserver'; import Renderer from '../components/modules/renderer'; import Tools from '../components/modules/tools'; import API from '../components/modules/api/index'; @@ -32,6 +31,7 @@ import TooltipAPI from '../components/modules/api/tooltip'; import ReadOnly from '../components/modules/readonly'; import ReadOnlyAPI from '../components/modules/api/readonly'; import I18nAPI from '../components/modules/api/i18n'; +import ModificationsObserver from '../components/modules/modificationsObserver'; export interface EditorModules { UI: UI; @@ -45,7 +45,6 @@ export interface EditorModules { ConversionToolbar: ConversionToolbar; Paste: Paste; DragNDrop: DragNDrop; - ModificationsObserver: ModificationsObserver; Renderer: Renderer; Tools: Tools; API: API; @@ -68,4 +67,5 @@ export interface EditorModules { ReadOnly: ReadOnly; ReadOnlyAPI: ReadOnlyAPI; I18nAPI: I18nAPI; + ModificationsObserver: ModificationsObserver; } diff --git a/test/cypress/plugins/index.ts b/test/cypress/plugins/index.ts index 06d4b245..e3a3c4e3 100644 --- a/test/cypress/plugins/index.ts +++ b/test/cypress/plugins/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* tslint:disable:no-var-requires */ /** * This file contains connection of Cypres plugins @@ -6,7 +7,7 @@ const webpackConfig = require('../../../webpack.config.js'); const preprocessor = require('@cypress/webpack-preprocessor'); const codeCoverageTask = require('@cypress/code-coverage/task'); -module.exports = (on, config): any => { +module.exports = (on, config): unknown => { /** * Add Cypress task to get code coverage */ diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index fe250e4a..b2a24276 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * This file contains custom commands for Cypress. * Also it can override the existing commands. @@ -40,7 +41,7 @@ Cypress.Commands.add('createEditor', (editorConfig: EditorConfig = {}): Chainabl /** * Paste command to dispatch paste event * - * @usage + * Usage * cy.get('div').paste({'text/plain': 'Text', 'text/html': 'Text'}) * * @param data - map with MIME type as a key and data as value @@ -66,7 +67,7 @@ Cypress.Commands.add('paste', { /** * Copy command to dispatch copy event on subject * - * @usage + * Usage: * cy.get('div').copy().then(data => {}) */ Cypress.Commands.add('copy', { prevSubject: true }, async (subject) => { @@ -92,7 +93,7 @@ Cypress.Commands.add('copy', { prevSubject: true }, async (subject) => { /** * Cut command to dispatch cut event on subject * - * @usage + * Usage: * cy.get('div').cut().then(data => {}) */ Cypress.Commands.add('cut', { prevSubject: true }, async (subject) => { diff --git a/test/cypress/tests/api/blocks.spec.ts b/test/cypress/tests/api/blocks.spec.ts index db14d9a2..f17325fd 100644 --- a/test/cypress/tests/api/blocks.spec.ts +++ b/test/cypress/tests/api/blocks.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * There will be described test cases of 'blocks.*' API */ diff --git a/test/cypress/tests/block-ids.spec.ts b/test/cypress/tests/block-ids.spec.ts index 3486f789..4faccaec 100644 --- a/test/cypress/tests/block-ids.spec.ts +++ b/test/cypress/tests/block-ids.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import Header from '../../../example/tools/header'; import { nanoid } from 'nanoid'; diff --git a/test/cypress/tests/modules/Tools.spec.ts b/test/cypress/tests/modules/Tools.spec.ts index e6719825..d3c43d68 100644 --- a/test/cypress/tests/modules/Tools.spec.ts +++ b/test/cypress/tests/modules/Tools.spec.ts @@ -1,5 +1,5 @@ /* tslint:disable:max-classes-per-file */ -/* eslint-disable @typescript-eslint/ban-ts-ignore */ +/* eslint-disable @typescript-eslint/ban-ts-ignore,@typescript-eslint/no-explicit-any,jsdoc/require-jsdoc */ import Tools from '../../../../src/components/modules/tools'; import { EditorConfig } from '../../../../types'; import BlockTool from '../../../../src/components/tools/block'; @@ -86,8 +86,8 @@ describe('Tools module', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function public static prepare(): void {} } as any, - inlineToolbar: ['inlineTool2'], - tunes: ['blockTune2'] + inlineToolbar: [ 'inlineTool2' ], + tunes: [ 'blockTune2' ], }, withFailedPrepare: class { public static prepare(): void { diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts new file mode 100644 index 00000000..9e8bdaea --- /dev/null +++ b/test/cypress/tests/onchange.spec.ts @@ -0,0 +1,127 @@ +import Header from '../../../example/tools/header'; + +/** + * @todo Add checks that correct block API object is passed to onChange + * @todo Add cases for native inputs changes + */ +describe('onChange callback', () => { + const config = { + tools: { + header: Header, + }, + onChange: (): void => { + console.log('something changed'); + }, + }; + + beforeEach(() => { + if (this && this.editorInstance) { + this.editorInstance.destroy(); + } else { + cy.spy(config, 'onChange').as('onChange'); + + cy.createEditor(config).as('editorInstance'); + } + }); + + it('should fire onChange callback on block insertion', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('{enter}'); + + cy.get('@onChange').should('be.called'); + }); + + it('should fire onChange callback on typing into block', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('some text'); + + cy.get('@onChange').should('be.called'); + }); + + it('should fire onChange callback on block replacement', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-toolbar__plus') + .click(); + + cy.get('[data-cy=editorjs]') + .get('li.ce-toolbox__button[data-tool=header]') + .click(); + + cy.get('@onChange').should('be.calledWithMatch', Cypress.sinon.match.any, Cypress.sinon.match({ name: 'header' })); + }); + + it('should fire onChange callback on tune modifier', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-toolbar__plus') + .click(); + + cy.get('[data-cy=editorjs]') + .get('li.ce-toolbox__button[data-tool=header]') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('span.ce-toolbar__settings-btn') + .click(); + + cy.get('[data-cy=editorjs]') + .get('span.cdx-settings-button[data-level=1]') + .click(); + + cy.get('@onChange').should('be.calledWithMatch', Cypress.sinon.match.any, Cypress.sinon.match({ name: 'header' })); + }); + + it('should fire onChange callback when block is removed', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('span.ce-toolbar__settings-btn') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-settings__button--delete') + .click() + .click(); + + cy.get('@onChange').should('be.called'); + }); + + it('should fire onChange callback when block is moved', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('{enter}'); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .last() + .click(); + + cy.get('[data-cy=editorjs]') + .get('span.ce-toolbar__settings-btn') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-tune-move-up') + .click(); + + cy.get('@onChange').should('be.called'); + }); +}); diff --git a/test/cypress/tests/sanitisation.spec.ts b/test/cypress/tests/sanitisation.spec.ts index 2c40206f..73326f69 100644 --- a/test/cypress/tests/sanitisation.spec.ts +++ b/test/cypress/tests/sanitisation.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ describe('Output sanitisation', () => { beforeEach(() => { if (this && this.editorInstance) { diff --git a/test/cypress/tests/tools/BlockTool.spec.ts b/test/cypress/tests/tools/BlockTool.spec.ts index e6306a18..114b820e 100644 --- a/test/cypress/tests/tools/BlockTool.spec.ts +++ b/test/cypress/tests/tools/BlockTool.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* tslint:disable:max-classes-per-file */ import { BlockToolData, ToolSettings } from '../../../../types'; import { ToolType } from '../../../../src/components/tools/base'; diff --git a/test/cypress/tests/tools/BlockTune.spec.ts b/test/cypress/tests/tools/BlockTune.spec.ts index f922a254..9704a5dc 100644 --- a/test/cypress/tests/tools/BlockTune.spec.ts +++ b/test/cypress/tests/tools/BlockTune.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* tslint:disable:max-classes-per-file */ import { ToolSettings } from '../../../../types'; import { ToolType } from '../../../../src/components/tools/base'; diff --git a/test/cypress/tests/tools/InlineTool.spec.ts b/test/cypress/tests/tools/InlineTool.spec.ts index 41c56d9b..70bcbc37 100644 --- a/test/cypress/tests/tools/InlineTool.spec.ts +++ b/test/cypress/tests/tools/InlineTool.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* tslint:disable:max-classes-per-file */ import { ToolSettings } from '../../../../types'; import { ToolType } from '../../../../src/components/tools/base'; diff --git a/test/cypress/tests/tools/ToolsCollection.spec.ts b/test/cypress/tests/tools/ToolsCollection.spec.ts index 1cc62dd8..12609324 100644 --- a/test/cypress/tests/tools/ToolsCollection.spec.ts +++ b/test/cypress/tests/tools/ToolsCollection.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import ToolsCollection from '../../../../src/components/tools/collection'; import BlockTool from '../../../../src/components/tools/block'; import InlineTool from '../../../../src/components/tools/inline'; diff --git a/test/cypress/tests/tools/ToolsFactory.spec.ts b/test/cypress/tests/tools/ToolsFactory.spec.ts index 84d2ff56..4aff4c41 100644 --- a/test/cypress/tests/tools/ToolsFactory.spec.ts +++ b/test/cypress/tests/tools/ToolsFactory.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import LinkInlineTool from '../../../../src/components/inline-tools/inline-tool-link'; import MoveUpTune from '../../../../src/components/block-tunes/block-tune-move-up'; import ToolsFactory from '../../../../src/components/tools/factory'; diff --git a/types/configs/editor-config.d.ts b/types/configs/editor-config.d.ts index dcb213e7..787ed2e5 100644 --- a/types/configs/editor-config.d.ts +++ b/types/configs/editor-config.d.ts @@ -1,5 +1,5 @@ import {ToolConstructable, ToolSettings} from '../tools'; -import {API, LogLevels, OutputData} from '../index'; +import {API, BlockAPI, LogLevels, OutputData} from '../index'; import {SanitizerConfig} from './sanitizer-config'; import {I18nConfig} from './i18n-config'; @@ -88,8 +88,9 @@ export interface EditorConfig { /** * Fires when something changed in DOM * @param {API} api - editor.js api + * @param block - changed block API */ - onChange?(api: API): void; + onChange?(api: API, block: BlockAPI): void; /** * Defines default toolbar for all tools.