fix(renderer): handle empty array as data.blocks (#2454)

This commit is contained in:
Peter Savchenko 2023-09-05 11:24:47 +03:00 committed by GitHub
parent 59c8d28da5
commit 0369ff5827
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 52 deletions

View file

@ -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 to access a Block's element by id.

View file

@ -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];
}

View file

@ -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;
}

View file

@ -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

View file

@ -694,17 +694,10 @@ export default class UI extends Module<UINodes> {
* - 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<UINodes> {
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<UINodes> {
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<UINodes> {
lastBlockBottomCoord < clickedCoord;
if (isClickedBottom) {
stopPropagation();
event.stopImmediatePropagation();
event.stopPropagation();
const { BlockManager, Caret, Toolbar } = this.Editor;

View file

@ -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<EditorJS>('@editorInstance')
.then((editor) => {
editor.blocks.render({
blocks: [],
});
});
cy.get('[data-cy=editorjs]')
.find('.ce-block')
.should('have.length', 1);
});
});