From fb96780a652b5779344f1c6786de6dbc2f6917fa Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 24 Aug 2023 23:39:35 +0300 Subject: [PATCH] fix(renderer): handle empty array as data.blocks --- docs/CHANGELOG.md | 4 ++ src/components/blocks.ts | 2 +- src/components/modules/blockManager.ts | 17 ++++- src/components/modules/renderer.ts | 84 ++++++++++++----------- src/components/modules/ui.ts | 27 +++++--- test/cypress/tests/modules/Renderer.cy.ts | 30 ++++++++ 6 files changed, 112 insertions(+), 52 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 55ca1e5aa..5a498cb25 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.29.0 + +- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor + ### 2.28.0 - `New` - Block ids now displayed in DOM via a data-id attribute. Could be useful for plugins that want access a Block's element by id. diff --git a/src/components/blocks.ts b/src/components/blocks.ts index cb0ba22fc..c0a4d9332 100644 --- a/src/components/blocks.ts +++ b/src/components/blocks.ts @@ -323,7 +323,7 @@ export default class Blocks { * @param {number} index — Block index * @returns {Block} */ - public get(index: number): Block { + public get(index: number): Block | undefined { return this.blocks[index]; } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 0f5a9de80..91a8573d1 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -587,13 +587,28 @@ export default class BlockManager extends Module { return this.insert({ data }); } + /** + * Returns Block by passed index + * + * If we pass -1 as index, the last block will be returned + * There shouldn't be a case when there is no blocks at all — at least one always should exist + */ + public getBlockByIndex(index: -1): Block; + + /** + * Returns Block by passed index. + * + * Could return undefined if there is no block with such index + */ + public getBlockByIndex(index: number): Block | undefined; + /** * Returns Block by passed index * * @param {number} index - index to get. -1 to get last * @returns {Block} */ - public getBlockByIndex(index): Block { + public getBlockByIndex(index: number): Block | undefined { if (index === -1) { index = this._blocks.length - 1; } diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index a3d38e650..940ff179b 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -18,53 +18,57 @@ export default class Renderer extends Module { return new Promise((resolve) => { const { Tools, BlockManager } = this.Editor; - /** - * Create Blocks instances - */ - const blocks = blocksData.map(({ type: tool, data, tunes, id }) => { - if (Tools.available.has(tool) === false) { - _.logLabeled(`Tool «${tool}» is not found. Check 'tools' property at the Editor.js config.`, 'warn'); + if (blocksData.length === 0) { + BlockManager.insert(); + } else { + /** + * Create Blocks instances + */ + const blocks = blocksData.map(({ type: tool, data, tunes, id }) => { + if (Tools.available.has(tool) === false) { + _.logLabeled(`Tool «${tool}» is not found. Check 'tools' property at the Editor.js config.`, 'warn'); - data = this.composeStubDataForTool(tool, data, id); - tool = Tools.stubTool; - } + data = this.composeStubDataForTool(tool, data, id); + tool = Tools.stubTool; + } - let block: Block; + let block: Block; - try { - block = BlockManager.composeBlock({ - id, - tool, - data, - tunes, - }); - } catch (error) { - _.log(`Block «${tool}» skipped because of plugins error`, 'error', { - data, - error, - }); + try { + block = BlockManager.composeBlock({ + id, + tool, + data, + tunes, + }); + } catch (error) { + _.log(`Block «${tool}» skipped because of plugins error`, 'error', { + data, + error, + }); - /** - * If tool throws an error during render, we should render stub instead of it - */ - data = this.composeStubDataForTool(tool, data, id); - tool = Tools.stubTool; + /** + * If tool throws an error during render, we should render stub instead of it + */ + data = this.composeStubDataForTool(tool, data, id); + tool = Tools.stubTool; - block = BlockManager.composeBlock({ - id, - tool, - data, - tunes, - }); - } + block = BlockManager.composeBlock({ + id, + tool, + data, + tunes, + }); + } - return block; - }); + return block; + }); - /** - * Insert batch of Blocks - */ - BlockManager.insertMany(blocks); + /** + * Insert batch of Blocks + */ + BlockManager.insertMany(blocks); + } /** * Wait till browser will render inserted Blocks and resolve a promise diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 1378bd0cb..0bf253eb9 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -694,17 +694,10 @@ export default class UI extends Module { * - otherwise, add a new empty Block and set a Caret to that */ private redactorClicked(event: MouseEvent): void { - const { BlockSelection } = this.Editor; - if (!Selection.isCollapsed) { return; } - const stopPropagation = (): void => { - event.stopImmediatePropagation(); - event.stopPropagation(); - }; - /** * case when user clicks on anchor element * if it is clicked via ctrl key, then we open new window with url @@ -713,7 +706,8 @@ export default class UI extends Module { const ctrlKey = event.metaKey || event.ctrlKey; if ($.isAnchor(element) && ctrlKey) { - stopPropagation(); + event.stopImmediatePropagation(); + event.stopPropagation(); const href = element.getAttribute('href'); const validUrl = _.getValidUrl(href); @@ -723,10 +717,22 @@ export default class UI extends Module { return; } + this.processBottomZoneClick(event); + } + + /** + * Check if user clicks on the Editor's bottom zone: + * - set caret to the last block + * - or add new empty block + * + * @param event - click event + */ + private processBottomZoneClick(event: MouseEvent): void { const lastBlock = this.Editor.BlockManager.getBlockByIndex(-1); + const lastBlockBottomCoord = $.offset(lastBlock.holder).bottom; const clickedCoord = event.pageY; - + const { BlockSelection } = this.Editor; const isClickedBottom = event.target instanceof Element && event.target.isEqualNode(this.nodes.redactor) && /** @@ -740,7 +746,8 @@ export default class UI extends Module { lastBlockBottomCoord < clickedCoord; if (isClickedBottom) { - stopPropagation(); + event.stopImmediatePropagation(); + event.stopPropagation(); const { BlockManager, Caret, Toolbar } = this.Editor; diff --git a/test/cypress/tests/modules/Renderer.cy.ts b/test/cypress/tests/modules/Renderer.cy.ts index 6ccf164e5..e81271e6d 100644 --- a/test/cypress/tests/modules/Renderer.cy.ts +++ b/test/cypress/tests/modules/Renderer.cy.ts @@ -1,4 +1,5 @@ import ToolMock from '../../fixtures/tools/ToolMock'; +import type EditorJS from '../../../../types/index'; describe('Renderer module', function () { it('should not cause onChange firing during initial rendering', function () { @@ -146,4 +147,33 @@ describe('Renderer module', function () { } }); }); + + it('should insert default empty block when [] passed as data.blocks', function () { + cy.createEditor({ + data: { + blocks: [], + }, + }) + .as('editorInstance'); + + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .should('have.length', 1); + }); + + it('should insert default empty block when [] passed via blocks.render() API', function () { + cy.createEditor({}) + .as('editorInstance'); + + cy.get('@editorInstance') + .then((editor) => { + editor.blocks.render({ + blocks: [], + }); + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .should('have.length', 1); + }); });