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
### 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)

View file

@ -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!')}
});
```

View file

@ -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);
},
});

View file

@ -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);
}
});

View file

@ -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",

View file

@ -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<string, unknown> = {}): Block {
const event = new CustomEvent(mutationType, {
detail: {
target: new BlockAPI(block),
...details,
},
});
this.Editor.ModificationsObserver.onChange(event);
return block;
}

View file

@ -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);
}
}

View file

@ -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,
},
}));
});
});

View file

@ -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.

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