Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle text replacements explicitly #3807

Merged
merged 1 commit into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import History from './modules/history';
import Keyboard from './modules/keyboard';
import Uploader from './modules/uploader';
import Delta, { Op, OpIterator, AttributeMap } from 'quill-delta';
import Input from './modules/input';

export { Delta, Op, OpIterator, AttributeMap };

Expand All @@ -32,6 +33,7 @@ Quill.register({
'modules/history': History,
'modules/keyboard': Keyboard,
'modules/uploader': Uploader,
'modules/input': Input,
});

export default Quill;
44 changes: 44 additions & 0 deletions core/composition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Embed from '../blots/embed';
import Scroll from '../blots/scroll';
import Emitter from './emitter';

class Composition {
isComposing = false;

constructor(private scroll: Scroll, private emitter: Emitter) {
scroll.domNode.addEventListener('compositionstart', event => {
if (!this.isComposing) {
this.handleCompositionStart(event);
}
});

scroll.domNode.addEventListener('compositionend', event => {
if (this.isComposing) {
this.handleCompositionEnd(event);
}
});
}

private handleCompositionStart(event: CompositionEvent) {
const blot =
event.target instanceof Node
? this.scroll.find(event.target, true)
: null;

if (blot && !(blot instanceof Embed)) {
this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_START, event);
this.scroll.batchStart();
this.emitter.emit(Emitter.events.COMPOSITION_START, event);
this.isComposing = true;
}
}

private handleCompositionEnd(event: CompositionEvent) {
this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_END, event);
this.scroll.batchEnd();
this.emitter.emit(Emitter.events.COMPOSITION_END, event);
this.isComposing = false;
}
}

export default Composition;
4 changes: 4 additions & 0 deletions core/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class Emitter extends EventEmitter<string> {
SCROLL_EMBED_UPDATE: 'scroll-embed-update',
SELECTION_CHANGE: 'selection-change',
TEXT_CHANGE: 'text-change',
COMPOSITION_BEFORE_START: 'composition-before-start',
COMPOSITION_START: 'composition-start',
COMPOSITION_BEFORE_END: 'composition-before-end',
COMPOSITION_END: 'composition-end',
} as const;

