From 4f15bbc0cbc048ec7a4131c8491f3f8ae2f249e0 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Tue, 5 Oct 2021 20:40:44 +0300 Subject: [PATCH] feat(onchange): callback now accepts custom event (#1791) * feat(onchange): callback now accepts custom event * Delete block-added.ts * testd updated, changelog added * Update example-dev.html * indexes added to all events * block-removed dispatching on block replacing * Update example-dev.html --- docs/CHANGELOG.md | 4 + docs/installation.md | 3 +- example/example-dev.html | 4 +- example/example.html | 4 +- package.json | 5 +- src/components/modules/blockManager.ts | 47 ++++- .../modules/modificationsObserver.ts | 7 +- test/cypress/tests/onchange.spec.ts | 169 ++++++++++++++---- types/configs/editor-config.d.ts | 4 +- types/events/block/mutation-type.ts | 24 +++ 10 files changed, 214 insertions(+), 57 deletions(-) create mode 100644 types/events/block/mutation-type.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2c783388..718f1a88 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.23.0 + +- `Improvement` — The `onChange` callback now accepts two arguments: EditorJS API and the CustomEvent with `type` and `detail` allowing to determine what happened with a Block + ### 2.22.3 - `Fix` — Tool config is passed to `prepare` method [editor-js/embed#68](https://github.com/editor-js/embed/issues/68) diff --git a/docs/installation.md b/docs/installation.md index 29565665..6d90c635 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -192,8 +192,9 @@ var editor = new EditorJS({ /** * onChange callback + * Accepts CustomEvent describing what happened */ - onChange: (editorAPI, affectedBlockAPI) => {console.log('Now I know that Editor\'s content changed!')} + onChange: (editorAPI, event) => {console.log('Now I know that Editor\'s content changed!')} }); ``` diff --git a/example/example-dev.html b/example/example-dev.html index 6a69c428..8064cb07 100644 --- a/example/example-dev.html +++ b/example/example-dev.html @@ -318,8 +318,8 @@ onReady: function(){ saveButton.click(); }, - onChange: function(api, block) { - console.log('something changed', block); + onChange: function(api, event) { + console.log('something changed', event); }, }); diff --git a/example/example.html b/example/example.html index abc6a5f6..af62eb72 100644 --- a/example/example.html +++ b/example/example.html @@ -281,8 +281,8 @@ onReady: function(){ saveButton.click(); }, - onChange: function(api, block) { - console.log('something changed', block); + onChange: function(api, event) { + console.log('something changed', event); } }); diff --git a/package.json b/package.json index 8f9f11f4..63c0a0c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.22.3", + "version": "2.23.0", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", @@ -29,7 +29,8 @@ "_tools:build": "git submodule foreach yarn build", "_tools:make": "yarn _tools:yarn && yarn _tools:build", "tools:update": "yarn _tools:checkout && yarn _tools:pull && yarn _tools:make", - "test:e2e": "yarn build && cypress run" + "test:e2e": "yarn build && cypress run", + "test:e2e:open": "yarn build && cypress open" }, "author": "CodeX", "license": "Apache-2.0", diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 41e18865..10da5ef2 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -14,6 +14,7 @@ import Blocks from '../blocks'; import { BlockToolData, PasteEvent } from '../../../types'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import BlockAPI from '../block/api'; +import { BlockMutationType } from '../../../types/events/block/mutation-type'; /** * @typedef {BlockManager} BlockManager @@ -289,12 +290,24 @@ export default class BlockManager extends Module { tunes, }); + /** + * In case of block replacing (Converting OR from Toolbox or Shortcut on empty block OR on-paste to empty block) + * we need to dispatch the 'block-removing' event for the replacing block + */ + if (replace) { + this.blockDidMutated(BlockMutationType.Removed, this.getBlockByIndex(newIndex), { + index: newIndex, + }); + } + this._blocks.insert(newIndex, block, replace); /** * Force call of didMutated event on Block insertion */ - this.blockDidMutated(block); + this.blockDidMutated(BlockMutationType.Added, block, { + index: newIndex, + }); if (needToFocus) { this.currentBlockIndex = newIndex; @@ -370,7 +383,9 @@ export default class BlockManager extends Module { /** * Force call of didMutated event on Block insertion */ - this.blockDidMutated(block); + this.blockDidMutated(BlockMutationType.Added, block, { + index, + }); if (needToFocus) { this.currentBlockIndex = index; @@ -445,7 +460,9 @@ export default class BlockManager extends Module { /** * Force call of didMutated event on Block removal */ - this.blockDidMutated(blockToRemove); + this.blockDidMutated(BlockMutationType.Removed, blockToRemove, { + index, + }); if (this.currentBlockIndex >= index) { this.currentBlockIndex--; @@ -721,7 +738,10 @@ export default class BlockManager extends Module { /** * Force call of didMutated event on Block movement */ - this.blockDidMutated(this.currentBlock); + this.blockDidMutated(BlockMutationType.Moved, this.currentBlock, { + fromIndex, + toIndex, + }); } /** @@ -788,7 +808,11 @@ export default class BlockManager extends Module { BlockEvents.dragLeave(event); }); - block.on('didMutated', (affectedBlock: Block) => this.blockDidMutated(affectedBlock)); + block.on('didMutated', (affectedBlock: Block) => { + return this.blockDidMutated(BlockMutationType.Changed, affectedBlock, { + index: this.getBlockIndex(affectedBlock), + }); + }); } /** @@ -828,10 +852,19 @@ export default class BlockManager extends Module { /** * Block mutation callback * + * @param mutationType - what happened with block * @param block - mutated block + * @param details - additional data to pass with change event */ - private blockDidMutated(block: Block): Block { - this.Editor.ModificationsObserver.onChange(new BlockAPI(block)); + private blockDidMutated(mutationType: BlockMutationType, block: Block, details: Record = {}): Block { + const event = new CustomEvent(mutationType, { + detail: { + target: new BlockAPI(block), + ...details, + }, + }); + + this.Editor.ModificationsObserver.onChange(event); return block; } diff --git a/src/components/modules/modificationsObserver.ts b/src/components/modules/modificationsObserver.ts index c1fc5123..af4f4d33 100644 --- a/src/components/modules/modificationsObserver.ts +++ b/src/components/modules/modificationsObserver.ts @@ -1,5 +1,4 @@ import Module from '../__module'; -import { BlockAPI } from '../../../types'; import * as _ from '../utils'; /** @@ -28,13 +27,13 @@ export default class ModificationsObserver extends Module { /** * Call onChange event passed to Editor.js configuration * - * @param block - changed Block + * @param event - some of our custom change events */ - public onChange(block: BlockAPI): void { + public onChange(event: CustomEvent): void { if (this.disabled || !_.isFunction(this.config.onChange)) { return; } - this.config.onChange(this.Editor.API.methods, block); + this.config.onChange(this.Editor.API.methods, event); } } diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts index 9e8bdaea..ba41574f 100644 --- a/test/cypress/tests/onchange.spec.ts +++ b/test/cypress/tests/onchange.spec.ts @@ -1,48 +1,98 @@ import Header from '../../../example/tools/header'; +import { BlockMutationType } from '../../../types/events/block/mutation-type'; /** * @todo Add checks that correct block API object is passed to onChange * @todo Add cases for native inputs changes + * @todo debug onChange firing on Block Tune toggling (see below) */ describe('onChange callback', () => { - const config = { - tools: { - header: Header, - }, - onChange: (): void => { - console.log('something changed'); - }, - }; + /** + * Creates Editor instance + * + * @param blocks - list of blocks to prefill the editor + */ + function createEditor(blocks = null): void { + const config = { + tools: { + header: Header, + }, + onChange: (api, event): void => { + console.log('something changed', api, event); + }, + data: blocks ? { + blocks, + } : null, + }; - beforeEach(() => { - if (this && this.editorInstance) { - this.editorInstance.destroy(); - } else { - cy.spy(config, 'onChange').as('onChange'); + cy.spy(config, 'onChange').as('onChange'); - cy.createEditor(config).as('editorInstance'); - } - }); + cy.createEditor(config).as('editorInstance'); + } + + /** + * EditorJS API is passed as the first parameter of the onChange callback + */ + const EditorJSApiMock = Cypress.sinon.match.any; + + it('should fire onChange callback with correct index on block insertion above the current (by pressing Enter at the start)', () => { + createEditor(); - 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'); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Added, + detail: { + target: { + name: 'paragraph' + }, + index: 0, + }, + })); + }); + + it('should fire onChange callback with correct index on block insertion below the current (by pressing enter at the end)', () => { + createEditor(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('some text') + .type('{enter}'); + + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Added, + detail: { + target: { + name: 'paragraph' + }, + index: 1, + }, + })); }); it('should fire onChange callback on typing into block', () => { + createEditor(); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() .type('some text'); - cy.get('@onChange').should('be.called'); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Changed, + detail: { + index: 0 + }, + })); }); - it('should fire onChange callback on block replacement', () => { + it('should fire onChange callback on block replacement for both of blocks', () => { + createEditor(); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click(); @@ -55,21 +105,36 @@ describe('onChange callback', () => { .get('li.ce-toolbox__button[data-tool=header]') .click(); - cy.get('@onChange').should('be.calledWithMatch', Cypress.sinon.match.any, Cypress.sinon.match({ name: 'header' })); + cy.get('@onChange').should('be.calledTwice'); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Removed, + detail: { + index: 0, + target: { + name: 'paragraph', + }, + }, + })); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Added, + detail: { + index: 0, + target: { + 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(); + it('should fire onChange callback on tune modifying', () => { + createEditor([ + { + type: 'header', + data: { + text: 'Header block', + }, + }, + ]); cy.get('[data-cy=editorjs]') .get('div.ce-block') @@ -80,13 +145,30 @@ describe('onChange callback', () => { .click(); cy.get('[data-cy=editorjs]') - .get('span.cdx-settings-button[data-level=1]') + .get('span.cdx-settings-button[data-level=4]') + .click() + /** + * For some reason, the first click fires the mutation of removeFakeCursor only, so we need to click again. + * Reproduced only in Cypress. + * + * @todo debug it later + */ .click(); - cy.get('@onChange').should('be.calledWithMatch', Cypress.sinon.match.any, Cypress.sinon.match({ name: 'header' })); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Changed, + detail: { + index: 0, + target: { + name: 'header', + }, + }, + })); }); it('should fire onChange callback when block is removed', () => { + createEditor(); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click(); @@ -100,10 +182,17 @@ describe('onChange callback', () => { .click() .click(); - cy.get('@onChange').should('be.called'); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Removed, + detail: { + index: 0 + }, + })); }); it('should fire onChange callback when block is moved', () => { + createEditor(); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -122,6 +211,12 @@ describe('onChange callback', () => { .get('div.ce-tune-move-up') .click(); - cy.get('@onChange').should('be.called'); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Moved, + detail: { + fromIndex: 1, + toIndex: 0, + }, + })); }); }); diff --git a/types/configs/editor-config.d.ts b/types/configs/editor-config.d.ts index 4e93f6ce..cb5a93ed 100644 --- a/types/configs/editor-config.d.ts +++ b/types/configs/editor-config.d.ts @@ -90,9 +90,9 @@ export interface EditorConfig { /** * Fires when something changed in DOM * @param {API} api - editor.js api - * @param block - changed block API + * @param event - custom event describing mutation */ - onChange?(api: API, block: BlockAPI): void; + onChange?(api: API, event: CustomEvent): void; /** * Defines default toolbar for all tools. diff --git a/types/events/block/mutation-type.ts b/types/events/block/mutation-type.ts new file mode 100644 index 00000000..b8165607 --- /dev/null +++ b/types/events/block/mutation-type.ts @@ -0,0 +1,24 @@ +/** + * What kind of modification happened with the Block + */ +export enum BlockMutationType { + /** + * New Block added + */ + Added = 'block-added', + + /** + * On Block deletion + */ + Removed = 'block-removed', + + /** + * Moving of a Block + */ + Moved = 'block-moved', + + /** + * Any changes inside the Block + */ + Changed = 'block-changed', +}