mirror of
https://github.com/codex-team/editor.js
synced 2026-03-18 00:19:53 +01:00
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|