mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 09:52:36 +02:00
chore(perf): initialisation and rendering performance optimisations (#2430)
* renderer batching * initialization and rendering performance optimized * insertMany api method added * Update index.html * rm old method * upd changelog * upd paragraph * paste tests fixed * api blocks tests fixed * backspace event tests fixed * async issues in tests fixed * eslint * stub block added, tests added * eslint * eslint * add test for insertMany() * Update package.json
This commit is contained in:
parent
0e64665b0f
commit
b39996616c
|
@ -11,9 +11,22 @@ export default defineConfig({
|
|||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./test/cypress/plugins/index.ts')(on, config);
|
||||
/**
|
||||
* Plugin for cypress that adds better terminal output for easier debugging.
|
||||
* Prints cy commands, browser console logs, cy.request and cy.intercept data. Great for your pipelines.
|
||||
* https://github.com/archfz/cypress-terminal-report
|
||||
*/
|
||||
require('cypress-terminal-report/src/installLogsPrinter')(on);
|
||||
|
||||
require('./test/cypress/plugins/index.ts')(on, config);
|
||||
},
|
||||
specPattern: 'test/cypress/tests/**/*.cy.{js,jsx,ts,tsx}',
|
||||
supportFile: 'test/cypress/support/index.ts',
|
||||
},
|
||||
'retries': {
|
||||
// Configure retry attempts for `cypress run`
|
||||
'runMode': 2,
|
||||
// Configure retry attempts for `cypress open`
|
||||
'openMode': 0,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,12 +3,21 @@
|
|||
### 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.
|
||||
- `New` - The `.convert(blockId, newType)` API method added
|
||||
- `New` - The `blocks.convert(blockId, newType)` API method added. It allows to convert existed Block to a Block of another type.
|
||||
- `New` - The `blocks.insertMany()` API method added. It allows to insert several Blocks to specified index.
|
||||
- `Improvement` - The Delete keydown at the end of the Block will now work opposite a Backspace at the start. Next Block will be removed (if empty) or merged with the current one.
|
||||
- `Improvement` - The Delete keydown will work like a Backspace when several Blocks are selected.
|
||||
- `Improvement` - If we have two empty Blocks, and press Backspace at the start of the second one, the previous will be removed instead of current.
|
||||
- `Improvement` - Tools shortcuts could be used to convert one Block to another.
|
||||
- `Improvement` - Tools shortcuts displayed in the Conversion Toolbar
|
||||
- `Improvement` - Initialization Loader has been removed.
|
||||
- `Improvement` - Selection style won't override your custom style for `::selection` outside the editor.
|
||||
- `Improvement` - Performance optimizations: initialization speed increased, `blocks.render()` API method optimized. Big documents will be displayed faster.
|
||||
- `Improvement` - "Editor saving" log removed
|
||||
- `Improvement` - "I'm ready" log removed
|
||||
- `Improvement` - The stub-block style simplified.
|
||||
- `Improvement` - If some Block's tool will throw an error during construction, we will show Stub block instead of skipping it during render
|
||||
- `Improvement` - Call of `blocks.clear()` now will trigger onChange will "block-removed" event for all removed blocks.
|
||||
|
||||
### 2.27.2
|
||||
|
||||
|
|
|
@ -404,6 +404,8 @@
|
|||
|
||||
localStorage.setItem('theme', document.body.classList.contains("dark-mode") ? 'dark' : 'default');
|
||||
})
|
||||
|
||||
window.editor = editor;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@editorjs/editorjs",
|
||||
"version": "2.28.0-rc.1",
|
||||
"version": "2.28.0-rc.2",
|
||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||
"main": "dist/editorjs.umd.js",
|
||||
"module": "dist/editorjs.mjs",
|
||||
|
@ -44,7 +44,7 @@
|
|||
"@editorjs/code": "^2.7.0",
|
||||
"@editorjs/delimiter": "^1.2.0",
|
||||
"@editorjs/header": "^2.7.0",
|
||||
"@editorjs/paragraph": "^2.9.0",
|
||||
"@editorjs/paragraph": "^2.10.0",
|
||||
"@editorjs/simple-image": "^1.4.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"chai-subset": "^1.6.0",
|
||||
|
@ -53,6 +53,7 @@
|
|||
"core-js": "3.30.0",
|
||||
"cypress": "^12.9.0",
|
||||
"cypress-intellij-reporter": "^0.0.7",
|
||||
"cypress-terminal-report": "^5.3.2",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-codex": "^1.7.1",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
|
|
|
@ -251,6 +251,10 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
|
||||
this.holder = this.compose();
|
||||
|
||||
/**
|
||||
* Bind block events in RIC for optimizing of constructing process time
|
||||
*/
|
||||
window.requestIdleCallback(() => {
|
||||
/**
|
||||
* Start watching block mutations
|
||||
*/
|
||||
|
@ -261,6 +265,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
* so we need to track focus events to update current input and clear cache.
|
||||
*/
|
||||
this.addInputEvents();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -220,6 +220,44 @@ export default class Blocks {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts several blocks at once
|
||||
*
|
||||
* @param blocks - blocks to insert
|
||||
* @param index - index to insert blocks at
|
||||
*/
|
||||
public insertMany(blocks: Block[], index: number ): void {
|
||||
const fragment = new DocumentFragment();
|
||||
|
||||
for (const block of blocks) {
|
||||
fragment.appendChild(block.holder);
|
||||
}
|
||||
|
||||
if (this.length > 0) {
|
||||
if (index > 0) {
|
||||
const previousBlockIndex = Math.min(index - 1, this.length - 1);
|
||||
const previousBlock = this.blocks[previousBlockIndex];
|
||||
|
||||
previousBlock.holder.after(fragment);
|
||||
} else if (index === 0) {
|
||||
this.workingArea.prepend(fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert blocks to the array at the specified index
|
||||
*/
|
||||
this.blocks.splice(index, 0, ...blocks);
|
||||
} else {
|
||||
this.blocks.push(...blocks);
|
||||
this.workingArea.appendChild(fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Rendered event for each block
|
||||
*/
|
||||
blocks.forEach((block) => block.call(BlockToolAPI.RENDERED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove block
|
||||
*
|
||||
|
|
|
@ -39,7 +39,8 @@ export default class Core {
|
|||
/**
|
||||
* Ready promise. Resolved if Editor.js is ready to work, rejected otherwise
|
||||
*/
|
||||
let onReady, onFail;
|
||||
let onReady: (value?: void | PromiseLike<void>) => void;
|
||||
let onFail: (reason?: unknown) => void;
|
||||
|
||||
this.isReady = new Promise((resolve, reject) => {
|
||||
onReady = resolve;
|
||||
|
@ -50,33 +51,22 @@ export default class Core {
|
|||
.then(async () => {
|
||||
this.configuration = config;
|
||||
|
||||
await this.validate();
|
||||
await this.init();
|
||||
this.validate();
|
||||
this.init();
|
||||
await this.start();
|
||||
|
||||
_.logLabeled('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');
|
||||
|
||||
setTimeout(async () => {
|
||||
await this.render();
|
||||
|
||||
if ((this.configuration as EditorConfig).autofocus) {
|
||||
const { BlockManager, Caret } = this.moduleInstances;
|
||||
const { BlockManager, Caret, UI, ModificationsObserver } = this.moduleInstances;
|
||||
|
||||
UI.checkEmptiness();
|
||||
ModificationsObserver.enable();
|
||||
|
||||
if ((this.configuration as EditorConfig).autofocus) {
|
||||
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
|
||||
BlockManager.highlightCurrentNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove loader, show content
|
||||
*/
|
||||
this.moduleInstances.UI.removeLoader();
|
||||
|
||||
/**
|
||||
* Resolve this.isReady promise
|
||||
*/
|
||||
onReady();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 500);
|
||||
})
|
||||
.catch((error) => {
|
||||
_.log(`Editor.js is not ready because of ${error}`, 'error');
|
||||
|
@ -210,10 +200,8 @@ export default class Core {
|
|||
|
||||
/**
|
||||
* Checks for required fields in Editor's config
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async validate(): Promise<void> {
|
||||
public validate(): void {
|
||||
const { holderId, holder } = this.config;
|
||||
|
||||
if (holderId && holder) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
|
||||
import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
|
||||
import { BlockToolData, OutputBlockData, OutputData, ToolConfig } from '../../../../types';
|
||||
import * as _ from './../../utils';
|
||||
import BlockAPI from '../../block/api';
|
||||
import Module from '../../__module';
|
||||
|
@ -32,6 +32,7 @@ export default class BlocksAPI extends Module {
|
|||
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
|
||||
insertNewBlock: (): void => this.insertNewBlock(),
|
||||
insert: this.insert,
|
||||
insertMany: this.insertMany,
|
||||
update: this.update,
|
||||
composeBlockData: this.composeBlockData,
|
||||
convert: this.convert,
|
||||
|
@ -181,8 +182,12 @@ export default class BlocksAPI extends Module {
|
|||
*
|
||||
* @param {OutputData} data — Saved Editor data
|
||||
*/
|
||||
public render(data: OutputData): Promise<void> {
|
||||
this.Editor.BlockManager.clear();
|
||||
public async render(data: OutputData): Promise<void> {
|
||||
if (data === undefined || data.blocks === undefined) {
|
||||
throw new Error('Incorrect data passed to the render() method');
|
||||
}
|
||||
|
||||
await this.Editor.BlockManager.clear();
|
||||
|
||||
return this.Editor.Renderer.render(data.blocks);
|
||||
}
|
||||
|
@ -351,4 +356,51 @@ export default class BlocksAPI extends Module {
|
|||
throw new Error(`Conversion from "${blockToConvert.name}" to "${newType}" is not possible. ${unsupportedBlockTypes} tool(s) should provide a "conversionConfig"`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inserts several Blocks to a specified index
|
||||
*
|
||||
* @param blocks - blocks data to insert
|
||||
* @param index - index to insert the blocks at
|
||||
*/
|
||||
private insertMany = (
|
||||
blocks: OutputBlockData[],
|
||||
index: number = this.Editor.BlockManager.blocks.length - 1
|
||||
): BlockAPIInterface[] => {
|
||||
this.validateIndex(index);
|
||||
|
||||
const blocksToInsert = blocks.map(({ id, type, data }) => {
|
||||
return this.Editor.BlockManager.composeBlock({
|
||||
id,
|
||||
tool: type || (this.config.defaultBlock as string),
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
this.Editor.BlockManager.insertMany(blocksToInsert, index);
|
||||
|
||||
// we cast to any because our BlockAPI has no "new" signature
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return blocksToInsert.map((block) => new (BlockAPI as any)(block));
|
||||
};
|
||||
|
||||
/**
|
||||
* Validated block index and throws an error if it's invalid
|
||||
*
|
||||
* @param index - index to validate
|
||||
*/
|
||||
private validateIndex(index: unknown): void {
|
||||
if (typeof index !== 'number') {
|
||||
throw new Error('Index should be a number');
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error(`Index should be greater than or equal to 0`);
|
||||
}
|
||||
|
||||
if (index === null) {
|
||||
throw new Error(`Index should be greater than or equal to 0`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -454,11 +454,13 @@ export default class BlockEvents extends Module {
|
|||
BlockManager
|
||||
.mergeBlocks(targetBlock, blockToMerge)
|
||||
.then(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
/** Restore caret position after merge */
|
||||
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
|
||||
targetBlock.pluginsContent.normalize();
|
||||
Toolbar.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,7 @@ import { BlockChangedMutationType } from '../../../types/events/block/BlockChang
|
|||
import { BlockChanged } from '../events';
|
||||
import { clean } from '../utils/sanitizer';
|
||||
import { convertStringToBlockData } from '../utils/blocks';
|
||||
import PromiseQueue from '../utils/promise-queue';
|
||||
|
||||
/**
|
||||
* @typedef {BlockManager} BlockManager
|
||||
|
@ -244,7 +245,9 @@ export default class BlockManager extends Module {
|
|||
}, this.eventsDispatcher);
|
||||
|
||||
if (!readOnly) {
|
||||
window.requestIdleCallback(() => {
|
||||
this.bindBlockEvents(block);
|
||||
}, { timeout: 2000 });
|
||||
}
|
||||
|
||||
return block;
|
||||
|
@ -320,6 +323,16 @@ export default class BlockManager extends Module {
|
|||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts several blocks at once
|
||||
*
|
||||
* @param blocks - blocks to insert
|
||||
* @param index - index where to insert
|
||||
*/
|
||||
public insertMany(blocks: Block[], index = 0): void {
|
||||
this._blocks.insertMany(blocks, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace passed Block with the new one with specified Tool and data
|
||||
*
|
||||
|
@ -433,8 +446,10 @@ export default class BlockManager extends Module {
|
|||
* Remove passed Block
|
||||
*
|
||||
* @param block - Block to remove
|
||||
* @param addLastBlock - if true, adds new default block at the end. @todo remove this logic and use event-bus instead
|
||||
*/
|
||||
public removeBlock(block: Block): void {
|
||||
public removeBlock(block: Block, addLastBlock = true): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const index = this._blocks.indexOf(block);
|
||||
|
||||
/**
|
||||
|
@ -463,10 +478,16 @@ export default class BlockManager extends Module {
|
|||
*/
|
||||
if (!this.blocks.length) {
|
||||
this.currentBlockIndex = -1;
|
||||
|
||||
if (addLastBlock) {
|
||||
this.insert();
|
||||
}
|
||||
} else if (index === 0) {
|
||||
this.currentBlockIndex = 0;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -804,8 +825,17 @@ export default class BlockManager extends Module {
|
|||
* we don't need to add an empty default block
|
||||
* 2) in api.blocks.clear we should add empty block
|
||||
*/
|
||||
public clear(needToAddDefaultBlock = false): void {
|
||||
this._blocks.removeAll();
|
||||
public async clear(needToAddDefaultBlock = false): Promise<void> {
|
||||
const queue = new PromiseQueue();
|
||||
|
||||
this.blocks.forEach((block) => {
|
||||
queue.add(async () => {
|
||||
await this.removeBlock(block, false);
|
||||
});
|
||||
});
|
||||
|
||||
await queue.completed;
|
||||
|
||||
this.dropPointer();
|
||||
|
||||
if (needToAddDefaultBlock) {
|
||||
|
|
|
@ -503,13 +503,10 @@ export default class Caret extends Module {
|
|||
|
||||
sel.expandToTag(shadowCaret as HTMLElement);
|
||||
|
||||
setTimeout(() => {
|
||||
const newRange = document.createRange();
|
||||
|
||||
newRange.selectNode(shadowCaret);
|
||||
newRange.extractContents();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -534,7 +531,7 @@ export default class Caret extends Module {
|
|||
fragment.appendChild(new Text());
|
||||
}
|
||||
|
||||
const lastChild = fragment.lastChild;
|
||||
const lastChild = fragment.lastChild as ChildNode;
|
||||
|
||||
range.deleteContents();
|
||||
range.insertNode(fragment);
|
||||
|
@ -542,7 +539,11 @@ export default class Caret extends Module {
|
|||
/** Cross-browser caret insertion */
|
||||
const newRange = document.createRange();
|
||||
|
||||
newRange.setStart(lastChild, lastChild.textContent.length);
|
||||
const nodeToSetCaret = lastChild.nodeType === Node.TEXT_NODE ? lastChild : lastChild.firstChild;
|
||||
|
||||
if (nodeToSetCaret !== null && nodeToSetCaret.textContent !== null) {
|
||||
newRange.setStart(nodeToSetCaret, nodeToSetCaret.textContent.length);
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
|
|
|
@ -479,9 +479,14 @@ export default class Paste extends Module {
|
|||
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
|
||||
const { BlockManager, Toolbar } = this.Editor;
|
||||
|
||||
/**
|
||||
* When someone pasting into a block, its more stable to set current block by event target, instead of relying on current block set before
|
||||
*/
|
||||
const currentBlock = BlockManager.setCurrentBlockByChildNode(event.target as HTMLElement);
|
||||
|
||||
/** If target is native input or is not Block, use browser behaviour */
|
||||
if (
|
||||
!BlockManager.currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))
|
||||
!currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -489,7 +494,7 @@ export default class Paste extends Module {
|
|||
/**
|
||||
* If Tools is in list of errors, skip processing of paste event
|
||||
*/
|
||||
if (BlockManager.currentBlock && this.exceptionList.includes(BlockManager.currentBlock.name)) {
|
||||
if (currentBlock && this.exceptionList.includes(currentBlock.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,119 +1,107 @@
|
|||
import Module from '../__module';
|
||||
import * as _ from '../utils';
|
||||
import { OutputBlockData } from '../../../types';
|
||||
import BlockTool from '../tools/block';
|
||||
import type { BlockId, BlockToolData, OutputBlockData } from '../../../types';
|
||||
import type BlockTool from '../tools/block';
|
||||
import type { StubData } from '../../tools/stub';
|
||||
import Block from '../block';
|
||||
|
||||
/**
|
||||
* Editor.js Renderer Module
|
||||
*
|
||||
* @module Renderer
|
||||
* @author CodeX Team
|
||||
* @version 2.0.0
|
||||
* Module that responsible for rendering Blocks on editor initialization
|
||||
*/
|
||||
export default class Renderer extends Module {
|
||||
/**
|
||||
* @typedef {object} RendererBlocks
|
||||
* @property {string} type - tool name
|
||||
* @property {object} data - tool data
|
||||
*/
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Renders passed blocks as one batch
|
||||
*
|
||||
* blocks: [
|
||||
* {
|
||||
* id : 'oDe-EVrGWA',
|
||||
* type : 'paragraph',
|
||||
* data : {
|
||||
* text : 'Hello from Codex!'
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* id : 'Ld5BJjJCHs',
|
||||
* type : 'paragraph',
|
||||
* data : {
|
||||
* text : 'Leave feedback if you like it!'
|
||||
* }
|
||||
* },
|
||||
* ]
|
||||
* @param blocksData - blocks to render
|
||||
*/
|
||||
public async render(blocksData: OutputBlockData[]): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const { Tools, BlockManager } = this.Editor;
|
||||
|
||||
/**
|
||||
* Make plugin blocks from array of plugin`s data
|
||||
*
|
||||
* @param {OutputBlockData[]} blocks - blocks to render
|
||||
* Create Blocks instances
|
||||
*/
|
||||
public async render(blocks: OutputBlockData[]): Promise<void> {
|
||||
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
|
||||
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');
|
||||
|
||||
/**
|
||||
* Disable onChange callback on render to not to spam those events
|
||||
*/
|
||||
this.Editor.ModificationsObserver.disable();
|
||||
|
||||
const sequence = await _.sequence(chainData as _.ChainData[]);
|
||||
|
||||
this.Editor.ModificationsObserver.enable();
|
||||
|
||||
this.Editor.UI.checkEmptiness();
|
||||
|
||||
return sequence;
|
||||
data = this.composeStubDataForTool(tool, data, id);
|
||||
tool = Tools.stubTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin instance
|
||||
* Add plugin instance to BlockManager
|
||||
* Insert block to working zone
|
||||
*
|
||||
* @param {object} item - Block data to insert
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async insertBlock(item: OutputBlockData): Promise<void> {
|
||||
const { Tools, BlockManager } = this.Editor;
|
||||
const { type: tool, data, tunes, id } = item;
|
||||
let block: Block;
|
||||
|
||||
if (Tools.available.has(tool)) {
|
||||
try {
|
||||
BlockManager.insert({
|
||||
block = BlockManager.composeBlock({
|
||||
id,
|
||||
tool,
|
||||
data,
|
||||
tunes,
|
||||
});
|
||||
} catch (error) {
|
||||
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', {
|
||||
_.log(`Block «${tool}» skipped because of plugins error`, 'error', {
|
||||
data,
|
||||
error,
|
||||
});
|
||||
throw Error(error);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
/** If Tool is unavailable, create stub Block for it */
|
||||
const stubData = {
|
||||
|
||||
return block;
|
||||
});
|
||||
|
||||
/**
|
||||
* Insert batch of Blocks
|
||||
*/
|
||||
BlockManager.insertMany(blocks);
|
||||
|
||||
/**
|
||||
* Wait till browser will render inserted Blocks and resolve a promise
|
||||
*/
|
||||
window.requestIdleCallback(() => {
|
||||
resolve();
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create data for the Stub Tool that will be used instead of unavailable tool
|
||||
*
|
||||
* @param tool - unavailable tool name to stub
|
||||
* @param data - data of unavailable block
|
||||
* @param [id] - id of unavailable block
|
||||
*/
|
||||
private composeStubDataForTool(tool: string, data: BlockToolData, id?: BlockId): StubData {
|
||||
const { Tools } = this.Editor;
|
||||
|
||||
let title = tool;
|
||||
|
||||
if (Tools.unavailable.has(tool)) {
|
||||
const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
|
||||
|
||||
if (toolboxSettings !== undefined && toolboxSettings[0].title !== undefined) {
|
||||
title = toolboxSettings[0].title;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
savedData: {
|
||||
id,
|
||||
type: tool,
|
||||
data,
|
||||
},
|
||||
title: tool,
|
||||
title,
|
||||
};
|
||||
|
||||
if (Tools.unavailable.has(tool)) {
|
||||
const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
|
||||
const toolboxTitle = toolboxSettings[0]?.title;
|
||||
|
||||
stubData.title = toolboxTitle || stubData.title;
|
||||
}
|
||||
|
||||
const stub = BlockManager.insert({
|
||||
id,
|
||||
tool: Tools.stubTool,
|
||||
data: stubData,
|
||||
});
|
||||
|
||||
stub.stretched = true;
|
||||
|
||||
_.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,26 +70,11 @@ export default class Saver extends Module {
|
|||
* @returns {OutputData}
|
||||
*/
|
||||
private makeOutput(allExtractedData): OutputData {
|
||||
let totalTime = 0;
|
||||
const blocks = [];
|
||||
|
||||
_.log('[Editor.js saving]:', 'groupCollapsed');
|
||||
|
||||
allExtractedData.forEach(({ id, tool, data, tunes, time, isValid }) => {
|
||||
totalTime += time;
|
||||
|
||||
/**
|
||||
* Capitalize Tool name
|
||||
*/
|
||||
_.log(`${tool.charAt(0).toUpperCase() + tool.slice(1)}`, 'group');
|
||||
|
||||
if (isValid) {
|
||||
/** Group process info */
|
||||
_.log(data);
|
||||
_.log(undefined, 'groupEnd');
|
||||
} else {
|
||||
allExtractedData.forEach(({ id, tool, data, tunes, isValid }) => {
|
||||
if (!isValid) {
|
||||
_.log(`Block «${tool}» skipped because saved data is invalid`);
|
||||
_.log(undefined, 'groupEnd');
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -113,9 +98,6 @@ export default class Saver extends Module {
|
|||
blocks.push(output);
|
||||
});
|
||||
|
||||
_.log('Total', 'log', totalTime);
|
||||
_.log(undefined, 'groupEnd');
|
||||
|
||||
return {
|
||||
time: +new Date(),
|
||||
blocks,
|
||||
|
|
|
@ -103,8 +103,9 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
/**
|
||||
* Toolbox class instance
|
||||
* It will be created in requestIdleCallback so it can be null in some period of time
|
||||
*/
|
||||
private toolboxInstance: Toolbox;
|
||||
private toolboxInstance: Toolbox | null = null;
|
||||
|
||||
/**
|
||||
* @class
|
||||
|
@ -155,18 +156,27 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* Public interface for accessing the Toolbox
|
||||
*/
|
||||
public get toolbox(): {
|
||||
opened: boolean;
|
||||
opened: boolean | undefined; // undefined is for the case when Toolbox is not initialized yet
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
toggle: () => void;
|
||||
hasFocus: () => boolean;
|
||||
hasFocus: () => boolean | undefined;
|
||||
} {
|
||||
return {
|
||||
opened: this.toolboxInstance.opened,
|
||||
close: (): void => {
|
||||
this.toolboxInstance.close();
|
||||
opened: this.toolboxInstance?.opened,
|
||||
close: () => {
|
||||
this.toolboxInstance?.close();
|
||||
},
|
||||
open: (): void => {
|
||||
open: () => {
|
||||
/**
|
||||
* If Toolbox is not initialized yet, do nothing
|
||||
*/
|
||||
if (this.toolboxInstance === null) {
|
||||
_.log('toolbox.open() called before initialization is finished', 'warn');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
|
||||
*/
|
||||
|
@ -174,8 +184,19 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
this.toolboxInstance.open();
|
||||
},
|
||||
toggle: (): void => this.toolboxInstance.toggle(),
|
||||
hasFocus: (): boolean => this.toolboxInstance.hasFocus(),
|
||||
toggle: () => {
|
||||
/**
|
||||
* If Toolbox is not initialized yet, do nothing
|
||||
*/
|
||||
if (this.toolboxInstance === null) {
|
||||
_.log('toolbox.toggle() called before initialization is finished', 'warn');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.toolboxInstance.toggle();
|
||||
},
|
||||
hasFocus: () => this.toolboxInstance?.hasFocus(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -210,8 +231,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
if (!readOnlyEnabled) {
|
||||
window.requestIdleCallback(() => {
|
||||
this.drawUI();
|
||||
this.enableModuleBindings();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
this.destroy();
|
||||
this.Editor.BlockSettings.destroy();
|
||||
|
@ -225,6 +248,15 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* @param block - block to move Toolbar near it
|
||||
*/
|
||||
public moveAndOpen(block: Block = this.Editor.BlockManager.currentBlock): void {
|
||||
/**
|
||||
* Some UI elements creates inside requestIdleCallback, so the can be not ready yet
|
||||
*/
|
||||
if (this.toolboxInstance === null) {
|
||||
_.log('Can\'t open Toolbar since Editor initialization is not finished yet', 'warn');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Toolbox when we move toolbar
|
||||
*/
|
||||
|
@ -294,7 +326,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
/** Close components */
|
||||
this.blockActions.hide();
|
||||
this.toolboxInstance.close();
|
||||
this.toolboxInstance?.close();
|
||||
this.Editor.BlockSettings.close();
|
||||
}
|
||||
|
||||
|
@ -454,7 +486,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
*/
|
||||
this.Editor.BlockManager.currentBlock = this.hoveredBlock;
|
||||
|
||||
this.toolboxInstance.toggle();
|
||||
this.toolboxInstance?.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -476,7 +508,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
this.settingsTogglerClicked();
|
||||
|
||||
if (this.toolboxInstance.opened) {
|
||||
if (this.toolboxInstance?.opened) {
|
||||
this.toolboxInstance.close();
|
||||
}
|
||||
|
||||
|
@ -496,7 +528,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
/**
|
||||
* Do not move toolbar if Block Settings or Toolbox opened
|
||||
*/
|
||||
if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) {
|
||||
if (this.Editor.BlockSettings.opened || this.toolboxInstance?.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -122,7 +122,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
if (!readOnlyEnabled) {
|
||||
window.requestIdleCallback(() => {
|
||||
this.make();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
this.destroy();
|
||||
this.Editor.ConversionToolbar.destroy();
|
||||
|
@ -359,8 +361,11 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
|
||||
/**
|
||||
* Recalculate initial width with all buttons
|
||||
* We use RIC to prevent forced layout during editor initialization to make it faster
|
||||
*/
|
||||
window.requestAnimationFrame(() => {
|
||||
this.recalculateWidth();
|
||||
});
|
||||
|
||||
/**
|
||||
* Allow to leaf buttons by arrows / tab
|
||||
|
|
|
@ -22,7 +22,6 @@ interface UINodes {
|
|||
holder: HTMLElement;
|
||||
wrapper: HTMLElement;
|
||||
redactor: HTMLElement;
|
||||
loader: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,14 +48,13 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
public get CSS(): {
|
||||
editorWrapper: string; editorWrapperNarrow: string; editorZone: string; editorZoneHidden: string;
|
||||
editorLoader: string; editorEmpty: string; editorRtlFix: string;
|
||||
editorEmpty: string; editorRtlFix: string;
|
||||
} {
|
||||
return {
|
||||
editorWrapper: 'codex-editor',
|
||||
editorWrapperNarrow: 'codex-editor--narrow',
|
||||
editorZone: 'codex-editor__redactor',
|
||||
editorZoneHidden: 'codex-editor__redactor--hidden',
|
||||
editorLoader: 'codex-editor__loader',
|
||||
editorEmpty: 'codex-editor--empty',
|
||||
editorRtlFix: 'codex-editor--rtl',
|
||||
};
|
||||
|
@ -115,23 +113,6 @@ export default class UI extends Module<UINodes> {
|
|||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
* Adds loader to editor while content is not ready
|
||||
*/
|
||||
public addLoader(): void {
|
||||
this.nodes.loader = $.make('div', this.CSS.editorLoader);
|
||||
this.nodes.wrapper.prepend(this.nodes.loader);
|
||||
this.nodes.redactor.classList.add(this.CSS.editorZoneHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes loader when content has loaded
|
||||
*/
|
||||
public removeLoader(): void {
|
||||
this.nodes.loader.remove();
|
||||
this.nodes.redactor.classList.remove(this.CSS.editorZoneHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Making main interface
|
||||
*/
|
||||
|
@ -146,11 +127,6 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
this.make();
|
||||
|
||||
/**
|
||||
* Loader for rendering process
|
||||
*/
|
||||
this.addLoader();
|
||||
|
||||
/**
|
||||
* Load and append CSS
|
||||
*/
|
||||
|
@ -277,6 +253,8 @@ export default class UI extends Module<UINodes> {
|
|||
|
||||
/**
|
||||
* If Editor has injected into the narrow container, enable Narrow Mode
|
||||
*
|
||||
* @todo Forced layout. Get rid of this feature
|
||||
*/
|
||||
if (this.nodes.holder.offsetWidth < this.contentRect.width) {
|
||||
this.nodes.wrapper.classList.add(this.CSS.editorWrapperNarrow);
|
||||
|
@ -684,12 +662,7 @@ export default class UI extends Module<UINodes> {
|
|||
* Select clicked Block as Current
|
||||
*/
|
||||
try {
|
||||
/**
|
||||
* Renew Current Block. Use RAF to wait until Selection is set.
|
||||
*/
|
||||
window.requestAnimationFrame(() => {
|
||||
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
|
||||
});
|
||||
|
||||
/**
|
||||
* Highlight Current Node
|
||||
|
|
|
@ -136,3 +136,27 @@ if (!Element.prototype.scrollIntoViewIfNeeded) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestIdleCallback polyfill (shims)
|
||||
*
|
||||
* @see https://developer.chrome.com/blog/using-requestidlecallback/
|
||||
* @param cb - callback to be executed when the browser is idle
|
||||
*/
|
||||
window.requestIdleCallback = window.requestIdleCallback || function (cb) {
|
||||
const start = Date.now();
|
||||
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
return Math.max(0, 50 - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
|
||||
window.cancelIdleCallback = window.cancelIdleCallback || function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
|
|
|
@ -225,7 +225,7 @@ export default class SelectionUtils {
|
|||
*
|
||||
* @param selection - Selection object to get Range from
|
||||
*/
|
||||
public static getRangeFromSelection(selection: Selection): Range {
|
||||
public static getRangeFromSelection(selection: Selection): Range | null {
|
||||
return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
|
||||
}
|
||||
|
||||
|
|
|
@ -311,6 +311,7 @@ export function isPrintableKey(keyCode: number): boolean {
|
|||
* @param {Function} success - success callback
|
||||
* @param {Function} fallback - callback that fires in case of errors
|
||||
* @returns {Promise}
|
||||
* @deprecated use PromiseQueue.ts instead
|
||||
*/
|
||||
export async function sequence(
|
||||
chains: ChainData[],
|
||||
|
|
|
@ -94,6 +94,12 @@ export default class EventsDispatcher<EventMap> {
|
|||
* @param callback - event handler
|
||||
*/
|
||||
public off<Name extends keyof EventMap>(eventName: Name, callback: Listener<EventMap[Name]>): void {
|
||||
if (this.subscribers[eventName] === undefined) {
|
||||
console.warn(`EventDispatcher .off(): there is no subscribers for event "${eventName.toString()}". Probably, .off() called before .on()`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.subscribers[eventName].length; i++) {
|
||||
if (this.subscribers[eventName][i] === callback) {
|
||||
delete this.subscribers[eventName][i];
|
||||
|
@ -107,6 +113,6 @@ export default class EventsDispatcher<EventMap> {
|
|||
* clears subscribers list
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.subscribers = null;
|
||||
this.subscribers = {} as Subscriptions<EventMap>;
|
||||
}
|
||||
}
|
||||
|
|
28
src/components/utils/promise-queue.ts
Normal file
28
src/components/utils/promise-queue.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Class allows to make a queue of async jobs and wait until they all will be finished one by one
|
||||
*
|
||||
* @example const queue = new PromiseQueue();
|
||||
* queue.add(async () => { ... });
|
||||
* queue.add(async () => { ... });
|
||||
* await queue.completed;
|
||||
*/
|
||||
export default class PromiseQueue {
|
||||
/**
|
||||
* Queue of promises to be executed
|
||||
*/
|
||||
public completed = Promise.resolve();
|
||||
|
||||
/**
|
||||
* Add new promise to queue
|
||||
*
|
||||
* @param operation - promise should be added to queue
|
||||
*/
|
||||
public add(operation: (value: void) => void | PromiseLike<void>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.completed = this.completed
|
||||
.then(operation)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,26 +1,25 @@
|
|||
.ce-stub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 3.5em 0;
|
||||
margin: 17px 0;
|
||||
border-radius: 3px;
|
||||
background: #fcf7f7;
|
||||
color: #b46262;
|
||||
padding: 12px 18px;
|
||||
margin: 10px 0;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-light);
|
||||
border: 1px solid var(--color-line-gray);
|
||||
color: var(--grayText);
|
||||
font-size: 14px;
|
||||
|
||||
svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-left: 20px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: 3px;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
}
|
||||
|
||||
&__redactor {
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround firefox bug: empty content editable elements has collapsed height
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1098151#c18
|
||||
|
@ -46,28 +42,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__loader {
|
||||
position: relative;
|
||||
height: 30vh;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-top: -15px;
|
||||
margin-left: -15px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-gray-border);
|
||||
border-top-color: transparent;
|
||||
box-sizing: border-box;
|
||||
animation: editor-loader-spin 800ms infinite linear;
|
||||
will-change: transform;
|
||||
}
|
||||
}
|
||||
|
||||
&-copyable {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
|
@ -107,29 +81,21 @@
|
|||
path {
|
||||
stroke: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Set color for native selection
|
||||
*/
|
||||
::selection{
|
||||
::selection{
|
||||
background-color: var(--inlineSelectionColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.codex-editor--toolbox-opened [contentEditable=true][data-placeholder]:focus::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
@keyframes editor-loader-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ce-scroll-locked {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import $ from '../../components/dom';
|
||||
import { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types';
|
||||
import { IconWarning } from '@codexteam/icons';
|
||||
|
||||
export interface StubData extends BlockToolData {
|
||||
title: string;
|
||||
|
@ -92,7 +93,7 @@ export default class Stub implements BlockTool {
|
|||
*/
|
||||
private make(): HTMLElement {
|
||||
const wrapper = $.make('div', this.CSS.wrapper);
|
||||
const icon = `<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#D76B6B" fill-rule="nonzero" d="M26 52C11.64 52 0 40.36 0 26S11.64 0 26 0s26 11.64 26 26-11.64 26-26 26zm0-3.25c12.564 0 22.75-10.186 22.75-22.75S38.564 3.25 26 3.25 3.25 13.436 3.25 26 13.436 48.75 26 48.75zM15.708 33.042a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm23.834 0a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm-15.875 5.452a1.083 1.083 0 1 1-1.834-1.155c1.331-2.114 3.49-3.179 6.334-3.179 2.844 0 5.002 1.065 6.333 3.18a1.083 1.083 0 1 1-1.833 1.154c-.913-1.45-2.366-2.167-4.5-2.167s-3.587.717-4.5 2.167z"/></svg>`;
|
||||
const icon = IconWarning;
|
||||
const infoContainer = $.make('div', this.CSS.info);
|
||||
const title = $.make('div', this.CSS.title, {
|
||||
textContent: this.title,
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
"plugin:chai-friendly/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"cypress/require-data-selectors": 2
|
||||
"cypress/require-data-selectors": 2,
|
||||
"cypress/no-unnecessary-waiting": 0,
|
||||
"@typescript-eslint/no-magic-numbers": 0
|
||||
},
|
||||
"globals": {
|
||||
"EditorJS": true
|
||||
|
|
|
@ -61,7 +61,7 @@ Cypress.Commands.add('paste', {
|
|||
|
||||
subject[0].dispatchEvent(pasteEvent);
|
||||
|
||||
return subject;
|
||||
cy.wait(200); // wait a little since some tools (paragraph) could have async hydration
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -79,7 +79,6 @@ Cypress.Commands.add('copy', { prevSubject: true }, (subject) => {
|
|||
}), {
|
||||
clipboardData: {
|
||||
setData: (type: string, data: any): void => {
|
||||
console.log(type, data);
|
||||
clipboardData[type] = data;
|
||||
},
|
||||
},
|
||||
|
@ -105,7 +104,6 @@ Cypress.Commands.add('cut', { prevSubject: true }, (subject) => {
|
|||
}), {
|
||||
clipboardData: {
|
||||
setData: (type: string, data: any): void => {
|
||||
console.log(type, data);
|
||||
clipboardData[type] = data;
|
||||
},
|
||||
},
|
||||
|
@ -122,9 +120,10 @@ Cypress.Commands.add('cut', { prevSubject: true }, (subject) => {
|
|||
* @param data — data to render
|
||||
*/
|
||||
Cypress.Commands.add('render', { prevSubject: true }, (subject: EditorJS, data: OutputData) => {
|
||||
subject.render(data);
|
||||
|
||||
return cy.wrap(subject.render(data))
|
||||
.then(() => {
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -154,5 +153,5 @@ Cypress.Commands.add('selectText', {
|
|||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(range);
|
||||
|
||||
return subject;
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
*/
|
||||
|
||||
import '@cypress/code-coverage/support';
|
||||
import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector';
|
||||
|
||||
installLogsCollector();
|
||||
|
||||
/**
|
||||
* File with the helpful commands
|
||||
|
|
|
@ -63,9 +63,7 @@ describe('api.blocks', () => {
|
|||
it('should update block in DOM', () => {
|
||||
cy.createEditor({
|
||||
data: editorDataMock,
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||
}).then((editor) => {
|
||||
const idToUpdate = firstBlock.id;
|
||||
const newBlockData = {
|
||||
text: 'Updated text',
|
||||
|
@ -75,10 +73,7 @@ describe('api.blocks', () => {
|
|||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.invoke('text')
|
||||
.then(blockText => {
|
||||
expect(blockText).to.be.eq(newBlockData.text);
|
||||
});
|
||||
.should('have.text', newBlockData.text);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -88,9 +83,7 @@ describe('api.blocks', () => {
|
|||
it('should update block in saved data', () => {
|
||||
cy.createEditor({
|
||||
data: editorDataMock,
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||
}).then((editor) => {
|
||||
const idToUpdate = firstBlock.id;
|
||||
const newBlockData = {
|
||||
text: 'Updated text',
|
||||
|
@ -98,12 +91,16 @@ describe('api.blocks', () => {
|
|||
|
||||
editor.blocks.update(idToUpdate, newBlockData);
|
||||
|
||||
const output = await editor.save();
|
||||
// wait a little since some tools (paragraph) could have async hydration
|
||||
cy.wait(100).then(() => {
|
||||
editor.save().then((output) => {
|
||||
const text = output.blocks[0].data.text;
|
||||
|
||||
expect(text).to.be.eq(newBlockData.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When incorrect id passed, editor should not update any block
|
||||
|
@ -111,9 +108,7 @@ describe('api.blocks', () => {
|
|||
it('shouldn\'t update any block if not-existed id passed', () => {
|
||||
cy.createEditor({
|
||||
data: editorDataMock,
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||
}).then((editor) => {
|
||||
const idToUpdate = 'wrong-id-123';
|
||||
const newBlockData = {
|
||||
text: 'Updated text',
|
||||
|
@ -138,9 +133,7 @@ describe('api.blocks', () => {
|
|||
it('should preserve block id if it is passed', function () {
|
||||
cy.createEditor({
|
||||
data: editorDataMock,
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||
}).then((editor) => {
|
||||
const type = 'paragraph';
|
||||
const data = { text: 'codex' };
|
||||
const config = undefined;
|
||||
|
@ -157,6 +150,53 @@ describe('api.blocks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* api.blocks.insertMany(blocks, index)
|
||||
*/
|
||||
describe('.insertMany()', function () {
|
||||
it('should insert several blocks to passed index', function () {
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: { text: 'first block' },
|
||||
}
|
||||
],
|
||||
},
|
||||
}).then((editor) => {
|
||||
const index = 0;
|
||||
|
||||
cy.wrap(editor.blocks.insertMany([
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: { text: 'inserting block #1' },
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: { text: 'inserting block #2' },
|
||||
},
|
||||
], index)); // paste to the 0 index
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.each(($el, i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
cy.wrap($el).should('have.text', 'inserting block #1');
|
||||
break;
|
||||
case 1:
|
||||
cy.wrap($el).should('have.text', 'inserting block #2');
|
||||
break;
|
||||
case 2:
|
||||
cy.wrap($el).should('have.text', 'first block');
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.convert()', function () {
|
||||
it('should convert a Block to another type if original Tool has "conversionConfig.export" and target Tool has "conversionConfig.import"', function () {
|
||||
/**
|
||||
|
@ -202,42 +242,31 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
/**
|
||||
* Call the 'convert' api method
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
}).then((editor) => {
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
convert(existingBlock.id, 'convertableTool');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers -- wait for block to be converted
|
||||
cy.wait(100);
|
||||
|
||||
// wait for block to be converted
|
||||
cy.wait(100).then(() => {
|
||||
/**
|
||||
* Check that block was converted
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const { blocks } = await editor.save();
|
||||
|
||||
editor.save().then(( { blocks }) => {
|
||||
expect(blocks.length).to.eq(1);
|
||||
expect(blocks[0].type).to.eq('convertableTool');
|
||||
expect(blocks[0].data.text).to.eq(existingBlock.data.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if nonexisting Block id passed', function () {
|
||||
cy.createEditor({}).as('editorInstance');
|
||||
|
||||
cy.createEditor({})
|
||||
.then((editor) => {
|
||||
/**
|
||||
* Call the 'convert' api method with nonexisting Block id
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const fakeId = 'WRNG_ID';
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
|
@ -262,13 +291,10 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
}).then((editor) => {
|
||||
/**
|
||||
* Call the 'convert' api method with nonexisting tool name
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const nonexistingToolName = 'WRNG_TOOL_NAME';
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
|
@ -304,13 +330,10 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
}).then((editor) => {
|
||||
/**
|
||||
* Call the 'convert' api method with tool that does not provide "conversionConfig"
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
const exec = (): void => convert(existingBlock.id, 'nonConvertableTool');
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Header from '@editorjs/header';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type EditorJS from '../../../types/index';
|
||||
|
||||
|
||||
describe('Block ids', () => {
|
||||
beforeEach(function () {
|
||||
it('Should generate unique block ids for new blocks', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('Should generate unique block ids for new blocks', () => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -42,8 +35,8 @@ describe('Block ids', () => {
|
|||
.click()
|
||||
.type('Header');
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.then(async (editor: any) => {
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const data = await editor.save();
|
||||
|
||||
data.blocks.forEach(block => {
|
||||
|
@ -53,6 +46,9 @@ describe('Block ids', () => {
|
|||
});
|
||||
|
||||
it('should preserve passed ids', () => {
|
||||
cy.createEditor({})
|
||||
.as('editorInstance');
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
id: nanoid(),
|
||||
|
@ -70,19 +66,13 @@ describe('Block ids', () => {
|
|||
},
|
||||
];
|
||||
|
||||
cy.get('@editorInstance')
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.render({
|
||||
blocks,
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.first()
|
||||
.click()
|
||||
.type('{movetoend} Some more text');
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.then(async (editor: any) => {
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const data = await editor.save();
|
||||
|
||||
data.blocks.forEach((block, index) => {
|
||||
|
@ -92,6 +82,9 @@ describe('Block ids', () => {
|
|||
});
|
||||
|
||||
it('should preserve passed ids if blocks were added', () => {
|
||||
cy.createEditor({})
|
||||
.as('editorInstance');
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
id: nanoid(),
|
||||
|
@ -109,7 +102,7 @@ describe('Block ids', () => {
|
|||
},
|
||||
];
|
||||
|
||||
cy.get('@editorInstance')
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.render({
|
||||
blocks,
|
||||
});
|
||||
|
@ -122,16 +115,20 @@ describe('Block ids', () => {
|
|||
.next()
|
||||
.type('Middle block');
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.then(async (editor: any) => {
|
||||
const data = await editor.save();
|
||||
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
cy.wrap(await editor.save())
|
||||
.then((data) => {
|
||||
expect(data.blocks[0].id).to.eq(blocks[0].id);
|
||||
expect(data.blocks[2].id).to.eq(blocks[1].id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be stored at the Block wrapper\'s data-id attribute', () => {
|
||||
cy.createEditor({})
|
||||
.as('editorInstance');
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
id: nanoid(),
|
||||
|
@ -149,7 +146,7 @@ describe('Block ids', () => {
|
|||
},
|
||||
];
|
||||
|
||||
cy.get('@editorInstance')
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.render({
|
||||
blocks,
|
||||
});
|
||||
|
|
|
@ -1,51 +1,44 @@
|
|||
import Header from '@editorjs/header';
|
||||
import Image from '@editorjs/simple-image';
|
||||
import * as _ from '../../../src/components/utils';
|
||||
import EditorJS, { BlockTool, BlockToolData } from '../../../types';
|
||||
import { BlockTool, BlockToolData } from '../../../types';
|
||||
import $ from '../../../src/components/dom';
|
||||
|
||||
describe('Copy pasting from Editor', function () {
|
||||
beforeEach(function () {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
image: Image,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance && this.editorInstance.destroy) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
context('pasting', function () {
|
||||
it('should paste plain text', function () {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.as('block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/plain': 'Some plain text',
|
||||
})
|
||||
.wait(0)
|
||||
.should('contain', 'Some plain text');
|
||||
});
|
||||
|
||||
cy.get('@block').should('contain', 'Some plain text');
|
||||
});
|
||||
|
||||
it('should paste inline html data', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.as('block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<p><b>Some text</b></p>',
|
||||
})
|
||||
.should('contain.html', '<b>Some text</b>');
|
||||
});
|
||||
|
||||
cy.get('@block').should('contain.html', '<b>Some text</b>');
|
||||
});
|
||||
|
||||
it('should paste several blocks if plain text contains new lines', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -63,6 +56,8 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should paste several blocks if html contains several paragraphs', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -80,6 +75,8 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should paste using custom data type', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -110,6 +107,12 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should parse block tags', function () {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -128,6 +131,12 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should parse pattern', function () {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
image: Image,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -143,12 +152,6 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should not prevent default behaviour if block\'s paste config equals false', function () {
|
||||
/**
|
||||
* Destroy default Editor to render custom one with different tools
|
||||
*/
|
||||
cy.get('@editorInstance')
|
||||
.then((editorInstance: unknown) => (editorInstance as EditorJS).destroy());
|
||||
|
||||
const onPasteStub = cy.stub().as('onPaste');
|
||||
|
||||
/**
|
||||
|
@ -182,7 +185,8 @@ describe('Copy pasting from Editor', function () {
|
|||
tools: {
|
||||
blockToolWithPasteHandler: BlockToolWithPasteHandler,
|
||||
},
|
||||
}).as('editorInstanceWithBlockToolWithPasteHandler');
|
||||
})
|
||||
.as('editorInstanceWithBlockToolWithPasteHandler');
|
||||
|
||||
cy.get('@editorInstanceWithBlockToolWithPasteHandler')
|
||||
.render({
|
||||
|
@ -192,7 +196,8 @@ describe('Copy pasting from Editor', function () {
|
|||
data: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
.wait(100);
|
||||
|
||||
cy.get('@editorInstanceWithBlockToolWithPasteHandler')
|
||||
.get('div.ce-block-with-disabled-prevent-default')
|
||||
|
@ -211,6 +216,8 @@ describe('Copy pasting from Editor', function () {
|
|||
|
||||
context('copying', function () {
|
||||
it('should copy inline fragment', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -225,6 +232,8 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should copy several blocks', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -247,7 +256,6 @@ describe('Copy pasting from Editor', function () {
|
|||
/**
|
||||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(function () {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
|
@ -264,6 +272,8 @@ describe('Copy pasting from Editor', function () {
|
|||
|
||||
context('cutting', function () {
|
||||
it('should cut inline fragment', function () {
|
||||
cy.createEditor({});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -278,15 +288,25 @@ describe('Copy pasting from Editor', function () {
|
|||
});
|
||||
|
||||
it('should cut several blocks', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('First block{enter}');
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: { text: 'First block' },
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: { text: 'Second block' },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs')
|
||||
.get('div.ce-block')
|
||||
.next()
|
||||
.type('Second block')
|
||||
.last()
|
||||
.click()
|
||||
.type('{movetostart}')
|
||||
.trigger('keydown', {
|
||||
shiftKey: true,
|
||||
|
@ -300,7 +320,6 @@ describe('Copy pasting from Editor', function () {
|
|||
/**
|
||||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(function () {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
|
@ -320,15 +339,23 @@ describe('Copy pasting from Editor', function () {
|
|||
|
||||
it('should cut lots of blocks', function () {
|
||||
const numberOfBlocks = 50;
|
||||
const blocks = [];
|
||||
|
||||
for (let i = 0; i < numberOfBlocks; i++) {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.last()
|
||||
.click()
|
||||
.type(`Block ${i}{enter}`);
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: `Block ${i}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.first()
|
||||
|
@ -340,13 +367,12 @@ describe('Copy pasting from Editor', function () {
|
|||
/**
|
||||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(function () {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
||||
expect(data.length).to.eq(numberOfBlocks + 1);
|
||||
expect(data.length).to.eq(numberOfBlocks);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -127,7 +127,8 @@ describe('Editor i18n', () => {
|
|||
toolNames: toolNamesDictionary,
|
||||
},
|
||||
},
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import type EditorJS from '../../../../../types/index';
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
|
||||
/**
|
||||
* Creates Editor instance with list of Paragraph blocks of passed texts
|
||||
*
|
||||
* @param textBlocks - list of texts for Paragraph blocks
|
||||
*/
|
||||
function createEditorWithTextBlocks(textBlocks: string[]): void {
|
||||
cy.createEditor({
|
||||
function createEditorWithTextBlocks(textBlocks: string[]): Chainable<EditorJS> {
|
||||
return cy.createEditor({
|
||||
data: {
|
||||
blocks: textBlocks.map((text) => ({
|
||||
type: 'paragraph',
|
||||
|
|
|
@ -13,8 +13,6 @@ describe('Enter keydown', function () {
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.click()
|
||||
|
@ -22,7 +20,6 @@ describe('Enter keydown', function () {
|
|||
.wait(0)
|
||||
.type('{enter}');
|
||||
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('div.ce-block')
|
||||
.then((blocks) => {
|
||||
|
|
149
test/cypress/tests/modules/Renderer.cy.ts
Normal file
149
test/cypress/tests/modules/Renderer.cy.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
import ToolMock from '../../fixtures/tools/ToolMock';
|
||||
|
||||
describe('Renderer module', function () {
|
||||
it('should not cause onChange firing during initial rendering', function () {
|
||||
const config = {
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'some text',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'some other text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
cy.createEditor(config)
|
||||
.as('editorInstance');
|
||||
|
||||
cy.spy(config, 'onChange').as('onChange');
|
||||
|
||||
cy.get('@onChange').should('not.be.called');
|
||||
});
|
||||
|
||||
it('should show Stub block if block tool is not registered', function () {
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'some text',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'non-existing tool',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'some other text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.should('have.length', 3);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.each(($el, index) => {
|
||||
/**
|
||||
* Check that the second block is stub
|
||||
*/
|
||||
if (index === 1) {
|
||||
cy.wrap($el)
|
||||
.find('.ce-stub')
|
||||
.should('have.length', 1);
|
||||
|
||||
/**
|
||||
* Tool title displayed
|
||||
*/
|
||||
cy.wrap($el)
|
||||
.find('.ce-stub__title')
|
||||
.should('have.text', 'non-existing tool');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should show Stub block if block tool throws error during construction', function () {
|
||||
/**
|
||||
* Mock of tool that triggers error during construction
|
||||
*/
|
||||
class ToolWithError extends ToolMock {
|
||||
/**
|
||||
* @param options - tool options
|
||||
*/
|
||||
constructor(options) {
|
||||
super(options);
|
||||
throw new Error('Tool error');
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'some text',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'failedTool',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'some other text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tools: {
|
||||
failedTool: ToolWithError,
|
||||
},
|
||||
})
|
||||
.as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.should('have.length', 3);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.each(($el, index) => {
|
||||
/**
|
||||
* Check that the second block is stub
|
||||
*/
|
||||
if (index === 1) {
|
||||
cy.wrap($el)
|
||||
.find('.ce-stub')
|
||||
.should('have.length', 1);
|
||||
|
||||
/**
|
||||
* Tool title displayed
|
||||
*/
|
||||
cy.wrap($el)
|
||||
.find('.ce-stub__title')
|
||||
.should('have.text', 'failedTool');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@ import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
|
|||
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
|
||||
import { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';
|
||||
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
|
||||
import type EditorJS from '../../../types/index';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -132,7 +133,6 @@ describe('onChange callback', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -483,7 +483,6 @@ describe('onChange callback', () => {
|
|||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers
|
||||
cy.wait(500).then(() => {
|
||||
cy.get('@onChange').should('have.callCount', 0);
|
||||
});
|
||||
|
@ -562,7 +561,6 @@ describe('onChange callback', () => {
|
|||
/**
|
||||
* Emulate tool's internal attribute mutation
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers
|
||||
cy.wait(100).then(() => {
|
||||
toolWrapper.setAttribute('some-changed-attr', 'some-new-value');
|
||||
});
|
||||
|
@ -570,9 +568,86 @@ describe('onChange callback', () => {
|
|||
/**
|
||||
* Check that onChange callback was not called
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers
|
||||
cy.wait(500).then(() => {
|
||||
cy.get('@onChange').should('have.callCount', 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be called on blocks.clear() with removed and added blocks', () => {
|
||||
createEditor([
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The first paragraph',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The second paragraph',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async editor => {
|
||||
cy.wrap(editor.blocks.clear());
|
||||
});
|
||||
|
||||
cy.get('@onChange').should(($callback) => {
|
||||
return beCalledWithBatchedEvents($callback, [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
},
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be called on blocks.render() on non-empty editor with removed blocks', () => {
|
||||
createEditor([
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The first paragraph',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The second paragraph',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async editor => {
|
||||
cy.wrap(editor.blocks.render({
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The new paragraph',
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
cy.get('@onChange').should(($callback) => {
|
||||
return beCalledWithBatchedEvents($callback, [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
},
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
import type EditorJS from '../../../types/index';
|
||||
import { OutputData } from '../../../types/index';
|
||||
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
describe('Output sanitization', () => {
|
||||
beforeEach(function () {
|
||||
cy.createEditor({}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
context('Output should save inline formatting', () => {
|
||||
it('should save initial formatting for paragraph', () => {
|
||||
cy.createEditor({
|
||||
|
@ -19,16 +13,21 @@ describe('Output sanitization', () => {
|
|||
data: { text: '<b>Bold text</b>' },
|
||||
} ],
|
||||
},
|
||||
}).then(async editor => {
|
||||
const output = await (editor as any).save();
|
||||
|
||||
})
|
||||
.then(async editor => {
|
||||
cy.wrap<OutputData>(await editor.save())
|
||||
.then((output) => {
|
||||
const boldText = output.blocks[0].data.text;
|
||||
|
||||
expect(boldText).to.eq('<b>Bold text</b>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should save formatting for paragraph', () => {
|
||||
cy.createEditor({})
|
||||
.as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -42,16 +41,21 @@ describe('Output sanitization', () => {
|
|||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('@editorInstance').then(async editorInstance => {
|
||||
const output = await (editorInstance as any).save();
|
||||
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async editorInstance => {
|
||||
cy.wrap(await editorInstance.save())
|
||||
.then((output) => {
|
||||
const text = output.blocks[0].data.text;
|
||||
|
||||
expect(text).to.match(/<b>This text should be bold\.(<br>)?<\/b>/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should save formatting for paragraph on paste', () => {
|
||||
cy.createEditor({})
|
||||
.as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.paste({
|
||||
|
@ -59,13 +63,15 @@ describe('Output sanitization', () => {
|
|||
'text/html': '<p>Text</p><p><b>Bold text</b></p>',
|
||||
});
|
||||
|
||||
cy.get('@editorInstance').then(async editorInstance => {
|
||||
const output = await (editorInstance as any).save();
|
||||
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async editorInstance => {
|
||||
cy.wrap<OutputData>(await editorInstance.save())
|
||||
.then((output) => {
|
||||
const boldText = output.blocks[1].data.text;
|
||||
|
||||
expect(boldText).to.eq('<b>Bold text</b>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,20 +37,6 @@ class SomePlugin {
|
|||
}
|
||||
|
||||
describe('Flipper', () => {
|
||||
beforeEach(function () {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
sometool: SomePlugin,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should prevent plugins event handlers from being called while keyboard navigation', () => {
|
||||
const TAB_KEY_CODE = 9;
|
||||
const ARROW_DOWN_KEY_CODE = 40;
|
||||
|
@ -58,15 +44,23 @@ describe('Flipper', () => {
|
|||
|
||||
const sampleText = 'sample text';
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
sometool: SomePlugin,
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'sometool',
|
||||
data: {
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
cy.spy(SomePlugin, 'pluginInternalKeydownHandler');
|
||||
|
||||
// Insert sometool block and enter sample text
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.trigger('keydown', { keyCode: TAB_KEY_CODE });
|
||||
|
||||
cy.get('[data-item-name=sometool]').click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-some-plugin')
|
||||
.focus()
|
||||
|
|
11
types/api/blocks.d.ts
vendored
11
types/api/blocks.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
import {OutputData} from '../data-formats/output-data';
|
||||
import {OutputBlockData, OutputData} from '../data-formats/output-data';
|
||||
import {BlockToolData, ToolConfig} from '../tools';
|
||||
import {BlockAPI} from './block';
|
||||
|
||||
|
@ -103,7 +103,6 @@ export interface Blocks {
|
|||
* @param {boolean?} needToFocus - flag to focus inserted Block
|
||||
* @param {boolean?} replace - should the existed Block on that index be replaced or not
|
||||
* @param {string} id — An optional id for the new block. If omitted then the new id will be generated
|
||||
|
||||
*/
|
||||
insert(
|
||||
type?: string,
|
||||
|
@ -115,6 +114,14 @@ export interface Blocks {
|
|||
id?: string,
|
||||
): BlockAPI;
|
||||
|
||||
/**
|
||||
* Inserts several Blocks to specified index
|
||||
*/
|
||||
insertMany(
|
||||
blocks: OutputBlockData[],
|
||||
index?: number,
|
||||
): BlockAPI[];
|
||||
|
||||
|
||||
/**
|
||||
* Creates data of an empty block with a passed type.
|
||||
|
|
36
yarn.lock
36
yarn.lock
|
@ -571,10 +571,10 @@
|
|||
dependencies:
|
||||
"@codexteam/icons" "^0.0.5"
|
||||
|
||||
"@editorjs/paragraph@^2.9.0":
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.9.0.tgz#22f508b3771a4f98650e8bb37d628e230f0a378c"
|
||||
integrity sha512-lI6x1eiqFx2X3KmMak6gBlimFqXhG35fQpXMxzrjIphNLr4uPsXhVbyMPimPoLUnS9rM6Q00vptXmP2QNDd0zg==
|
||||
"@editorjs/paragraph@^2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.10.0.tgz#dcf152e69738a9399b4af83262606d76cf1376e5"
|
||||
integrity sha512-AzaGxR9DQAdWhx43yupBcwqtwH0WWi5jBDOCSeALIK86IYOnO6Lp4anEbH8IYmYrE/5MdnRiTwdU8/Xs8W15Nw==
|
||||
dependencies:
|
||||
"@codexteam/icons" "^0.0.4"
|
||||
|
||||
|
@ -1649,6 +1649,17 @@ cypress-intellij-reporter@^0.0.7:
|
|||
dependencies:
|
||||
mocha latest
|
||||
|
||||
cypress-terminal-report@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/cypress-terminal-report/-/cypress-terminal-report-5.3.2.tgz#3a6b1cbda6101498243d17c5a2a646cb69af0336"
|
||||
integrity sha512-0Gf/pXjrYpTkf2aR3LAFGoxEM0KulWsMKCu+52YJB6l7GEP2RLAOAr32tcZHZiL2EWnS0vE4ollomMzGvCci0w==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
fs-extra "^10.1.0"
|
||||
safe-json-stringify "^1.2.0"
|
||||
semver "^7.3.5"
|
||||
tv4 "^1.3.0"
|
||||
|
||||
cypress@^12.9.0:
|
||||
version "12.9.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.9.0.tgz#e6ab43cf329fd7c821ef7645517649d72ccf0a12"
|
||||
|
@ -4443,6 +4454,11 @@ safe-buffer@^5.1.0:
|
|||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
|
||||
|
||||
safe-json-stringify@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd"
|
||||
integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==
|
||||
|
||||
safe-regex-test@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
|
||||
|
@ -4474,6 +4490,13 @@ semver@^7.0.0, semver@^7.3.2, semver@^7.3.4, semver@^7.3.7, semver@^7.3.8:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
serialize-javascript@6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
|
||||
|
@ -4936,6 +4959,11 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tv4@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963"
|
||||
integrity sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
|
Loading…
Reference in a new issue