mirror of
https://github.com/codex-team/editor.js
synced 2026-03-15 15:15:47 +01:00
fix: partially replace deprecated APIs with the modern ones
This commit is contained in:
parent
5d5f19a61c
commit
408b160e39
19 changed files with 523 additions and 195 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
186
test/unit/components/inline-tools/inline-tool-bold.test.ts
Normal file
186
test/unit/components/inline-tools/inline-tool-bold.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
8
types/api/blocks.d.ts
vendored
8
types/api/blocks.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
12
types/tools/tool-settings.d.ts
vendored
12
types/tools/tool-settings.d.ts
vendored
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue