From 4743f53cb5da985bda98b7bef11d16d611343cb1 Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:54:13 +0300 Subject: [PATCH 01/22] Fixed display of convert menu for blocks without export rule According to the workflow script from the documentation: https://editorjs.io/tools-api/#conversionconfig --- src/components/utils/blocks.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index 471bb8647..b10cd0d93 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -51,6 +51,14 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools const savedData = await block.save() as SavedData; const blockData = savedData.data; + /** + * Checking that the block has an «export» rule + */ + const blockTool = allBlockTools.find((tool) => tool.name === block.name); + if (!isToolConvertable(blockTool, 'export')) { + return []; + } + return allBlockTools.reduce((result, tool) => { /** * Skip tools without «import» rule specified From ca96c932c662a2d1448ea35a7f7cea3bf1ebffd8 Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:52:21 +0300 Subject: [PATCH 02/22] Block conversion update: ability to convert to complex objects --- docs/tools.md | 4 +-- src/components/block/index.ts | 8 +++--- src/components/modules/blockManager.ts | 28 ++++++++++++--------- src/components/utils/blocks.ts | 35 ++++++++++++++++++-------- types/configs/conversion-config.ts | 6 ++--- 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index 7cc4fd210..8570fad2f 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -449,7 +449,7 @@ It can be a `String` or a `Function`. `String` means a key of your Tool data object that should be used as string to export. -`Function` is a method that accepts your Tool data and compose a string to export from it. See example below: +`Function` is a method that accepts your Tool data and compose a string or object to export from it. See example below: ```js class ListTool { @@ -484,7 +484,7 @@ It can be a `String` or a `Function`. `String` means the key in tool data that will be filled by an exported string. For example, `import: 'text'` means that `constructor` of your block will accept a `data` object with `text` property filled with string composed by original block. -`Function` allows you to specify own logic, how a string should be converted to your tool data. For example: +`Function` allows you to specify own logic, how a string or object should be converted to your tool data. For example: ```js class ListTool { diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 36c4aa2ae..d4e4cd2b2 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -26,7 +26,7 @@ import { isMutationBelongsToElement } from '../utils/mutations'; import type { EditorEventMap } from '../events'; import { FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events'; import type { RedactorDomChangedPayload } from '../events/RedactorDomChanged'; -import { convertBlockDataToString, isSameBlockData } from '../utils/blocks'; +import { convertBlockDataForExport, isSameBlockData } from '../utils/blocks'; import { PopoverItemType } from '@/types/utils/popover/popover-item-type'; /** @@ -729,12 +729,12 @@ export default class Block extends EventsDispatcher { } /** - * Exports Block data as string using conversion config + * Exports Block data using conversion config */ - public async exportDataAsString(): Promise { + public async exportData(): Promise { const blockData = await this.data; - return convertBlockDataToString(blockData, this.tool.conversionConfig); + return convertBlockDataForExport(blockData, this.tool.conversionConfig); } /** diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index fd06dd71b..66b6c1d38 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -19,7 +19,7 @@ import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved'; import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged'; import { BlockChanged } from '../events'; import { clean, sanitizeBlocks } from '../utils/sanitizer'; -import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks'; +import { convertExportToBlockData, isBlockConvertable } from '../utils/blocks'; import PromiseQueue from '../utils/promise-queue'; /** @@ -501,10 +501,12 @@ export default class BlockManager extends Module { * 2) Blocks with different Tools if they provides conversionConfig */ } else if (targetBlock.mergeable && isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import')) { - const blockToMergeDataStringified = await blockToMerge.exportDataAsString(); - const cleanData = clean(blockToMergeDataStringified, targetBlock.tool.sanitizeConfig); + let blockToMergeExportData = await blockToMerge.exportData(); + if (_.isString(blockToMergeExportData)) { + blockToMergeExportData = clean(blockToMergeExportData, targetBlock.tool.sanitizeConfig); + } - blockToMergeData = convertStringToBlockData(cleanData, targetBlock.tool.conversionConfig); + blockToMergeData = convertExportToBlockData(blockToMergeExportData, targetBlock.tool.conversionConfig); } if (blockToMergeData === undefined) { @@ -848,22 +850,24 @@ export default class BlockManager extends Module { } /** - * Using Conversion Config "export" we get a stringified version of the Block data + * Using Conversion Config "export" we get a exported version of the Block data */ - const exportedData = await blockToConvert.exportDataAsString(); + let exportedData = await blockToConvert.exportData(); /** - * Clean exported data with replacing sanitizer config + * Clean exported data, if it is a string, with replacing sanitizer config */ - const cleanData: string = clean( - exportedData, - replacingTool.sanitizeConfig - ); + if (_.isString(exportedData)) { + exportedData = clean( + exportedData, + replacingTool.sanitizeConfig + ); + } /** * Now using Conversion Config "import" we compose a new Block data */ - let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig); + let newBlockData = convertExportToBlockData(exportedData, replacingTool.conversionConfig); /** * Optional data overrides. diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index b10cd0d93..b5a89396a 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -4,7 +4,7 @@ import type { SavedData } from '../../../types/data-formats'; import type { BlockToolData } from '../../../types/tools/block-tool-data'; import type Block from '../block'; import type BlockToolAdapter from '../tools/block'; -import { isFunction, isString, log, equals, isEmpty } from '../utils'; +import { isFunction, isString, log, equals, isEmpty, isUndefined } from '../utils'; import { isToolConvertable } from './tools'; @@ -59,6 +59,8 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools return []; } + const exportData = convertBlockDataForExport(blockData, blockTool.conversionConfig); + return allBlockTools.reduce((result, tool) => { /** * Skip tools without «import» rule specified @@ -67,6 +69,14 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools return result; } + /** + * Checking that the block is not empty after conversion + */ + const importData = convertExportToBlockData(exportData, tool.conversionConfig); + if (isUndefined(importData) || isEmpty(importData)) { + return result; + } + /** Filter out invalid toolbox entries */ const actualToolboxItems = tool.toolbox.filter((toolboxItem) => { /** @@ -141,7 +151,7 @@ export function areBlocksMergeable(targetBlock: Block, blockToMerge: Block): boo * @param blockData - block data to convert * @param conversionConfig - tool's conversion config */ -export function convertBlockDataToString(blockData: BlockToolData, conversionConfig?: ConversionConfig ): string { +export function convertBlockDataForExport(blockData: BlockToolData, conversionConfig?: ConversionConfig ): string | object { const exportProp = conversionConfig?.export; if (isFunction(exportProp)) { @@ -154,7 +164,7 @@ export function convertBlockDataToString(blockData: BlockToolData, conversionCon */ if (exportProp !== undefined) { log('Conversion «export» property must be a string or function. ' + - 'String means key of saved data object to export. Function should export processed string to export.'); + 'String means key of saved data object to export. Function should export processed string or object to export.'); } return ''; @@ -162,19 +172,24 @@ export function convertBlockDataToString(blockData: BlockToolData, conversionCon } /** - * Using conversionConfig, convert string to block data. + * Using conversionConfig, convert export string|object to block data. * - * @param stringToImport - string to convert + * @param dataToImport - string|object to convert * @param conversionConfig - tool's conversion config */ -export function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig): BlockToolData { +export function convertExportToBlockData(dataToImport: string | object, conversionConfig?: ConversionConfig): BlockToolData { const importProp = conversionConfig?.import; if (isFunction(importProp)) { - return importProp(stringToImport); - } else if (isString(importProp)) { + try { + return importProp(dataToImport); + } catch (err) { + log('Conversion «import» function returned an error'); + return {}; + } + } else if (isString(importProp) && isString(dataToImport)) { return { - [importProp]: stringToImport, + [importProp]: dataToImport, }; } else { /** @@ -182,7 +197,7 @@ export function convertStringToBlockData(stringToImport: string, conversionConfi */ if (importProp !== undefined) { log('Conversion «import» property must be a string or function. ' + - 'String means key of tool data to import. Function accepts a imported string and return composed tool data.'); + 'String means key of tool data to import. Function accepts a imported string or object and return composed tool data.'); } return {}; diff --git a/types/configs/conversion-config.ts b/types/configs/conversion-config.ts index b61aa478d..d8f621bd6 100644 --- a/types/configs/conversion-config.ts +++ b/types/configs/conversion-config.ts @@ -5,14 +5,14 @@ import type { BlockToolData } from '../tools'; */ export interface ConversionConfig { /** - * How to import string to this Tool. + * How to import data to this Tool. * * Can be a String or Function: * * 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 | object) => BlockToolData) | string; /** * How to export this Tool to make other Block. @@ -22,5 +22,5 @@ export interface ConversionConfig { * 1. String — which property of saved Tool data should be used as exported string. * 2. Function — accepts saved Tool data and create a string to export */ - export?: ((data: BlockToolData) => string) | string; + export?: ((data: BlockToolData) => string | object) | string; } From 9b7fd5f5b38a6ee0acfa7de6fe63c1e2c9aef815 Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:37:04 +0300 Subject: [PATCH 03/22] Merge fix --- src/components/modules/blockManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 5db72161b..5b5c931dd 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -867,7 +867,7 @@ export default class BlockManager extends Module { /** * Now using Conversion Config "import" we compose a new Block data */ - let newBlockData = convertStringToBlockData(exportedData, replacingTool.conversionConfig, replacingTool.settings); + let newBlockData = convertExportToBlockData(exportedData, replacingTool.conversionConfig, replacingTool.settings); /** * Optional data overrides. From 959c6b13363e27bd48b19227c03c7784d69a9e0b Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 21 Dec 2024 11:28:55 +0300 Subject: [PATCH 04/22] fix(inline-tools): inline tools shortcuts now works in read-only mode (#2891) * fix(inline-tools): inline tools shortcuts now works in read-only mode * use ubuntu-20.04 instead of latest --- .../workflows/bump-version-on-merge-next.yml | 4 +- .github/workflows/create-a-release-draft.yml | 10 ++-- .github/workflows/cypress.yml | 2 +- .github/workflows/eslint.yml | 2 +- .github/workflows/publish-package-to-npm.yml | 4 +- docs/CHANGELOG.md | 1 + src/components/modules/toolbar/inline.ts | 5 +- src/components/utils/shortcuts.ts | 2 +- test/cypress/tests/ui/InlineToolbar.cy.ts | 58 ++++++++++++++++++- test/cypress/tests/utils/popover.cy.ts | 4 +- 10 files changed, 75 insertions(+), 17 deletions(-) diff --git a/.github/workflows/bump-version-on-merge-next.yml b/.github/workflows/bump-version-on-merge-next.yml index 28c346885..2a592e3c5 100644 --- a/.github/workflows/bump-version-on-merge-next.yml +++ b/.github/workflows/bump-version-on-merge-next.yml @@ -1,6 +1,6 @@ name: Bump version on merge -# Caution: +# Caution: # the use of "pull_request_target" trigger allows to successfully # run workflow even when triggered from a fork. The trigger grants # access to repo's secrets and gives write permission to the runner. @@ -17,7 +17,7 @@ jobs: # If pull request was merged then we should check for a package version update check-for-no-version-changing: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 permissions: actions: write steps: diff --git a/.github/workflows/create-a-release-draft.yml b/.github/workflows/create-a-release-draft.yml index 176fe2858..ec728b8ac 100644 --- a/.github/workflows/create-a-release-draft.yml +++ b/.github/workflows/create-a-release-draft.yml @@ -1,6 +1,6 @@ name: Create a release draft -# Caution: +# Caution: # the use of "pull_request_target" trigger allows to successfully # run workflow even when triggered from a fork. The trigger grants # access to repo's secrets and gives write permission to the runner. @@ -17,7 +17,7 @@ jobs: # If pull request was merged then we should check for a package version update check-version-changing: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 permissions: actions: write steps: @@ -113,7 +113,7 @@ jobs: asset_path: dist/editorjs.umd.js asset_name: editorjs.umd.js asset_content_type: application/javascript - + # Build and upload target Editor.js MJS build to release as artifact - name: Upload Release Asset uses: actions/upload-release-asset@v1 @@ -123,7 +123,7 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: dist/editorjs.mjs asset_name: editorjs.mjs - asset_content_type: application/javascript + asset_content_type: application/javascript # Send a notification message - name: Send a message @@ -132,4 +132,4 @@ jobs: webhook: ${{ secrets.CODEX_BOT_WEBHOOK_FRONTEND }} message: '🦥 [Draft release v${{ steps.package.outputs.version }}](${{ steps.create_release.outputs.html_url }}) for package [${{ steps.package.outputs.name }}](${{ steps.package.outputs.npmjs-link }}) has been created. Add changelog and publish it!' parse_mode: 'markdown' - disable_web_page_preview: true \ No newline at end of file + disable_web_page_preview: true diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 766075563..517180066 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -8,7 +8,7 @@ jobs: matrix: browser: [firefox, chrome, edge] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 47d1e3a9d..1db62120c 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -5,7 +5,7 @@ on: [pull_request] jobs: lint: name: ESlint - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/publish-package-to-npm.yml b/.github/workflows/publish-package-to-npm.yml index 41d5131e0..b6ec6935e 100644 --- a/.github/workflows/publish-package-to-npm.yml +++ b/.github/workflows/publish-package-to-npm.yml @@ -7,7 +7,7 @@ on: jobs: publish: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: # Checkout to target branch - uses: actions/checkout@v4 @@ -62,4 +62,4 @@ jobs: webhook: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }} message: '📦 [${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }}](${{ env.GITHUB_LINK }}) was published' parse_mode: 'markdown' - disable_web_page_preview: true \ No newline at end of file + disable_web_page_preview: true diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4863ffbe0..5e7bffe1f 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 +- `New` - Inline tools (those with `isReadOnlySupported` specified) shortcuts now work 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 diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 2e596203a..5aa5f7dab 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -549,7 +549,10 @@ export default class InlineToolbar extends Module { this.popover?.activateItemByName(toolName); }, - on: this.Editor.UI.nodes.redactor, + /** + * We need to bind shortcut to the document to make it work in read-only mode + */ + on: document, }); } diff --git a/src/components/utils/shortcuts.ts b/src/components/utils/shortcuts.ts index 12adf10c1..8cf51ff93 100644 --- a/src/components/utils/shortcuts.ts +++ b/src/components/utils/shortcuts.ts @@ -28,7 +28,7 @@ export interface ShortcutData { /** * Element handler should be added for */ - on: HTMLElement; + on: HTMLElement | Document; } /** diff --git a/test/cypress/tests/ui/InlineToolbar.cy.ts b/test/cypress/tests/ui/InlineToolbar.cy.ts index eca588172..5c337b196 100644 --- a/test/cypress/tests/ui/InlineToolbar.cy.ts +++ b/test/cypress/tests/ui/InlineToolbar.cy.ts @@ -1,6 +1,6 @@ - import Header from '@editorjs/header'; -import type { MenuConfig } from '../../../../types/tools'; +import type { InlineTool, MenuConfig } from '../../../../types/tools'; +import { createEditorWithTextBlocks } from '../../support/utils/createEditorWithTextBlocks'; describe('Inline Toolbar', () => { describe('Separators', () => { @@ -174,4 +174,58 @@ describe('Inline Toolbar', () => { .should('have.attr', 'data-item-name', 'test-tool'); }); }); + + describe('Shortcuts', () => { + it('should work in read-only mode', () => { + const toolSurround = cy.stub().as('toolSurround'); + + /* eslint-disable jsdoc/require-jsdoc */ + class Marker implements InlineTool { + public static isInline = true; + public static shortcut = 'CMD+SHIFT+M'; + public static isReadOnlySupported = true; + public render(): MenuConfig { + return { + icon: 'm', + title: 'Marker', + onActivate: () => { + toolSurround(); + }, + }; + } + } + /* eslint-enable jsdoc/require-jsdoc */ + + createEditorWithTextBlocks([ + 'some text', + ], { + tools: { + marker: Marker, + }, + readOnly: true, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .selectText('text'); + + cy.wait(300); + + cy.document().then((doc) => { + doc.dispatchEvent(new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'M', + code: 'KeyM', + keyCode: 77, + which: 77, + metaKey: true, + shiftKey: true, + })); + }); + + cy.get('@toolSurround').should('have.been.called'); + }); + }); }); + diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index 28ae3001f..5d42a492f 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -881,7 +881,7 @@ describe('Popover', () => { .should('exist'); }); - it.only('shoould support i18n in nested popover', () => { + it('shoould support i18n in nested popover', () => { /** * */ @@ -1076,7 +1076,7 @@ describe('Popover', () => { .should('exist'); }); - it.only('should allow to reach nested popover via keyboard', () => { + it('should allow to reach nested popover via keyboard', () => { cy.createEditor({ tools: { header: { From 5362750049ba93d652437785786f1b570cba30e9 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 21 Dec 2024 14:31:22 +0300 Subject: [PATCH 05/22] chore(submodules): update all submodules to the latest version (#2880) * update all submodules * rm checklist and nested list submodule * Update .gitmodules * rm list submodule * add list submodule * all submodules updated * fix(inline-tools): inline tools shortcuts now works in read-only mode --- .gitmodules | 12 +++--------- example/tools/checklist | 1 - example/tools/code | 2 +- example/tools/delimiter | 2 +- example/tools/embed | 2 +- example/tools/header | 2 +- example/tools/image | 2 +- example/tools/inline-code | 2 +- example/tools/list | 2 +- example/tools/nested-list | 1 - example/tools/quote | 2 +- example/tools/raw | 2 +- example/tools/table | 2 +- example/tools/warning | 2 +- 14 files changed, 14 insertions(+), 22 deletions(-) delete mode 160000 example/tools/checklist delete mode 160000 example/tools/nested-list diff --git a/.gitmodules b/.gitmodules index 66320775b..44e1a2ff4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,6 @@ [submodule "example/tools/delimiter"] path = example/tools/delimiter url = https://github.com/editor-js/delimiter -[submodule "example/tools/list"] - path = example/tools/list - url = https://github.com/editor-js/list [submodule "example/tools/quote"] path = example/tools/quote url = https://github.com/editor-js/quote @@ -31,9 +28,6 @@ [submodule "example/tools/table"] path = example/tools/table url = https://github.com/editor-js/table -[submodule "example/tools/checklist"] - path = example/tools/checklist - url = https://github.com/editor-js/checklist [submodule "example/tools/link"] path = example/tools/link url = https://github.com/editor-js/link @@ -46,9 +40,9 @@ [submodule "example/tools/underline"] path = example/tools/underline url = https://github.com/editor-js/underline -[submodule "example/tools/nested-list"] - path = example/tools/nested-list - url = https://github.com/editor-js/nested-list [submodule "example/tools/text-variant-tune"] path = example/tools/text-variant-tune url = https://github.com/editor-js/text-variant-tune +[submodule "example/tools/list"] + path = example/tools/list + url = https://github.com/editor-js/list diff --git a/example/tools/checklist b/example/tools/checklist deleted file mode 160000 index 1c116d5e0..000000000 --- a/example/tools/checklist +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1c116d5e09e19951948d6166047aa2f30877aaf9 diff --git a/example/tools/code b/example/tools/code index f281996f8..82402cb86 160000 --- a/example/tools/code +++ b/example/tools/code @@ -1 +1 @@ -Subproject commit f281996f82c7ac676172757e45687cae27443427 +Subproject commit 82402cb86a327e3c64bef653d953533e2c3613dd diff --git a/example/tools/delimiter b/example/tools/delimiter index 4ca1c1c97..95a5eb90d 160000 --- a/example/tools/delimiter +++ b/example/tools/delimiter @@ -1 +1 @@ -Subproject commit 4ca1c1c972261f47dd34f6b8754763a4a79a4866 +Subproject commit 95a5eb90dd2e2e8ab153eb66b59a70cdafdf2d7f diff --git a/example/tools/embed b/example/tools/embed index f2585abb9..801580fbd 160000 --- a/example/tools/embed +++ b/example/tools/embed @@ -1 +1 @@ -Subproject commit f2585abb9019abf93c18f1dcfa63b07a3dd08318 +Subproject commit 801580fbdb7ab0ad1e975cfdaab38ada6625e301 diff --git a/example/tools/header b/example/tools/header index 477853c16..3e457cbac 160000 --- a/example/tools/header +++ b/example/tools/header @@ -1 +1 @@ -Subproject commit 477853c1646ae479867603847e49071438ffd80c +Subproject commit 3e457cbac2c5da53fff1b02b99ddaccaa577f401 diff --git a/example/tools/image b/example/tools/image index 25d46cd8d..c8236e576 160000 --- a/example/tools/image +++ b/example/tools/image @@ -1 +1 @@ -Subproject commit 25d46cd8d3930851b14ddc26ee80fb5b485e1496 +Subproject commit c8236e5765294f6b6590573910a68d3826671838 diff --git a/example/tools/inline-code b/example/tools/inline-code index dcd4c1774..31a086d7d 160000 --- a/example/tools/inline-code +++ b/example/tools/inline-code @@ -1 +1 @@ -Subproject commit dcd4c17740c9ba636140751596aff1e9f6ef6b01 +Subproject commit 31a086d7dc97169de34b9c191735cba7d63562d6 diff --git a/example/tools/list b/example/tools/list index a6dc6a692..bbc46d557 160000 --- a/example/tools/list +++ b/example/tools/list @@ -1 +1 @@ -Subproject commit a6dc6a692b88c9eff3d87223b239e7517b160c67 +Subproject commit bbc46d557bb5711dd27517272ae2754e1da04697 diff --git a/example/tools/nested-list b/example/tools/nested-list deleted file mode 160000 index 591bd2ca6..000000000 --- a/example/tools/nested-list +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 591bd2ca6839f923e3d37a1ebaad6e5d15075a89 diff --git a/example/tools/quote b/example/tools/quote index 9377ca713..78f70cf23 160000 --- a/example/tools/quote +++ b/example/tools/quote @@ -1 +1 @@ -Subproject commit 9377ca713f552576b8b11f77cf371b67261ec00b +Subproject commit 78f70cf2391cc8aaf2d2e59615de3ad833d180c3 diff --git a/example/tools/raw b/example/tools/raw index cae470fde..84b7d56b2 160000 --- a/example/tools/raw +++ b/example/tools/raw @@ -1 +1 @@ -Subproject commit cae470fded570ef9a82a45734526ccf45959e204 +Subproject commit 84b7d56b26a66d121edb6682ca205bf995d39034 diff --git a/example/tools/table b/example/tools/table index 2948cd759..5a57621c4 160000 --- a/example/tools/table +++ b/example/tools/table @@ -1 +1 @@ -Subproject commit 2948cd7595e632f7555e2dc09e6bac050a2b87ea +Subproject commit 5a57621c4e1abb884fd03e70862cb05b10bfe405 diff --git a/example/tools/warning b/example/tools/warning index e63e91aa8..0f3ec98b9 160000 --- a/example/tools/warning +++ b/example/tools/warning @@ -1 +1 @@ -Subproject commit e63e91aa833d774be9bf4a76013b1025a009989d +Subproject commit 0f3ec98b9a3b0ea8a9a71cffcb4e596e5c98aecb From 8b71e4939c6529b293e428d985a5ec16064a185e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:41:46 +0300 Subject: [PATCH 06/22] Bump version (#2895) Co-authored-by: github-actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 705733576..ef7b5f7d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.5", + "version": "2.31.0-rc.6", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From 83ddc00caa561ee09f323e9809274f5a93261f5d Mon Sep 17 00:00:00 2001 From: Vineeth Date: Sat, 21 Dec 2024 19:40:15 +0530 Subject: [PATCH 07/22] fix: properly handle visible whitespaces in empty blocks (#2865) * fix: handle whitespace input in empty placeholders correctly * fix: isNodeEmpty() to handle visible whitespaces * chore: bump version from 2.31.0-rc.5 to 2.31.0-rc.6 * chore: bump version from 2.31.0-rc.5 to 2.31.0-rc.6 * fix: submodules updated * fix: eslint errors * test: backspace removes trailing spaces, hides placeholder in empty blocks * fix: update incorrect tests * fix: resolving submodules issue * Create list --------- Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 1 + src/components/dom.ts | 2 +- .../tests/modules/BlockEvents/Backspace.cy.ts | 28 +++++++++++++++++-- test/cypress/tests/ui/Placeholders.cy.ts | 17 +++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5e7bffe1f..33aa0a099 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,7 @@ - `Fix` - Several toolbox items exported by the one tool have the same shortcut displayed in toolbox - `Improvement` - The current block reference will be updated in read-only mode when blocks are clicked - `Fix` - codex-notifier and codex-tooltip moved from devDependencies to dependencies in package.json to solve type errors +- `Fix` - Handle whitespace input in empty placeholder elements to prevent caret from moving unexpectedly to the end of the placeholder ### 2.30.7 diff --git a/src/components/dom.ts b/src/components/dom.ts index 83e875413..241041315 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -373,7 +373,7 @@ export default class Dom { nodeText = nodeText.replace(new RegExp(ignoreChars, 'g'), ''); } - return nodeText.trim().length === 0; + return nodeText.length === 0; } /** diff --git a/test/cypress/tests/modules/BlockEvents/Backspace.cy.ts b/test/cypress/tests/modules/BlockEvents/Backspace.cy.ts index ad39440e1..bb36e6503 100644 --- a/test/cypress/tests/modules/BlockEvents/Backspace.cy.ts +++ b/test/cypress/tests/modules/BlockEvents/Backspace.cy.ts @@ -118,6 +118,28 @@ describe('Backspace keydown', function () { .last() .should('have.text', '12'); }); + + it('   | — should delete visible and invisble whitespaces in the abscence of any non whitespace characters', function () { + createEditorWithTextBlocks([ + '1', + '   ', + ]); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .last() + .click() + .type('{downArrow}') + .type('{backspace}') + .type('{backspace}') + .type('{backspace}') + .type('{backspace}'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .last() + .should('have.text', '1'); + }); }); it('should just delete chars (native behaviour) when some fragment is selected', function () { @@ -184,7 +206,7 @@ describe('Backspace keydown', function () { * Saving logic is not necessary for this test */ // eslint-disable-next-line @typescript-eslint/no-empty-function - public save(): void {} + public save(): void { } } cy.createEditor({ @@ -545,7 +567,7 @@ describe('Backspace keydown', function () { * Saving logic is not necessary for this test */ // eslint-disable-next-line @typescript-eslint/no-empty-function - public save(): void {} + public save(): void { } } cy.createEditor({ @@ -678,7 +700,7 @@ describe('Backspace keydown', function () { describe('at the start of the first Block', function () { it('should do nothing if Block is not empty', function () { - createEditorWithTextBlocks([ 'The only block. Not empty' ]); + createEditorWithTextBlocks(['The only block. Not empty']); cy.get('[data-cy=editorjs]') .find('.ce-paragraph') diff --git a/test/cypress/tests/ui/Placeholders.cy.ts b/test/cypress/tests/ui/Placeholders.cy.ts index 79f19e699..d4a7ce968 100644 --- a/test/cypress/tests/ui/Placeholders.cy.ts +++ b/test/cypress/tests/ui/Placeholders.cy.ts @@ -77,4 +77,21 @@ describe('Placeholders', function () { .getPseudoElementContent('::before') .should('eq', 'none'); }); + + it('should be hidden when user adds trailing whitespace characters', function () { + cy.createEditor({ + placeholder: PLACEHOLDER_TEXT, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .as('firstBlock') + .getPseudoElementContent('::before') + .should('eq', PLACEHOLDER_TEXT); + + cy.get('@firstBlock') + .type(' ') + .getPseudoElementContent('::before') + .should('eq', 'none'); + }); }); From 75491204eb823585e6c39c9aaa0a3c62216a9852 Mon Sep 17 00:00:00 2001 From: Vineeth Date: Sat, 21 Dec 2024 20:15:53 +0530 Subject: [PATCH 08/22] chore: bump version for release (#2896) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef7b5f7d3..8081225f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.6", + "version": "2.31.0-rc.7", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From dae2668178bc30ab2e3838a1d3ff296c6562df52 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 7 Jan 2025 19:34:59 +0300 Subject: [PATCH 09/22] dx(submodules): remove tools submodules from the repo (#2898) * rm all submodules * rm commands * Update package.json --- .gitmodules | 48 ---- devserver.js | 128 ---------- docs/CHANGELOG.md | 1 + example/example-dev.html | 422 -------------------------------- example/example-rtl.html | 30 +-- example/tools/code | 1 - example/tools/delimiter | 1 - example/tools/embed | 1 - example/tools/header | 1 - example/tools/image | 1 - example/tools/inline-code | 1 - example/tools/link | 1 - example/tools/list | 1 - example/tools/marker | 1 - example/tools/quote | 1 - example/tools/raw | 1 - example/tools/simple-image | 1 - example/tools/table | 1 - example/tools/text-variant-tune | 1 - example/tools/warning | 1 - package.json | 10 +- 21 files changed, 17 insertions(+), 637 deletions(-) delete mode 100644 .gitmodules delete mode 100644 devserver.js delete mode 100644 example/example-dev.html delete mode 160000 example/tools/code delete mode 160000 example/tools/delimiter delete mode 160000 example/tools/embed delete mode 160000 example/tools/header delete mode 160000 example/tools/image delete mode 160000 example/tools/inline-code delete mode 160000 example/tools/link delete mode 160000 example/tools/list delete mode 160000 example/tools/marker delete mode 160000 example/tools/quote delete mode 160000 example/tools/raw delete mode 160000 example/tools/simple-image delete mode 160000 example/tools/table delete mode 160000 example/tools/text-variant-tune delete mode 160000 example/tools/warning diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 44e1a2ff4..000000000 --- a/.gitmodules +++ /dev/null @@ -1,48 +0,0 @@ -[submodule "example/tools/inline-code"] - path = example/tools/inline-code - url = https://github.com/editor-js/inline-code -[submodule "example/tools/header"] - path = example/tools/header - url = https://github.com/editor-js/header -[submodule "example/tools/delimiter"] - path = example/tools/delimiter - url = https://github.com/editor-js/delimiter -[submodule "example/tools/quote"] - path = example/tools/quote - url = https://github.com/editor-js/quote -[submodule "example/tools/simple-image"] - path = example/tools/simple-image - url = https://github.com/editor-js/simple-image -[submodule "example/tools/marker"] - path = example/tools/marker - url = https://github.com/editor-js/marker -[submodule "example/tools/code"] - path = example/tools/code - url = https://github.com/editor-js/code -[submodule "example/tools/image"] - path = example/tools/image - url = https://github.com/editor-js/image -[submodule "example/tools/embed"] - path = example/tools/embed - url = https://github.com/editor-js/embed -[submodule "example/tools/table"] - path = example/tools/table - url = https://github.com/editor-js/table -[submodule "example/tools/link"] - path = example/tools/link - url = https://github.com/editor-js/link -[submodule "example/tools/raw"] - path = example/tools/raw - url = https://github.com/editor-js/raw -[submodule "example/tools/warning"] - path = example/tools/warning - url = https://github.com/editor-js/warning -[submodule "example/tools/underline"] - path = example/tools/underline - url = https://github.com/editor-js/underline -[submodule "example/tools/text-variant-tune"] - path = example/tools/text-variant-tune - url = https://github.com/editor-js/text-variant-tune -[submodule "example/tools/list"] - path = example/tools/list - url = https://github.com/editor-js/list diff --git a/devserver.js b/devserver.js deleted file mode 100644 index 5087a7b47..000000000 --- a/devserver.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Server for testing example page on mobile devices. - * - * Usage: - * 1. run `yarn devserver:start` - * 2. Open `http://{ip_address}:3000/example/example-dev.html` - * where {ip_address} is IP of your machine. - * - * Also, can serve static files from `/example` or `/dist` on any device in local network. - */ -const path = require('path'); -const fs = require('fs'); -const http = require('http'); -const { networkInterfaces } = require('os'); - -const port = 3000; -const localhost = '127.0.0.1'; -const nonRoutableAddress = '0.0.0.0'; -const host = getHost(); -const server = http.createServer(serveStatic([ - '/example', - '/dist', -])); - -server.listen(port, nonRoutableAddress, () => { - console.log(` - -${wrapInColor('Editor.js 💖', consoleColors.hiColor)} devserver is running ᕕ(⌐■_■)ᕗ ✨ ---------------------------------------------- -${wrapInColor('http://' + host + ':' + port + '/example/example-dev.html', consoleColors.fgGreen)} ---------------------------------------------- -Page can be opened from any device connected to the same local network. -`); - - if (host === localhost) { - console.log(wrapInColor('Looks like you are not connected to any Network so you couldn\'t debug the Editor on your mobile device at the moment.', consoleColors.fgRed)); - } -}); - -/** - * Serves files from specified directories - * - * @param {string[]} paths - directories files from which should be served - * @returns {Function} - */ -function serveStatic(paths) { - return (request, response) => { - const resource = request.url; - const isPathAllowed = paths.find(p => resource.startsWith(p)); - - if (!isPathAllowed) { - response.writeHead(404); - response.end(); - - return; - } - const filePath = path.join(__dirname, resource); - - try { - const stat = fs.statSync(filePath); - - response.writeHead(200, { - 'Content-Length': stat.size, - }); - const readStream = fs.createReadStream(filePath); - - readStream.on('error', e => { - throw e; - }); - readStream.pipe(response); - } catch (e) { - response.writeHead(500); - response.end(e.toString()); - } - }; -} - -/** - * Returns IP address of a machine - * - * @returns {string} - */ -function getHost() { - const nets = networkInterfaces(); - const results = {}; - - for (const name of Object.keys(nets)) { - for (const net of nets[name]) { - // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses - if (net.family === 'IPv4' && !net.internal) { - if (!results[name]) { - results[name] = []; - } - results[name].push(net.address); - } - } - } - - /** - * Offline case - */ - if (Object.keys(results).length === 0) { - return localhost; - } - - return results['en0'][0]; -} - -/** - * Terminal output colors - */ -const consoleColors = { - fgMagenta: 35, - fgRed: 31, - fgGreen: 32, - hiColor: 1, -}; - -/** - * Set a terminal color to the message - * - * @param {string} msg - text to wrap - * @param {string} color - color - * @returns {string} - */ -function wrapInColor(msg, color) { - return '\x1b[' + color + 'm' + msg + '\x1b[0m'; -} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 33aa0a099..fcb6078e9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,7 @@ - `Improvement` - The current block reference will be updated in read-only mode when blocks are clicked - `Fix` - codex-notifier and codex-tooltip moved from devDependencies to dependencies in package.json to solve type errors - `Fix` - Handle whitespace input in empty placeholder elements to prevent caret from moving unexpectedly to the end of the placeholder +- `DX` - Tools submodules removed from the repository ### 2.30.7 diff --git a/example/example-dev.html b/example/example-dev.html deleted file mode 100644 index 05d713175..000000000 --- a/example/example-dev.html +++ /dev/null @@ -1,422 +0,0 @@ - - - - - - Editor.js 🤩🧦🤨 example - - - - - - - -
- -
-
-
- No core bundle file found. Run yarn build -
-
- No submodules found. Run yarn pull_tools && yarn tools:update -
-
- editor.save() -
-
-
- Readonly: - - Off - -   -
- toggle -
-
-
-
- Show - blocks boundaries -
-
-
-
- Enable - thin mode -
-
-
-
-
-
-
-
-
-

