From d4028f7b8804d344ec23159edc9f6a7d1a0560cd Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Thu, 11 Jun 2026 14:54:34 -0700 Subject: [PATCH] Resolve Blocks by holder identity instead of working-area child index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BlockManager.getBlock() and setCurrentBlockByChildNode() resolved a Block by taking indexOf(wrapper) over Blocks.nodes — which is Array.from(workingArea.children) — and using that position to index the blocks array. The two lists are only aligned when the working area contains nothing but Block holders. When a host application adds non-block elements between blocks (visual decorations such as page-break spacers in a paginated view), every Block below such an element resolves to the wrong index: the editor marks the wrong Block as current, and for the last Block the index lands past the end of the blocks array, throwing TypeError: Cannot read properties of undefined (reading 'updateCurrentInput') on any click into the block or below it (Ui.processBottomZoneClick → Caret.setToTheLastBlock, and Ui.selectionChanged). Both methods now find the Block by holder identity, the same idiom already used by getBlockByChildNode() and RectangleSelection. setCurrentBlockByChildNode() also returns undefined for a wrapper element that matches no managed Block instead of corrupting currentBlockIndex, matching its documented return type. Adds a mousedown regression test that inserts a non-block element into the redactor and clicks the block below it — it fails with the updateCurrentInput TypeError before this change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/modules/blockManager.ts | 29 ++++++++++++++----- test/cypress/tests/modules/Ui.cy.ts | 39 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index be8e1e247..8c063b0ad 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -689,13 +689,15 @@ export default class BlockManager extends Module { element = element.parentNode as HTMLElement; } - const nodes = this._blocks.nodes, - firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`), - index = nodes.indexOf(firstLevelBlock as HTMLElement); + const firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`); - if (index >= 0) { - return this._blocks[index]; - } + /** + * Resolve the Block by holder identity (as getBlockByChildNode does) instead of + * indexing into the working area's children: the working area may contain + * non-block elements (e.g. decoration nodes added by the host application), + * which would skew a child-list index against the blocks array + */ + return this.blocks.find((block) => block.holder === firstLevelBlock); } /** @@ -732,12 +734,25 @@ export default class BlockManager extends Module { return; } + /** + * Resolve the Block's index by holder identity instead of indexing into the + * working area's children: the working area may contain non-block elements + * (e.g. decoration nodes added by the host application), which would skew a + * child-list index against the blocks array — selecting the wrong Block, or + * crashing on 'updateCurrentInput' when the found index is past the array end + */ + const index = this.blocks.findIndex((block) => block.holder === parentFirstLevelBlock); + + if (index === -1) { + return; + } + /** * Update current Block's index * * @type {number} */ - this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement); + this.currentBlockIndex = index; /** * Update current block active input diff --git a/test/cypress/tests/modules/Ui.cy.ts b/test/cypress/tests/modules/Ui.cy.ts index eaf2246a8..e6f9f63ff 100644 --- a/test/cypress/tests/modules/Ui.cy.ts +++ b/test/cypress/tests/modules/Ui.cy.ts @@ -117,6 +117,45 @@ describe('Ui module', function () { }); }); + it('should update current block by click on block when the redactor contains non-block elements', function () { + createEditorWithTextBlocks([ + 'first block', + 'second block', + 'third block', + ]) + .as('editorInstance'); + + /** + * Insert a non-block element between blocks — host applications do this + * for visual decorations (e.g. page-break spacers in a paginated view) + */ + cy.get('[data-cy=editorjs]') + .find('.codex-editor__redactor') + .then(($redactor) => { + const spacer = document.createElement('div'); + + spacer.setAttribute('data-mutation-free', 'true'); + $redactor[0].insertBefore(spacer, $redactor[0].lastElementChild); + }); + + /** + * Click the block BELOW the non-block element: a child-list index would + * be skewed by the extra element (resolving past the end of the blocks + * array and throwing on 'updateCurrentInput') + */ + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .eq(2) + .click(); + + cy.get('@editorInstance') + .then(async (editor) => { + const currentBlockIndex = await editor.blocks.getCurrentBlockIndex(); + + expect(currentBlockIndex).to.eq(2); + }); + }); + it('(in readonly) should update current block by click on block', function () { createEditorWithTextBlocks([ 'first block',