Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 2.31.7

- `Fix` - Trigger `onChange` for native `<select>` changes

### 2.31.6

- `Fix` - Widen `sanitize` type on `BlockTool` and `BaseToolConstructable` to accept per-field `SanitizerConfig`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.31.6",
"version": "2.31.7",
"description": "Editor.js — open source block-style WYSIWYG editor with JSON output",
"main": "dist/editorjs.umd.js",
"module": "dist/editorjs.mjs",
Expand Down
25 changes: 25 additions & 0 deletions src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
/**
* @class Block
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
* @property {BlockTool} tool — current block tool (Paragraph, for example)

Check warning on line 70 in src/components/block/index.ts

View workflow job for this annotation

GitHub Actions / ESlint

The type 'BlockTool' is undefined
* @property {object} CSS — block`s css classes
*/

Expand Down Expand Up @@ -98,7 +98,7 @@

/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
* @property {BlockTool} tool - Tool instance

Check warning on line 101 in src/components/block/index.ts

View workflow job for this annotation

GitHub Actions / ESlint

The type 'BlockTool' is undefined
* @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
*/
Expand Down Expand Up @@ -226,7 +226,7 @@
this.name = tool.name;
this.id = id;
this.settings = tool.settings;
this.config = tool.settings.config || {};

Check warning on line 229 in src/components/block/index.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any value in conditional. An explicit comparison or type cast is required
this.editorEventBus = eventBus || null;
this.blockAPI = new BlockAPI(this);

Expand All @@ -242,6 +242,13 @@

this.holder = this.compose();

/**
* Select value changes do not produce DOM mutations. Listen on the holder
* after Tool handlers run, including when the Tool replaces its root.
* Bind synchronously so deferred initialization cannot rebind after destroy.
*/
this.holder.addEventListener('change', this.handleSelectChange);

/**
* Bind block events in RIC for optimizing of constructing process time
*/
Expand Down Expand Up @@ -679,6 +686,7 @@
* Call Tool instance destroy method
*/
public destroy(): void {
this.holder.removeEventListener('change', this.handleSelectChange);
this.unwatchBlockMutations();
this.removeInputEvents();

Expand Down Expand Up @@ -829,6 +837,23 @@
this.updateCurrentInput();
};

/**
* Handles change events from native select elements.
*
* Select elements are delegated to the Block holder so that dynamically
* replaced Tool roots are observed without binding a new listener to every
* select element.
*
* @param event - native change event
*/
private readonly handleSelectChange = (event: Event): void => {
const target = event.target;

if ($.isElement(target) && target.tagName === 'SELECT' && this.holder.contains(target)) {
this.didMutated();
}
};

/**
* Adds focus event listeners to all inputs and contenteditable
*/
Expand Down Expand Up @@ -917,7 +942,7 @@
node = node.parentElement;
}

return node && (node as HTMLElement).closest('[data-mutation-free="true"]') !== null;

Check warning on line 945 in src/components/block/index.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected object value in conditional. The condition is always true
});
});

Expand Down
46 changes: 46 additions & 0 deletions test/cypress/fixtures/tools/CompositeSelectTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { BlockTool } from '../../../../types';

/**
* A composite Tool containing more than one native select.
*/
export default class CompositeSelectTool implements BlockTool {
/**
* Render two selects inside a single Tool root.
*/
public render(): HTMLDivElement {
const wrapper = document.createElement('div');

['first', 'second'].forEach(name => {
const select = document.createElement('select');

select.dataset.cy = `${name}-select`;

['first', 'second'].forEach(value => {
const option = document.createElement('option');

option.value = value;
option.textContent = value;
select.appendChild(option);
});

wrapper.appendChild(select);
});

return wrapper;
}

/**
* Save both selected values.
*
* @param element - rendered Tool element
*/
public save(element: HTMLElement): { first: string; second: string } {
const first = element.querySelector('[data-cy="first-select"]') as HTMLSelectElement;
const second = element.querySelector('[data-cy="second-select"]') as HTMLSelectElement;

return {
first: first.value,
second: second.value,
};
}
}
34 changes: 34 additions & 0 deletions test/cypress/fixtures/tools/SelectTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { BlockTool } from '../../../../types';

/**
* A native select without a custom change handler.
*/
export default class SelectTool implements BlockTool {
/**
* Render the select as the Tool's root element.
*/
public render(): HTMLSelectElement {
const select = document.createElement('select');

['first', 'second'].forEach(value => {
const option = document.createElement('option');

option.value = value;
option.textContent = value;
select.appendChild(option);
});

return select;
}

/**
* Save the selected value.
*
* @param element - rendered Tool element
*/
public save(element: HTMLElement): { value: string } {
const select = (element.tagName === 'SELECT' ? element : element.querySelector('select')) as HTMLSelectElement;

return { value: select.value };
}
}
Loading
Loading