fix: partially replace deprecated APIs with the modern ones

This commit is contained in:
JackUait 2025-11-16 01:00:11 +03:00
commit 408b160e39
19 changed files with 523 additions and 195 deletions

View file

@ -59,9 +59,6 @@ Methods that working with Blocks
`renderFromHTML(data)` - parse and render passed HTML string (*not for production use*)
`swap(fromIndex, toIndex)` - swaps two Blocks by their positions (deprecated:
use 'move' instead)
`move(toIndex, fromIndex)` - moves block from one index to another position.
`fromIndex` will be the current block's index by default.

View file

@ -6,7 +6,7 @@
import type { API, BlockTune } from '../../../types';
import { IconChevronDown } from '@codexteam/icons';
import type { TunesMenuConfig } from '../../../types/tools';
import type { MenuConfig } from '../../../types/tools';
/**
@ -44,7 +44,7 @@ export default class MoveDownTune implements BlockTune {
/**
* Tune's appearance in block settings menu
*/
public render(): TunesMenuConfig {
public render(): MenuConfig {
return {
icon: IconChevronDown,
title: this.api.i18n.t('Move down'),

View file

@ -5,7 +5,7 @@
*/
import type { API, BlockTune } from '../../../types';
import { IconChevronUp } from '@codexteam/icons';
import type { TunesMenuConfig } from '../../../types/tools';
import type { MenuConfig } from '../../../types/tools';
/**
*
@ -42,7 +42,7 @@ export default class MoveUpTune implements BlockTune {
/**
* Tune's appearance in block settings menu
*/
public render(): TunesMenuConfig {
public render(): MenuConfig {
return {
icon: IconChevronUp,
title: this.api.i18n.t('Move up'),

View file

@ -21,7 +21,7 @@ import type BlockTuneAdapter from '../tools/tune';
import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
import type ToolsCollection from '../tools/collection';
import EventsDispatcher from '../utils/events';
import type { TunesMenuConfigItem } from '../../../types/tools';
import type { MenuConfigItem } from '../../../types/tools';
import { isMutationBelongsToElement } from '../utils/mutations';
import type { EditorEventMap } from '../events';
import { FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
@ -386,7 +386,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
const toolTunesPopoverParams: PopoverItemParams[] = [];
const commonTunesPopoverParams: PopoverItemParams[] = [];
const pushTuneConfig = (
tuneConfig: TunesMenuConfigItem | TunesMenuConfigItem[] | HTMLElement | undefined,
tuneConfig: MenuConfigItem | MenuConfigItem[] | HTMLElement | undefined,
target: PopoverItemParams[]
): void => {
if (!tuneConfig) {

View file

@ -1,5 +1,4 @@
import * as _ from './utils';
import $ from './dom';
import type Block from './block';
import { BlockToolAPI } from './block';
import type { MoveEvent } from '../../types/tools';
@ -119,28 +118,6 @@ export default class Blocks {
this.insertToDOM(block);
}
/**
* Swaps blocks with indexes first and second
*
* @param {number} first - first block index
* @param {number} second - second block index
* @deprecated use 'move' instead
*/
public swap(first: number, second: number): void {
const secondBlock = this.blocks[second];
/**
* Change in DOM
*/
$.swap(this.blocks[first].holder, secondBlock.holder);
/**
* Change in array
*/
this.blocks[second] = this.blocks[first];
this.blocks[first] = secondBlock;
}
/**
* Move a block from one to another index
*

View file

@ -130,30 +130,6 @@ export default class Dom {
}
}
/**
* Swap two elements in parent
*
* @param {HTMLElement} el1 - from
* @param {HTMLElement} el2 - to
* @deprecated
*/
public static swap(el1: HTMLElement, el2: HTMLElement): void {
// create marker element and insert it where el1 is
const temp = document.createElement('div');
const parent = el1.parentNode;
parent?.insertBefore(temp, el1);
// move el1 to right before el2
parent?.insertBefore(el1, el2);
// move el2 to right before where el1 used to be
parent?.insertBefore(el2, temp);
// remove temporary marker node
parent?.removeChild(temp);
}
/**
* Selector Decorator
*

View file

@ -1575,8 +1575,8 @@ export default class BoldInlineTool implements InlineTool {
* @param event - The keyboard event to check
*/
private static isBoldShortcut(event: KeyboardEvent): boolean {
const platform = typeof navigator !== 'undefined' ? navigator.platform : '';
const isMac = platform.toUpperCase().includes('MAC');
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : '';
const isMac = userAgent.includes('mac');
const primaryModifier = isMac ? event.metaKey : event.ctrlKey;
if (!primaryModifier || event.altKey) {

View file

@ -23,7 +23,6 @@ export default class BlocksAPI extends Module {
render: (data: OutputData): Promise<void> => this.render(data),
renderFromHTML: (data: string): Promise<void> => this.renderFromHTML(data),
delete: (index?: number): void => this.delete(index),
swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),
move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),
getBlockByIndex: (index: number): BlockAPIInterface | undefined => this.getBlockByIndex(index),
getById: (id: string): BlockAPIInterface | null => this.getById(id),
@ -127,23 +126,6 @@ export default class BlocksAPI extends Module {
return new BlockAPI(block);
}
/**
* Call Block Manager method that swap Blocks
*
* @param {number} fromIndex - position of first Block
* @param {number} toIndex - position of second Block
* @deprecated use 'move' instead
*/
public swap(fromIndex: number, toIndex: number): void {
_.log(
'`blocks.swap()` method is deprecated and will be removed in the next major release. ' +
'Use `block.move()` method instead',
'info'
);
this.Editor.BlockManager.swap(fromIndex, toIndex);
}
/**
* Move block from one index to another
*

View file

@ -10,6 +10,19 @@ import { areBlocksMergeable } from '../utils/blocks';
import * as caretUtils from '../utils/caret';
import { focus } from '@editorjs/caret';
const KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP: Record<string, number> = {
Backspace: _.keyCodes.BACKSPACE,
Delete: _.keyCodes.DELETE,
Enter: _.keyCodes.ENTER,
Tab: _.keyCodes.TAB,
ArrowDown: _.keyCodes.DOWN,
ArrowRight: _.keyCodes.RIGHT,
ArrowUp: _.keyCodes.UP,
ArrowLeft: _.keyCodes.LEFT,
};
const PRINTABLE_SPECIAL_KEYS = new Set(['Enter', 'Process', 'Spacebar', 'Space', 'Dead']);
/**
*
*/
@ -29,10 +42,12 @@ export default class BlockEvents extends Module {
return;
}
const keyCode = this.getKeyCode(event);
/**
* Fire keydown processor by event.keyCode
* Fire keydown processor by normalized keyboard code
*/
switch (event.keyCode) {
switch (keyCode) {
case _.keyCodes.BACKSPACE:
this.backspace(event);
break;
@ -87,7 +102,7 @@ export default class BlockEvents extends Module {
*/
private handleSelectedBlocksDeletion(event: KeyboardEvent): boolean {
const { BlockSelection, BlockManager, Caret } = this.Editor;
const isRemoveKey = event.keyCode === _.keyCodes.BACKSPACE || event.keyCode === _.keyCodes.DELETE;
const isRemoveKey = event.key === 'Backspace' || event.key === 'Delete';
const selectionExists = SelectionUtils.isSelectionExists;
const selectionCollapsed = SelectionUtils.isCollapsed === true;
const shouldHandleSelectionDeletion = isRemoveKey &&
@ -133,7 +148,7 @@ export default class BlockEvents extends Module {
* - close Toolbar
* - clear block highlighting
*/
if (!_.isPrintableKey(event.keyCode)) {
if (!this.isPrintableKeyEvent(event)) {
return;
}
@ -601,8 +616,14 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keyboard event
*/
private arrowRightAndDown(event: KeyboardEvent): void {
const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) &&
(!event.shiftKey || event.keyCode === _.keyCodes.TAB);
const keyCode = this.getKeyCode(event);
if (keyCode === null) {
return;
}
const isFlipperCombination = Flipper.usedKeys.includes(keyCode) &&
(!event.shiftKey || keyCode === _.keyCodes.TAB);
/**
* Arrows might be handled on toolbars by flipper
@ -624,7 +645,7 @@ export default class BlockEvents extends Module {
const caretAtEnd = currentBlock?.currentInput !== undefined ? caretUtils.isCaretAtEndOfInput(currentBlock.currentInput) : undefined;
const shouldEnableCBS = caretAtEnd || this.Editor.BlockSelection.anyBlockSelected;
const isShiftDownKey = event.shiftKey && event.keyCode === _.keyCodes.DOWN;
const isShiftDownKey = event.shiftKey && keyCode === _.keyCodes.DOWN;
if (isShiftDownKey && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState();
@ -636,7 +657,7 @@ export default class BlockEvents extends Module {
void this.Editor.InlineToolbar.tryToShow();
}
const navigateNext = event.keyCode === _.keyCodes.DOWN || (event.keyCode === _.keyCodes.RIGHT && !this.isRtl);
const navigateNext = keyCode === _.keyCodes.DOWN || (keyCode === _.keyCodes.RIGHT && !this.isRtl);
const isNavigated = navigateNext ? this.Editor.Caret.navigateNext() : this.Editor.Caret.navigatePrevious();
if (isNavigated) {
@ -677,7 +698,13 @@ export default class BlockEvents extends Module {
*/
const toolbarOpened = this.Editor.UI.someToolbarOpened;
if (toolbarOpened && Flipper.usedKeys.includes(event.keyCode) && (!event.shiftKey || event.keyCode === _.keyCodes.TAB)) {
const keyCode = this.getKeyCode(event);
if (keyCode === null) {
return;
}
if (toolbarOpened && Flipper.usedKeys.includes(keyCode) && (!event.shiftKey || keyCode === _.keyCodes.TAB)) {
return;
}
@ -696,7 +723,7 @@ export default class BlockEvents extends Module {
const caretAtStart = currentBlock?.currentInput !== undefined ? caretUtils.isCaretAtStartOfInput(currentBlock.currentInput) : undefined;
const shouldEnableCBS = caretAtStart || this.Editor.BlockSelection.anyBlockSelected;
const isShiftUpKey = event.shiftKey && event.keyCode === _.keyCodes.UP;
const isShiftUpKey = event.shiftKey && keyCode === _.keyCodes.UP;
if (isShiftUpKey && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);
@ -708,7 +735,7 @@ export default class BlockEvents extends Module {
void this.Editor.InlineToolbar.tryToShow();
}
const navigatePrevious = event.keyCode === _.keyCodes.UP || (event.keyCode === _.keyCodes.LEFT && !this.isRtl);
const navigatePrevious = keyCode === _.keyCodes.UP || (keyCode === _.keyCodes.LEFT && !this.isRtl);
const isNavigated = navigatePrevious ? this.Editor.Caret.navigatePrevious() : this.Editor.Caret.navigateNext();
if (isNavigated) {
@ -743,10 +770,13 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keyboard event
*/
private needToolbarClosing(event: KeyboardEvent): boolean {
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened);
const blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened);
const inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened);
const flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
const keyCode = this.getKeyCode(event);
const isEnter = keyCode === _.keyCodes.ENTER;
const isTab = keyCode === _.keyCodes.TAB;
const toolboxItemSelected = (isEnter && this.Editor.Toolbar.toolbox.opened);
const blockSettingsItemSelected = (isEnter && this.Editor.BlockSettings.opened);
const inlineToolbarItemSelected = (isEnter && this.Editor.InlineToolbar.opened);
const flippingToolbarItems = isTab;
/**
* Do not close Toolbar in cases:
@ -798,4 +828,38 @@ export default class BlockEvents extends Module {
});
}
}
/**
* Convert KeyboardEvent.key or code to the legacy numeric keyCode
*
* @param event - keyboard event
*/
private getKeyCode(event: KeyboardEvent): number | null {
const keyFromEvent = event.key && KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP[event.key];
if (keyFromEvent !== undefined && typeof keyFromEvent === 'number') {
return keyFromEvent;
}
const codeFromEvent = event.code && KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP[event.code];
if (codeFromEvent !== undefined && typeof codeFromEvent === 'number') {
return codeFromEvent;
}
return null;
}
/**
* Detect whether KeyDown should be treated as printable input
*
* @param event - keyboard event
*/
private isPrintableKeyEvent(event: KeyboardEvent): boolean {
if (!event.key) {
return false;
}
return event.key.length === 1 || PRINTABLE_SPECIAL_KEYS.has(event.key);
}
}

View file

@ -865,21 +865,6 @@ export default class BlockManager extends Module {
return this.blocks.find((block) => block.holder === firstLevelBlock);
}
/**
* Swap Blocks Position
*
* @param {number} fromIndex - index of first block
* @param {number} toIndex - index of second block
* @deprecated use 'move' instead
*/
public swap(fromIndex: number, toIndex: number): void {
/** Move up current Block */
this.blocksStore.swap(fromIndex, toIndex);
/** Now actual block moved up so that current block index decreased */
this.currentBlockIndex = toIndex;
}
/**
* Move a block to a new index
*

View file

@ -238,7 +238,8 @@ export default class BlockSelection extends Module {
this.readyToBlockSelection = false;
const isKeyboard = reason && (reason instanceof KeyboardEvent);
const isPrintableKey = isKeyboard && _.isPrintableKey((reason as KeyboardEvent).keyCode);
const keyboardEvent = reason as KeyboardEvent;
const isPrintableKey = isKeyboard && keyboardEvent.key && keyboardEvent.key.length === 1;
/**
* If reason caused clear of the selection was printable key and any block is selected,

View file

@ -233,36 +233,6 @@ describe('Blocks', () => {
});
});
describe('swap', () => {
it('should swap two blocks', () => {
const blocks = createBlocks();
const block1 = createMockBlock('block-1');
const block2 = createMockBlock('block-2');
blocks.push(block1);
blocks.push(block2);
blocks.swap(0, 1);
expect(blocks.blocks[0]).toBe(block2);
expect(blocks.blocks[1]).toBe(block1);
});
it('should swap blocks in DOM', () => {
const blocks = createBlocks();
const block1 = createMockBlock('block-1');
const block2 = createMockBlock('block-2');
blocks.push(block1);
blocks.push(block2);
blocks.swap(0, 1);
expect(workingArea.children[0]).toBe(block2.holder);
expect(workingArea.children[1]).toBe(block1.holder);
});
});
describe('move', () => {
it('should move block from one index to another', () => {
const blocks = createBlocks();

View file

@ -63,21 +63,6 @@ describe('Dom helper utilities', () => {
expect(Array.from(parent.children)).toEqual([second, first, initial]);
});
it('swaps elements in place via swap()', () => {
const parent = document.createElement('div');
const first = document.createElement('span');
const second = document.createElement('span');
first.id = 'first';
second.id = 'second';
parent.append(first, second);
Dom.swap(first, second);
expect(Array.from(parent.children).map((el) => el.id)).toEqual(['second', 'first']);
});
});
describe('selectors', () => {

View file

@ -0,0 +1,186 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import BoldInlineTool from '../../../../src/components/inline-tools/inline-tool-bold';
import SelectionUtils from '../../../../src/components/selection';
import type { PopoverItemDefaultBaseParams } from '../../../../types/utils/popover';
type BoldInlineToolInternals = {
shortcutListenerRegistered: boolean;
selectionListenerRegistered: boolean;
inputListenerRegistered: boolean;
instances: Set<unknown>;
handleShortcut: EventListener;
handleGlobalSelectionChange: EventListener;
handleGlobalInput: EventListener;
};
const getInternals = (): BoldInlineToolInternals => {
return BoldInlineTool as unknown as BoldInlineToolInternals;
};
const clearSelection = (): void => {
const selection = window.getSelection();
selection?.removeAllRanges();
};
const resetBoldInlineTool = (): void => {
const internals = getInternals();
document.removeEventListener('keydown', internals.handleShortcut, true);
document.removeEventListener('selectionchange', internals.handleGlobalSelectionChange, true);
document.removeEventListener('input', internals.handleGlobalInput, true);
internals.shortcutListenerRegistered = false;
internals.selectionListenerRegistered = false;
internals.inputListenerRegistered = false;
internals.instances.clear();
};
const setupEditor = (html: string): { block: HTMLElement } => {
const wrapper = document.createElement('div');
wrapper.className = SelectionUtils.CSS.editorWrapper;
const block = document.createElement('div');
block.dataset.blockTool = 'paragraph';
block.contentEditable = 'true';
block.innerHTML = html;
wrapper.appendChild(block);
const toolbar = document.createElement('div');
toolbar.dataset.cy = 'inline-toolbar';
const button = document.createElement('button');
button.dataset.itemName = 'bold';
toolbar.appendChild(button);
wrapper.appendChild(toolbar);
document.body.appendChild(wrapper);
return { block };
};
const setRange = (node: Text, start: number, end?: number): void => {
const selection = window.getSelection();
const range = document.createRange();
range.setStart(node, start);
if (typeof end === 'number') {
range.setEnd(node, end);
} else {
range.collapse(true);
}
selection?.removeAllRanges();
selection?.addRange(range);
};
describe('BoldInlineTool', () => {
beforeEach(() => {
document.body.innerHTML = '';
clearSelection();
resetBoldInlineTool();
});
afterEach(() => {
document.body.innerHTML = '';
clearSelection();
resetBoldInlineTool();
});
it('describes the tool metadata', () => {
expect(BoldInlineTool.isInline).toBe(true);
expect(BoldInlineTool.title).toBe('Bold');
expect(BoldInlineTool.sanitize).toEqual({
strong: {},
b: {},
});
const tool = new BoldInlineTool();
expect(tool.shortcut).toBe('CMD+B');
});
it('wraps selected text and reports bold state', () => {
const { block } = setupEditor('Hello world');
const textNode = block.firstChild as Text;
setRange(textNode, 0, 5);
const tool = new BoldInlineTool();
const menu = tool.render() as PopoverItemDefaultBaseParams;
menu.onActivate(menu);
const strong = block.querySelector('strong');
expect(strong?.textContent).toBe('Hello');
expect(typeof menu.isActive === 'function' ? menu.isActive() : menu.isActive).toBe(true);
});
it('unwraps existing bold text when toggled again', () => {
const { block } = setupEditor('<strong>Hello</strong> world');
const strong = block.querySelector('strong');
expect(strong).not.toBeNull();
const textNode = strong?.firstChild as Text;
setRange(textNode, 0, textNode.textContent?.length ?? 0);
const tool = new BoldInlineTool();
const menu = tool.render() as PopoverItemDefaultBaseParams;
menu.onActivate(menu);
expect(block.querySelector('strong')).toBeNull();
expect(typeof menu.isActive === 'function' ? menu.isActive() : menu.isActive).toBe(false);
});
it('starts a collapsed bold segment when caret is not inside bold', () => {
const { block } = setupEditor('Hello');
const textNode = block.firstChild as Text;
setRange(textNode, 0);
const tool = new BoldInlineTool();
const menu = tool.render() as PopoverItemDefaultBaseParams;
menu.onActivate(menu);
const strong = block.querySelector('strong');
expect(strong).not.toBeNull();
expect(strong?.getAttribute('data-bold-collapsed-active')).toBe('true');
expect(typeof menu.isActive === 'function' ? menu.isActive() : menu.isActive).toBe(true);
expect(window.getSelection()?.anchorNode).toBe(strong?.firstChild ?? null);
});
it('exits collapsed bold when caret is inside bold content', () => {
const { block } = setupEditor('<strong>BOLD</strong> text');
const strong = block.querySelector('strong');
expect(strong).not.toBeNull();
const textNode = strong?.firstChild as Text;
const length = textNode.textContent?.length ?? 0;
setRange(textNode, length);
const tool = new BoldInlineTool();
const menu = tool.render() as PopoverItemDefaultBaseParams;
menu.onActivate(menu);
const strongAfter = block.querySelector('strong');
expect(strongAfter?.getAttribute('data-bold-collapsed-length')).toBe(length.toString());
expect(strongAfter?.getAttribute('data-bold-collapsed-active')).toBeNull();
expect(typeof menu.isActive === 'function' ? menu.isActive() : menu.isActive).toBe(false);
expect(window.getSelection()?.anchorNode?.parentNode).toBe(block);
});
});

View file

@ -69,7 +69,6 @@ type BlockManagerMock = {
getBlockById: ReturnType<typeof vi.fn>;
getBlockIndex: ReturnType<typeof vi.fn>;
getBlock: ReturnType<typeof vi.fn>;
swap: ReturnType<typeof vi.fn>;
move: ReturnType<typeof vi.fn>;
removeBlock: ReturnType<typeof vi.fn>;
insert: ReturnType<typeof vi.fn>;
@ -97,7 +96,6 @@ const createBlockManagerMock = (initialBlocks: BlockStub[] = [ createBlockStub()
getBlock: vi.fn((element: HTMLElement) => {
return blockManager.blocks.find((block) => block.holder === element);
}) as ReturnType<typeof vi.fn>,
swap: vi.fn() as ReturnType<typeof vi.fn>,
move: vi.fn() as ReturnType<typeof vi.fn>,
removeBlock: vi.fn((block?: BlockStub) => {
if (!block) {
@ -437,22 +435,6 @@ describe('BlocksAPI', () => {
});
describe('block ordering', () => {
it('delegates swap to BlockManager and logs deprecation notice', () => {
const { blocksApi, blockManager } = createBlocksApi();
const logSpy = vi.spyOn(utils, 'log').mockImplementation(() => {});
blocksApi.swap(1, 2);
expect(blockManager.swap).toHaveBeenCalledWith(1, 2);
expect(logSpy).toHaveBeenCalledWith(
'`blocks.swap()` method is deprecated and will be removed in the next major release. ' +
'Use `block.move()` method instead',
'info'
);
logSpy.mockRestore();
});
it('delegates move to BlockManager', () => {
const { blocksApi, blockManager } = createBlocksApi();

View file

@ -16,6 +16,18 @@ import * as caretUtils from '../../../../src/components/utils/caret';
import * as blocksUtils from '../../../../src/components/utils/blocks';
import { keyCodes } from '../../../../src/components/utils';
const KEY_CODE_TO_KEY_MAP: Record<number, string> = {
[keyCodes.BACKSPACE]: 'Backspace',
[keyCodes.DELETE]: 'Delete',
[keyCodes.DOWN]: 'ArrowDown',
[keyCodes.ENTER]: 'Enter',
[keyCodes.LEFT]: 'ArrowLeft',
[keyCodes.RIGHT]: 'ArrowRight',
[keyCodes.SLASH]: '/',
[keyCodes.TAB]: 'Tab',
[keyCodes.UP]: 'ArrowUp',
};
const createBlockEvents = (overrides: Partial<EditorModules> = {}): BlockEvents => {
const blockEvents = new BlockEvents({
config: {} as EditorConfig,
@ -109,10 +121,22 @@ const createBlockEvents = (overrides: Partial<EditorModules> = {}): BlockEvents
};
const createKeyboardEvent = (options: Partial<KeyboardEvent>): KeyboardEvent => {
let derivedKey: string;
if (options.key !== undefined) {
derivedKey = options.key;
} else if (options.keyCode !== undefined) {
derivedKey = KEY_CODE_TO_KEY_MAP[options.keyCode] ?? String.fromCharCode(options.keyCode);
} else {
derivedKey = '';
}
const derivedCode = options.code !== undefined ? options.code : derivedKey;
const derivedKeyCode = options.keyCode ?? 0;
return {
keyCode: 0,
key: '',
code: '',
keyCode: derivedKeyCode,
key: derivedKey,
code: derivedCode,
ctrlKey: false,
metaKey: false,
altKey: false,
@ -1113,4 +1137,3 @@ describe('BlockEvents', () => {
});
});
});

View file

@ -5,8 +5,9 @@ import BlockManager from '../../../../src/components/modules/blockManager';
import EventsDispatcher from '../../../../src/components/utils/events';
import type { EditorConfig } from '../../../../types';
import type { EditorModules } from '../../../../src/types-internal/editor-modules';
import { BlockChanged } from '../../../../src/components/events';
import type { EditorEventMap } from '../../../../src/components/events';
import type Block from '../../../../src/components/block';
import Block from '../../../../src/components/block';
import { BlockAddedMutationType } from '../../../../types/events/block/BlockAdded';
import { BlockRemovedMutationType } from '../../../../types/events/block/BlockRemoved';
import { BlockMovedMutationType } from '../../../../types/events/block/BlockMoved';
@ -40,6 +41,8 @@ const createBlockStub = (options: {
tunes?: Record<string, unknown>;
} = {}): Block => {
const holder = document.createElement('div');
holder.classList.add(Block.CSS.wrapper);
const inputs = [ document.createElement('div') ];
const data = options.data ?? {};
@ -392,4 +395,221 @@ describe('BlockManager', () => {
expect(blocksStub.replace).not.toHaveBeenCalled();
expect(blockDidMutatedSpy).not.toHaveBeenCalled();
});
it('inserts default block at provided index and shifts current index when not focusing it', () => {
const existingBlock = createBlockStub({ id: 'existing' });
const { blockManager, blocksStub } = createBlockManager({
initialBlocks: [ existingBlock ],
});
blockManager.currentBlockIndex = 0;
const defaultBlock = createBlockStub({ id: 'default' });
const composeBlockSpy = getComposeBlockSpy(blockManager).mockReturnValue(defaultBlock);
const blockDidMutatedSpy = getBlockDidMutatedSpy(blockManager);
const result = blockManager.insertDefaultBlockAtIndex(0);
expect(result).toBe(defaultBlock);
expect(blocksStub.blocks[0]).toBe(defaultBlock);
expect(blockManager.currentBlockIndex).toBe(1);
expect(composeBlockSpy).toHaveBeenCalledWith({ tool: 'paragraph' });
expect(blockDidMutatedSpy).toHaveBeenCalledWith(
BlockAddedMutationType,
defaultBlock,
expect.objectContaining({ index: 0 })
);
});
it('focuses inserted default block when needToFocus is true', () => {
const { blockManager, blocksStub } = createBlockManager({
initialBlocks: [createBlockStub({ id: 'first' }), createBlockStub({ id: 'second' })],
});
blockManager.currentBlockIndex = 1;
const defaultBlock = createBlockStub({ id: 'focus-me' });
getComposeBlockSpy(blockManager).mockReturnValue(defaultBlock);
blockManager.insertDefaultBlockAtIndex(1, true);
expect(blocksStub.blocks[1]).toBe(defaultBlock);
expect(blockManager.currentBlockIndex).toBe(1);
});
it('throws a descriptive error when default block tool is missing', () => {
const { blockManager } = createBlockManager();
(blockManager as unknown as { config: EditorConfig }).config.defaultBlock = undefined;
expect(() => blockManager.insertDefaultBlockAtIndex(0)).toThrow('Could not insert default Block. Default block tool is not defined in the configuration.');
});
it('removes only selected blocks and returns the first removed index', () => {
const blocks = [
createBlockStub({ id: 'first' }),
createBlockStub({ id: 'second' }),
createBlockStub({ id: 'third' }),
];
blocks[1].selected = true;
blocks[2].selected = true;
const { blockManager } = createBlockManager({
initialBlocks: blocks,
});
const removeSpy = vi
.spyOn(blockManager as unknown as { removeBlock: BlockManager['removeBlock'] }, 'removeBlock')
.mockResolvedValue();
const firstRemovedIndex = blockManager.removeSelectedBlocks();
expect(removeSpy).toHaveBeenNthCalledWith(1, blocks[2], false);
expect(removeSpy).toHaveBeenNthCalledWith(2, blocks[1], false);
expect(firstRemovedIndex).toBe(1);
});
it('splits the current block using caret fragment contents', () => {
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('Split content'));
const caretStub = {
extractFragmentFromCaretPosition: vi.fn().mockReturnValue(fragment),
} as unknown as EditorModules['Caret'];
const { blockManager } = createBlockManager({
initialBlocks: [ createBlockStub({ id: 'origin' }) ],
editorOverrides: {
Caret: caretStub,
},
});
const insertedBlock = createBlockStub({ id: 'split' });
const insertSpy = vi.spyOn(blockManager as unknown as { insert: BlockManager['insert'] }, 'insert').mockReturnValue(insertedBlock);
const result = blockManager.split();
expect(caretStub.extractFragmentFromCaretPosition).toHaveBeenCalledTimes(1);
expect(insertSpy).toHaveBeenCalledWith({ data: { text: 'Split content' } });
expect(result).toBe(insertedBlock);
});
it('converts a block using the target tool conversion config', async () => {
const blockToConvert = createBlockStub({ id: 'paragraph',
name: 'paragraph' });
(blockToConvert.save as Mock).mockResolvedValue({ data: { text: 'Old' } });
(blockToConvert.exportDataAsString as Mock).mockResolvedValue('<p>Converted</p>');
const replacingTool = {
name: 'header',
sanitizeConfig: { p: true },
conversionConfig: {
import: (text: string, settings?: { level?: number }) => ({
text: text.toUpperCase(),
level: settings?.level ?? 1,
}),
},
settings: { level: 2 },
};
const { blockManager } = createBlockManager({
initialBlocks: [ blockToConvert ],
editorOverrides: {
Tools: {
blockTools: new Map([ [replacingTool.name, replacingTool] ]),
} as unknown as EditorModules['Tools'],
},
});
const replacedBlock = createBlockStub({ id: 'header',
name: 'header' });
const replaceSpy = vi.spyOn(blockManager as unknown as { replace: BlockManager['replace'] }, 'replace').mockReturnValue(replacedBlock);
const result = await blockManager.convert(blockToConvert, 'header', { level: 4 });
expect(replaceSpy).toHaveBeenCalledWith(blockToConvert, 'header', {
text: '<P>CONVERTED</P>',
level: 4,
});
expect(result).toBe(replacedBlock);
});
it('sets current block by a child node that belongs to the current editor instance', () => {
const blocks = [
createBlockStub({ id: 'first' }),
createBlockStub({ id: 'second' }),
];
const { blockManager } = createBlockManager({
initialBlocks: blocks,
});
const ui = (blockManager as unknown as { Editor: EditorModules }).Editor.UI;
ui.nodes.wrapper.classList.add(ui.CSS.editorWrapper);
ui.nodes.wrapper.appendChild(blocks[0].holder);
ui.nodes.wrapper.appendChild(blocks[1].holder);
document.body.appendChild(ui.nodes.wrapper);
const childNode = document.createElement('span');
blocks[1].holder.appendChild(childNode);
const current = blockManager.setCurrentBlockByChildNode(childNode);
expect(current).toBe(blocks[1]);
expect(blockManager.currentBlockIndex).toBe(1);
expect(blocks[1].updateCurrentInput).toHaveBeenCalledTimes(1);
const alienWrapper = document.createElement('div');
alienWrapper.classList.add(ui.CSS.editorWrapper);
const alienBlock = createBlockStub({ id: 'alien' });
alienWrapper.appendChild(alienBlock.holder);
const alienChild = document.createElement('span');
alienBlock.holder.appendChild(alienChild);
expect(blockManager.setCurrentBlockByChildNode(alienChild)).toBeUndefined();
ui.nodes.wrapper.remove();
});
it('emits enumerable events when blockDidMutated is invoked', () => {
const block = createBlockStub({ id: 'block-1' });
const { blockManager } = createBlockManager({
initialBlocks: [ block ],
});
const emitSpy = vi.spyOn(
(blockManager as unknown as { eventsDispatcher: EventsDispatcher<EditorEventMap> }).eventsDispatcher,
'emit'
);
const detail = { index: 0 };
const result = (blockManager as unknown as { blockDidMutated: BlockDidMutated }).blockDidMutated(
BlockAddedMutationType,
block,
detail
);
expect(result).toBe(block);
expect(emitSpy).toHaveBeenCalledTimes(1);
const [eventName, payload] = emitSpy.mock.calls[0] as [string, { event: CustomEvent }];
expect(eventName).toBe(BlockChanged);
expect(payload.event).toBeInstanceOf(CustomEvent);
expect(payload.event.type).toBe(BlockAddedMutationType);
expect(payload.event.detail).toEqual(expect.objectContaining(detail));
expect(payload.event.detail.target).toEqual(expect.objectContaining({
id: block.id,
name: block.name,
}));
expect(Object.prototype.propertyIsEnumerable.call(payload.event, 'type')).toBe(true);
expect(Object.prototype.propertyIsEnumerable.call(payload.event, 'detail')).toBe(true);
});
});

View file

@ -34,14 +34,6 @@ export interface Blocks {
*/
delete(index?: number): void;
/**
* Swaps two Blocks
* @param {number} fromIndex - block to swap
* @param {number} toIndex - block to swap with
* @deprecated use 'move' instead
*/
swap(fromIndex: number, toIndex: number): void;
/**
* Moves a block to a new index
* @param {number} toIndex - index where the block is moved to

View file

@ -68,18 +68,6 @@ export interface ExternalToolSettings<Config extends object = any> {
toolbox?: ToolboxConfig | false;
}
/**
* Tool's tunes configuration.
* @deprecated use {@link MenuConfig} type instead
*/
export type TunesMenuConfig = MenuConfig;
/**
* Single Tunes Menu Config item
* @deprecated use {@link MenuConfigItem} type instead
*/
export type TunesMenuConfigItem = MenuConfigItem;
/**
* For internal Tools 'class' property is optional
*/