-
-      
-    
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/example-rtl.html b/example/example-rtl.html index abeab23b4..1a3473166 100644 --- a/example/example-rtl.html +++ b/example/example-rtl.html @@ -53,21 +53,21 @@ Read more in Tool's README file. For example: https://github.com/editor-js/header#installation --> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/example/tools/code b/example/tools/code deleted file mode 160000 index 82402cb86..000000000 --- a/example/tools/code +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 82402cb86a327e3c64bef653d953533e2c3613dd diff --git a/example/tools/delimiter b/example/tools/delimiter deleted file mode 160000 index 95a5eb90d..000000000 --- a/example/tools/delimiter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 95a5eb90dd2e2e8ab153eb66b59a70cdafdf2d7f diff --git a/example/tools/embed b/example/tools/embed deleted file mode 160000 index 801580fbd..000000000 --- a/example/tools/embed +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 801580fbdb7ab0ad1e975cfdaab38ada6625e301 diff --git a/example/tools/header b/example/tools/header deleted file mode 160000 index 3e457cbac..000000000 --- a/example/tools/header +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3e457cbac2c5da53fff1b02b99ddaccaa577f401 diff --git a/example/tools/image b/example/tools/image deleted file mode 160000 index c8236e576..000000000 --- a/example/tools/image +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c8236e5765294f6b6590573910a68d3826671838 diff --git a/example/tools/inline-code b/example/tools/inline-code deleted file mode 160000 index 31a086d7d..000000000 --- a/example/tools/inline-code +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 31a086d7dc97169de34b9c191735cba7d63562d6 diff --git a/example/tools/link b/example/tools/link deleted file mode 160000 index aaa69d540..000000000 --- a/example/tools/link +++ /dev/null @@ -1 +0,0 @@ -Subproject commit aaa69d5408bad34027d6252a3892d40f9fa121be diff --git a/example/tools/list b/example/tools/list deleted file mode 160000 index bbc46d557..000000000 --- a/example/tools/list +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bbc46d557bb5711dd27517272ae2754e1da04697 diff --git a/example/tools/marker b/example/tools/marker deleted file mode 160000 index 8d6897fca..000000000 --- a/example/tools/marker +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8d6897fca43e387bcdf4a681380be975fe8f2a07 diff --git a/example/tools/quote b/example/tools/quote deleted file mode 160000 index 78f70cf23..000000000 --- a/example/tools/quote +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 78f70cf2391cc8aaf2d2e59615de3ad833d180c3 diff --git a/example/tools/raw b/example/tools/raw deleted file mode 160000 index 84b7d56b2..000000000 --- a/example/tools/raw +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 84b7d56b26a66d121edb6682ca205bf995d39034 diff --git a/example/tools/simple-image b/example/tools/simple-image deleted file mode 160000 index 963883520..000000000 --- a/example/tools/simple-image +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 963883520c7bbe5040366335c9a37bbdc7cf60fd diff --git a/example/tools/table b/example/tools/table deleted file mode 160000 index 5a57621c4..000000000 --- a/example/tools/table +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5a57621c4e1abb884fd03e70862cb05b10bfe405 diff --git a/example/tools/text-variant-tune b/example/tools/text-variant-tune deleted file mode 160000 index 7f51a16d4..000000000 --- a/example/tools/text-variant-tune +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7f51a16d4ab46eff9364f09cea52e09518896d2a diff --git a/example/tools/warning b/example/tools/warning deleted file mode 160000 index 0f3ec98b9..000000000 --- a/example/tools/warning +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0f3ec98b9a3b0ea8a9a71cffcb4e596e5c98aecb diff --git a/package.json b/package.json index 8081225f2..72780aee1 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,8 @@ "lint:errors": "eslint src/ --ext .ts --quiet", "lint:fix": "eslint src/ --ext .ts --fix", "lint:tests": "eslint test/ --ext .ts", - "pull_tools": "git submodule update --init --recursive", - "_tools:checkout": "git submodule foreach \"git checkout master || git checkout main\"", - "_tools:pull": "git submodule foreach git pull", - "_tools:yarn": "git submodule foreach yarn", - "_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:test && cypress run", - "test:e2e:open": "yarn build:test && cypress open", - "devserver:start": "yarn build && node ./devserver.js" + "test:e2e:open": "yarn build:test && cypress open" }, "author": "CodeX", "license": "Apache-2.0", From df81003007e2acf896853337f08786ce1cb6220d Mon Sep 17 00:00:00 2001 From: Omotayo Obafemi <33766411+obafemitayor@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:23:38 +0000 Subject: [PATCH 10/22] Bug Fix For When "/" Overides external text (#2894) * Bug Fix For When / Overides external text * Moved fix to blockEvents * Moved fix to blockEvents * Moved fix to blockEvents * Refactored test to simulate behaviour * Added fix to change log * Refactored test to mimick exact behaviour of the bug --------- Co-authored-by: Omotayo Obafemi Co-authored-by: Peter --- docs/CHANGELOG.md | 1 + src/components/modules/blockEvents.ts | 6 +++ .../tests/modules/BlockEvents/Slash.cy.ts | 54 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fcb6078e9..73a8fc024 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,7 @@ - `Improvement` - The current block reference will be updated in read-only mode when blocks are clicked - `Fix` - codex-notifier and codex-tooltip moved from devDependencies to dependencies in package.json to solve type errors - `Fix` - Handle whitespace input in empty placeholder elements to prevent caret from moving unexpectedly to the end of the placeholder +- `Fix` - Fix when / overides selected text outside of the editor - `DX` - Tools submodules removed from the repository ### 2.30.7 diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index c48bba536..40e0973e2 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -237,6 +237,12 @@ export default class BlockEvents extends Module { * @param event - keydown */ private slashPressed(event: KeyboardEvent): void { + const wasEventTriggeredInsideEditor = this.Editor.UI.nodes.wrapper.contains(event.target as Node); + + if (!wasEventTriggeredInsideEditor) { + return; + } + const currentBlock = this.Editor.BlockManager.currentBlock; const canOpenToolbox = currentBlock.isEmpty; diff --git a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts index 0d9db5fc1..49fe1fddb 100644 --- a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts +++ b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts @@ -92,6 +92,60 @@ describe('Slash keydown', function () { .should('eq', 'Hello/'); }); }); + + describe('pressed outside editor', function () { + it('should not modify any text outside editor when text block is selected', () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: '', + }, + }, + ], + }, + }); + + cy.document().then((doc) => { + const title = doc.querySelector('h1'); + + if (title) { + title.setAttribute('data-cy', 'page-title'); + } + }); + + // Step 1 + // Click on the plus button and select the text option + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click(); + cy.get('[data-cy=editorjs]') + .find('.ce-toolbar__plus') + .click({ force: true }); + cy.get('[data-cy="toolbox"] .ce-popover__container') + .contains('Text') + .click(); + + // Step 2 + // Select the 'Editor.js test page' text + cy.get('[data-cy=page-title]') + .invoke('attr', 'contenteditable', 'true') + .click() + .type('{selectall}') + .invoke('removeAttr', 'contenteditable'); + + // Step 3 + // Press the Slash key + cy.get('[data-cy=page-title]') + .trigger('keydown', { key: '/', + code: 'Slash', + which: 191 }); + + cy.get('[data-cy=page-title]').should('have.text', 'Editor.js test page'); + }); + }); }); describe('CMD+Slash keydown', function () { From 4827bdb6ed6dcc132c6d1a1ff36548467800f184 Mon Sep 17 00:00:00 2001 From: Omotayo Obafemi <33766411+obafemitayor@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:52:40 +0000 Subject: [PATCH 11/22] Added fix for memory leak issue (#2893) * Added fix for memory leak issue * Documented the fix in docs/CHANGELOG.md * v2.31.0 * Documented the fix in docs/CHANGELOG.md * Documented the fix in docs/CHANGELOG.md * Documented the fix in docs/CHANGELOG.md * Documented the fix in docs/CHANGELOG.md --------- Co-authored-by: Omotayo Obafemi Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 1 + src/components/utils/shortcuts.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 73a8fc024..e3e4dd9d7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,7 @@ - `Improvement` - The current block reference will be updated in read-only mode when blocks are clicked - `Fix` - codex-notifier and codex-tooltip moved from devDependencies to dependencies in package.json to solve type errors - `Fix` - Handle whitespace input in empty placeholder elements to prevent caret from moving unexpectedly to the end of the placeholder +- `Fix` - Fix the memory leak issue in `Shortcuts` class - `Fix` - Fix when / overides selected text outside of the editor - `DX` - Tools submodules removed from the repository diff --git a/src/components/utils/shortcuts.ts b/src/components/utils/shortcuts.ts index 8cf51ff93..967243d27 100644 --- a/src/components/utils/shortcuts.ts +++ b/src/components/utils/shortcuts.ts @@ -86,7 +86,15 @@ class Shortcuts { const shortcuts = this.registeredShortcuts.get(element); - this.registeredShortcuts.set(element, shortcuts.filter(el => el !== shortcut)); + const filteredShortcuts = shortcuts.filter(el => el !== shortcut); + + if (filteredShortcuts.length === 0) { + this.registeredShortcuts.delete(element); + + return; + } + + this.registeredShortcuts.set(element, filteredShortcuts); } /** From ae9b03b2474f5b516ee01e3acf95c0140d20793a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:25:32 +0300 Subject: [PATCH 12/22] Bump version (#2899) Co-authored-by: github-actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72780aee1..7541710b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.7", + "version": "2.31.0-rc.8", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From 1b5c37dad1c21e89e578a991538f11f3750c0b55 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 11 Mar 2025 10:01:46 +0300 Subject: [PATCH 13/22] imrovement(flipper): allow to select next/prev line by shift arrows (#2918) * fix: prevent flipper navigation when shift key is pressed * rm logs * feat: improve line selection with Shift + Up/Down * fix lint action * fix action * upd --- .github/workflows/eslint.yml | 10 +++--- docs/CHANGELOG.md | 2 ++ src/components/flipper.ts | 12 +++++++- test/cypress/tests/utils/flipper.cy.ts | 42 ++++++++++++++++++++++++-- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 1db62120c..973af8897 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -9,15 +9,13 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Cache node modules - uses: actions/cache@v1 + - name: Cache dependencies + uses: actions/cache@v4 with: - path: node_modules + path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} restore-keys: | - ${{ runner.OS }}-build-${{ env.cache-name }}- - ${{ runner.OS }}-build- - ${{ runner.OS }}- + ${{ runner.os }}-node- - run: yarn - run: yarn lint diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e3e4dd9d7..82aab6541 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,8 @@ - `Fix` - Fix the memory leak issue in `Shortcuts` class - `Fix` - Fix when / overides selected text outside of the editor - `DX` - Tools submodules removed from the repository +- `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping + ### 2.30.7 diff --git a/src/components/flipper.ts b/src/components/flipper.ts index 516e2b620..5b6e04359 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -199,13 +199,23 @@ export default class Flipper { * * @param event - keydown event */ - private onKeyDown = (event): void => { + private onKeyDown = (event: KeyboardEvent): void => { const isReady = this.isEventReadyForHandling(event); if (!isReady) { return; } + const isShiftKey = event.shiftKey; + + /** + * If shift key is pressed, do nothing + * Allows to select next/prev lines of text using keyboard + */ + if (isShiftKey === true) { + return; + } + /** * Prevent only used keys default behaviour * (allows to navigate by ARROW DOWN, for example) diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 114a38e1e..1fad530d6 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -46,10 +46,10 @@ class SomePlugin { } describe('Flipper', () => { - it('should prevent plugins event handlers from being called while keyboard navigation', () => { - const ARROW_DOWN_KEY_CODE = 40; - const ENTER_KEY_CODE = 13; + const ARROW_DOWN_KEY_CODE = 40; + const ENTER_KEY_CODE = 13; + it('should prevent plugins event handlers from being called while keyboard navigation', () => { const sampleText = 'sample text'; cy.createEditor({ @@ -101,4 +101,40 @@ describe('Flipper', () => { expect(SomePlugin.pluginInternalKeydownHandler).to.have.not.been.called; }); + + it('should not flip when shift key is pressed', () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.', + }, + }, + ], + }, + autofocus: true, + }); + + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .as('paragraph') + .selectTextByOffset([0, 10]) + .wait(200); + + cy.get('@paragraph') + .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE, + shiftKey: true }); + + // eslint-disable-next-line cypress/require-data-selectors + cy.get('[data-cy="inline-toolbar"]') + .get('.ce-popover--opened') + .as('popover') + .should('exist'); + + cy.get('@popover') + .get('.ce-popover-item--focused') + .should('not.exist'); + }); }); From 92e9594477e5cfbe4f1517abb2909020c1f41569 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 11:17:40 +0300 Subject: [PATCH 14/22] Bump version (#2919) Co-authored-by: github-actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7541710b5..172be4105 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.8", + "version": "2.31.0-rc.9", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From 64ad725c0c05f124f13409dd47d784b94a73a82a Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 30 Apr 2025 19:48:20 +0300 Subject: [PATCH 15/22] improvement(caret): caret.setToBlock() offset argument improved (#2922) * chore(caret): caret.setToBlock offset improved * handle empty block * Update caret.cy.ts * fix eslint --- docs/CHANGELOG.md | 2 +- package.json | 2 +- src/components/dom.ts | 63 ++++ src/components/modules/caret.ts | 35 +- .../support/utils/createParagraphMock.ts | 19 + test/cypress/tests/api/caret.cy.ts | 328 +++++++++++++----- 6 files changed, 338 insertions(+), 111 deletions(-) create mode 100644 test/cypress/support/utils/createParagraphMock.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 82aab6541..5770a1e74 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,7 +15,7 @@ - `Fix` - Fix when / overides selected text outside of the editor - `DX` - Tools submodules removed from the repository - `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping - +- `Improvement` - The API `caret.setToBlock()` offset now works across the entire block content, not just the first or last node. ### 2.30.7 diff --git a/package.json b/package.json index 172be4105..8f60f1a0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.9", + "version": "2.31.0-rc.10", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/dom.ts b/src/components/dom.ts index 241041315..675735555 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -587,6 +587,69 @@ export default class Dom { right: left + rect.width, }; } + + /** + * Find text node and offset by total content offset + * + * @param {Node} root - root node to start search from + * @param {number} totalOffset - offset relative to the root node content + * @returns {{node: Node | null, offset: number}} - node and offset inside node + */ + public static getNodeByOffset(root: Node, totalOffset: number): {node: Node | null; offset: number} { + let currentOffset = 0; + let lastTextNode: Node | null = null; + + const walker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT, + null + ); + + let node: Node | null = walker.nextNode(); + + while (node) { + const textContent = node.textContent; + const nodeLength = textContent === null ? 0 : textContent.length; + + lastTextNode = node; + + if (currentOffset + nodeLength >= totalOffset) { + break; + } + + currentOffset += nodeLength; + node = walker.nextNode(); + } + + /** + * If no node found or last node is empty, return null + */ + if (!lastTextNode) { + return { + node: null, + offset: 0, + }; + } + + const textContent = lastTextNode.textContent; + + if (textContent === null || textContent.length === 0) { + return { + node: null, + offset: 0, + }; + } + + /** + * Calculate offset inside found node + */ + const nodeOffset = Math.min(totalOffset - currentOffset, textContent.length); + + return { + node: lastTextNode, + offset: nodeOffset, + }; + } } /** diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index 276eef4b0..db8a4f3b0 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -43,7 +43,7 @@ export default class Caret extends Module { * @param {Block} block - Block class * @param {string} position - position where to set caret. * If default - leave default behaviour and apply offset if it's passed - * @param {number} offset - caret offset regarding to the text node + * @param {number} offset - caret offset regarding to the block content */ public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void { const { BlockManager, BlockSelection } = this.Editor; @@ -88,23 +88,32 @@ export default class Caret extends Module { return; } - const nodeToSet = $.getDeepestNode(element, position === this.positions.END); - const contentLength = $.getContentLength(nodeToSet); + let nodeToSet: Node; + let offsetToSet = offset; - switch (true) { - case position === this.positions.START: - offset = 0; - break; - case position === this.positions.END: - case offset > contentLength: - offset = contentLength; - break; + if (position === this.positions.START) { + nodeToSet = $.getDeepestNode(element, false) as Node; + offsetToSet = 0; + } else if (position === this.positions.END) { + nodeToSet = $.getDeepestNode(element, true) as Node; + offsetToSet = $.getContentLength(nodeToSet); + } else { + const { node, offset: nodeOffset } = $.getNodeByOffset(element, offset); + + if (node) { + nodeToSet = node; + offsetToSet = nodeOffset; + } else { // case for empty block's input + nodeToSet = $.getDeepestNode(element, false) as Node; + offsetToSet = 0; + } } - this.set(nodeToSet as HTMLElement, offset); + this.set(nodeToSet as HTMLElement, offsetToSet); BlockManager.setCurrentBlockByChildNode(block.holder); - BlockManager.currentBlock.currentInput = element; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + BlockManager.currentBlock!.currentInput = element; } /** diff --git a/test/cypress/support/utils/createParagraphMock.ts b/test/cypress/support/utils/createParagraphMock.ts new file mode 100644 index 000000000..30166e87e --- /dev/null +++ b/test/cypress/support/utils/createParagraphMock.ts @@ -0,0 +1,19 @@ +import { nanoid } from 'nanoid'; + +/** + * Creates a paragraph mock + * + * @param text - text for the paragraph + * @returns paragraph mock + */ +export function createParagraphMock(text: string): { + id: string; + type: string; + data: { text: string }; +} { + return { + id: nanoid(), + type: 'paragraph', + data: { text }, + }; +} \ No newline at end of file diff --git a/test/cypress/tests/api/caret.cy.ts b/test/cypress/tests/api/caret.cy.ts index 882bc1534..53a7d1fc4 100644 --- a/test/cypress/tests/api/caret.cy.ts +++ b/test/cypress/tests/api/caret.cy.ts @@ -1,113 +1,249 @@ +import { createParagraphMock } from '../../support/utils/createParagraphMock'; import type EditorJS from '../../../../types'; /** * Test cases for Caret API */ describe('Caret API', () => { - const paragraphDataMock = { - id: 'bwnFX5LoX7', - type: 'paragraph', - data: { - text: 'The first block content mock.', - }, - }; - describe('.setToBlock()', () => { - /** - * The arrange part of the following tests are the same: - * - create an editor - * - move caret out of the block by default - */ - beforeEach(() => { - cy.createEditor({ - data: { - blocks: [ - paragraphDataMock, - ], - }, - }).as('editorInstance'); + describe('first argument', () => { + const paragraphDataMock = createParagraphMock('The first block content mock.'); /** - * Blur caret from the block before setting via api + * The arrange part of the following tests are the same: + * - create an editor + * - move caret out of the block by default */ - cy.get('[data-cy=editorjs]') - .click(); - }); + beforeEach(() => { + cy.createEditor({ + data: { + blocks: [ + paragraphDataMock, + ], + }, + }).as('editorInstance'); - it('should set caret to a block (and return true) if block index is passed as argument', () => { - cy.get('@editorInstance') - .then(async (editor) => { - const returnedValue = editor.caret.setToBlock(0); - - /** - * Check that caret belongs block - */ - cy.window() - .then((window) => { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - cy.get('[data-cy=editorjs]') - .find('.ce-block') - .first() - .should(($block) => { - expect($block[0].contains(range.startContainer)).to.be.true; - }); - }); - - expect(returnedValue).to.be.true; - }); - }); + /** + * Blur caret from the block before setting via api + */ + cy.get('[data-cy=editorjs]') + .click(); + }); + it('should set caret to a block (and return true) if block index is passed as argument', () => { + cy.get('@editorInstance') + .then(async (editor) => { + const returnedValue = editor.caret.setToBlock(0); + + /** + * Check that caret belongs block + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + const range = selection.getRangeAt(0); + + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .first() + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); + + expect(returnedValue).to.be.true; + }); + }); + + it('should set caret to a block (and return true) if block id is passed as argument', () => { + cy.get('@editorInstance') + .then(async (editor) => { + const returnedValue = editor.caret.setToBlock(paragraphDataMock.id); + + /** + * Check that caret belongs block + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + const range = selection.getRangeAt(0); + + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .first() + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); + + expect(returnedValue).to.be.true; + }); + }); + + it('should set caret to a block (and return true) if Block API is passed as argument', () => { + cy.get('@editorInstance') + .then(async (editor) => { + const block = editor.blocks.getById(paragraphDataMock.id); + const returnedValue = editor.caret.setToBlock(block); - it('should set caret to a block (and return true) if block id is passed as argument', () => { - cy.get('@editorInstance') - .then(async (editor) => { - const returnedValue = editor.caret.setToBlock(paragraphDataMock.id); - - /** - * Check that caret belongs block - */ - cy.window() - .then((window) => { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - cy.get('[data-cy=editorjs]') - .find('.ce-block') - .first() - .should(($block) => { - expect($block[0].contains(range.startContainer)).to.be.true; - }); - }); - - expect(returnedValue).to.be.true; - }); + /** + * Check that caret belongs block + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + const range = selection.getRangeAt(0); + + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .first() + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); + + expect(returnedValue).to.be.true; + }); + }); }); - it('should set caret to a block (and return true) if Block API is passed as argument', () => { - cy.get('@editorInstance') - .then(async (editor) => { - const block = editor.blocks.getById(paragraphDataMock.id); - const returnedValue = editor.caret.setToBlock(block); - - /** - * Check that caret belongs block - */ - cy.window() - .then((window) => { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - cy.get('[data-cy=editorjs]') - .find('.ce-block') - .first() - .should(($block) => { - expect($block[0].contains(range.startContainer)).to.be.true; - }); - }); - - expect(returnedValue).to.be.true; - }); + describe('offset', () => { + it('should set caret at specific offset in text content', () => { + const paragraphDataMock = createParagraphMock('Plain text content.'); + + cy.createEditor({ + data: { + blocks: [ + paragraphDataMock, + ], + }, + }).as('editorInstance'); + + cy.get('@editorInstance') + .then(async (editor) => { + const block = editor.blocks.getById(paragraphDataMock.id); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + editor.caret.setToBlock(block!, 'default', 5); + + cy.window() + .then((window) => { + const selection = window.getSelection(); + + if (!selection) { + throw new Error('Selection not found'); + } + const range = selection.getRangeAt(0); + + expect(range.startOffset).to.equal(5); + }); + }); + }); + + it('should set caret at correct offset when text contains HTML elements', () => { + const paragraphDataMock = createParagraphMock('1234567!'); + + cy.createEditor({ + data: { + blocks: [ + paragraphDataMock, + ], + }, + }).as('editorInstance'); + + cy.get('@editorInstance') + .then(async (editor) => { + const block = editor.blocks.getById(paragraphDataMock.id); + + // Set caret after "12345" + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + editor.caret.setToBlock(block!, 'default', 6); + + cy.window() + .then((window) => { + const selection = window.getSelection(); + + if (!selection) { + throw new Error('Selection not found'); + } + const range = selection.getRangeAt(0); + + expect(range.startContainer.textContent).to.equal('567'); + expect(range.startOffset).to.equal(2); + }); + }); + }); + + it('should handle offset beyond content length', () => { + const paragraphDataMock = createParagraphMock('1234567890'); + + cy.createEditor({ + data: { + blocks: [ + paragraphDataMock, + ], + }, + }).as('editorInstance'); + + cy.get('@editorInstance') + .then(async (editor) => { + const block = editor.blocks.getById(paragraphDataMock.id); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const contentLength = block!.holder.textContent?.length ?? 0; + + // Set caret beyond content length + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + editor.caret.setToBlock(block!, 'default', contentLength + 10); + + cy.window() + .then((window) => { + const selection = window.getSelection(); + + if (!selection) { + throw new Error('Selection not found'); + } + const range = selection.getRangeAt(0); + + // Should be at the end of content + expect(range.startOffset).to.equal(contentLength); + }); + }); + }); + + it('should handle offset in nested HTML structure', () => { + const paragraphDataMock = createParagraphMock('123456789!'); + + cy.createEditor({ + data: { + blocks: [ + paragraphDataMock, + ], + }, + }).as('editorInstance'); + + cy.get('@editorInstance') + .then(async (editor) => { + const block = editor.blocks.getById(paragraphDataMock.id); + + + // Set caret after "8" + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + editor.caret.setToBlock(block!, 'default', 8); + + cy.window() + .then((window) => { + const selection = window.getSelection(); + + if (!selection) { + throw new Error('Selection not found'); + } + const range = selection.getRangeAt(0); + + expect(range.startContainer.textContent).to.equal('789'); + expect(range.startOffset).to.equal(2); + }); + }); + }); }); }); }); From aac846945c7955e807f29437acfc4070a4909361 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sat, 6 Sep 2025 16:14:27 +0300 Subject: [PATCH 16/22] Create .nvmrc --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..ef33d6510 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.20.1 From ba2223a44a68805dd3958fb3d1b854b5fd67e681 Mon Sep 17 00:00:00 2001 From: narpat-ps Date: Sat, 6 Sep 2025 18:50:08 +0530 Subject: [PATCH 17/22] resolve "Can't find a Block to remove" error in renderFromHTML (#2941) * fix(blocks):Error occurred when calling renderFromHTML: Can't find a Block to remove. * fix: resolve "Can't find a Block to remove" error in renderFromHTML - Make renderFromHTML async and await BlockManager.clear() to prevent race condition - Change removeBlock order: remove from array before destroy to prevent index invalidation - Fix clear() method to copy blocks array before iteration to avoid modification during loop Fixes issue where renderFromHTML would fail with "Can't find a Block to remove" error due to concurrent block removal operations and array modification during iteration. Resolves #2518 --- src/components/modules/api/blocks.ts | 4 ++-- src/components/modules/blockManager.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index f9297d5d4..9ad22176a 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -224,8 +224,8 @@ export default class BlocksAPI extends Module { * @param {string} data - HTML string to render * @returns {Promise} */ - public renderFromHTML(data: string): Promise { - this.Editor.BlockManager.clear(); + public async renderFromHTML(data: string): Promise { + await this.Editor.BlockManager.clear(); return this.Editor.Paste.processText(data, true); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 5b5c931dd..0f9cdd314 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -535,8 +535,8 @@ export default class BlockManager extends Module { throw new Error('Can\'t find a Block to remove'); } - block.destroy(); this._blocks.remove(index); + block.destroy(); /** * Force call of didMutated event on Block removal @@ -898,7 +898,10 @@ export default class BlockManager extends Module { public async clear(needToAddDefaultBlock = false): Promise { const queue = new PromiseQueue(); - this.blocks.forEach((block) => { + // Create a copy of the blocks array to avoid issues with array modification during iteration + const blocksToRemove = [...this.blocks]; + + blocksToRemove.forEach((block) => { queue.add(async () => { await this.removeBlock(block, false); }); From e3d310e2a2ead4e867bc255eaa8aedab3f01de4c Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sat, 6 Sep 2025 16:20:35 +0300 Subject: [PATCH 18/22] add changelog --- docs/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5770a1e74..7eff455e3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,11 @@ - `DX` - Tools submodules removed from the repository - `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping - `Improvement` - The API `caret.setToBlock()` offset now works across the entire block content, not just the first or last node. +- `Improvement` - The API `blocks.renderFromHTML()` became async and now can be awaited. +- `Fix` - `blocks.renderFromHTML()` — Error "Can't find a Block to remove." fixed +- `Fix` - The API `.clear()` index invalidation fixed + + ### 2.30.7 From 8b15c00c11b302fcdcc76e84538485dbbb4a95dd Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 6 Sep 2025 16:31:44 +0300 Subject: [PATCH 19/22] release 2.31 (#2956) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f60f1a0d..5039584f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.10", + "version": "2.31.0", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From de6fd78aa7842e4b8327fd226506c2e358e92f55 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 15 Sep 2025 20:14:25 +0300 Subject: [PATCH 20/22] add example of how to use i18n for Convert To buttons --- example/example-i18n.html | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/example/example-i18n.html b/example/example-i18n.html index bc1aaf7a9..f765f1e98 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -107,7 +107,7 @@ image: ImageTool, list: { - class: List, + class: EditorjsList, inlineToolbar: true, shortcut: 'CMD+SHIFT+L' }, @@ -198,7 +198,11 @@ }, "popover": { "Filter": "Поиск", - "Nothing found": "Ничего не найдено" + "Nothing found": "Ничего не найдено", + /** + * Translation of "Convert To" at the Block Tunes Popover + */ + "Convert to": "Конвертировать в" } }, @@ -221,7 +225,7 @@ "Bold": "Полужирный", "Italic": "Курсив", "InlineCode": "Моноширинный", - "Image": "Картинка" + "Image": "Картинка", }, /** @@ -274,7 +278,13 @@ "list": { "Ordered": "Нумерованный", "Unordered": "Маркированный", - } + }, + /** + * Translation of "Convert To" at the Inline Toolbar hint + */ + "convertTo": { + "Convert to": "Конвертировать в" + }, }, /** @@ -295,7 +305,7 @@ }, "moveDown": { "Move down": "Переместить вниз" - } + }, }, } }, From d2fe844b80f75ae77db24c46dfef554b724a5c2e Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:52:21 +0300 Subject: [PATCH 21/22] Block conversion update: ability to convert to complex objects --- src/components/utils/blocks.ts | 9 ++++++++- types/configs/conversion-config.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index e59a1358d..479a162b4 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -61,7 +61,6 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools } const exportData = convertBlockDataForExport(blockData, blockTool.conversionConfig); - return allBlockTools.reduce((result, tool) => { /** * Skip tools without «import» rule specified @@ -85,6 +84,14 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools return result; } + /** + * Checking that the block is not empty after conversion + */ + const importData = convertExportToBlockData(exportData, tool.conversionConfig); + if (isUndefined(importData) || isEmpty(importData)) { + return result; + } + /** Filter out invalid toolbox entries */ const actualToolboxItems = tool.toolbox.filter((toolboxItem) => { /** diff --git a/types/configs/conversion-config.ts b/types/configs/conversion-config.ts index 02e7ade98..e185ef968 100644 --- a/types/configs/conversion-config.ts +++ b/types/configs/conversion-config.ts @@ -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, config: ToolConfig) => BlockToolData) | string; + import?: ((data: string | object, config: ToolConfig) => BlockToolData) | string; /** * How to export this Tool to make other Block. From 1103e60fd294fbcb8633933f65adce807b5d9ae7 Mon Sep 17 00:00:00 2001 From: VolgaIgor <43250768+VolgaIgor@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:38:15 +0300 Subject: [PATCH 22/22] Fixed merge mistake --- src/components/utils/blocks.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index 479a162b4..49c8b9b60 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -68,15 +68,7 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools if (!isToolConvertable(tool, 'import')) { return result; } - - /** - * Checking that the block is not empty after conversion - */ - const importData = convertExportToBlockData(exportData, tool.conversionConfig); - if (isUndefined(importData) || isEmpty(importData)) { - return result; - } - + /** * Skip tools that does not specify toolbox */