Skip to content

Commit

Permalink
Add new module to handle ui node interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Nov 3, 2023
1 parent bc35035 commit dc303a9
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 21 deletions.
11 changes: 1 addition & 10 deletions assets/core.styl
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ resets(arr)
li
list-style-type: none
padding-left: LIST_STYLE_OUTER_WIDTH
position: relative

> .ql-ui:before
display: inline-block
Expand All @@ -80,12 +81,6 @@ resets(arr)
white-space: nowrap
width: LIST_STYLE_WIDTH

@supports (display: contents)
li[data-list=bullet],
li[data-list=ordered]
> .ql-ui
display: contents

li[data-list=checked],
li[data-list=unchecked]
> .ql-ui
Expand Down Expand Up @@ -210,10 +205,6 @@ resets(arr)
.ql-ui
position: absolute

li
> .ql-ui
position: static;

.ql-editor.ql-blank::before
color: rgba(0,0,0,0.6)
content: attr(data-placeholder)
Expand Down
2 changes: 2 additions & 0 deletions core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Keyboard from './modules/keyboard';
import Uploader from './modules/uploader';
import Delta, { Op, OpIterator, AttributeMap } from 'quill-delta';
import Input from './modules/input';
import UINode from './modules/uiNode';

export { Delta, Op, OpIterator, AttributeMap };

Expand All @@ -34,6 +35,7 @@ Quill.register({
'modules/keyboard': Keyboard,
'modules/uploader': Uploader,
'modules/input': Input,
'modules/uiNode': UINode,
});

export default Quill;
1 change: 1 addition & 0 deletions core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class Quill {
this.history = this.theme.addModule('history');
this.uploader = this.theme.addModule('uploader');
this.theme.addModule('input');
this.theme.addModule('uiNode');
this.theme.init();
this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {
if (type === Emitter.events.TEXT_CHANGE) {
Expand Down
78 changes: 67 additions & 11 deletions e2e/list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,78 @@ import { expect } from '@playwright/test';
import { test } from './fixtures';
import { isMac } from './utils';

const listTypes = ['bullet', 'ordered', 'checked'];

test.describe('list', () => {
test.beforeEach(async ({ editorPage }) => {
await editorPage.open();
});

test('navigating with shortcuts', async ({ page, editorPage }) => {
await editorPage.setContents([
{ insert: 'item 1' },
{ insert: '\n', attributes: { list: 'bullet' } },
]);
for (const list of listTypes) {
test(`jump to line start (${list})`, async ({ page, editorPage }) => {
await editorPage.setContents([
{ insert: 'item 1' },
{ insert: '\n', attributes: { list } },
]);

await editorPage.moveCursorAfterText('item 1');
await page.keyboard.press(isMac ? `Meta+ArrowLeft` : 'Home');
expect(await editorPage.getSelection()).toEqual({ index: 0, length: 0 });
await editorPage.moveCursorAfterText('item 1');
await page.keyboard.press(isMac ? `Meta+ArrowLeft` : 'Home');
expect(await editorPage.getSelection()).toEqual({ index: 0, length: 0 });

await page.keyboard.press(isMac ? `Meta+ArrowRight` : 'End');
expect(await editorPage.getSelection()).toEqual({ index: 6, length: 0 });
});
await page.keyboard.type('start ');
expect(await editorPage.getContents()).toEqual([
{ insert: 'start item 1' },
{ insert: '\n', attributes: { list } },
]);
});

test.describe('navigation with left/right arrow keys', () => {
test(`move to previous/next line (${list})`, async ({
page,
editorPage,
}) => {
await editorPage.setContents([
{ insert: 'first line' },
{ insert: '\n', attributes: { list } },
{ insert: 'second line' },
{ insert: '\n', attributes: { list } },
]);

await editorPage.moveCursorTo('s_econd');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
expect(await editorPage.getSelection()).toEqual({
index: 'first line'.length,
length: 0,
});
await page.keyboard.press('ArrowRight');
await page.keyboard.press('ArrowRight');
expect(await editorPage.getSelection()).toEqual({
index: 'first line\ns'.length,
length: 0,
});
});

test(`extend selection to previous/next line (${list})`, async ({
page,
editorPage,
}) => {
await editorPage.setContents([
{ insert: 'first line' },
{ insert: '\n', attributes: { list } },
{ insert: 'second line' },
{ insert: '\n', attributes: { list } },
]);

await editorPage.moveCursorTo('s_econd');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.type('a');
expect(await editorPage.getContents()).toEqual([
{ insert: 'first lineaecond line' },
{ insert: '\n', attributes: { list } },
]);
});
});
}
});
98 changes: 98 additions & 0 deletions modules/uiNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { ParentBlot } from 'parchment';
import Module from '../core/module';
import Quill from '../core/quill';

const isMac = /Mac/i.test(navigator.platform);

const canMoveCaretBeforeUINode = (event: KeyboardEvent) => {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Home'
) {
return true;
}

if (isMac && event.key === 'a' && event.ctrlKey === true) {
return true;
}

return false;
};

class UINode extends Module {
isListening = false;
selectionChangeDeadline = 0;

constructor(quill: Quill, options: Record<string, never>) {
super(quill, options);

this.handleArrowKeys();
this.handleNavigationShortcuts();
}

private handleArrowKeys() {
this.quill.keyboard.addBinding({
key: 'ArrowLeft',
shiftKey: null,
handler(range, { line, offset, event }) {
if (offset === 0 && line instanceof ParentBlot && line.uiNode) {
this.quill.setSelection(
range.index - 1,
range.length + (event.shiftKey ? 1 : 0),
Quill.sources.USER,
);
return false;
}
return true;
},
});
}

private handleNavigationShortcuts() {
this.quill.root.addEventListener('keydown', (event) => {
if (!event.defaultPrevented && canMoveCaretBeforeUINode(event)) {
this.ensureListeningToSelectionChange();
}
});
}

private ensureListeningToSelectionChange() {
if (this.isListening) return;

this.isListening = true;
this.selectionChangeDeadline = Date.now() + 100;

const listener = () => {
this.isListening = false;

if (Date.now() <= this.selectionChangeDeadline) {
this.handleSelectionChange();
}
};

document.addEventListener('selectionchange', listener, {
once: true,
});
}

private handleSelectionChange() {
const selection = document.getSelection();
if (!selection) return;
const range = selection.getRangeAt(0);
if (range.collapsed !== true || range.startOffset !== 0) return;

const line = this.quill.scroll.find(range.startContainer);
if (!(line instanceof ParentBlot) || !line.uiNode) return;

const newRange = document.createRange();
newRange.setStartAfter(line.uiNode);
newRange.setEndAfter(line.uiNode);
selection.removeAllRanges();
selection.addRange(newRange);
}
}

export default UINode;
2 changes: 2 additions & 0 deletions test/unit/modules/toolbar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SizeClass } from '../../../formats/size';
import Bold from '../../../formats/bold';
import Link from '../../../formats/link';
import { AlignClass } from '../../../formats/align';
import UINode from '../../../modules/uiNode';

const createContainer = (html = '') => {
const container = document.body.appendChild(document.createElement('div'));
Expand Down Expand Up @@ -152,6 +153,7 @@ describe('Toolbar', () => {
'modules/history': History,
'modules/uploader': Uploader,
'modules/input': Input,
'modules/uiNode': UINode,
},
true,
);
Expand Down

0 comments on commit dc303a9

Please sign in to comment.