Skip to content

Commit

Permalink
chore(on-change): check data-mutation-free of parent nodes (#2548)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
bettysteger and neSpecc authored Jan 10, 2024
1 parent c5ddf91 commit 4bdf7a1
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});

Expand Down
138 changes: 138 additions & 0 deletions test/cypress/tests/onchange.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down

0 comments on commit 4bdf7a1

Please sign in to comment.