Skip to content

Commit

Permalink
Support pasting links copied from iOS share sheets (#4064)
Browse files Browse the repository at this point in the history
  • Loading branch information
luin authored Mar 19, 2024
1 parent d7d5ae5 commit 4a4a61f
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# [Unreleased]

- Include source maps for Parchment
- **Clipboard** Support pasting links copied from iOS share sheets

# 2.0.0-rc.3

Expand Down
2 changes: 1 addition & 1 deletion packages/quill/src/core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Quill {
readOnly: false,
registry: globalRegistry,
theme: 'default',
} as const satisfies Partial<Options>;
} satisfies Partial<Options>;
static events = Emitter.events;
static sources = Emitter.sources;
static version = typeof QUILL_VERSION === 'undefined' ? 'dev' : QUILL_VERSION;
Expand Down
35 changes: 26 additions & 9 deletions packages/quill/src/modules/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ interface ClipboardOptions {
}

class Clipboard extends Module<ClipboardOptions> {
static DEFAULTS: ClipboardOptions;
static DEFAULTS: ClipboardOptions = {
matchers: [],
};

matchers: [Selector, Matcher][];

Expand All @@ -84,8 +86,7 @@ class Clipboard extends Module<ClipboardOptions> {
this.quill.root.addEventListener('cut', (e) => this.onCaptureCopy(e, true));
this.quill.root.addEventListener('paste', this.onCapturePaste.bind(this));
this.matchers = [];
// @ts-expect-error Fix me later
CLIPBOARD_CONFIG.concat(this.options.matchers).forEach(
CLIPBOARD_CONFIG.concat(this.options.matchers ?? []).forEach(
([selector, matcher]) => {
this.addMatcher(selector, matcher);
},
Expand Down Expand Up @@ -180,13 +181,32 @@ class Clipboard extends Module<ClipboardOptions> {
}
}

/*
* https://www.iana.org/assignments/media-types/text/uri-list
*/
private normalizeURIList(urlList: string) {
return (
urlList
.split(/\r?\n/)
// Ignore all comments
.filter((url) => url[0] !== '#')
.join('\n')
);
}

onCapturePaste(e: ClipboardEvent) {
if (e.defaultPrevented || !this.quill.isEnabled()) return;
e.preventDefault();
const range = this.quill.getSelection(true);
if (range == null) return;
const html = e.clipboardData?.getData('text/html');
const text = e.clipboardData?.getData('text/plain');
let text = e.clipboardData?.getData('text/plain');
if (!html && !text) {
const urlList = e.clipboardData?.getData('text/uri-list');
if (urlList) {
text = this.normalizeURIList(urlList);
}
}
const files = Array.from(e.clipboardData?.files || []);
if (!html && files.length > 0) {
this.quill.uploader.upload(range, files);
Expand Down Expand Up @@ -256,9 +276,6 @@ class Clipboard extends Module<ClipboardOptions> {
return [elementMatchers, textMatchers];
}
}
Clipboard.DEFAULTS = {
matchers: [],
};

function applyFormat(
delta: Delta,
Expand All @@ -271,11 +288,11 @@ function applyFormat(
}

return delta.reduce((newDelta, op) => {
if (!op.insert) return newDelta;
if (op.attributes && op.attributes[format]) {
return newDelta.push(op);
}
const formats = value ? { [format]: value } : {};
// @ts-expect-error Fix me later
return newDelta.insert(op.insert, { ...formats, ...op.attributes });
}, new Delta());
}
Expand Down Expand Up @@ -513,10 +530,10 @@ function matchIndent(node: Node, delta: Delta, scroll: ScrollBlot) {
}
if (indent <= 0) return delta;
return delta.reduce((composed, op) => {
if (!op.insert) return composed;
if (op.attributes && typeof op.attributes.indent === 'number') {
return composed.push(op);
}
// @ts-expect-error Fix me later
return composed.insert(op.insert, { indent, ...(op.attributes || {}) });
}, new Delta());
}
Expand Down
11 changes: 5 additions & 6 deletions packages/quill/src/modules/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ interface Stack {
}

class History extends Module<HistoryOptions> {
static DEFAULTS: HistoryOptions;
static DEFAULTS: HistoryOptions = {
delay: 1000,
maxStack: 100,
userOnly: false,
};

lastRecorded = 0;
ignoreChange = false;
Expand Down Expand Up @@ -153,11 +157,6 @@ class History extends Module<HistoryOptions> {
}
}
}
History.DEFAULTS = {
delay: 1000,
maxStack: 100,
userOnly: false,
};

function transformStack(stack: StackItem[], delta: Delta) {
let remoteDelta = delta;
Expand Down
37 changes: 30 additions & 7 deletions packages/quill/test/unit/modules/clipboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from '../../../src/formats/table.js';
import Video from '../../../src/formats/video.js';
import { createRegistry } from '../__helpers__/factory.js';
import { sleep } from '../__helpers__/utils.js';
import type { RegistryDefinition } from 'parchment';
import {
DirectionAttribute,
Expand Down Expand Up @@ -52,7 +51,6 @@ describe('Clipboard', () => {
test('pastes html data', async () => {
const quill = createQuill();
quill.clipboard.onCapturePaste(clipboardEvent);
await sleep(2);
expect(quill.root).toEqualHTML(
'<p>01<strong>|</strong><em>7</em>8</p>',
);
Expand All @@ -73,13 +71,42 @@ describe('Clipboard', () => {
},
preventDefault: () => {},
} as ClipboardEvent);
await sleep(2);
expect(quill.getContents().ops).toEqual([
{ insert: 'abcdef', attributes: { bold: true } },
{ insert: '\n' },
]);
});

test('pastes links from iOS share sheets', async () => {
const quill = createQuill();
quill.setContents(new Delta().insert('\n'));
quill.clipboard.onCapturePaste({
clipboardData: {
getData: (type: string) =>
type === 'text/uri-list' ? 'https://example.com' : undefined,
},
preventDefault: () => {},
} as ClipboardEvent);
expect(quill.getContents().ops).toEqual([
{ insert: 'https://example.com\n' },
]);

// Ignore comments
quill.setContents(new Delta().insert('\n'));
quill.clipboard.onCapturePaste({
clipboardData: {
getData: (type: string) =>
type === 'text/uri-list'
? 'https://example.com\r\n# Comment\r\nhttps://example.com/a'
: undefined,
},
preventDefault: () => {},
} as ClipboardEvent);
expect(quill.getContents().ops).toEqual([
{ insert: 'https://example.com\nhttps://example.com/a\n' },
]);
});

// Copying from Word includes both html and files
test('pastes html data if present with file', async () => {
const quill = createQuill();
Expand All @@ -92,7 +119,6 @@ describe('Clipboard', () => {
files: ['file'],
},
});
await sleep(2);
expect(upload).not.toHaveBeenCalled();
expect(quill.root).toEqualHTML(
'<p>01<strong>|</strong><em>7</em>8</p>',
Expand All @@ -114,7 +140,6 @@ describe('Clipboard', () => {
files: ['file'],
},
});
await sleep(2);
expect(upload).toHaveBeenCalled();
});

Expand All @@ -123,7 +148,6 @@ describe('Clipboard', () => {
const change = vitest.fn();
quill.on('selection-change', change);
quill.clipboard.onCapturePaste(clipboardEvent);
await sleep(2);
expect(change).not.toHaveBeenCalled();
});
});
Expand All @@ -146,7 +170,6 @@ describe('Clipboard', () => {
const quill = createQuill();
const { clipboardData, clipboardEvent } = setup();
quill.clipboard.onCaptureCopy(clipboardEvent, true);
await sleep(2);
expect(quill.root).toEqualHTML('<h1>01<em>7</em>8</h1>');
expect(quill.getSelection()).toEqual(new Range(2));
expect(clipboardData['text/plain']).toEqual('23\n56');
Expand Down

0 comments on commit 4a4a61f

Please sign in to comment.