feat(writekit): rename @robonen/editor to @robonen/writekit
Rename the rich-text editor package and all Editor* exports to Writekit*; remove the old vue/editor tree.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { render } from 'vitest-browser-vue';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
import { createDoc, createNode, textSelection } from '../../model';
|
||||
import { createDefaultRegistry } from '../../preset';
|
||||
import { createTransaction, createWritekit, createWritekitState } from '../../state';
|
||||
import WritekitRoot from '../WritekitRoot.vue';
|
||||
|
||||
function para(id: string, text: string) {
|
||||
return createNode('paragraph', { id, content: text ? [{ text, marks: [] }] : [] });
|
||||
}
|
||||
|
||||
function mount(blocks: Array<ReturnType<typeof para>>) {
|
||||
const registry = createDefaultRegistry();
|
||||
const writekit = createWritekit({ state: createWritekitState({ registry, doc: createDoc(blocks) }) });
|
||||
render(WritekitRoot, { props: { writekit, platform: 'mac' } });
|
||||
return writekit;
|
||||
}
|
||||
|
||||
function selectNative(anchor: { node: Node; offset: number }, focus: { node: Node; offset: number }) {
|
||||
const sel = getSelection()!;
|
||||
sel.removeAllRanges();
|
||||
const range = document.createRange();
|
||||
range.setStart(anchor.node, anchor.offset);
|
||||
range.setEnd(focus.node, focus.offset);
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
describe('WritekitRoot (single contenteditable)', () => {
|
||||
it('renders ONE editable root containing non-editable block elements', async () => {
|
||||
mount([para('a', 'hello')]);
|
||||
await nextTick();
|
||||
|
||||
const ce = document.querySelector('[data-writekit-content]')!;
|
||||
expect(ce.getAttribute('contenteditable')).toBe('true');
|
||||
|
||||
const host = document.querySelector('[data-block-content]') as HTMLElement;
|
||||
expect(host.textContent).toBe('hello');
|
||||
// The block element itself is NOT a separate editing host.
|
||||
expect(host.getAttribute('contenteditable')).toBeNull();
|
||||
});
|
||||
|
||||
it('maps a cross-block native selection to a cross-block model range', async () => {
|
||||
const writekit = mount([para('a', 'hello'), para('b', 'world')]);
|
||||
await nextTick();
|
||||
|
||||
const hosts = document.querySelectorAll('[data-block-content]');
|
||||
const aText = hosts[0]!.firstChild!; // text node "hello"
|
||||
const bText = hosts[1]!.firstChild!; // text node "world"
|
||||
|
||||
selectNative({ node: aText, offset: 1 }, { node: bText, offset: 3 });
|
||||
|
||||
// `selectionchange` is dispatched on a macrotask, so awaiting microtasks
|
||||
// (nextTick) isn't enough — poll until the writekit has synced the model.
|
||||
const sel = await vi.waitFor(() => {
|
||||
const s = writekit.state.selection;
|
||||
if (s.kind !== 'text' || s.anchor.offset !== 1)
|
||||
throw new Error('selection not synced yet');
|
||||
return s;
|
||||
});
|
||||
|
||||
expect(sel.anchor.blockId).toBe('a');
|
||||
expect(sel.anchor.offset).toBe(1);
|
||||
expect(sel.focus.blockId).toBe('b');
|
||||
expect(sel.focus.offset).toBe(3);
|
||||
});
|
||||
|
||||
it('writes a cross-block model selection back to a native range spanning blocks', async () => {
|
||||
const writekit = mount([para('a', 'hello'), para('b', 'world')]);
|
||||
await nextTick();
|
||||
|
||||
writekit.dispatch(createTransaction(writekit.state).setSelection(
|
||||
textSelection({ blockId: 'a', offset: 2 }, { blockId: 'b', offset: 4 }),
|
||||
));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
const sel = getSelection()!;
|
||||
const hosts = document.querySelectorAll('[data-block-content]');
|
||||
expect(hosts[0]!.contains(sel.anchorNode)).toBe(true);
|
||||
expect(hosts[1]!.contains(sel.focusNode)).toBe(true);
|
||||
expect(sel.isCollapsed).toBe(false);
|
||||
});
|
||||
|
||||
it('applies bold via Mod-b to a selected range', async () => {
|
||||
const writekit = mount([para('a', 'hello')]);
|
||||
await nextTick();
|
||||
|
||||
writekit.dispatch(createTransaction(writekit.state).setSelection(
|
||||
textSelection({ blockId: 'a', offset: 0 }, { blockId: 'a', offset: 5 }),
|
||||
));
|
||||
await nextTick();
|
||||
|
||||
const root = document.querySelector<HTMLElement>('[data-writekit-root]')!;
|
||||
root.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', metaKey: true, bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(document.querySelector('[data-block-content] strong')?.textContent).toBe('hello');
|
||||
});
|
||||
|
||||
it('splits a block on Enter', async () => {
|
||||
const writekit = mount([para('a', 'hello')]);
|
||||
await nextTick();
|
||||
|
||||
writekit.dispatch(createTransaction(writekit.state).setSelection(textSelection({ blockId: 'a', offset: 2 })));
|
||||
await nextTick();
|
||||
|
||||
const root = document.querySelector<HTMLElement>('[data-writekit-root]')!;
|
||||
root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
const hosts = document.querySelectorAll('[data-block-content]');
|
||||
expect(hosts.length).toBe(2);
|
||||
expect(hosts[0]!.textContent).toBe('he');
|
||||
expect(hosts[1]!.textContent).toBe('llo');
|
||||
});
|
||||
|
||||
it('merges into the previous block on Backspace at block start', async () => {
|
||||
const writekit = mount([para('a', 'foo'), para('b', 'bar')]);
|
||||
await nextTick();
|
||||
|
||||
writekit.dispatch(createTransaction(writekit.state).setSelection(textSelection({ blockId: 'b', offset: 0 })));
|
||||
await nextTick();
|
||||
|
||||
const root = document.querySelector<HTMLElement>('[data-writekit-root]')!;
|
||||
root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
const hosts = document.querySelectorAll('[data-block-content]');
|
||||
expect(hosts.length).toBe(1);
|
||||
expect(hosts[0]!.textContent).toBe('foobar');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user