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

Make Quill instances detachable and reconfigurable #4402

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 1 addition & 2 deletions packages/quill/src/core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { DebugLevel } from './logger.js';
import Module from './module.js';
import Selection, { Range } from './selection.js';
import type { Bounds } from './selection.js';
import { createSubscriber, getSubscriber } from './subscriber.js';
import { getSubscriber } from './subscriber.js';
import Composition from './composition.js';
import Theme from './theme.js';
import type { ThemeConstructor } from './theme.js';
Expand Down Expand Up @@ -230,7 +230,6 @@ class Quill {
instances.set(this.container, this);
this.root = this.addContainer('ql-editor');
this.root.classList.add('ql-blank');
createSubscriber(this.root);
this.emitter = new Emitter();
}
const scrollBlotName = Parchment.ScrollBlot.blotName;
Expand Down
23 changes: 9 additions & 14 deletions packages/quill/src/core/subscriber.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import logger from './logger.js';

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

/**
* Any object with a named constructor can request an event subscription.
*/
Expand All @@ -20,25 +24,16 @@ interface Subscription {

const subscribers = new WeakMap<object, Subscriber>();

/**
* Creates a Subscriber instance, and binds it to an object.
*/
export function createSubscriber(object: Source): Subscriber {
const subscriber = new Subscriber();
subscribers.set(object, subscriber);
return subscriber;
}

/**
* Gets the Subscriber instance bound to the given object.
* Throws an error if the binding does not exist.
* Creates a new one if the binding does not exist yet.
*/
export function getSubscriber(object: Source): Subscriber {
singintime marked this conversation as resolved.
Show resolved Hide resolved
const subscriber = subscribers.get(object);
let subscriber = subscribers.get(object);
if (!subscriber) {
throw new Error(
`Subscriber not found for object ${object.constructor.name}`,
);
debug.info(`Creating new Subscriber for ${object.constructor.name}`);
subscriber = new Subscriber();
subscribers.set(object, subscriber);
}
return subscriber;
}
Expand Down
2 changes: 0 additions & 2 deletions packages/quill/test/unit/__helpers__/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import ListItem, { ListContainer } from '../../../src/formats/list.js';
import Inline from '../../../src/blots/inline.js';
import Emitter from '../../../src/core/emitter.js';
import { normalizeHTML } from './utils.js';
import { createSubscriber } from '../../../src/core/subscriber.js';

export const createRegistry = (formats: unknown[] = []) => {
const registry = new Registry();
Expand Down Expand Up @@ -38,7 +37,6 @@ export const createScroll = (
const emitter = new Emitter();
const root = container.appendChild(document.createElement('div'));
root.innerHTML = normalizeHTML(html);
createSubscriber(root);
const scroll = new Scroll(registry, root, { emitter });
return scroll;
};
6 changes: 1 addition & 5 deletions packages/quill/test/unit/blots/scroll.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@ import { createRegistry } from '../__helpers__/factory.js';
import { normalizeHTML, sleep } from '../__helpers__/utils.js';
import Underline from '../../../src/formats/underline.js';
import Strike from '../../../src/formats/strike.js';
import {
createSubscriber,
getSubscriber,
} from '../../../src/core/subscriber.js';
import { getSubscriber } from '../../../src/core/subscriber.js';

const createScroll = (html: string) => {
const emitter = new Emitter();
const registry = createRegistry([Underline, Strike]);
const container = document.body.appendChild(document.createElement('div'));
container.innerHTML = normalizeHTML(html);
createSubscriber(container);
return new Scroll(registry, container, { emitter });
};

Expand Down
5 changes: 1 addition & 4 deletions packages/quill/test/unit/core/composition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import Scroll from '../../../src/blots/scroll.js';
import { describe, expect, test, vitest } from 'vitest';
import { createRegistry } from '../__helpers__/factory.js';
import Quill from '../../../src/core.js';
import { createSubscriber } from '../../../src/core/subscriber.js';

describe('Composition', () => {
test('triggers events on compositionstart', async () => {
const emitter = new Emitter();
const container = document.createElement('div');
createSubscriber(container);
const scroll = new Scroll(createRegistry(), container, {
const scroll = new Scroll(createRegistry(), document.createElement('div'), {
emitter,
});
new Composition(scroll, emitter);
Expand Down
5 changes: 1 addition & 4 deletions packages/quill/test/unit/core/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import IndentClass from '../../../src/formats/indent.js';
import { ColorClass } from '../../../src/formats/color.js';
import Quill from '../../../src/core.js';
import { normalizeHTML } from '../__helpers__/utils.js';
import { createSubscriber } from '../../../src/core/subscriber.js';

const createEditor = (html: string) => {
const container = document.createElement('div');
Expand Down Expand Up @@ -881,10 +880,8 @@ describe('Editor', () => {

const registry = new Registry();
registry.register(MyBlot, Block, Break, Text);
const container = document.createElement('div');
createSubscriber(container);
const editor = new Editor(
new Scroll(registry, container, {
new Scroll(registry, document.createElement('div'), {
emitter: new Emitter(),
}),
);
Expand Down
35 changes: 22 additions & 13 deletions packages/quill/test/unit/core/subscriber.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, test, vitest } from 'vitest';
import { createSubscriber, getSubscriber } from '../../../src/core/subscriber';
import logger from '../../../src/core/logger.js';
import { getSubscriber } from '../../../src/core/subscriber.js';

describe('Subscriber', () => {
class Test {}
Expand All @@ -9,23 +10,31 @@ describe('Subscriber', () => {
object = new Test();
});

describe('registration', () => {
describe('getSubscriber()', () => {
test('maps a Subscriber to an object', () => {
const subscriber = createSubscriber(object);
expect(subscriber).toBeTruthy();
const subscriber = getSubscriber(object);
expect(getSubscriber(object)).toBe(subscriber);
expect(getSubscriber(new Test())).not.toBe(subscriber);
});

test('throws an error if no Subscriber is bound to an object', () => {
expect(() => getSubscriber(object)).toThrow(
'Subscriber not found for object Test',
test('logs the creation of a new Subscriber instance', () => {
logger.level('info');
vitest.spyOn(console, 'info');
getSubscriber(object);
expect(console.info).toHaveBeenCalledWith(
'quill:subscriber',
'Creating new Subscriber for Test',
);
getSubscriber(object);
expect(console.info).toHaveBeenCalledTimes(1);
getSubscriber(new Test());
expect(console.info).toHaveBeenCalledTimes(2);
});
});

describe('on()', () => {
test('calls addEventListener on the target', () => {
const subscriber = createSubscriber(object);
const subscriber = getSubscriber(object);
const source = new Test();
const target = document.createElement('div');
const event = 'keydown';
Expand All @@ -41,7 +50,7 @@ describe('Subscriber', () => {
});

test('keeps track of the subscription', () => {
const subscriber = createSubscriber(object);
const subscriber = getSubscriber(object);
const source = new Test();
const target = document.createElement('div');
const event = 'keydown';
Expand All @@ -56,7 +65,7 @@ describe('Subscriber', () => {

describe('off()', () => {
test('calls removeEventListener on the target', () => {
const subscriber = createSubscriber(object);
const subscriber = getSubscriber(object);
const target = document.createElement('div');
const event = 'keydown';
const handler = () => {};
Expand All @@ -71,7 +80,7 @@ describe('Subscriber', () => {
});

test('forgets the subscription', () => {
const subscriber = createSubscriber(object);
const subscriber = getSubscriber(object);
const source = new Test();
const target = document.createElement('div');
const event = 'keydown';
Expand All @@ -85,7 +94,7 @@ describe('Subscriber', () => {

describe('removeSourceListeners()', () => {
test('removes all listeners related to a source', () => {
const subscriber = createSubscriber(object);
const subscriber = getSubscriber(object);
const source1 = new Test();
const source2 = new Test();
const target1 = document.createElement('div');
Expand All @@ -110,7 +119,7 @@ describe('Subscriber', () => {

describe('removeAllListeners()', () => {
test('removes all listeners', () => {
const subscriber = createSubscriber(object);
const subscriber = getSubscriber(object);
const source1 = new Test();
const source2 = new Test();
const target1 = document.createElement('div');
Expand Down
4 changes: 2 additions & 2 deletions packages/quill/test/unit/ui/picker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
import Picker from '../../../src/ui/picker.js';
import { createSubscriber } from '../../../src/core/subscriber.js';
import { getSubscriber } from '../../../src/core/subscriber.js';

describe('Picker', () => {
const setup = () => {
Expand All @@ -9,7 +9,7 @@ describe('Picker', () => {
'<select><option selected>0</option><option value="1">1</option></select>';
const pickerSelectorInstance = new Picker(
container.firstChild as HTMLSelectElement,
createSubscriber(container),
getSubscriber(container),
);
const pickerSelector = container.querySelector('.ql-picker') as HTMLElement;
return { container, pickerSelectorInstance, pickerSelector };
Expand Down
38 changes: 38 additions & 0 deletions packages/website/content/docs/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ title: API

## Content

### configure

Applies a new [configuration](/docs/configuration/) to an existing instance of Quill.
singintime marked this conversation as resolved.
Show resolved Hide resolved

**Methods**

```typescript
configure(options: QuillOptions = {}): void
```

**Example 1**
singintime marked this conversation as resolved.
Show resolved Hide resolved

```typescript
quill.configure();
```

**Example 2**
singintime marked this conversation as resolved.
Show resolved Hide resolved

```typescript
quill.configure({ theme: 'bubble', readOnly: true });
```

### deleteText

Deletes text from the editor, returning a [Delta](/docs/delta/) representing the change. [Source](/docs/api/#events) may be `"user"`, `"api"`, or `"silent"`. Calls where the `source` is `"user"` when the editor is [disabled](#disable) are ignored.
Expand Down Expand Up @@ -59,6 +81,22 @@ document.querySelector('#deleteButton').addEventListener('click', () => {
}}
/>

### detach

Deactivates a Quill instance by removing all event subscriptions. Useful to cleanup the environment and prevent memory leaks.

**Methods**

```typescript
detach(): void
```

**Examples**

```typescript
quill.detach();
```

### getContents

Retrieves contents of the editor, with formatting data, represented by a [Delta](/docs/delta/) object.
Expand Down