static sources = {
Expand Down
13 changes: 12 additions & 1 deletion core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import instances from './instances';
import logger, { DebugLevel } from './logger';
import Module from './module';
import Selection, { Range } from './selection';
import Composition from './composition';
import Theme, { ThemeConstructor } from './theme';

const debug = logger('quill');
Expand Down Expand Up @@ -135,6 +136,7 @@ class Quill {
emitter: Emitter;
allowReadOnlyEdits: boolean;
editor: Editor;
composition: Composition;
selection: Selection;

theme: Theme;
Expand Down Expand Up @@ -172,11 +174,13 @@ class Quill {
});
this.editor = new Editor(this.scroll);
this.selection = new Selection(this.scroll, this.emitter);
this.composition = new Composition(this.scroll, this.emitter);
this.theme = new this.options.theme(this, this.options); // eslint-disable-line new-cap
this.keyboard = this.theme.addModule('keyboard');
this.clipboard = this.theme.addModule('clipboard');
this.history = this.theme.addModule('history');
this.uploader = this.theme.addModule('uploader');
this.theme.addModule('input');
this.theme.init();
this.emitter.on(Emitter.events.EDITOR_CHANGE, type => {
if (type === Emitter.events.TEXT_CHANGE) {
Expand Down Expand Up @@ -528,6 +532,12 @@ class Quill {
}

insertText(index: number, text: string, source: EmitterSource): Delta;
insertText(
index: number,
text: string,
formats: Record<string, unknown>,
source: EmitterSource,
): Delta;
insertText(
index: number,
text: string,
Expand All @@ -538,12 +548,13 @@ class Quill {
insertText(
index: number,
text: string,
name: string | EmitterSource,
name: string | Record<string, unknown> | EmitterSource,
value?: unknown,
source?: EmitterSource,
): Delta {
let formats;
// eslint-disable-next-line prefer-const
// @ts-expect-error
[index, , formats, source] = overload(index, 0, name, value, source);
return modify.call(
this,
Expand Down
8 changes: 3 additions & 5 deletions core/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Scroll from '../blots/scroll';

const debug = logger('quill:selection');

type NativeRange = ReturnType<Document['createRange']>;
type NativeRange = AbstractRange;

interface NormalizedRange {
start: {
Expand Down Expand Up @@ -95,12 +95,10 @@ class Selection {
}

handleComposition() {
this.root.addEventListener('compositionstart', () => {
this.emitter.on(Emitter.events.COMPOSITION_BEFORE_START, () => {
this.composing = true;
this.scroll.batchStart();
});
this.root.addEventListener('compositionend', () => {
this.scroll.batchEnd();
this.emitter.on(Emitter.events.COMPOSITION_END, () => {
this.composing = false;
if (this.cursor.parent) {
const range = this.cursor.restore();
Expand Down
12 changes: 12 additions & 0 deletions e2e/utils/fixtures.ts → e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
import { test as base } from '@playwright/test';
import EditorPage from '../pageobjects/EditorPage';

export const test = base.extend<{
editorPage: EditorPage;
clipboard: Clipboard;
}>({
editorPage: ({ page }, use) => {
use(new EditorPage(page));
},
});

export const CHAPTER = 'Chapter 1. Loomings.';
export const P1 =
'Call me Ishmael. Some years ago—never mind how long precisely-having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.';
Expand Down
39 changes: 19 additions & 20 deletions e2e/full.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { test, expect } from '@playwright/test';
import { expect } from '@playwright/test';
import { getSelectionInTextNode, SHORTKEY } from './utils';
import { CHAPTER, P1, P2 } from './utils/fixtures';
import QuillPage from './utils/QuillPage';
import { test, CHAPTER, P1, P2 } from './fixtures';

test('compose an epic', async ({ page }) => {
await page.goto('http://localhost:9000/standalone/full');
const quillPage = new QuillPage(page);
await page.waitForSelector('.ql-editor', { timeout: 10000 });
test('compose an epic', async ({ page, editorPage }) => {
await editorPage.open();
await expect(page).toHaveTitle('Full Editor - Quill Rich Text Editor');

await page.type('.ql-editor', 'The Whale');
expect(await quillPage.editorHTML()).toEqual('<p>The Whale</p>');
expect(await editorPage.root.innerHTML()).toEqual('<p>The Whale</p>');

await page.keyboard.press('Enter');
expect(await quillPage.editorHTML()).toEqual('<p>The Whale</p><p><br></p>');
expect(await editorPage.root.innerHTML()).toEqual(
'<p>The Whale</p><p><br></p>',
);

await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.type('.ql-editor', P1);
await page.keyboard.press('Enter');
await page.keyboard.press('Enter');
await page.type('.ql-editor', P2);
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p>The Whale</p>',
'<p><br></p>',
Expand All @@ -41,7 +40,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.press('Enter');
await page.type('.ql-editor', CHAPTER);
await page.keyboard.press('Enter');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p>The Whale</p>',
'<p><br></p>',
Expand All @@ -67,7 +66,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p>Whale</p>',
'<p><br></p>',
Expand All @@ -84,7 +83,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.press('Delete');
await page.keyboard.press('Delete');
await page.keyboard.press('Delete');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p><br></p>',
'<p><br></p>',
Expand All @@ -97,7 +96,7 @@ test('compose an epic', async ({ page }) => {
);

await page.keyboard.press('Delete');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p><br></p>',
`<p>${CHAPTER}</p>`,
Expand All @@ -110,7 +109,7 @@ test('compose an epic', async ({ page }) => {

await page.click('.ql-toolbar .ql-bold');
await page.click('.ql-toolbar .ql-italic');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p><strong><em><span class="ql-cursor">\uFEFF</span></em></strong></p>',
`<p>${CHAPTER}</p>`,
Expand All @@ -126,7 +125,7 @@ test('compose an epic', async ({ page }) => {
expect(italic).not.toBe(null);

await page.type('.ql-editor', 'Moby Dick');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p><strong><em>Moby Dick</em></strong></p>',
`<p>${CHAPTER}</p>`,
Expand Down Expand Up @@ -159,7 +158,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.up(SHORTKEY);
bold = await page.$('.ql-toolbar .ql-bold.ql-active');
expect(bold).not.toBe(null);
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<p><strong><em>Moby Dick</em></strong></p>',
`<p><strong>${CHAPTER}</strong></p>`,
Expand All @@ -173,7 +172,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowUp');
await page.click('.ql-toolbar .ql-header[value="1"]');
expect(await quillPage.editorHTML()).toEqual(
expect(await editorPage.root.innerHTML()).toEqual(
[
'<h1><strong><em>Moby Dick</em></strong></h1>',
`<p><strong>${CHAPTER}</strong></p>`,
Expand All @@ -198,7 +197,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.press('b');
await page.keyboard.up(SHORTKEY);
await page.type('.ql-editor', 'B');
expect(await quillPage.root.locator('p').nth(2).innerHTML()).toBe('ABA');
expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe('ABA');
await page.keyboard.down(SHORTKEY);
await page.keyboard.press('b');
await page.keyboard.up(SHORTKEY);
Expand All @@ -207,7 +206,7 @@ test('compose an epic', async ({ page }) => {
await page.keyboard.press('b');
await page.keyboard.up(SHORTKEY);
await page.type('.ql-editor', 'D');
expect(await quillPage.root.locator('p').nth(2).innerHTML()).toBe(
expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe(
'AB<strong>C</strong>DA',
);
const selection = await page.evaluate(getSelectionInTextNode);
Expand Down
Loading