Skip to content

Commit 0369ff5

Browse files
authored
fix(renderer): handle empty array as data.blocks (codex-team#2454)
1 parent 59c8d28 commit 0369ff5

File tree

6 files changed

+112
-52
lines changed

6 files changed

+112
-52
lines changed

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 2.29.0
4+
5+
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor
6+
37
### 2.28.0
48

59
- `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.

src/components/blocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ export default class Blocks {
323323
* @param {number} index — Block index
324324
* @returns {Block}
325325
*/
326-
public get(index: number): Block {
326+
public get(index: number): Block | undefined {
327327
return this.blocks[index];
328328
}
329329

src/components/modules/blockManager.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,13 +587,28 @@ export default class BlockManager extends Module {
587587
return this.insert({ data });
588588
}
589589

590+
/**
591+
* Returns Block by passed index
592+
*
593+
* If we pass -1 as index, the last block will be returned
594+
* There shouldn't be a case when there is no blocks at all — at least one always should exist
595+
*/
596+
public getBlockByIndex(index: -1): Block;
597+
598+
/**
599+
* Returns Block by passed index.
600+
*
601+
* Could return undefined if there is no block with such index
602+
*/
603+
public getBlockByIndex(index: number): Block | undefined;
604+
590605
/**
591606
* Returns Block by passed index
592607
*
593608
* @param {number} index - index to get. -1 to get last
594609
* @returns {Block}
595610
*/
596-
public getBlockByIndex(index): Block {
611+
public getBlockByIndex(index: number): Block | undefined {
597612
if (index === -1) {
598613
index = this._blocks.length - 1;
599614
}

src/components/modules/renderer.ts

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,53 +18,57 @@ export default class Renderer extends Module {
1818
return new Promise((resolve) => {
1919
const { Tools, BlockManager } = this.Editor;
2020

21-
/**
22-
* Create Blocks instances
23-
*/
24-
const blocks = blocksData.map(({ type: tool, data, tunes, id }) => {
25-
if (Tools.available.has(tool) === false) {
26-
_.logLabeled(`Tool «${tool}» is not found. Check 'tools' property at the Editor.js config.`, 'warn');
21+
if (blocksData.length === 0) {
22+
BlockManager.insert();
23+
} else {
24+
/**
25+
* Create Blocks instances
26+
*/
27+
const blocks = blocksData.map(({ type: tool, data, tunes, id }) => {
28+
if (Tools.available.has(tool) === false) {
29+
_.logLabeled(`Tool «${tool}» is not found. Check 'tools' property at the Editor.js config.`, 'warn');
2730

28-
data = this.composeStubDataForTool(tool, data, id);
29-
tool = Tools.stubTool;
30-
}
31+
data = this.composeStubDataForTool(tool, data, id);
32+
tool = Tools.stubTool;
33+
}
3134

32-
let block: Block;
35+
let block: Block;
3336

34-
try {
35-
block = BlockManager.composeBlock({
36-
id,
37-
tool,
38-
data,
39-
tunes,
40-
});
41-
} catch (error) {
42-
_.log(`Block «${tool}» skipped because of plugins error`, 'error', {
43-
data,
44-
error,
45-
});
37+
try {
38+
block = BlockManager.composeBlock({
39+
id,
40+
tool,
41+
data,
42+
tunes,
43+
});
44+
} catch (error) {
45+
_.log(`Block «${tool}» skipped because of plugins error`, 'error', {
46+
data,
47+
error,
48+
});
4649

47-
/**
48-
* If tool throws an error during render, we should render stub instead of it
49-
*/
50-
data = this.composeStubDataForTool(tool, data, id);
51-
tool = Tools.stubTool;
50+
/**
51+
* If tool throws an error during render, we should render stub instead of it
52+
*/
53+
data = this.composeStubDataForTool(tool, data, id);
54+
tool = Tools.stubTool;
5255

53-
block = BlockManager.composeBlock({
54-
id,
55-
tool,
56-
data,
57-
tunes,
58-
});
59-
}
56+
block = BlockManager.composeBlock({
57+
id,
58+
tool,
59+
data,
60+
tunes,
61+
});
62+
}
6063

61-
return block;
62-
});
64+
return block;
65+
});
6366

64-
/**
65-
* Insert batch of Blocks
66-
*/
67-
BlockManager.insertMany(blocks);
67+
/**
68+
* Insert batch of Blocks
69+
*/
70+
BlockManager.insertMany(blocks);
71+
}
6872

6973
/**
7074
* Wait till browser will render inserted Blocks and resolve a promise

src/components/modules/ui.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -694,17 +694,10 @@ export default class UI extends Module<UINodes> {
694694
* - otherwise, add a new empty Block and set a Caret to that
695695
*/
696696
private redactorClicked(event: MouseEvent): void {
697-
const { BlockSelection } = this.Editor;
698-
699697
if (!Selection.isCollapsed) {
700698
return;
701699
}
702700

703-
const stopPropagation = (): void => {
704-
event.stopImmediatePropagation();
705-
event.stopPropagation();
706-
};
707-
708701
/**
709702
* case when user clicks on anchor element
710703
* 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> {
713706
const ctrlKey = event.metaKey || event.ctrlKey;
714707

715708
if ($.isAnchor(element) && ctrlKey) {
716-
stopPropagation();
709+
event.stopImmediatePropagation();
710+
event.stopPropagation();
717711

718712
const href = element.getAttribute('href');
719713
const validUrl = _.getValidUrl(href);
@@ -723,10 +717,22 @@ export default class UI extends Module<UINodes> {
723717
return;
724718
}
725719

720+
this.processBottomZoneClick(event);
721+
}
722+
723+
/**
724+
* Check if user clicks on the Editor's bottom zone:
725+
* - set caret to the last block
726+
* - or add new empty block
727+
*
728+
* @param event - click event
729+
*/
730+
private processBottomZoneClick(event: MouseEvent): void {
726731
const lastBlock = this.Editor.BlockManager.getBlockByIndex(-1);
732+
727733
const lastBlockBottomCoord = $.offset(lastBlock.holder).bottom;
728734
const clickedCoord = event.pageY;
729-
735+
const { BlockSelection } = this.Editor;
730736
const isClickedBottom = event.target instanceof Element &&
731737
event.target.isEqualNode(this.nodes.redactor) &&
732738
/**
@@ -740,7 +746,8 @@ export default class UI extends Module<UINodes> {
740746
lastBlockBottomCoord < clickedCoord;
741747

742748
if (isClickedBottom) {
743-
stopPropagation();
749+
event.stopImmediatePropagation();
750+
event.stopPropagation();
744751

745752
const { BlockManager, Caret, Toolbar } = this.Editor;
746753

test/cypress/tests/modules/Renderer.cy.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ToolMock from '../../fixtures/tools/ToolMock';
2+
import type EditorJS from '../../../../types/index';
23

34
describe('Renderer module', function () {
45
it('should not cause onChange firing during initial rendering', function () {
@@ -146,4 +147,33 @@ describe('Renderer module', function () {
146147
}
147148
});
148149
});
150+
151+
it('should insert default empty block when [] passed as data.blocks', function () {
152+
cy.createEditor({
153+
data: {
154+
blocks: [],
155+
},
156+
})
157+
.as('editorInstance');
158+
159+
cy.get('[data-cy=editorjs]')
160+
.find('.ce-block')
161+
.should('have.length', 1);
162+
});
163+
164+
it('should insert default empty block when [] passed via blocks.render() API', function () {
165+
cy.createEditor({})
166+
.as('editorInstance');
167+
168+
cy.get<EditorJS>('@editorInstance')
169+
.then((editor) => {
170+
editor.blocks.render({
171+
blocks: [],
172+
});
173+
});
174+
175+
cy.get('[data-cy=editorjs]')
176+
.find('.ce-block')
177+
.should('have.length', 1);
178+
});
149179
});

0 commit comments

Comments
 (0)