chore(on-change): check data-mutation-free of parent nodes (#2548)

* add data-mutation-free=deep

* just use closest and reduce waiting time in test

* Update src/components/block/index.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Update test/cypress/tests/onchange.cy.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* add data-mutation-free=deep

* just use closest and reduce waiting time in test

* Update src/components/block/index.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Update test/cypress/tests/onchange.cy.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* add line in Changelog

* Update docs/CHANGELOG.md

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* add support for characterData mutations

* Update onchange.cy.ts

---------

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
Betty Steger 2024-01-10 20:27:19 +01:00 committed by GitHub
parent c5ddf91189
commit 4bdf7a12b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 2 deletions

View file

@ -17,6 +17,8 @@
- `Fix` — Caret losing on Mobile Devices when adding a block via Toolbox or via Backspace at the beginning of a Block
- `Improvement` — Now you can set focus via arrows/Tab to "contentless" (decorative) blocks like Delimiter which have no inputs.
- `Improvement` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.
- `Improvement` - Now the `data-mutation-free` supports deep nesting, so you can mark some element with it to prevent the onChange call caused by child element mutating
- `Improvement` - Now the `data-mutation-free` also allows to skip "characterData" mutations (eg. text content change)
- `Refactoring``ce-block--focused` class toggling removed as unused.
### 2.28.2

View file

@ -898,10 +898,13 @@ export default class Block extends EventsDispatcher<BlockEvents> {
return changedNodes.some((node) => {
if (!$.isElement(node)) {
return false;
/**
* "characterData" mutation record has Text node as a target, so we need to get parent element to check it for mutation-free attribute
*/
node = node.parentElement;
}
return (node as HTMLElement).dataset.mutationFree === 'true';
return node && (node as HTMLElement).closest('[data-mutation-free="true"]') !== null;
});
});

View file

@ -544,6 +544,144 @@ describe('onChange callback', () => {
});
});
it('should not be fired when mutation happened in a child of element with the "data-mutation-free" mark', () => {
/**
* Mock for tool wrapper which we will mutate in a test
*/
const toolWrapper = document.createElement('div');
const toolChild = document.createElement('div');
toolWrapper.appendChild(toolChild);
/**
* Mark it as mutation-free
*/
toolWrapper.dataset.mutationFree = 'true';
/**
* Mock of tool with data-mutation-free attribute
*/
class ToolWithMutationFreeAttribute {
/**
* Simply return mocked element
*/
public render(): HTMLElement {
return toolWrapper;
}
/**
* Saving logic is not necessary for this test
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
public save(): void {}
}
const editorConfig = {
tools: {
testTool: ToolWithMutationFreeAttribute,
},
onChange: (api, event): void => {
console.log('something changed', event);
},
data: {
blocks: [
{
type: 'testTool',
data: {},
},
],
},
};
cy.spy(editorConfig, 'onChange').as('onChange');
cy.createEditor(editorConfig).as('editorInstance');
/**
* Emulate tool's internal attribute mutation
*/
cy.wait(100).then(() => {
toolChild.setAttribute('some-changed-attr', 'some-new-value');
});
/**
* Check that onChange callback was not called
*/
cy.wait(500).then(() => {
cy.get('@onChange').should('have.callCount', 0);
});
});
it('should not be fired when "characterData" mutation happened in a child of element with the "data-mutation-free" mark', () => {
/**
* Mock for tool wrapper which we will mutate in a test
*/
const toolWrapper = document.createElement('div');
const toolChild = document.createElement('div');
toolChild.setAttribute('data-cy', 'tool-child');
toolChild.setAttribute('contenteditable', 'true');
toolWrapper.appendChild(toolChild);
/**
* Mark it as mutation-free
*/
toolWrapper.dataset.mutationFree = 'true';
/**
* Mock of tool with data-mutation-free attribute
*/
class ToolWithMutationFreeAttribute {
/**
* Simply return mocked element
*/
public render(): HTMLElement {
return toolWrapper;
}
/**
* Saving logic is not necessary for this test
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
public save(): void {}
}
const editorConfig = {
tools: {
testTool: ToolWithMutationFreeAttribute,
},
onChange: function (api, event) {
console.log('something changed!!!!!!!!', event);
},
data: {
blocks: [
{
type: 'testTool',
data: {},
},
],
},
};
cy.spy(editorConfig, 'onChange').as('onChange');
cy.createEditor(editorConfig).as('editorInstance');
/**
* Emulate tool's child-element text typing
*/
cy.get('[data-cy=editorjs')
.get('[data-cy=tool-child]')
.click()
.type('some text');
/**
* Check that onChange callback was not called
*/
cy.wait(500).then(() => {
cy.get('@onChange').should('have.callCount', 0);
});
});
it('should be called on blocks.clear() with removed and added blocks', () => {
createEditor([
{