From 8f365f006328a36faacf60352e20a0fc365c227b Mon Sep 17 00:00:00 2001 From: Nikita Melnikov Date: Tue, 20 Aug 2024 22:31:21 +0100 Subject: [PATCH 01/11] chore: updgrade cypress to solve the issue with firefox 129 (#2817) --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 15405f6e..1647190f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "codex-notifier": "^1.1.2", "codex-tooltip": "^1.0.5", "core-js": "3.30.0", - "cypress": "^13.7.1", + "cypress": "^13.13.3", "cypress-intellij-reporter": "^0.0.7", "cypress-plugin-tab": "^1.0.5", "cypress-terminal-report": "^5.3.2", diff --git a/yarn.lock b/yarn.lock index f616c23e..29cccc68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -504,7 +504,7 @@ js-yaml "4.1.0" nyc "15.1.0" -"@cypress/request@^3.0.0": +"@cypress/request@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== @@ -1718,12 +1718,12 @@ cypress-vite@^1.5.0: chokidar "^3.5.3" debug "^4.3.4" -cypress@^13.7.1: - version "13.7.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.1.tgz#d1208eb04efd46ef52a30480a5da71a03373261a" - integrity sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w== +cypress@^13.13.3: + version "13.13.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.3.tgz#21ee054bb4e00b3858f2e33b4f8f4e69128470a9" + integrity sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw== dependencies: - "@cypress/request" "^3.0.0" + "@cypress/request" "^3.0.1" "@cypress/xvfb" "^1.2.4" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" @@ -1762,7 +1762,7 @@ cypress@^13.7.1: request-progress "^3.0.0" semver "^7.5.3" supports-color "^8.1.1" - tmp "~0.2.1" + tmp "~0.2.3" untildify "^4.0.0" yauzl "^2.10.0" @@ -4999,7 +4999,7 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tmp@~0.2.1: +tmp@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== From c82933616c1f4f7afbb45ca839916c1decba13dd Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Sat, 14 Sep 2024 00:39:19 +0300 Subject: [PATCH 02/11] Fixed display of conversion menu for blocks without export rule (#2799) * Fixed display of convert menu for blocks without export rule According to the workflow script from the documentation: https://editorjs.io/tools-api/#conversionconfig * Update CHANGELOG.md * some improvements and tests --------- Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 4 ++ src/components/utils/blocks.ts | 22 ++++++++-- .../tools/ToolWithoutConversionExport.ts | 23 +++++++++++ test/cypress/tests/ui/BlockTunes.cy.ts | 40 +++++++++++++++++-- test/cypress/tests/utils/flipper.cy.ts | 4 +- types/tools/block-tool.d.ts | 6 +-- types/tools/tool.d.ts | 23 +++++++---- 7 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 test/cypress/fixtures/tools/ToolWithoutConversionExport.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bce98d8e..eacada6d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.30.6 + +– `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified + ### 2.30.5 – `Fix` – Fix exported types diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index 471bb864..fb564223 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -51,6 +51,15 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools const savedData = await block.save() as SavedData; const blockData = savedData.data; + /** + * Checking that the block's tool has an «export» rule + */ + const blockTool = allBlockTools.find((tool) => tool.name === block.name); + + if (blockTool !== undefined && !isToolConvertable(blockTool, 'export')) { + return []; + } + return allBlockTools.reduce((result, tool) => { /** * Skip tools without «import» rule specified @@ -59,12 +68,19 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools return result; } + /** + * Skip tools that does not specify toolbox + */ + if (tool.toolbox === undefined) { + return result; + } + /** Filter out invalid toolbox entries */ const actualToolboxItems = tool.toolbox.filter((toolboxItem) => { /** * Skip items that don't pass 'toolbox' property or do not have an icon */ - if (isEmpty(toolboxItem) || !toolboxItem.icon) { + if (isEmpty(toolboxItem) || toolboxItem.icon === undefined) { return false; } @@ -86,10 +102,10 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools result.push({ ...tool, toolbox: actualToolboxItems, - }); + } as BlockToolAdapter); return result; - }, []); + }, [] as BlockToolAdapter[]); } diff --git a/test/cypress/fixtures/tools/ToolWithoutConversionExport.ts b/test/cypress/fixtures/tools/ToolWithoutConversionExport.ts new file mode 100644 index 00000000..77f43f98 --- /dev/null +++ b/test/cypress/fixtures/tools/ToolWithoutConversionExport.ts @@ -0,0 +1,23 @@ +import type { ConversionConfig } from '@/types/configs/conversion-config'; +import ToolMock from './ToolMock'; + +/** + * This tool has a conversionConfig, but it doesn't have export property. + * + * That means that tool can be created from string, but can't be converted to string. + */ +export class ToolWithoutConversionExport extends ToolMock { + /** + * Rules specified how our Tool can be converted to/from other Tool. + */ + public static get conversionConfig(): ConversionConfig { + return { + import: 'text', // this tool can be created from string + + /** + * Here is no "export" property, so this tool can't be converted to string + */ + // export: (data) => data.text, + }; + } +} diff --git a/test/cypress/tests/ui/BlockTunes.cy.ts b/test/cypress/tests/ui/BlockTunes.cy.ts index b5f39c07..43d7e0e5 100644 --- a/test/cypress/tests/ui/BlockTunes.cy.ts +++ b/test/cypress/tests/ui/BlockTunes.cy.ts @@ -1,8 +1,8 @@ import { selectionChangeDebounceTimeout } from '../../../../src/components/constants'; import Header from '@editorjs/header'; -import type { ToolboxConfig } from '../../../../types'; +import type { ConversionConfig, ToolboxConfig } from '../../../../types'; import type { MenuConfig } from '../../../../types/tools'; - +import { ToolWithoutConversionExport } from '../../fixtures/tools/ToolWithoutConversionExport'; describe('BlockTunes', function () { describe('Search', () => { @@ -185,6 +185,39 @@ describe('BlockTunes', function () { .should('not.exist'); }); + it('should not display the ConvertTo control if block has no conversionConfig.export specified', () => { + cy.createEditor({ + tools: { + testTool: ToolWithoutConversionExport, + }, + data: { + blocks: [ + { + type: 'testTool', + data: { + text: 'Some text', + }, + }, + ], + }, + }).as('editorInstance'); + + cy.get('@editorInstance') + .get('[data-cy=editorjs]') + .find('.ce-block') + .click(); + + cy.get('@editorInstance') + .get('[data-cy=editorjs]') + .find('.ce-toolbar__settings-btn') + .click(); + + cy.get('@editorInstance') + .get('[data-cy=editorjs]') + .find('.ce-popover-item[data-item-name=convert-to]') + .should('not.exist'); + }); + it('should not display tool with the same data in "Convert to" menu', () => { /** * Tool with several toolbox entries configured @@ -193,9 +226,10 @@ describe('BlockTunes', function () { /** * Tool is convertable */ - public static get conversionConfig(): { import: string } { + public static get conversionConfig(): ConversionConfig { return { import: 'text', + export: 'text', }; } diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 09be6815..114a38e1 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -87,9 +87,9 @@ describe('Flipper', () => { .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE }); /** - * Check whether we focus the Move Up Tune or not + * Check whether we focus the Delete Tune or not */ - cy.get('[data-item-name="move-up"]') + cy.get('[data-item-name="delete"]') .should('have.class', 'ce-popover-item--focused'); cy.get('[data-cy=editorjs]') diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index ae02161b..ddf47896 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -1,6 +1,6 @@ import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs'; import { BlockToolData } from './block-tool-data'; -import { BaseTool, BaseToolConstructable } from './tool'; +import { BaseTool, BaseToolConstructable, BaseToolConstructorOptions } from './tool'; import { ToolConfig } from './tool-config'; import { API, BlockAPI, ToolboxConfig } from '../index'; import { PasteEvent } from './paste-events'; @@ -83,10 +83,8 @@ export interface BlockTool extends BaseTool { /** * Describe constructor parameters */ -export interface BlockToolConstructorOptions { - api: API; +export interface BlockToolConstructorOptions extends BaseToolConstructorOptions { data: BlockToolData; - config: ToolConfig; block: BlockAPI; readOnly: boolean; } diff --git a/types/tools/tool.d.ts b/types/tools/tool.d.ts index 184000eb..17aa0f2d 100644 --- a/types/tools/tool.d.ts +++ b/types/tools/tool.d.ts @@ -9,15 +9,27 @@ import {MenuConfig} from './menu-config'; export interface BaseTool { /** * Tool`s render method - * - * For Inline Tools may return either HTMLElement (deprecated) or {@link MenuConfig} + * + * For Inline Tools may return either HTMLElement (deprecated) or {@link MenuConfig} * @see https://editorjs.io/menu-config - * + * * For Block Tools returns tool`s wrapper html element */ render(): RenderReturnType | Promise; } +export interface BaseToolConstructorOptions { + /** + * Editor.js API + */ + api: API; + + /** + * Tool configuration + */ + config?: ToolConfig; +} + export interface BaseToolConstructable { /** * Define Tool type as Inline @@ -35,11 +47,6 @@ export interface BaseToolConstructable { */ title?: string; - /** - * Describe constructor parameters - */ - new (config: {api: API, config?: ToolConfig}): BaseTool; - /** * Tool`s prepare method. Can be async * @param data From 2f9696a000a79cd0cf7df695be039969d5e6a6b2 Mon Sep 17 00:00:00 2001 From: Angus MacIsaac Date: Fri, 13 Sep 2024 19:00:52 -0300 Subject: [PATCH 03/11] Swallow getLayoutMap() error (#2790) * Fix getLayoutMap() bug * Update CHANGELOG.md --------- Co-authored-by: Angus Co-authored-by: Peter --- docs/CHANGELOG.md | 1 + src/components/utils/keyboard.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index eacada6d..5c50865e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ ### 2.30.6 – `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified +– `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome ### 2.30.5 diff --git a/src/components/utils/keyboard.ts b/src/components/utils/keyboard.ts index 62586d57..65d8e614 100644 --- a/src/components/utils/keyboard.ts +++ b/src/components/utils/keyboard.ts @@ -47,8 +47,15 @@ export async function getKeyboardKeyForCode(code: string, fallback: string): Pro return fallback; } - const map = await keyboard.getLayoutMap(); - const key = map.get(code); + try { + const map = await keyboard.getLayoutMap(); - return key || fallback; + const key = map.get(code); + + return key || fallback; + } catch (e) { + console.error(e); + + return fallback; + } } From b6ba44d61039ddca08275673a81695fe637b66b3 Mon Sep 17 00:00:00 2001 From: Ilya Gorenburg Date: Fri, 13 Sep 2024 18:12:46 -0400 Subject: [PATCH 04/11] fix: prevent inline toolbar from closing in nested instance of editor (#2780) * fix: prevent inline toolbar from closing in nested instance of editor * docs: updated changelog.md with fix description * fix: fix import to use `type` --------- Co-authored-by: Peter --- docs/CHANGELOG.md | 1 + src/components/modules/ui.ts | 6 ++- .../support/utils/nestedEditorInstance.ts | 31 +++++++++++ .../cypress/tests/modules/InlineToolbar.cy.ts | 53 +++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 test/cypress/support/utils/nestedEditorInstance.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5c50865e..eecd87f9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ – `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified – `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome +- `Fix` - Prevent inline toolbar from closing in nested instance of editor ### 2.30.5 diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index f9c3cbef..6bbcdb8c 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -847,9 +847,11 @@ export default class UI extends Module { /** * Event can be fired on clicks at non-block-content elements, - * for example, at the Inline Toolbar or some Block Tune element + * for example, at the Inline Toolbar or some Block Tune element. + * We also make sure that the closest block belongs to the current editor and not a parent */ - const clickedOutsideBlockContent = focusedElement.closest(`.${Block.CSS.content}`) === null; + const closestBlock = focusedElement.closest(`.${Block.CSS.content}`); + const clickedOutsideBlockContent = closestBlock === null || (closestBlock.closest(`.${Selection.CSS.editorWrapper}`) !== this.nodes.wrapper); if (clickedOutsideBlockContent) { /** diff --git a/test/cypress/support/utils/nestedEditorInstance.ts b/test/cypress/support/utils/nestedEditorInstance.ts new file mode 100644 index 00000000..f335cbce --- /dev/null +++ b/test/cypress/support/utils/nestedEditorInstance.ts @@ -0,0 +1,31 @@ +import type { BlockTool, BlockToolConstructorOptions } from '../../../../types'; +import { createEditorWithTextBlocks } from './createEditorWithTextBlocks'; + +export const NESTED_EDITOR_ID = 'nested-editor'; + +/** + * Creates nested Editor instance with paragraph block + */ +export default class NestedEditor implements BlockTool { + private data: { text: string }; + + constructor(value: BlockToolConstructorOptions) { + this.data = value.data; + } + + public render(): HTMLDivElement { + const editorEl = Object.assign(document.createElement('div'), { + id: NESTED_EDITOR_ID, + }); + + editorEl.setAttribute('data-cy', NESTED_EDITOR_ID); + + createEditorWithTextBlocks([ this.data.text ], { holder: NESTED_EDITOR_ID }); + + return editorEl; + } + + public save(): string { + return this.data.text; + } +} diff --git a/test/cypress/tests/modules/InlineToolbar.cy.ts b/test/cypress/tests/modules/InlineToolbar.cy.ts index bc8990bd..ebdb71d6 100644 --- a/test/cypress/tests/modules/InlineToolbar.cy.ts +++ b/test/cypress/tests/modules/InlineToolbar.cy.ts @@ -1,4 +1,5 @@ import Header from '@editorjs/header'; +import NestedEditor, { NESTED_EDITOR_ID } from '../../support/utils/nestedEditorInstance'; describe('Inline Toolbar', () => { it('should appear aligned with left coord of selection rect', () => { @@ -164,4 +165,56 @@ describe('Inline Toolbar', () => { }); }); }); + + describe('Nested Editor instance inline toolbar', () => { + it('should not close inline toolbar of the nested Editor instance when clicking within that toolbar', () => { + cy.createEditor({ + tools: { + nestedEditor: { + class: NestedEditor, + }, + }, + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + }, + { + type: 'nestedEditor', + data: { + text: 'Nunc pellentesque, tortor nec luctus venenatis', + }, + }, + ], + }, + }); + + cy.get(`[data-cy=${NESTED_EDITOR_ID}]`) + .find('.ce-paragraph') + .selectText('tortor nec luctus'); + + cy.get(`[data-cy=${NESTED_EDITOR_ID}]`) + .find('[data-item-name=link]') + .click(); + + // `wait()` function below is required. without it the test will always pass + // because cypress types the text in the field without delay, while we need some delay (just like user) + // to test the actual case that nested editor inline toolbar is still visible and not closed + + cy.get(`[data-cy=${NESTED_EDITOR_ID}]`) + .find('.ce-inline-tool-input') + .click() + .wait(100) + .type('https://editorjs.io'); + + cy.get(`[data-cy=${NESTED_EDITOR_ID}]`) + .find('.ce-popover__container') + .then(($toolbar) => { + expect($toolbar).to.be.visible; + }); + }); + }); }); From 3aa164d2d082890ca2eaff799b1d951a08fc4ba7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 01:18:43 +0300 Subject: [PATCH 05/11] Bump version up to 2.30.6-rc.0 (#2818) * Bump version * 2.30.6 --------- Co-authored-by: github-actions Co-authored-by: Peter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1647190f..6d0917c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.30.5", + "version": "2.30.6", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From 2275ddfc3ae9a3c5913d9f2e79c5ab13c2d479c2 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 8 Oct 2024 11:38:22 +0300 Subject: [PATCH 06/11] feat(inline-toolbar): inline tools now can be used in the readonly mode (#2832) * feat(inline-toolbar): inline tools now can be used in the readonly mode * tests added * docs improved --- docs/CHANGELOG.md | 11 +- src/components/core.ts | 2 +- src/components/modules/blockManager.ts | 2 +- src/components/modules/toolbar/inline.ts | 144 +++++++++++------- src/components/modules/ui.ts | 64 +++++--- src/components/selection.ts | 2 +- src/components/tools/base.ts | 6 + src/components/tools/inline.ts | 8 + .../cypress/tests/modules/InlineToolbar.cy.ts | 54 +++++++ test/cypress/tests/tools/InlineTool.cy.ts | 18 +++ types/tools/inline-tool.d.ts | 6 + 11 files changed, 233 insertions(+), 84 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index eecd87f9..d6015b24 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,9 +1,14 @@ # Changelog +### 2.31.0 + +- `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode +- `Fix` - Fix selection of first block in read-only initialization with "autofocus=true" + ### 2.30.6 -– `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified -– `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome +- `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified +- `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome - `Fix` - Prevent inline toolbar from closing in nested instance of editor ### 2.30.5 @@ -33,7 +38,7 @@ - `New` – Block Tunes now supports nesting items - `New` – Block Tunes now supports separator items - `New` – *Menu Config* – New item type – HTML -– `New` – *Menu Config* – Default and HTML items now support hints +- `New` – *Menu Config* – Default and HTML items now support hints - `New` – Inline Toolbar has new look 💅 - `New` – Inline Tool's `render()` now supports [Menu Config](https://editorjs.io/menu-config/) format - `New` – *ToolsAPI* – All installed block tools now accessible via ToolsAPI `getBlockTools()` method diff --git a/src/components/core.ts b/src/components/core.ts index 87f8beac..a7e307c3 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -61,7 +61,7 @@ export default class Core { UI.checkEmptiness(); ModificationsObserver.enable(); - if ((this.configuration as EditorConfig).autofocus) { + if ((this.configuration as EditorConfig).autofocus === true && this.configuration.readOnly !== true) { Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index fd06dd71..3f3ee99b 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -684,7 +684,7 @@ export default class BlockManager extends Module { * * @param {Node} element - html element to get Block by */ - public getBlock(element: HTMLElement): Block { + public getBlock(element: HTMLElement): Block | undefined { if (!$.isElement(element) as boolean) { element = element.parentNode as HTMLElement; } diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 4a138c20..2e596203 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -12,6 +12,7 @@ import { CommonInternalSettings } from '../../tools/base'; import type { Popover, PopoverItemHtmlParams, PopoverItemParams, WithChildren } from '../../utils/popover'; import { PopoverItemType } from '../../utils/popover'; import { PopoverInline } from '../../utils/popover/popover-inline'; +import type InlineToolAdapter from 'src/components/tools/inline'; /** * Inline Toolbar elements @@ -54,7 +55,7 @@ export default class InlineToolbar extends Module { /** * Currently visible tools instances */ - private toolsInstances: Map | null = new Map(); + private tools: Map = new Map(); /** * @param moduleConfiguration - Module Configuration @@ -66,21 +67,10 @@ export default class InlineToolbar extends Module { config, eventsDispatcher, }); - } - /** - * Toggles read-only mode - * - * @param {boolean} readOnlyEnabled - read-only mode - */ - public toggleReadOnly(readOnlyEnabled: boolean): void { - if (!readOnlyEnabled) { - window.requestIdleCallback(() => { - this.make(); - }, { timeout: 2000 }); - } else { - this.destroy(); - } + window.requestIdleCallback(() => { + this.make(); + }, { timeout: 2000 }); } /** @@ -116,14 +106,10 @@ export default class InlineToolbar extends Module { return; } - if (this.Editor.ReadOnly.isEnabled) { - return; - } + for (const [tool, toolInstance] of this.tools) { + const shortcut = this.getToolShortcut(tool.name); - Array.from(this.toolsInstances.entries()).forEach(([name, toolInstance]) => { - const shortcut = this.getToolShortcut(name); - - if (shortcut) { + if (shortcut !== undefined) { Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut); } @@ -133,9 +119,9 @@ export default class InlineToolbar extends Module { if (_.isFunction(toolInstance.clear)) { toolInstance.clear(); } - }); + } - this.toolsInstances = null; + this.tools = new Map(); this.reset(); this.opened = false; @@ -204,10 +190,12 @@ export default class InlineToolbar extends Module { this.popover.destroy(); } - const inlineTools = await this.getInlineTools(); + this.createToolsInstances(); + + const popoverItems = await this.getPopoverItems(); this.popover = new PopoverInline({ - items: inlineTools, + items: popoverItems, scopeElement: this.Editor.API.methods.ui.nodes.redactor, messages: { nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'), @@ -290,25 +278,36 @@ export default class InlineToolbar extends Module { return false; } - if (currentSelection && tagsConflictsWithSelection.includes(target.tagName)) { + if (currentSelection !== null && tagsConflictsWithSelection.includes(target.tagName)) { return false; } - // The selection of the element only in contenteditable - const contenteditable = target.closest('[contenteditable="true"]'); - - if (contenteditable === null) { - return false; - } - - // is enabled by current Block's Tool + /** + * Check if there is at leas one tool enabled by current Block's Tool + */ const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement); if (!currentBlock) { return false; } - return currentBlock.tool.inlineTools.size !== 0; + /** + * Check that at least one tool is available for the current block + */ + const toolsAvailable = this.getTools(); + const isAtLeastOneToolAvailable = toolsAvailable.some((tool) => currentBlock.tool.inlineTools.has(tool.name)); + + if (isAtLeastOneToolAvailable === false) { + return false; + } + + /** + * Inline toolbar will be shown only if the target is contenteditable + * In Read-Only mode, the target should be contenteditable with "false" value + */ + const contenteditable = target.closest('[contenteditable]'); + + return contenteditable !== null; } /** @@ -317,32 +316,63 @@ export default class InlineToolbar extends Module { */ /** - * Returns Inline Tools segregated by their appearance type: popover items and custom html elements. - * Sets this.toolsInstances map + * Returns tools that are available for current block + * + * Used to check if Inline Toolbar could be shown + * and to render tools in the Inline Toolbar */ - private async getInlineTools(): Promise { - const currentSelection = SelectionUtils.get(); - const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement); + private getTools(): InlineToolAdapter[] { + const currentBlock = this.Editor.BlockManager.currentBlock; + + if (!currentBlock) { + return []; + } const inlineTools = Array.from(currentBlock.tool.inlineTools.values()); + return inlineTools.filter((tool) => { + /** + * We support inline tools in read only mode. + * Such tools should have isReadOnlySupported flag set to true + */ + if (this.Editor.ReadOnly.isEnabled && tool.isReadOnlySupported !== true) { + return false; + } + + return true; + }); + } + + /** + * Constructs tools instances and saves them to this.tools + */ + private createToolsInstances(): void { + this.tools = new Map(); + + const tools = this.getTools(); + + tools.forEach((tool) => { + const instance = tool.create(); + + this.tools.set(tool, instance); + }); + } + + /** + * Returns Popover Items for tools segregated by their appearance type: regular items and custom html elements. + */ + private async getPopoverItems(): Promise { const popoverItems = [] as PopoverItemParams[]; - if (this.toolsInstances === null) { - this.toolsInstances = new Map(); - } + let i = 0; - for (let i = 0; i < inlineTools.length; i++) { - const tool = inlineTools[i]; - const instance = tool.create(); + for (const [tool, instance] of this.tools) { const renderedTool = await instance.render(); - this.toolsInstances.set(tool.name, instance); - /** Enable tool shortcut */ const shortcut = this.getToolShortcut(tool.name); - if (shortcut) { + if (shortcut !== undefined) { try { this.enableShortcuts(tool.name, shortcut); } catch (e) {} @@ -429,7 +459,9 @@ export default class InlineToolbar extends Module { type: PopoverItemType.Default, } as PopoverItemParams; - /** Prepend with separator if item has children and not the first one */ + /** + * Prepend the separator if item has children and not the first one + */ if ('children' in popoverItem && i !== 0) { popoverItems.push({ type: PopoverItemType.Separator, @@ -438,14 +470,18 @@ export default class InlineToolbar extends Module { popoverItems.push(popoverItem); - /** Append separator after the item is it has children and not the last one */ - if ('children' in popoverItem && i < inlineTools.length - 1) { + /** + * Append a separator after the item if it has children and not the last one + */ + if ('children' in popoverItem && i < this.tools.size - 1) { popoverItems.push({ type: PopoverItemType.Separator, }); } } }); + + i++; } return popoverItems; @@ -533,7 +569,7 @@ export default class InlineToolbar extends Module { * Check Tools` state by selection */ private checkToolsState(): void { - this.toolsInstances?.forEach((toolInstance) => { + this.tools?.forEach((toolInstance) => { toolInstance.checkState?.(SelectionUtils.get()); }); } diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 6bbcdb8c..df633e93 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -68,7 +68,7 @@ export default class UI extends Module { * @returns {DOMRect} */ public get contentRect(): DOMRect { - if (this.contentRectCache) { + if (this.contentRectCache !== null) { return this.contentRectCache; } @@ -85,7 +85,7 @@ export default class UI extends Module { } as DOMRect; } - this.contentRectCache = someBlock.getBoundingClientRect() as DOMRect; + this.contentRectCache = someBlock.getBoundingClientRect(); return this.contentRectCache; } @@ -104,7 +104,7 @@ export default class UI extends Module { * * @type {DOMRect} */ - private contentRectCache: DOMRect = undefined; + private contentRectCache: DOMRect | null = null; /** * Handle window resize only when it finished @@ -116,6 +116,13 @@ export default class UI extends Module { // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 200); + /** + * Handle selection change to manipulate Inline Toolbar appearance + */ + private selectionChangeDebounced = _.debounce(() => { + this.selectionChanged(); + }, selectionChangeDebounceTimeout); + /** * Making main interface */ @@ -160,7 +167,7 @@ export default class UI extends Module { /** * Bind events for the UI elements */ - this.enableModuleBindings(); + this.bindReadOnlySensitiveListeners(); }, { timeout: 2000, }); @@ -169,7 +176,7 @@ export default class UI extends Module { * Unbind all events * */ - this.disableModuleBindings(); + this.unbindReadOnlySensitiveListeners(); } } @@ -222,6 +229,8 @@ export default class UI extends Module { */ public destroy(): void { this.nodes.holder.innerHTML = ''; + + this.unbindReadOnlyInsensitiveListeners(); } /** @@ -289,6 +298,8 @@ export default class UI extends Module { this.nodes.wrapper.appendChild(this.nodes.redactor); this.nodes.holder.appendChild(this.nodes.wrapper); + + this.bindReadOnlyInsensitiveListeners(); } /** @@ -332,9 +343,29 @@ export default class UI extends Module { } /** - * Bind events on the Editor.js interface + * Adds listeners that should work both in read-only and read-write modes */ - private enableModuleBindings(): void { + private bindReadOnlyInsensitiveListeners(): void { + this.listeners.on(document, 'selectionchange', this.selectionChangeDebounced); + + this.listeners.on(window, 'resize', this.resizeDebouncer, { + passive: true, + }); + } + + /** + * Removes listeners that should work both in read-only and read-write modes + */ + private unbindReadOnlyInsensitiveListeners(): void { + this.listeners.off(document, 'selectionchange', this.selectionChangeDebounced); + this.listeners.off(window, 'resize', this.resizeDebouncer); + } + + + /** + * Adds listeners that should work only in read-only mode + */ + private bindReadOnlySensitiveListeners(): void { this.readOnlyMutableListeners.on(this.nodes.redactor, 'click', (event: MouseEvent) => { this.redactorClicked(event); }, false); @@ -361,21 +392,6 @@ export default class UI extends Module { this.documentClicked(event); }, true); - /** - * Handle selection change to manipulate Inline Toolbar appearance - */ - const selectionChangeDebounced = _.debounce(() => { - this.selectionChanged(); - }, selectionChangeDebounceTimeout); - - this.readOnlyMutableListeners.on(document, 'selectionchange', selectionChangeDebounced, true); - - this.readOnlyMutableListeners.on(window, 'resize', () => { - this.resizeDebouncer(); - }, { - passive: true, - }); - /** * Start watching 'block-hovered' events that is used by Toolbar for moving */ @@ -428,9 +444,9 @@ export default class UI extends Module { } /** - * Unbind events on the Editor.js interface + * Unbind events that should work only in read-only mode */ - private disableModuleBindings(): void { + private unbindReadOnlySensitiveListeners(): void { this.readOnlyMutableListeners.clearAll(); } diff --git a/src/components/selection.ts b/src/components/selection.ts index fe5f961a..1ab2e568 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -317,7 +317,7 @@ export default class SelectionUtils { * * @returns {Selection} */ - public static get(): Selection { + public static get(): Selection | null { return window.getSelection(); } diff --git a/src/components/tools/base.ts b/src/components/tools/base.ts index 2e211707..fd58be7e 100644 --- a/src/components/tools/base.ts +++ b/src/components/tools/base.ts @@ -86,6 +86,12 @@ export enum InternalInlineToolSettings { * Inline Tool title for toolbar */ Title = 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop. + + /** + * Allows inline tool to be available in read-only mode + * Can be used, for example, by comments tool + */ + IsReadOnlySupported = 'isReadOnlySupported', } /** diff --git a/src/components/tools/inline.ts b/src/components/tools/inline.ts index a1f41929..7287f37e 100644 --- a/src/components/tools/inline.ts +++ b/src/components/tools/inline.ts @@ -34,4 +34,12 @@ export default class InlineToolAdapter extends BaseToolAdapter { it('should appear aligned with left coord of selection rect', () => { @@ -77,6 +78,59 @@ describe('Inline Toolbar', () => { }); }); + it('should be displayed in read-only mode if at least one inline tool of block supports it', () => { + cy.createEditor({ + tools: { + header: { + class: Header, + inlineToolbar: ['bold', 'testTool'], + }, + testTool: { + class: class { + public static isInline = true; + public static isReadOnlySupported = true; + // eslint-disable-next-line jsdoc/require-jsdoc + public render(): MenuConfig { + return { + title: 'Test Tool', + name: 'test-tool', + // eslint-disable-next-line @typescript-eslint/no-empty-function + onActivate: () => {}, + }; + } + }, + }, + }, + readOnly: true, + data: { + blocks: [ + { + type: 'header', + data: { + text: 'First block text', + }, + }, + ], + }, + }); + + /** Open Inline Toolbar */ + cy.get('[data-cy=editorjs]') + .find('.ce-header') + .selectText('block'); + + cy.get('[data-cy=editorjs]') + .get('[data-cy=inline-toolbar]') + .get('.ce-popover--opened') + .as('toolbar') + .should('exist'); + + cy.get('@toolbar') + .get('.ce-popover-item') + .should('have.length', 1) + .should('have.attr', 'data-item-name', 'test-tool'); + }); + it('should not submit form nesting editor when inline tool clicked', () => { cy.createEditor({ data: { diff --git a/test/cypress/tests/tools/InlineTool.cy.ts b/test/cypress/tests/tools/InlineTool.cy.ts index 62fd8868..e220f210 100644 --- a/test/cypress/tests/tools/InlineTool.cy.ts +++ b/test/cypress/tests/tools/InlineTool.cy.ts @@ -21,6 +21,7 @@ describe('InlineTool', () => { public static prepare; public static shortcut = 'CTRL+N'; + public static isReadOnlySupported = true; public api: object; public config: ToolSettings; @@ -192,4 +193,21 @@ describe('InlineTool', () => { expect(instance.config).to.be.deep.eq(options.config.config); }); }); + + context('.isReadOnlySupported', () => { + it('should return Tool provided value', () => { + const tool = new InlineToolAdapter(options as any); + + expect(tool.isReadOnlySupported).to.be.eq(options.constructable.isReadOnlySupported); + }); + + it('should return false if Tool provided value is not exist', () => { + const tool = new InlineToolAdapter({ + ...options, + constructable: {}, + } as any); + + expect(tool.isReadOnlySupported).to.be.false; + }); + }); }); diff --git a/types/tools/inline-tool.d.ts b/types/tools/inline-tool.d.ts index 42ebc3f2..e9b8b609 100644 --- a/types/tools/inline-tool.d.ts +++ b/types/tools/inline-tool.d.ts @@ -57,4 +57,10 @@ export interface InlineToolConstructable extends BaseToolConstructable { * @param {InlineToolConstructorOptions} config - constructor parameters */ new(config: InlineToolConstructorOptions): BaseTool; + + /** + * Allows inline tool to be available in read-only mode + * Can be used, for example, by comments tool + */ + isReadOnlySupported?: boolean; } From 21ac511a52eae33e16d4c3fb04d88fea32510018 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:05:26 +0300 Subject: [PATCH 07/11] Bump version up to 2.30.7-rc.0 (#2838) * Bump version * Update package.json --------- Co-authored-by: github-actions Co-authored-by: Peter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d0917c8..be2cc032 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.30.6", + "version": "2.31.0-rc.0", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From eb7ffcba3c1387404a61046bb9360ae1cd926efc Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 9 Oct 2024 21:28:04 +0300 Subject: [PATCH 08/11] fix(merge): after merge caret will be set in a place of glue (#2841) * fix merge caret loosing * changelog and patch * Update nested-list * Update Backspace.cy.ts * Update Backspace.cy.ts * fix tests * fix tests --- docs/CHANGELOG.md | 1 + package.json | 7 ++++-- src/components/modules/blockEvents.ts | 11 +++++--- test/cypress/fixtures/tools/SimpleHeader.ts | 28 ++------------------- yarn.lock | 27 +++++++++++++++++--- 5 files changed, 38 insertions(+), 36 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d6015b24..049d5609 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ - `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode - `Fix` - Fix selection of first block in read-only initialization with "autofocus=true" +- `Fix` - Incorrect caret position after blocks merging in Safari ### 2.30.6 diff --git a/package.json b/package.json index be2cc032..1a54f4f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.0", + "version": "2.31.0-rc.1", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", @@ -45,7 +45,7 @@ "@cypress/code-coverage": "^3.10.3", "@editorjs/code": "^2.7.0", "@editorjs/delimiter": "^1.2.0", - "@editorjs/header": "^2.8.7", + "@editorjs/header": "^2.8.8", "@editorjs/paragraph": "^2.11.6", "@editorjs/simple-image": "^1.4.1", "@types/node": "^18.15.11", @@ -77,5 +77,8 @@ "collective": { "type": "opencollective", "url": "https://opencollective.com/editorjs" + }, + "dependencies": { + "@editorjs/caret": "^1.0.1" } } diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index 4c4ebf91..c48bba53 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -8,6 +8,7 @@ import Flipper from '../flipper'; import type Block from '../block'; import { areBlocksMergeable } from '../utils/blocks'; import * as caretUtils from '../utils/caret'; +import { focus } from '@editorjs/caret'; /** * @@ -506,15 +507,17 @@ export default class BlockEvents extends Module { * @param blockToMerge - what Block we want to merge */ private mergeBlocks(targetBlock: Block, blockToMerge: Block): void { - const { BlockManager, Caret, Toolbar } = this.Editor; + const { BlockManager, Toolbar } = this.Editor; - Caret.createShadow(targetBlock.lastInput); + if (targetBlock.lastInput === undefined) { + return; + } + + focus(targetBlock.lastInput, false); BlockManager .mergeBlocks(targetBlock, blockToMerge) .then(() => { - /** Restore caret position after merge */ - Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement); Toolbar.close(); }); } diff --git a/test/cypress/fixtures/tools/SimpleHeader.ts b/test/cypress/fixtures/tools/SimpleHeader.ts index cd87aa7e..23421374 100644 --- a/test/cypress/fixtures/tools/SimpleHeader.ts +++ b/test/cypress/fixtures/tools/SimpleHeader.ts @@ -10,7 +10,7 @@ import type { */ export class SimpleHeader implements BaseTool { private _data: BlockToolData; - private element: HTMLHeadingElement; + private element: HTMLHeadingElement | null = null; /** * @@ -39,10 +39,7 @@ export class SimpleHeader implements BaseTool { * @param data - saved data to merger with current block */ public merge(data: BlockToolData): void { - this.data = { - text: this.data.text + data.text, - level: this.data.level, - }; + this.element?.insertAdjacentHTML('beforeend', data.text); } /** @@ -66,25 +63,4 @@ export class SimpleHeader implements BaseTool { import: 'text', // fill 'text' property from other block's export string }; } - - /** - * Data getter - */ - private get data(): BlockToolData { - this._data.text = this.element.innerHTML; - this._data.level = 1; - - return this._data; - } - - /** - * Data setter - */ - private set data(data: BlockToolData) { - this._data = data; - - if (data.text !== undefined) { - this.element.innerHTML = this._data.text || ''; - } - } } diff --git a/yarn.lock b/yarn.lock index 29cccc68..5294af93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -550,6 +550,13 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@editorjs/caret@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@editorjs/caret/-/caret-1.0.1.tgz#0d33ca67a2d29d09fdea10d3d30b660f0abc7cfd" + integrity sha512-yMewrc/dndBbgmluFory0GbVWXnD9rhcE/xgwM0ecHWQodyfY3ZIJLvSQhf+BbgncitMlUG/FYqjJCL2Axi4+g== + dependencies: + "@editorjs/dom" "^1.0.0" + "@editorjs/code@^2.7.0": version "2.8.0" resolved "https://registry.yarnpkg.com/@editorjs/code/-/code-2.8.0.tgz#d31fdd947b9c763daae2cd2eabdf8dc37c0c6f5a" @@ -564,19 +571,31 @@ dependencies: "@codexteam/icons" "^0.0.5" +"@editorjs/dom@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@editorjs/dom/-/dom-1.0.0.tgz#ddf7f17651a091570766c5fa44c89ecf8a183c82" + integrity sha512-P5qZaQaG8NQXm2XuEDlcfDm8S1Kvdegwf0E/ld2RnwZquY5l27hufaW57w0SikT75mscr+dARQ68Gx/xEQEUKw== + dependencies: + "@editorjs/helpers" "^1.0.0" + "@editorjs/editorjs@^2.29.1": version "2.30.2" resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.30.2.tgz#b045af18a9ebe0c02cb32be41b2a98e23ee08e59" integrity sha512-JjtUDs2/aHTEjNZzEf/2cugpIli1+aNeU8mloOd5USbVxv2vC02HTMpv7Vc1UyB7dIuc45JaYSJwgnBZp9duhA== -"@editorjs/header@^2.8.7": - version "2.8.7" - resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.8.7.tgz#6aa34e01638d18fbbc6d3bd75f1844869eca9193" - integrity sha512-rfxzYFR/Jhaocj3Xxx8XjEjyzfPbBIVkcPZ9Uy3rEz1n3ewhV0V4zwuxCjVfFhLUVgQQExq43BxJnTNlLOzqDQ== +"@editorjs/header@^2.8.8": + version "2.8.8" + resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.8.8.tgz#43cff7949c44866da7716fdb562d68116d0a806a" + integrity sha512-bsMSs34u2hoi0UBuRoc5EGWXIFzJiwYgkFUYQGVm63y5FU+s8zPBmVx5Ip2sw1xgs0fqfDROqmteMvvmbCy62w== dependencies: "@codexteam/icons" "^0.0.5" "@editorjs/editorjs" "^2.29.1" +"@editorjs/helpers@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@editorjs/helpers/-/helpers-1.0.0.tgz#4b0e0868e51e2772a73212f4aac5aff553725894" + integrity sha512-ih4yCm+x+7X9XCn1zxfNous2LQX8ZYMyTHMLdgbyjBf0Opf8GdLxVjdzSjkA+0mUp1tUe3JgWW3FTisYcSnbQA== + "@editorjs/paragraph@^2.11.6": version "2.11.6" resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.11.6.tgz#011444187a74dc603201dce37d2fc6d054022407" From 3cd24c4da17d21acd04ef1df95e17d2db93790c3 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:57:43 +0300 Subject: [PATCH 09/11] Chore (toolbox): improved shortcuts visibility when tool exports array of toolbox items (#2846) * toolbox items logic improved * typo * lint fix * logic improved * make displaySecondaryLabel true by default * eslint fix * added testcase * updated changelog * typo * lint fix --- docs/CHANGELOG.md | 1 + src/components/ui/toolbox.ts | 8 +-- test/cypress/tests/ui/toolbox.cy.ts | 100 ++++++++++++++++++++++++++++ types/tools/tool-settings.d.ts | 2 +- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 049d5609..26b2336d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,7 @@ - `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode - `Fix` - Fix selection of first block in read-only initialization with "autofocus=true" - `Fix` - Incorrect caret position after blocks merging in Safari +- `Fix` - Several toolbox items exported by the one tool have the same shortcut displayed in toolbox ### 2.30.6 diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 0cbcb85c..91e66358 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -308,7 +308,7 @@ export default class Toolbox extends EventsDispatcher { /** * Maps tool data to popover item structure */ - const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockToolAdapter): PopoverItemParams => { + const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockToolAdapter, displaySecondaryLabel = true): PopoverItemParams => { return { icon: toolboxItem.icon, title: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)), @@ -316,15 +316,15 @@ export default class Toolbox extends EventsDispatcher { onActivate: (): void => { this.toolButtonActivated(tool.name, toolboxItem.data); }, - secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', + secondaryLabel: (tool.shortcut && displaySecondaryLabel) ? _.beautifyShortcut(tool.shortcut) : '', }; }; return this.toolsToBeDisplayed .reduce((result, tool) => { if (Array.isArray(tool.toolbox)) { - tool.toolbox.forEach(item => { - result.push(toPopoverItem(item, tool)); + tool.toolbox.forEach((item, index) => { + result.push(toPopoverItem(item, tool, index === 0)); }); } else if (tool.toolbox !== undefined) { result.push(toPopoverItem(tool.toolbox, tool)); diff --git a/test/cypress/tests/ui/toolbox.cy.ts b/test/cypress/tests/ui/toolbox.cy.ts index d6d1ade6..127b5090 100644 --- a/test/cypress/tests/ui/toolbox.cy.ts +++ b/test/cypress/tests/ui/toolbox.cy.ts @@ -114,5 +114,105 @@ describe('Toolbox', function () { expect(blocks[1].type).to.eq('nonConvertableTool'); }); }); + + it('should display shortcut only for the first toolbox item if tool exports toolbox with several items', function () { + /** + * Mock of Tool with conversionConfig + */ + class ToolWithSeveralToolboxItems extends ToolMock { + /** + * Specify toolbox with several items related to one tool + */ + public static get toolbox(): ToolboxConfig { + return [ + { + icon: '', + title: 'first tool', + }, + { + icon: '', + title: 'second tool', + }, + ]; + } + } + + cy.createEditor({ + tools: { + severalToolboxItemsTool: { + class: ToolWithSeveralToolboxItems, + shortcut: 'CMD+SHIFT+L', + }, + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type('Some text') + .type('/'); // call a shortcut for toolbox + + /** + * Secondary title (shortcut) should exist for first toolbox item of the tool + */ + /* eslint-disable-next-line cypress/require-data-selectors */ + cy.get('.ce-popover') + .find('.ce-popover-item[data-item-name="severalToolboxItemsTool"]') + .first() + .find('.ce-popover-item__secondary-title') + .should('exist'); + + /** + * Secondary title (shortcut) should not exist for second toolbox item of the same tool + */ + /* eslint-disable-next-line cypress/require-data-selectors */ + cy.get('.ce-popover') + .find('.ce-popover-item[data-item-name="severalToolboxItemsTool"]') + .eq(1) + .find('.ce-popover-item__secondary-title') + .should('not.exist'); + }); + + it('should display shortcut for the item if tool exports toolbox as an one item object', function () { + /** + * Mock of Tool with conversionConfig + */ + class ToolWithOneToolboxItems extends ToolMock { + /** + * Specify toolbox with several items related to one tool + */ + public static get toolbox(): ToolboxConfig { + return { + icon: '', + title: 'tool', + }; + } + } + + cy.createEditor({ + tools: { + oneToolboxItemTool: { + class: ToolWithOneToolboxItems, + shortcut: 'CMD+SHIFT+L', + }, + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type('Some text') + .type('/'); // call a shortcut for toolbox + + /** + * Secondary title (shortcut) should exist for toolbox item of the tool + */ + /* eslint-disable-next-line cypress/require-data-selectors */ + cy.get('.ce-popover') + .find('.ce-popover-item[data-item-name="oneToolboxItemTool"]') + .first() + .find('.ce-popover-item__secondary-title') + .should('exist'); + }); }); }); diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 67935c11..1658ee9a 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -22,7 +22,7 @@ export interface ToolboxConfigEntry { icon?: string; /** - * May contain overrides for tool default config + * May contain overrides for tool default data */ data?: BlockToolData } From da4257a67fcdb668e59fdf45b0659b44fbe0534f Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:38:02 +0300 Subject: [PATCH 10/11] Feat (Conversion-config): pass target tool config to the conversionConfig.import method (#2848) * pass config to the conversionConfig.import method - Now `convertStringToBlockData` method passes target tool config the import method - Fixed types in convesion config file (somehow imprort could return function that returns string, but import should return method that would return ToolData) this caused just type error that never been reached because types were actually ignored - Added test that checks, that import method actualy gets passed config * update changelog * eslint fix * updated test description * jsdoc improved * typos in changelog --- docs/CHANGELOG.md | 5 +- src/components/modules/blockManager.ts | 2 +- src/components/utils/blocks.ts | 7 ++- test/cypress/fixtures/tools/ToolMock.ts | 2 +- test/cypress/tests/api/blocks.cy.ts | 83 ++++++++++++++++++++++++- types/configs/conversion-config.ts | 4 +- 6 files changed, 92 insertions(+), 11 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 26b2336d..43fca419 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ ### 2.31.0 - `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode +- `Improvement` - Block manager passes target tool config to the `conversionConfig.import` method on conversion - `Fix` - Fix selection of first block in read-only initialization with "autofocus=true" - `Fix` - Incorrect caret position after blocks merging in Safari - `Fix` - Several toolbox items exported by the one tool have the same shortcut displayed in toolbox @@ -49,11 +50,11 @@ - `New` – "Convert to" control is now also available in Block Tunes - `New` — Editor.js now supports contenteditable placeholders out of the box. Just add `data-placeholder` or `data-placeholder-active` attribute to make it work. The first one will work like native placeholder while the second one will show placeholder only when block is current. - `Improvement` — Now Paragraph placeholder will be shown for the current paragraph, not only the first one. -- `Improvment` - The API `blocks.update` now accepts `tunes` data as optional third argument and makes `data` - block data as optional. +- `Improvement` - The API `blocks.update` now accepts `tunes` data as optional third argument and makes `data` - block data as optional. - `Improvement` — The ability to merge blocks of different types (if both tools provide the conversionConfig) - `Improvement` - The API `blocks.convert()` now returns the new block API - `Improvement` - The API `caret.setToBlock()` now can accept either BlockAPI or block index or block id -- `Impovement` – *MenuConfig* – `TunesMenuConfig` type is deprecated, use the `MenuConfig` instead +- `Improvement` – *MenuConfig* – `TunesMenuConfig` type is deprecated, use the `MenuConfig` instead – `Improvement` — *Types* — `BlockToolConstructorOptions` type improved, `block` and `config` are not optional anymore - `Improvement` - The Plus button and Block Tunes toggler are now better aligned with large line-height blocks, such as Headings - `Improvement` — Creating links on Android devices: now the mobile keyboard will have an "Enter" key for accepting the inserted link. diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 3f3ee99b..48fec049 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -863,7 +863,7 @@ export default class BlockManager extends Module { /** * Now using Conversion Config "import" we compose a new Block data */ - let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig); + let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig, replacingTool.settings); /** * Optional data overrides. diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index fb564223..710cfa5f 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -1,4 +1,4 @@ -import type { BlockAPI } from '../../../types'; +import type { BlockAPI, ToolConfig } from '../../../types'; import type { ConversionConfig } from '../../../types/configs/conversion-config'; import type { SavedData } from '../../../types/data-formats'; import type { BlockToolData } from '../../../types/tools/block-tool-data'; @@ -174,12 +174,13 @@ export function convertBlockDataToString(blockData: BlockToolData, conversionCon * * @param stringToImport - string to convert * @param conversionConfig - tool's conversion config + * @param targetToolConfig - target tool config, used in conversionConfig.import method */ -export function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig): BlockToolData { +export function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig, targetToolConfig?: ToolConfig): BlockToolData { const importProp = conversionConfig?.import; if (isFunction(importProp)) { - return importProp(stringToImport); + return importProp(stringToImport, targetToolConfig); } else if (isString(importProp)) { return { [importProp]: stringToImport, diff --git a/test/cypress/fixtures/tools/ToolMock.ts b/test/cypress/fixtures/tools/ToolMock.ts index 67b29045..51ea3a95 100644 --- a/test/cypress/fixtures/tools/ToolMock.ts +++ b/test/cypress/fixtures/tools/ToolMock.ts @@ -3,7 +3,7 @@ import type { BlockTool, BlockToolConstructorOptions } from '../../../../types'; /** * Simple structure for Tool data */ -interface MockToolData { +export interface MockToolData { text: string; } diff --git a/test/cypress/tests/api/blocks.cy.ts b/test/cypress/tests/api/blocks.cy.ts index 17da54bf..ab97b8b5 100644 --- a/test/cypress/tests/api/blocks.cy.ts +++ b/test/cypress/tests/api/blocks.cy.ts @@ -1,6 +1,6 @@ import type EditorJS from '../../../../types/index'; -import type { ConversionConfig, ToolboxConfig } from '../../../../types'; -import ToolMock from '../../fixtures/tools/ToolMock'; +import type { ConversionConfig, ToolboxConfig, ToolConfig } from '../../../../types'; +import ToolMock, { type MockToolData } from '../../fixtures/tools/ToolMock'; import { nanoid } from 'nanoid'; /** @@ -444,5 +444,84 @@ describe('api.blocks', () => { }); }); }); + + it('should pass tool config to the conversionConfig.import method of the tool', function () { + const existingBlock = { + id: 'test-id-123', + type: 'paragraph', + data: { + text: 'Some text', + }, + }; + + const conversionTargetToolConfig = { + defaultStyle: 'defaultStyle', + }; + + /** + * Mock of Tool with conversionConfig + */ + class ToolWithConversionConfig extends ToolMock { + /** + * Specify conversion config of the tool + */ + public static get conversionConfig(): { + /** + * Method that is responsible for conversion from data to string + */ + export: (data: string) => string; + + /** + * Method that is responsible for conversion from string to data + * Should return stringified config to see, if Editor actually passed tool config to it + */ + import: (content: string, config: ToolConfig) => MockToolData; + } { + return { + export: (data) => data, + /** + * Passed config should be returned + */ + import: (_content, config) => { + return { text: JSON.stringify(config) }; + }, + }; + } + } + + cy.createEditor({ + tools: { + conversionTargetTool: { + class: ToolWithConversionConfig, + config: conversionTargetToolConfig, + }, + }, + data: { + blocks: [ + existingBlock, + ], + }, + }).then(async (editor) => { + const { convert } = editor.blocks; + + await convert(existingBlock.id, 'conversionTargetTool'); + + // wait for block to be converted + cy.wait(100).then(async () => { + /** + * Check that block was converted + */ + const { blocks } = await editor.save(); + + expect(blocks.length).to.eq(1); + expect(blocks[0].type).to.eq('conversionTargetTool'); + + /** + * Check that tool converted returned config as a result of import + */ + expect(blocks[0].data.text).to.eq(JSON.stringify(conversionTargetToolConfig)); + }); + }); + }); }); }); diff --git a/types/configs/conversion-config.ts b/types/configs/conversion-config.ts index b61aa478..0f7e2748 100644 --- a/types/configs/conversion-config.ts +++ b/types/configs/conversion-config.ts @@ -1,4 +1,4 @@ -import type { BlockToolData } from '../tools'; +import type { BlockToolData, ToolConfig } from '../tools'; /** * Config allows Tool to specify how it can be converted into/from another Tool @@ -12,7 +12,7 @@ export interface ConversionConfig { * 1. String — the key of Tool data object to fill it with imported string on render. * 2. Function — method that accepts importing string and composes Tool data to render. */ - import?: ((data: string) => string) | string; + import?: ((data: string, config: ToolConfig) => BlockToolData) | string; /** * How to export this Tool to make other Block. From 631340912e7a1b3db1e114e930ddf1e97ec4ea61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 20:02:31 +0300 Subject: [PATCH 11/11] Bump version (#2847) Co-authored-by: github-actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1a54f4f2..c8e74e97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.1", + "version": "2.31.0-rc.2", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs",