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
This commit is contained in:
Peter Savchenko 2021-10-05 20:40:44 +03:00 committed by GitHub
parent 1433c1a7dc
commit 4f15bbc0cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 57 deletions

View file

@ -1,5 +1,9 @@
# Changelog # 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 ### 2.22.3
- `Fix` — Tool config is passed to `prepare` method [editor-js/embed#68](https://github.com/editor-js/embed/issues/68) - `Fix` — Tool config is passed to `prepare` method [editor-js/embed#68](https://github.com/editor-js/embed/issues/68)

View file

@ -192,8 +192,9 @@ var editor = new EditorJS({
/** /**
* onChange callback * 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!')}
}); });
``` ```

View file

@ -318,8 +318,8 @@
onReady: function(){ onReady: function(){
saveButton.click(); saveButton.click();
}, },
onChange: function(api, block) { onChange: function(api, event) {
console.log('something changed', block); console.log('something changed', event);
}, },
}); });

View file

@ -281,8 +281,8 @@
onReady: function(){ onReady: function(){
saveButton.click(); saveButton.click();
}, },
onChange: function(api, block) { onChange: function(api, event) {
console.log('something changed', block); console.log('something changed', event);
} }
}); });

View file

@ -1,6 +1,6 @@
{ {
"name": "@editorjs/editorjs", "name": "@editorjs/editorjs",
"version": "2.22.3", "version": "2.23.0",
"description": "Editor.js — Native JS, based on API and Open Source", "description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js", "main": "dist/editor.js",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
@ -29,7 +29,8 @@
"_tools:build": "git submodule foreach yarn build", "_tools:build": "git submodule foreach yarn build",
"_tools:make": "yarn _tools:yarn && yarn _tools:build", "_tools:make": "yarn _tools:yarn && yarn _tools:build",
"tools:update": "yarn _tools:checkout && yarn _tools:pull && yarn _tools:make", "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", "author": "CodeX",
"license": "Apache-2.0", "license": "Apache-2.0",

View file

@ -14,6 +14,7 @@ import Blocks from '../blocks';
import { BlockToolData, PasteEvent } from '../../../types'; import { BlockToolData, PasteEvent } from '../../../types';
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
import BlockAPI from '../block/api'; import BlockAPI from '../block/api';
import { BlockMutationType } from '../../../types/events/block/mutation-type';
/** /**
* @typedef {BlockManager} BlockManager * @typedef {BlockManager} BlockManager
@ -289,12 +290,24 @@ export default class BlockManager extends Module {
tunes, 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); this._blocks.insert(newIndex, block, replace);
/** /**
* Force call of didMutated event on Block insertion * Force call of didMutated event on Block insertion
*/ */
this.blockDidMutated(block); this.blockDidMutated(BlockMutationType.Added, block, {
index: newIndex,
});
if (needToFocus) { if (needToFocus) {
this.currentBlockIndex = newIndex; this.currentBlockIndex = newIndex;
@ -370,7 +383,9 @@ export default class BlockManager extends Module {
/** /**
* Force call of didMutated event on Block insertion * Force call of didMutated event on Block insertion
*/ */
this.blockDidMutated(block); this.blockDidMutated(BlockMutationType.Added, block, {
index,
});
if (needToFocus) { if (needToFocus) {
this.currentBlockIndex = index; this.currentBlockIndex = index;
@ -445,7 +460,9 @@ export default class BlockManager extends Module {
/** /**
* Force call of didMutated event on Block removal * Force call of didMutated event on Block removal
*/ */
this.blockDidMutated(blockToRemove); this.blockDidMutated(BlockMutationType.Removed, blockToRemove, {
index,
});
if (this.currentBlockIndex >= index) { if (this.currentBlockIndex >= index) {
this.currentBlockIndex--; this.currentBlockIndex--;
@ -721,7 +738,10 @@ export default class BlockManager extends Module {
/** /**
* Force call of didMutated event on Block movement * 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); 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 * Block mutation callback
* *
* @param mutationType - what happened with block
* @param block - mutated block * @param block - mutated block
* @param details - additional data to pass with change event
*/ */
private blockDidMutated(block: Block): Block { private blockDidMutated(mutationType: BlockMutationType, block: Block, details: Record<string, unknown> = {}): Block {
this.Editor.ModificationsObserver.onChange(new BlockAPI(block)); const event = new CustomEvent(mutationType, {
detail: {
target: new BlockAPI(block),
...details,
},
});
this.Editor.ModificationsObserver.onChange(event);
return block; return block;
} }

View file

@ -1,5 +1,4 @@
import Module from '../__module'; import Module from '../__module';
import { BlockAPI } from '../../../types';
import * as _ from '../utils'; import * as _ from '../utils';
/** /**
@ -28,13 +27,13 @@ export default class ModificationsObserver extends Module {
/** /**
* Call onChange event passed to Editor.js configuration * 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)) { if (this.disabled || !_.isFunction(this.config.onChange)) {
return; return;
} }
this.config.onChange(this.Editor.API.methods, block); this.config.onChange(this.Editor.API.methods, event);
} }
} }

View file

@ -1,48 +1,98 @@
import Header from '../../../example/tools/header'; 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 checks that correct block API object is passed to onChange
* @todo Add cases for native inputs changes * @todo Add cases for native inputs changes
* @todo debug onChange firing on Block Tune toggling (see below)
*/ */
describe('onChange callback', () => { describe('onChange callback', () => {
const config = { /**
tools: { * Creates Editor instance
header: Header, *
}, * @param blocks - list of blocks to prefill the editor
onChange: (): void => { */
console.log('something changed'); 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(() => { cy.spy(config, 'onChange').as('onChange');
if (this && this.editorInstance) {
this.editorInstance.destroy();
} else {
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]') cy.get('[data-cy=editorjs]')
.get('div.ce-block') .get('div.ce-block')
.click() .click()
.type('{enter}'); .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', () => { it('should fire onChange callback on typing into block', () => {
createEditor();
cy.get('[data-cy=editorjs]') cy.get('[data-cy=editorjs]')
.get('div.ce-block') .get('div.ce-block')
.click() .click()
.type('some text'); .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]') cy.get('[data-cy=editorjs]')
.get('div.ce-block') .get('div.ce-block')
.click(); .click();
@ -55,21 +105,36 @@ describe('onChange callback', () => {
.get('li.ce-toolbox__button[data-tool=header]') .get('li.ce-toolbox__button[data-tool=header]')
.click(); .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', () => { it('should fire onChange callback on tune modifying', () => {
cy.get('[data-cy=editorjs]') createEditor([
.get('div.ce-block') {
.click(); type: 'header',
data: {
cy.get('[data-cy=editorjs]') text: 'Header block',
.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]') cy.get('[data-cy=editorjs]')
.get('div.ce-block') .get('div.ce-block')
@ -80,13 +145,30 @@ describe('onChange callback', () => {
.click(); .click();
cy.get('[data-cy=editorjs]') 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(); .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', () => { it('should fire onChange callback when block is removed', () => {
createEditor();
cy.get('[data-cy=editorjs]') cy.get('[data-cy=editorjs]')
.get('div.ce-block') .get('div.ce-block')
.click(); .click();
@ -100,10 +182,17 @@ describe('onChange callback', () => {
.click() .click()
.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', () => { it('should fire onChange callback when block is moved', () => {
createEditor();
cy.get('[data-cy=editorjs]') cy.get('[data-cy=editorjs]')
.get('div.ce-block') .get('div.ce-block')
.click() .click()
@ -122,6 +211,12 @@ describe('onChange callback', () => {
.get('div.ce-tune-move-up') .get('div.ce-tune-move-up')
.click(); .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,
},
}));
}); });
}); });

View file

@ -90,9 +90,9 @@ export interface EditorConfig {
/** /**
* Fires when something changed in DOM * Fires when something changed in DOM
* @param {API} api - editor.js api * @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. * Defines default toolbar for all tools.

View file

@ -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',
}