mirror of
https://github.com/codex-team/editor.js
synced 2024-06-01 21:42:26 +02:00
Merge branch 'next' into feature/issue2092-allow-block-tool-render-method-to-return-null
This commit is contained in:
commit
fb1302df06
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,14 +1,35 @@
|
|||
# Changelog
|
||||
|
||||
### 2.29.0
|
||||
|
||||
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor
|
||||
- `Fix` — Layout did not shrink when a large document cleared in Chrome
|
||||
- `Fix` — Multiple Tooltip elements creation fixed
|
||||
- `Fix` — When the focusing Block is out of the viewport, the page will be scrolled.
|
||||
|
||||
### 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` - 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.
|
||||
- `New` - The `blocks.convert(blockId, newType)` API method was added. It allows to convert existing Block to a Block of another type.
|
||||
- `New` - The `blocks.insertMany()` API method added. It allows to insert several Blocks to the 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` - If we have two empty Blocks, and press Backspace at the start of the second one, the previous will be removed instead of the 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 is simplified.
|
||||
- `Improvement` - If some Block's tool throws an error during construction, we will show Stub block instead of skipping it during render
|
||||
- `Improvement` - Call of `blocks.clear()` now will trigger onChange with "block-removed" event for all removed blocks.
|
||||
- `Improvement` - The `blocks.clear()` now can be awaited.
|
||||
- `Improvement` - `BlockMutationType` and `BlockMutationEvent` types exported
|
||||
- `Improvement` - `blocks.update(id, data)` now can accept partial data object — it will update only passed properties, others will remain the same.
|
||||
- `Improvement` - `blocks.update(id, data)` now will trigger onChange with only `block-change` event.
|
||||
- `Improvement` - `blocks.update(id, data)` will return a promise with BlockAPI object of the changed block.
|
||||
|
||||
### 2.27.2
|
||||
|
||||
|
|
|
@ -98,6 +98,8 @@
|
|||
<script type="module">
|
||||
import EditorJS from './src/codex.ts';
|
||||
|
||||
window.EditorJS = EditorJS;
|
||||
|
||||
/**
|
||||
* To initialize the Editor, create a new instance with configuration object
|
||||
* @see docs/installation.md for mode details
|
||||
|
@ -404,6 +406,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.29.0-rc.1",
|
||||
"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",
|
||||
|
|
|
@ -10,6 +10,7 @@ import '@babel/register';
|
|||
import './components/polyfills';
|
||||
import Core from './components/core';
|
||||
import * as _ from './components/utils';
|
||||
import { destroy as destroyTooltip } from './components/utils/tooltip';
|
||||
|
||||
declare const VERSION: string;
|
||||
|
||||
|
@ -67,6 +68,9 @@ export default class EditorJS {
|
|||
*/
|
||||
this.isReady = editor.isReady.then(() => {
|
||||
this.exportAPI(editor);
|
||||
/**
|
||||
* @todo pass API as an argument. It will allow to use Editor's API when editor is ready
|
||||
*/
|
||||
onReady();
|
||||
});
|
||||
}
|
||||
|
@ -87,6 +91,8 @@ export default class EditorJS {
|
|||
moduleInstance.listeners.removeAll();
|
||||
});
|
||||
|
||||
destroyTooltip();
|
||||
|
||||
editor = null;
|
||||
|
||||
for (const field in this) {
|
||||
|
|
|
@ -252,15 +252,20 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
this.holder = this.compose();
|
||||
|
||||
/**
|
||||
* Start watching block mutations
|
||||
* Bind block events in RIC for optimizing of constructing process time
|
||||
*/
|
||||
this.watchBlockMutations();
|
||||
window.requestIdleCallback(() => {
|
||||
/**
|
||||
* Start watching block mutations
|
||||
*/
|
||||
this.watchBlockMutations();
|
||||
|
||||
/**
|
||||
* Mutation observer doesn't track changes in "<input>" and "<textarea>"
|
||||
* so we need to track focus events to update current input and clear cache.
|
||||
*/
|
||||
this.addInputEvents();
|
||||
/**
|
||||
* Mutation observer doesn't track changes in "<input>" and "<textarea>"
|
||||
* so we need to track focus events to update current input and clear cache.
|
||||
*/
|
||||
this.addInputEvents();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -220,6 +220,62 @@ export default class Blocks {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces block under passed index with passed block
|
||||
*
|
||||
* @param index - index of existed block
|
||||
* @param block - new block
|
||||
*/
|
||||
public replace(index: number, block: Block): void {
|
||||
if (this.blocks[index] === undefined) {
|
||||
throw Error('Incorrect index');
|
||||
}
|
||||
|
||||
const prevBlock = this.blocks[index];
|
||||
|
||||
prevBlock.holder.replaceWith(block.holder);
|
||||
|
||||
this.blocks[index] = block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -267,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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
await this.render();
|
||||
|
||||
_.logLabeled('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');
|
||||
const { BlockManager, Caret, UI, ModificationsObserver } = this.moduleInstances;
|
||||
|
||||
setTimeout(async () => {
|
||||
await this.render();
|
||||
UI.checkEmptiness();
|
||||
ModificationsObserver.enable();
|
||||
|
||||
if ((this.configuration as EditorConfig).autofocus) {
|
||||
const { BlockManager, Caret } = this.moduleInstances;
|
||||
if ((this.configuration as EditorConfig).autofocus) {
|
||||
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
|
||||
BlockManager.highlightCurrentNode();
|
||||
}
|
||||
|
||||
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);
|
||||
onReady();
|
||||
})
|
||||
.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';
|
||||
|
@ -18,7 +18,7 @@ export default class BlocksAPI extends Module {
|
|||
*/
|
||||
public get methods(): Blocks {
|
||||
return {
|
||||
clear: (): void => this.clear(),
|
||||
clear: (): Promise<void> => this.clear(),
|
||||
render: (data: OutputData): Promise<void> => this.render(data),
|
||||
renderFromHTML: (data: string): Promise<void> => this.renderFromHTML(data),
|
||||
delete: (index?: number): void => this.delete(index),
|
||||
|
@ -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,
|
||||
|
@ -171,8 +172,8 @@ export default class BlocksAPI extends Module {
|
|||
/**
|
||||
* Clear Editor's area
|
||||
*/
|
||||
public clear(): void {
|
||||
this.Editor.BlockManager.clear(true);
|
||||
public async clear(): Promise<void> {
|
||||
await this.Editor.BlockManager.clear(true);
|
||||
this.Editor.InlineToolbar.close();
|
||||
}
|
||||
|
||||
|
@ -181,10 +182,21 @@ 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');
|
||||
}
|
||||
|
||||
return this.Editor.Renderer.render(data.blocks);
|
||||
/**
|
||||
* Semantic meaning of the "render" method: "Display the new document over the existing one that stays unchanged"
|
||||
* So we need to disable modifications observer temporarily
|
||||
*/
|
||||
this.Editor.ModificationsObserver.disable();
|
||||
|
||||
await this.Editor.BlockManager.clear();
|
||||
await this.Editor.Renderer.render(data.blocks);
|
||||
|
||||
this.Editor.ModificationsObserver.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,26 +304,19 @@ export default class BlocksAPI extends Module {
|
|||
* @param id - id of the block to update
|
||||
* @param data - the new data
|
||||
*/
|
||||
public update = (id: string, data: BlockToolData): void => {
|
||||
public update = async (id: string, data: Partial<BlockToolData>): Promise<BlockAPIInterface> => {
|
||||
const { BlockManager } = this.Editor;
|
||||
const block = BlockManager.getBlockById(id);
|
||||
|
||||
if (!block) {
|
||||
_.log('blocks.update(): Block with passed id was not found', 'warn');
|
||||
|
||||
return;
|
||||
if (block === undefined) {
|
||||
throw new Error(`Block with id "${id}" not found`);
|
||||
}
|
||||
|
||||
const blockIndex = BlockManager.getBlockIndex(block);
|
||||
const updatedBlock = await BlockManager.update(block, data);
|
||||
|
||||
BlockManager.insert({
|
||||
id: block.id,
|
||||
tool: block.name,
|
||||
data,
|
||||
index: blockIndex,
|
||||
replace: true,
|
||||
tunes: block.tunes,
|
||||
});
|
||||
// we cast to any because our BlockAPI has no "new" signature
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return new (BlockAPI as any)(updatedBlock);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,12 @@ import { Tooltip as ITooltip } from '../../../../types/api';
|
|||
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
|
||||
import Module from '../../__module';
|
||||
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||
import Tooltip from '../../utils/tooltip';
|
||||
import * as tooltip from '../../utils/tooltip';
|
||||
/**
|
||||
* @class TooltipAPI
|
||||
* @classdesc Tooltip API
|
||||
*/
|
||||
export default class TooltipAPI extends Module {
|
||||
/**
|
||||
* Tooltip utility Instance
|
||||
*/
|
||||
private tooltip: Tooltip;
|
||||
/**
|
||||
* @class
|
||||
* @param moduleConfiguration - Module Configuration
|
||||
|
@ -23,15 +19,6 @@ export default class TooltipAPI extends Module {
|
|||
config,
|
||||
eventsDispatcher,
|
||||
});
|
||||
|
||||
this.tooltip = new Tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy Module
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.tooltip.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,14 +46,14 @@ export default class TooltipAPI extends Module {
|
|||
* @param {TooltipOptions} options - tooltip options
|
||||
*/
|
||||
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||
this.tooltip.show(element, content, options);
|
||||
tooltip.show(element, content, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method hides tooltip on HTML page
|
||||
*/
|
||||
public hide(): void {
|
||||
this.tooltip.hide();
|
||||
tooltip.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,6 +64,6 @@ export default class TooltipAPI extends Module {
|
|||
* @param {TooltipOptions} options - tooltip options
|
||||
*/
|
||||
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||
this.tooltip.onHover(element, content, options);
|
||||
tooltip.onHover(element, content, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -454,10 +454,12 @@ export default class BlockEvents extends Module {
|
|||
BlockManager
|
||||
.mergeBlocks(targetBlock, blockToMerge)
|
||||
.then(() => {
|
||||
/** Restore caret position after merge */
|
||||
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
|
||||
targetBlock.pluginsContent.normalize();
|
||||
Toolbar.close();
|
||||
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) {
|
||||
this.bindBlockEvents(block);
|
||||
window.requestIdleCallback(() => {
|
||||
this.bindBlockEvents(block);
|
||||
}, { timeout: 2000 });
|
||||
}
|
||||
|
||||
return block;
|
||||
|
@ -320,6 +323,46 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Block data.
|
||||
*
|
||||
* Currently we don't have an 'update' method in the Tools API, so we just create a new block with the same id and type
|
||||
* Should not trigger 'block-removed' or 'block-added' events
|
||||
*
|
||||
* @param block - block to update
|
||||
* @param data - new data
|
||||
*/
|
||||
public async update(block: Block, data: Partial<BlockToolData>): Promise<Block> {
|
||||
const existingData = await block.data;
|
||||
|
||||
const newBlock = this.composeBlock({
|
||||
id: block.id,
|
||||
tool: block.name,
|
||||
data: Object.assign({}, existingData, data),
|
||||
tunes: block.tunes,
|
||||
});
|
||||
|
||||
const blockIndex = this.getBlockIndex(block);
|
||||
|
||||
this._blocks.replace(blockIndex, newBlock);
|
||||
|
||||
this.blockDidMutated(BlockChangedMutationType, newBlock, {
|
||||
index: blockIndex,
|
||||
});
|
||||
|
||||
return newBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace passed Block with the new one with specified Tool and data
|
||||
*
|
||||
|
@ -433,40 +476,48 @@ 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 {
|
||||
const index = this._blocks.indexOf(block);
|
||||
public removeBlock(block: Block, addLastBlock = true): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const index = this._blocks.indexOf(block);
|
||||
|
||||
/**
|
||||
* If index is not passed and there is no block selected, show a warning
|
||||
*/
|
||||
if (!this.validateIndex(index)) {
|
||||
throw new Error('Can\'t find a Block to remove');
|
||||
}
|
||||
/**
|
||||
* If index is not passed and there is no block selected, show a warning
|
||||
*/
|
||||
if (!this.validateIndex(index)) {
|
||||
throw new Error('Can\'t find a Block to remove');
|
||||
}
|
||||
|
||||
block.destroy();
|
||||
this._blocks.remove(index);
|
||||
block.destroy();
|
||||
this._blocks.remove(index);
|
||||
|
||||
/**
|
||||
* Force call of didMutated event on Block removal
|
||||
*/
|
||||
this.blockDidMutated(BlockRemovedMutationType, block, {
|
||||
index,
|
||||
/**
|
||||
* Force call of didMutated event on Block removal
|
||||
*/
|
||||
this.blockDidMutated(BlockRemovedMutationType, block, {
|
||||
index,
|
||||
});
|
||||
|
||||
if (this.currentBlockIndex >= index) {
|
||||
this.currentBlockIndex--;
|
||||
}
|
||||
|
||||
/**
|
||||
* If first Block was removed, insert new Initial Block and set focus on it`s first input
|
||||
*/
|
||||
if (!this.blocks.length) {
|
||||
this.currentBlockIndex = -1;
|
||||
|
||||
if (addLastBlock) {
|
||||
this.insert();
|
||||
}
|
||||
} else if (index === 0) {
|
||||
this.currentBlockIndex = 0;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
if (this.currentBlockIndex >= index) {
|
||||
this.currentBlockIndex--;
|
||||
}
|
||||
|
||||
/**
|
||||
* If first Block was removed, insert new Initial Block and set focus on it`s first input
|
||||
*/
|
||||
if (!this.blocks.length) {
|
||||
this.currentBlockIndex = -1;
|
||||
this.insert();
|
||||
} else if (index === 0) {
|
||||
this.currentBlockIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -536,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;
|
||||
}
|
||||
|
@ -804,8 +870,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) {
|
||||
|
|
|
@ -304,16 +304,17 @@ export default class Caret extends Module {
|
|||
* @param {number} offset - offset
|
||||
*/
|
||||
public set(element: HTMLElement, offset = 0): void {
|
||||
const scrollOffset = 30;
|
||||
const { top, bottom } = Selection.setCursor(element, offset);
|
||||
|
||||
/** If new cursor position is not visible, scroll to it */
|
||||
const { innerHeight } = window;
|
||||
|
||||
/**
|
||||
* If new cursor position is not visible, scroll to it
|
||||
*/
|
||||
if (top < 0) {
|
||||
window.scrollBy(0, top);
|
||||
}
|
||||
if (bottom > innerHeight) {
|
||||
window.scrollBy(0, bottom - innerHeight);
|
||||
window.scrollBy(0, top - scrollOffset);
|
||||
} else if (bottom > innerHeight) {
|
||||
window.scrollBy(0, bottom - innerHeight + scrollOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,13 +504,10 @@ export default class Caret extends Module {
|
|||
|
||||
sel.expandToTag(shadowCaret as HTMLElement);
|
||||
|
||||
setTimeout(() => {
|
||||
const newRange = document.createRange();
|
||||
const newRange = document.createRange();
|
||||
|
||||
newRange.selectNode(shadowCaret);
|
||||
newRange.extractContents();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 50);
|
||||
newRange.selectNode(shadowCaret);
|
||||
newRange.extractContents();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -534,7 +532,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 +540,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,111 @@
|
|||
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
|
||||
*/
|
||||
public async render(blocks: OutputBlockData[]): Promise<void> {
|
||||
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
|
||||
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');
|
||||
|
||||
/**
|
||||
* Disable onChange callback on render to not to spam those events
|
||||
*/
|
||||
this.Editor.ModificationsObserver.disable();
|
||||
data = this.composeStubDataForTool(tool, data, id);
|
||||
tool = Tools.stubTool;
|
||||
}
|
||||
|
||||
const sequence = await _.sequence(chainData as _.ChainData[]);
|
||||
let block: Block;
|
||||
|
||||
this.Editor.ModificationsObserver.enable();
|
||||
try {
|
||||
block = BlockManager.composeBlock({
|
||||
id,
|
||||
tool,
|
||||
data,
|
||||
tunes,
|
||||
});
|
||||
} catch (error) {
|
||||
_.log(`Block «${tool}» skipped because of plugins error`, 'error', {
|
||||
data,
|
||||
error,
|
||||
});
|
||||
|
||||
this.Editor.UI.checkEmptiness();
|
||||
/**
|
||||
* If tool throws an error during render, we should render stub instead of it
|
||||
*/
|
||||
data = this.composeStubDataForTool(tool, data, id);
|
||||
tool = Tools.stubTool;
|
||||
|
||||
return sequence;
|
||||
block = BlockManager.composeBlock({
|
||||
id,
|
||||
tool,
|
||||
data,
|
||||
tunes,
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin instance
|
||||
* Add plugin instance to BlockManager
|
||||
* Insert block to working zone
|
||||
* Create data for the Stub Tool that will be used instead of unavailable tool
|
||||
*
|
||||
* @param {object} item - Block data to insert
|
||||
* @returns {Promise<void>}
|
||||
* @param tool - unavailable tool name to stub
|
||||
* @param data - data of unavailable block
|
||||
* @param [id] - id of unavailable block
|
||||
*/
|
||||
public async insertBlock(item: OutputBlockData): Promise<void> {
|
||||
const { Tools, BlockManager } = this.Editor;
|
||||
const { type: tool, data, tunes, id } = item;
|
||||
private composeStubDataForTool(tool: string, data: BlockToolData, id?: BlockId): StubData {
|
||||
const { Tools } = this.Editor;
|
||||
|
||||
if (Tools.available.has(tool)) {
|
||||
try {
|
||||
BlockManager.insert({
|
||||
id,
|
||||
tool,
|
||||
data,
|
||||
tunes,
|
||||
});
|
||||
} catch (error) {
|
||||
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', {
|
||||
data,
|
||||
error,
|
||||
});
|
||||
throw Error(error);
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
/** If Tool is unavailable, create stub Block for it */
|
||||
const stubData = {
|
||||
savedData: {
|
||||
id,
|
||||
type: tool,
|
||||
data,
|
||||
},
|
||||
title: tool,
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
return {
|
||||
savedData: {
|
||||
id,
|
||||
type: tool,
|
||||
data,
|
||||
},
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -3,7 +3,7 @@ import $ from '../../dom';
|
|||
import * as _ from '../../utils';
|
||||
import I18n from '../../i18n';
|
||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||
import Tooltip from '../../utils/tooltip';
|
||||
import * as tooltip from '../../utils/tooltip';
|
||||
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||
import Block from '../../block';
|
||||
import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
|
||||
|
@ -91,11 +91,6 @@ interface ToolbarNodes {
|
|||
* @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
|
||||
*/
|
||||
export default class Toolbar extends Module<ToolbarNodes> {
|
||||
/**
|
||||
* Tooltip utility Instance
|
||||
*/
|
||||
private tooltip: Tooltip;
|
||||
|
||||
/**
|
||||
* Block near which we display the Toolbox
|
||||
*/
|
||||
|
@ -103,8 +98,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
|
||||
|
@ -117,7 +113,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
config,
|
||||
eventsDispatcher,
|
||||
});
|
||||
this.tooltip = new Tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -155,18 +150,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 +178,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 +225,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
if (!readOnlyEnabled) {
|
||||
this.drawUI();
|
||||
this.enableModuleBindings();
|
||||
window.requestIdleCallback(() => {
|
||||
this.drawUI();
|
||||
this.enableModuleBindings();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
this.destroy();
|
||||
this.Editor.BlockSettings.destroy();
|
||||
|
@ -225,6 +242,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,8 +320,16 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
/** Close components */
|
||||
this.blockActions.hide();
|
||||
this.toolboxInstance.close();
|
||||
this.toolboxInstance?.close();
|
||||
this.Editor.BlockSettings.close();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the Toolbar position to prevent DOM height growth, for example after blocks deletion
|
||||
*/
|
||||
private reset(): void {
|
||||
this.nodes.wrapper.style.top = 'unset';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -305,16 +339,13 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* This flag allows to open Toolbar without Actions.
|
||||
*/
|
||||
private open(withBlockActions = true): void {
|
||||
_.delay(() => {
|
||||
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
|
||||
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
|
||||
|
||||
if (withBlockActions) {
|
||||
this.blockActions.show();
|
||||
} else {
|
||||
this.blockActions.hide();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 50)();
|
||||
if (withBlockActions) {
|
||||
this.blockActions.show();
|
||||
} else {
|
||||
this.blockActions.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -350,7 +381,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
$.append(this.nodes.actions, this.nodes.plusButton);
|
||||
|
||||
this.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {
|
||||
this.tooltip.hide(true);
|
||||
tooltip.hide(true);
|
||||
this.plusButtonClicked();
|
||||
}, false);
|
||||
|
||||
|
@ -364,7 +395,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
textContent: '⇥ Tab',
|
||||
}));
|
||||
|
||||
this.tooltip.onHover(this.nodes.plusButton, tooltipContent, {
|
||||
tooltip.onHover(this.nodes.plusButton, tooltipContent, {
|
||||
hidingDelay: 400,
|
||||
});
|
||||
|
||||
|
@ -380,7 +411,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
$.append(this.nodes.actions, this.nodes.settingsToggler);
|
||||
|
||||
this.tooltip.onHover(
|
||||
tooltip.onHover(
|
||||
this.nodes.settingsToggler,
|
||||
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
|
||||
{
|
||||
|
@ -454,7 +485,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
*/
|
||||
this.Editor.BlockManager.currentBlock = this.hoveredBlock;
|
||||
|
||||
this.toolboxInstance.toggle();
|
||||
this.toolboxInstance?.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -476,11 +507,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
this.settingsTogglerClicked();
|
||||
|
||||
if (this.toolboxInstance.opened) {
|
||||
if (this.toolboxInstance?.opened) {
|
||||
this.toolboxInstance.close();
|
||||
}
|
||||
|
||||
this.tooltip.hide(true);
|
||||
tooltip.hide(true);
|
||||
}, true);
|
||||
|
||||
/**
|
||||
|
@ -496,7 +527,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;
|
||||
}
|
||||
|
||||
|
@ -561,6 +592,5 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
if (this.toolboxInstance) {
|
||||
this.toolboxInstance.destroy();
|
||||
}
|
||||
this.tooltip.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import Flipper from '../../flipper';
|
|||
import I18n from '../../i18n';
|
||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||
import Shortcuts from '../../utils/shortcuts';
|
||||
import Tooltip from '../../utils/tooltip';
|
||||
import * as tooltip from '../../utils/tooltip';
|
||||
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||
import InlineTool from '../../tools/inline';
|
||||
import { CommonInternalSettings } from '../../tools/base';
|
||||
|
@ -97,10 +97,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
*/
|
||||
private flipper: Flipper = null;
|
||||
|
||||
/**
|
||||
* Tooltip utility Instance
|
||||
*/
|
||||
private tooltip: Tooltip;
|
||||
/**
|
||||
* @class
|
||||
* @param moduleConfiguration - Module Configuration
|
||||
|
@ -112,7 +108,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
config,
|
||||
eventsDispatcher,
|
||||
});
|
||||
this.tooltip = new Tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,7 +117,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
if (!readOnlyEnabled) {
|
||||
this.make();
|
||||
window.requestIdleCallback(() => {
|
||||
this.make();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
this.destroy();
|
||||
this.Editor.ConversionToolbar.destroy();
|
||||
|
@ -155,52 +152,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
this.Editor.Toolbar.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Toolbar to the selected text
|
||||
*/
|
||||
public move(): void {
|
||||
const selectionRect = SelectionUtils.rect as DOMRect;
|
||||
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
|
||||
const newCoords = {
|
||||
x: selectionRect.x - wrapperOffset.left,
|
||||
y: selectionRect.y +
|
||||
selectionRect.height -
|
||||
// + window.scrollY
|
||||
wrapperOffset.top +
|
||||
this.toolbarVerticalMargin,
|
||||
};
|
||||
|
||||
/**
|
||||
* If we know selections width, place InlineToolbar to center
|
||||
*/
|
||||
if (selectionRect.width) {
|
||||
newCoords.x += Math.floor(selectionRect.width / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing
|
||||
*/
|
||||
const realLeftCoord = newCoords.x - this.width / 2;
|
||||
const realRightCoord = newCoords.x + this.width / 2;
|
||||
|
||||
/**
|
||||
* By default, Inline Toolbar has top-corner at the center
|
||||
* We are adding a modifiers for to move corner to the left or right
|
||||
*/
|
||||
this.nodes.wrapper.classList.toggle(
|
||||
this.CSS.inlineToolbarLeftOriented,
|
||||
realLeftCoord < this.Editor.UI.contentRect.left
|
||||
);
|
||||
|
||||
this.nodes.wrapper.classList.toggle(
|
||||
this.CSS.inlineToolbarRightOriented,
|
||||
realRightCoord > this.Editor.UI.contentRect.right
|
||||
);
|
||||
|
||||
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
|
||||
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides Inline Toolbar
|
||||
*/
|
||||
|
@ -229,6 +180,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
}
|
||||
});
|
||||
|
||||
this.reset();
|
||||
this.opened = false;
|
||||
|
||||
this.flipper.deactivate();
|
||||
|
@ -302,7 +254,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
}
|
||||
|
||||
this.removeAllNodes();
|
||||
this.tooltip.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -359,8 +310,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
|
||||
*/
|
||||
this.recalculateWidth();
|
||||
window.requestAnimationFrame(() => {
|
||||
this.recalculateWidth();
|
||||
});
|
||||
|
||||
/**
|
||||
* Allow to leaf buttons by arrows / tab
|
||||
|
@ -369,6 +323,66 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
this.enableFlipper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Toolbar to the selected text
|
||||
*/
|
||||
private move(): void {
|
||||
const selectionRect = SelectionUtils.rect as DOMRect;
|
||||
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
|
||||
const newCoords = {
|
||||
x: selectionRect.x - wrapperOffset.left,
|
||||
y: selectionRect.y +
|
||||
selectionRect.height -
|
||||
// + window.scrollY
|
||||
wrapperOffset.top +
|
||||
this.toolbarVerticalMargin,
|
||||
};
|
||||
|
||||
/**
|
||||
* If we know selections width, place InlineToolbar to center
|
||||
*/
|
||||
if (selectionRect.width) {
|
||||
newCoords.x += Math.floor(selectionRect.width / 2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing
|
||||
*/
|
||||
const realLeftCoord = newCoords.x - this.width / 2;
|
||||
const realRightCoord = newCoords.x + this.width / 2;
|
||||
|
||||
/**
|
||||
* By default, Inline Toolbar has top-corner at the center
|
||||
* We are adding a modifiers for to move corner to the left or right
|
||||
*/
|
||||
this.nodes.wrapper.classList.toggle(
|
||||
this.CSS.inlineToolbarLeftOriented,
|
||||
realLeftCoord < this.Editor.UI.contentRect.left
|
||||
);
|
||||
|
||||
this.nodes.wrapper.classList.toggle(
|
||||
this.CSS.inlineToolbarRightOriented,
|
||||
realRightCoord > this.Editor.UI.contentRect.right
|
||||
);
|
||||
|
||||
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
|
||||
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear orientation classes and reset position
|
||||
*/
|
||||
private reset(): void {
|
||||
this.nodes.wrapper.classList.remove(
|
||||
this.CSS.inlineToolbarLeftOriented,
|
||||
this.CSS.inlineToolbarRightOriented
|
||||
);
|
||||
|
||||
this.nodes.wrapper.style.left = 'unset';
|
||||
this.nodes.wrapper.style.top = 'unset';
|
||||
}
|
||||
|
||||
/**
|
||||
* Need to show Inline Toolbar or not
|
||||
*/
|
||||
|
@ -460,7 +474,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
});
|
||||
|
||||
if (_.isMobileScreen() === false ) {
|
||||
this.tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
|
||||
tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
|
@ -589,7 +603,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
}
|
||||
|
||||
if (_.isMobileScreen() === false ) {
|
||||
this.tooltip.onHover(button, tooltipContent, {
|
||||
tooltip.onHover(button, tooltipContent, {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
@ -332,11 +310,17 @@ export default class UI extends Module<UINodes> {
|
|||
|
||||
this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousedown', (event: MouseEvent | TouchEvent) => {
|
||||
this.documentTouched(event);
|
||||
}, true);
|
||||
}, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.readOnlyMutableListeners.on(this.nodes.redactor, 'touchstart', (event: MouseEvent | TouchEvent) => {
|
||||
this.documentTouched(event);
|
||||
}, true);
|
||||
}, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => {
|
||||
this.documentKeydown(event);
|
||||
|
@ -501,7 +485,9 @@ export default class UI extends Module<UINodes> {
|
|||
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
|
||||
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
|
||||
|
||||
Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
|
||||
const newBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
|
||||
|
||||
Caret.setToBlock(newBlock, Caret.positions.START);
|
||||
|
||||
/** Clear selection */
|
||||
BlockSelection.clearSelection(event);
|
||||
|
@ -684,12 +670,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);
|
||||
});
|
||||
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
|
||||
|
||||
/**
|
||||
* Highlight Current Node
|
||||
|
@ -721,17 +702,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
|
||||
|
@ -740,7 +714,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);
|
||||
|
@ -750,10 +725,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) &&
|
||||
/**
|
||||
|
@ -767,7 +754,8 @@ export default class UI extends Module<UINodes> {
|
|||
lastBlockBottomCoord < clickedCoord;
|
||||
|
||||
if (isClickedBottom) {
|
||||
stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
|
||||
const { BlockManager, Caret, Toolbar } = this.Editor;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,53 +6,66 @@ import CodeXTooltips from 'codex-tooltip';
|
|||
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
|
||||
|
||||
/**
|
||||
* Tooltip
|
||||
* Tooltips lib: CodeX Tooltips
|
||||
*
|
||||
* Decorates any tooltip module like adapter
|
||||
* @see https://github.com/codex-team/codex.tooltips
|
||||
*/
|
||||
export default class Tooltip {
|
||||
/**
|
||||
* Tooltips lib: CodeX Tooltips
|
||||
*
|
||||
* @see https://github.com/codex-team/codex.tooltips
|
||||
*/
|
||||
private lib: CodeXTooltips = new CodeXTooltips();
|
||||
let lib: null | CodeXTooltips = null;
|
||||
|
||||
/**
|
||||
* Release the library
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.lib.destroy();
|
||||
/**
|
||||
* If library is needed, but it is not initialized yet, this function will initialize it
|
||||
*
|
||||
* For example, if editor was destroyed and then initialized again
|
||||
*/
|
||||
function prepare(): void {
|
||||
if (lib) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows tooltip on element with passed HTML content
|
||||
*
|
||||
* @param {HTMLElement} element - any HTML element in DOM
|
||||
* @param content - tooltip's content
|
||||
* @param options - showing settings
|
||||
*/
|
||||
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||
this.lib.show(element, content, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides tooltip
|
||||
*
|
||||
* @param skipHidingDelay — pass true to immediately hide the tooltip
|
||||
*/
|
||||
public hide(skipHidingDelay = false): void {
|
||||
this.lib.hide(skipHidingDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - any HTML element in DOM
|
||||
* @param content - tooltip's content
|
||||
* @param options - showing settings
|
||||
*/
|
||||
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||
this.lib.onHover(element, content, options);
|
||||
}
|
||||
lib = new CodeXTooltips();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows tooltip on element with passed HTML content
|
||||
*
|
||||
* @param {HTMLElement} element - any HTML element in DOM
|
||||
* @param content - tooltip's content
|
||||
* @param options - showing settings
|
||||
*/
|
||||
export function show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||
prepare();
|
||||
|
||||
lib?.show(element, content, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides tooltip
|
||||
*
|
||||
* @param skipHidingDelay — pass true to immediately hide the tooltip
|
||||
*/
|
||||
export function hide(skipHidingDelay = false): void {
|
||||
prepare();
|
||||
|
||||
lib?.hide(skipHidingDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip
|
||||
*
|
||||
* @param {HTMLElement} element - any HTML element in DOM
|
||||
* @param content - tooltip's content
|
||||
* @param options - showing settings
|
||||
*/
|
||||
export function onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||
prepare();
|
||||
|
||||
lib?.onHover(element, content, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the library
|
||||
*/
|
||||
export function destroy(): void {
|
||||
lib?.destroy();
|
||||
lib = null;
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
background-color: var(--inlineSelectionColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color for native 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
|
||||
|
|
16
test/cypress/fixtures/types/PartialBlockMutationEvent.ts
Normal file
16
test/cypress/fixtures/types/PartialBlockMutationEvent.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { BlockMutationEvent, BlockMutationType } from '../../../../types';
|
||||
|
||||
/**
|
||||
* Simplified version of the BlockMutationEvent with optional fields that could be used in tests
|
||||
*/
|
||||
export default interface PartialBlockMutationEvent {
|
||||
/**
|
||||
* Event type
|
||||
*/
|
||||
type?: BlockMutationType,
|
||||
|
||||
/**
|
||||
* Details with partial properties
|
||||
*/
|
||||
detail?: Partial<BlockMutationEvent['detail']>
|
||||
}
|
|
@ -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);
|
||||
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);
|
||||
});
|
||||
|
|
59
test/cypress/support/e2e.ts
Normal file
59
test/cypress/support/e2e.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
|
||||
/* global chai */
|
||||
// because this file is imported from cypress/support/e2e.js
|
||||
// that means all other spec files will have this assertion plugin
|
||||
// available to them because the supportFile is bundled and served
|
||||
// prior to any spec files loading
|
||||
|
||||
import PartialBlockMutationEvent from '../fixtures/types/PartialBlockMutationEvent';
|
||||
|
||||
/**
|
||||
* Chai plugin for checking if passed onChange method is called with an array of passed events
|
||||
*
|
||||
* @param _chai - Chai instance
|
||||
*/
|
||||
const beCalledWithBatchedEvents = (_chai): void => {
|
||||
/**
|
||||
* Check if passed onChange method is called with an array of passed events
|
||||
*
|
||||
* @param expectedEvents - batched events to check
|
||||
*/
|
||||
function assertToBeCalledWithBatchedEvents(expectedEvents: PartialBlockMutationEvent[]): void {
|
||||
/**
|
||||
* EditorJS API is passed as the first parameter of the onChange callback
|
||||
*/
|
||||
const EditorJSApiMock = Cypress.sinon.match.any;
|
||||
const $onChange = this._obj;
|
||||
|
||||
this.assert(
|
||||
$onChange.calledOnce,
|
||||
'expected #{this} to be called once',
|
||||
'expected #{this} to not be called once'
|
||||
);
|
||||
|
||||
this.assert(
|
||||
$onChange.calledWithMatch(
|
||||
EditorJSApiMock,
|
||||
Cypress.sinon.match((events: PartialBlockMutationEvent[]) => {
|
||||
expect(events).to.be.an('array');
|
||||
|
||||
return events.every((event, index) => {
|
||||
const eventToCheck = expectedEvents[index];
|
||||
|
||||
return expect(event).to.containSubset(eventToCheck);
|
||||
});
|
||||
})
|
||||
),
|
||||
'expected #{this} to be called with #{exp}, but it was called with #{act}',
|
||||
'expected #{this} to not be called with #{exp}, but it was called with #{act} ',
|
||||
expectedEvents
|
||||
);
|
||||
}
|
||||
|
||||
_chai.Assertion.addMethod('calledWithBatchedEvents', assertToBeCalledWithBatchedEvents);
|
||||
};
|
||||
|
||||
/**
|
||||
* registers our assertion function "beCalledWithBatchedEvents" with Chai
|
||||
*/
|
||||
chai.use(beCalledWithBatchedEvents);
|
29
test/cypress/support/index.d.ts
vendored
29
test/cypress/support/index.d.ts
vendored
|
@ -1,9 +1,9 @@
|
|||
// in cypress/support/index.d.ts
|
||||
// load type definitions that come with Cypress module
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { EditorConfig, OutputData } from './../../../types/index';
|
||||
import type EditorJS from '../../../types/index'
|
||||
import PartialBlockMutationEvent from '../fixtures/types/PartialBlockMutationEvent';
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
|
@ -65,6 +65,22 @@ declare global {
|
|||
interface ApplicationWindow {
|
||||
EditorJS: typeof EditorJS
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends Cypress assertion Chainer interface with the new assertion methods
|
||||
*/
|
||||
interface Chainer<Subject> {
|
||||
/**
|
||||
* Custom Chai assertion that checks if given onChange method is called with an array of passed events
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* cy.get('@onChange').should('be.calledWithBatchedEvents', [{ type: 'block-added', detail: { index: 0 }}])
|
||||
* expect(onChange).to.be.calledWithBatchedEvents([{ type: 'block-added', detail: { index: 0 }}])
|
||||
* ```
|
||||
*/
|
||||
(chainer: 'be.calledWithBatchedEvents', expectedEvents: PartialBlockMutationEvent[]): Chainable<Subject>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,6 +92,17 @@ declare global {
|
|||
* "containSubset" object properties matcher
|
||||
*/
|
||||
containSubset(subset: any): Assertion;
|
||||
|
||||
/**
|
||||
* Custom Chai assertion that checks if given onChange method is called with an array of passed events
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* cy.get('@onChange').should('be.calledWithBatchedEvents', [{ type: 'block-added', detail: { index: 0 }}])
|
||||
* expect(onChange).to.be.calledWithBatchedEvents([{ type: 'block-added', detail: { index: 0 }}])
|
||||
* ```
|
||||
*/
|
||||
calledWithBatchedEvents(expectedEvents: PartialBlockMutationEvent[]): Assertion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,20 @@
|
|||
*/
|
||||
|
||||
import '@cypress/code-coverage/support';
|
||||
import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector';
|
||||
|
||||
installLogsCollector();
|
||||
|
||||
/**
|
||||
* File with the helpful commands
|
||||
*/
|
||||
import './commands';
|
||||
|
||||
/**
|
||||
* File with custom assertions
|
||||
*/
|
||||
import './e2e';
|
||||
|
||||
import chaiSubset from 'chai-subset';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,10 +91,14 @@ describe('api.blocks', () => {
|
|||
|
||||
editor.blocks.update(idToUpdate, newBlockData);
|
||||
|
||||
const output = await editor.save();
|
||||
const text = output.blocks[0].data.text;
|
||||
// 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);
|
||||
expect(text).to.be.eq(newBlockData.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -111,21 +108,23 @@ 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',
|
||||
};
|
||||
|
||||
editor.blocks.update(idToUpdate, newBlockData);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.invoke('text')
|
||||
.then(blockText => {
|
||||
expect(blockText).to.be.eq(firstBlock.data.text);
|
||||
editor.blocks.update(idToUpdate, newBlockData)
|
||||
.catch(error => {
|
||||
expect(error.message).to.be.eq(`Block with id "${idToUpdate}" not found`);
|
||||
})
|
||||
.finally(() => {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.invoke('text')
|
||||
.then(blockText => {
|
||||
expect(blockText).to.be.eq(firstBlock.data.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -138,9 +137,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 +154,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 +246,31 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
}).then((editor) => {
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
/**
|
||||
* Call the 'convert' api method
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const { convert } = editor.blocks;
|
||||
convert(existingBlock.id, 'convertableTool');
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* Check that block was converted
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const { blocks } = await editor.save();
|
||||
|
||||
expect(blocks.length).to.eq(1);
|
||||
expect(blocks[0].type).to.eq('convertableTool');
|
||||
expect(blocks[0].data.text).to.eq(existingBlock.data.text);
|
||||
// wait for block to be converted
|
||||
cy.wait(100).then(() => {
|
||||
/**
|
||||
* Check that block was converted
|
||||
*/
|
||||
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');
|
||||
|
||||
/**
|
||||
* Call the 'convert' api method with nonexisting Block id
|
||||
*/
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
cy.createEditor({})
|
||||
.then((editor) => {
|
||||
/**
|
||||
* Call the 'convert' api method with nonexisting Block id
|
||||
*/
|
||||
const fakeId = 'WRNG_ID';
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
|
@ -262,20 +295,17 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
}).then((editor) => {
|
||||
/**
|
||||
* Call the 'convert' api method with nonexisting tool name
|
||||
*/
|
||||
const nonexistingToolName = 'WRNG_TOOL_NAME';
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const exec = (): void => convert(existingBlock.id, nonexistingToolName);
|
||||
|
||||
const exec = (): void => convert(existingBlock.id, nonexistingToolName);
|
||||
|
||||
expect(exec).to.throw(`Block Tool with type "${nonexistingToolName}" not found`);
|
||||
});
|
||||
expect(exec).to.throw(`Block Tool with type "${nonexistingToolName}" not found`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if some tool does not provide "conversionConfig"', function () {
|
||||
|
@ -304,19 +334,16 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
}).then((editor) => {
|
||||
/**
|
||||
* Call the 'convert' api method with tool that does not provide "conversionConfig"
|
||||
*/
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
const exec = (): void => convert(existingBlock.id, 'nonConvertableTool');
|
||||
|
||||
expect(exec).to.throw(`Conversion from "paragraph" to "nonConvertableTool" is not possible. NonConvertableTool tool(s) should provide a "conversionConfig"`);
|
||||
});
|
||||
expect(exec).to.throw(`Conversion from "paragraph" to "nonConvertableTool" is not possible. NonConvertableTool tool(s) should provide a "conversionConfig"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
||||
expect(data.blocks[0].id).to.eq(blocks[0].id);
|
||||
expect(data.blocks[2].id).to.eq(blocks[1].id);
|
||||
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) => {
|
||||
|
|
179
test/cypress/tests/modules/Renderer.cy.ts
Normal file
179
test/cypress/tests/modules/Renderer.cy.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
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 () {
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -5,33 +5,13 @@ 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';
|
||||
|
||||
/**
|
||||
* EditorJS API is passed as the first parameter of the onChange callback
|
||||
*/
|
||||
const EditorJSApiMock = Cypress.sinon.match.any;
|
||||
|
||||
/**
|
||||
* Check if passed onChange method is called with an array of passed events
|
||||
*
|
||||
* @param $onChange - editor onChange spy
|
||||
* @param expectedEvents - batched events to check
|
||||
*/
|
||||
function beCalledWithBatchedEvents($onChange, expectedEvents): void {
|
||||
expect($onChange).to.be.calledOnce;
|
||||
expect($onChange).to.be.calledWithMatch(
|
||||
EditorJSApiMock,
|
||||
Cypress.sinon.match((events) => {
|
||||
return events.every((event, index) => {
|
||||
const eventToCheck = expectedEvents[index];
|
||||
|
||||
return expect(event).to.containSubset(eventToCheck);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Add checks that correct block API object is passed to onChange
|
||||
* @todo Add cases for native inputs changes
|
||||
|
@ -104,22 +84,20 @@ describe('onChange callback', () => {
|
|||
.type('change')
|
||||
.type('{enter}');
|
||||
|
||||
cy.get('@onChange').should(($callback) => {
|
||||
return beCalledWithBatchedEvents($callback, [
|
||||
{
|
||||
type: BlockChangedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
},
|
||||
cy.get('@onChange').should('be.calledWithBatchedEvents', [
|
||||
{
|
||||
type: BlockChangedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter out similar events on batching', () => {
|
||||
|
@ -132,7 +110,6 @@ describe('onChange callback', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -243,37 +220,35 @@ describe('onChange callback', () => {
|
|||
.get('div.ce-popover-item[data-item-name=delimiter]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should(($callback) => {
|
||||
return beCalledWithBatchedEvents($callback, [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'paragraph',
|
||||
},
|
||||
cy.get('@onChange').should('be.calledWithBatchedEvents', [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'paragraph',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'delimiter',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'delimiter',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 1,
|
||||
target: {
|
||||
name: 'paragraph',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 1,
|
||||
target: {
|
||||
name: 'paragraph',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be fired on block replacement for both of blocks', () => {
|
||||
|
@ -291,28 +266,26 @@ describe('onChange callback', () => {
|
|||
.get('div.ce-popover-item[data-item-name=header]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should(($callback) => {
|
||||
return beCalledWithBatchedEvents($callback, [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'paragraph',
|
||||
},
|
||||
cy.get('@onChange').should('be.calledWithBatchedEvents', [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'paragraph',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'header',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
name: 'header',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be fired on tune modifying', () => {
|
||||
|
@ -375,28 +348,26 @@ describe('onChange callback', () => {
|
|||
.get('div[data-item-name=delete]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should(($callback) => {
|
||||
return beCalledWithBatchedEvents($callback, [
|
||||
/**
|
||||
* "block-removed" fired since we have deleted a block
|
||||
*/
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
},
|
||||
cy.get('@onChange').should('be.calledWithBatchedEvents', [
|
||||
/**
|
||||
* "block-removed" fired since we have deleted a block
|
||||
*/
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
},
|
||||
/**
|
||||
* "block-added" fired since we have deleted the last block, so the new one is created
|
||||
*/
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* "block-added" fired since we have deleted the last block, so the new one is created
|
||||
*/
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be fired when block is moved', () => {
|
||||
|
@ -483,7 +454,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 +532,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 +539,114 @@ 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('be.calledWithBatchedEvents', [
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
},
|
||||
{
|
||||
type: BlockRemovedMutationType,
|
||||
},
|
||||
{
|
||||
type: BlockAddedMutationType,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not be called on blocks.render() on non-empty editor', () => {
|
||||
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('have.callCount', 0);
|
||||
});
|
||||
|
||||
it('should be called on blocks.update() with "block-changed" event', () => {
|
||||
const block = {
|
||||
id: 'bwnFX5LoX7',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The first block mock.',
|
||||
},
|
||||
};
|
||||
const config = {
|
||||
data: {
|
||||
blocks: [
|
||||
block,
|
||||
],
|
||||
},
|
||||
onChange: (api, event): void => {
|
||||
console.log('something changed', event);
|
||||
},
|
||||
};
|
||||
|
||||
cy.spy(config, 'onChange').as('onChange');
|
||||
|
||||
cy.createEditor(config)
|
||||
.then((editor) => {
|
||||
editor.blocks.update(block.id, {
|
||||
text: 'Updated text',
|
||||
});
|
||||
|
||||
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
|
||||
type: BlockChangedMutationType,
|
||||
detail: {
|
||||
index: 0,
|
||||
target: {
|
||||
id: block.id,
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
const boldText = output.blocks[0].data.text;
|
||||
|
||||
expect(boldText).to.eq('<b>Bold text</b>');
|
||||
});
|
||||
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;
|
||||
|
||||
const text = output.blocks[0].data.text;
|
||||
|
||||
expect(text).to.match(/<b>This text should be bold\.(<br>)?<\/b>/);
|
||||
});
|
||||
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;
|
||||
|
||||
const boldText = output.blocks[1].data.text;
|
||||
|
||||
expect(boldText).to.eq('<b>Bold text</b>');
|
||||
});
|
||||
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()
|
||||
|
|
17
types/api/blocks.d.ts
vendored
17
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';
|
||||
|
||||
|
@ -9,7 +9,7 @@ export interface Blocks {
|
|||
/**
|
||||
* Remove all blocks from Editor zone
|
||||
*/
|
||||
clear(): void;
|
||||
clear(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Render passed data
|
||||
|
@ -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.
|
||||
|
@ -127,9 +134,9 @@ export interface Blocks {
|
|||
* Updates block data by id
|
||||
*
|
||||
* @param id - id of the block to update
|
||||
* @param data - the new data
|
||||
* @param data - the new data. Can be partial.
|
||||
*/
|
||||
update(id: string, data: BlockToolData): void;
|
||||
update(id: string, data: Partial<BlockToolData>): Promise<BlockAPI>;
|
||||
|
||||
/**
|
||||
* Converts block to another type. Both blocks should provide the conversionConfig.
|
||||
|
|
4
types/index.d.ts
vendored
4
types/index.d.ts
vendored
|
@ -31,7 +31,7 @@ import {
|
|||
} from './api';
|
||||
|
||||
import { OutputData } from './data-formats';
|
||||
import { BlockMutationEventMap } from './events/block';
|
||||
import { BlockMutationEvent, BlockMutationEventMap, BlockMutationType } from './events/block';
|
||||
import { BlockAddedMutationType, BlockAddedEvent } from './events/block/BlockAdded';
|
||||
import { BlockChangedMutationType, BlockChangedEvent } from './events/block/BlockChanged';
|
||||
import { BlockMovedMutationType, BlockMovedEvent } from './events/block/BlockMoved';
|
||||
|
@ -85,6 +85,8 @@ export { OutputData, OutputBlockData} from './data-formats/output-data';
|
|||
export { BlockId } from './data-formats/block-id';
|
||||
export { BlockAPI } from './api'
|
||||
export {
|
||||
BlockMutationType,
|
||||
BlockMutationEvent,
|
||||
BlockMutationEventMap,
|
||||
BlockAddedMutationType,
|
||||
BlockAddedEvent,
|
||||
|
|
|
@ -65,4 +65,4 @@ export default {
|
|||
plugins: [
|
||||
cssInjectedByJsPlugin(),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
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