Compare commits

..

14 Commits
main ... cjs

Author SHA1 Message Date
Scott Nonnenberg
40d5781c6b v2.1.2 2025-01-27 15:06:03 -10:00
Scott Nonnenberg
2a0bd6dda2 Preserve formatting on entering newline
Restoring behavior removed here: https://github.com/slab/quill/pull/3428
2025-01-23 13:16:38 -10:00
Scott Nonnenberg
f6eeddde2d v2.1.1 2025-01-21 16:46:59 -10:00
Scott Nonnenberg
9394a4592d package-lock.json: Catch-up after recent changes 2025-01-21 16:46:38 -10:00
Scott Nonnenberg
ff34142824 scripts/build: Don't delete dist/dist - that's for css and built js 2025-01-21 16:46:23 -10:00
Scott Nonnenberg
b12bacbc12 Update the per-package README as well 2025-01-21 10:22:16 +10:00
Scott Nonnenberg
2e6261c62a Update package name/version, move to CJS version of parchment 2025-01-21 10:10:55 +10:00
Scott Nonnenberg
42100f814c New clipboard options: disable listeners, override default matchers 2025-01-21 09:01:07 +10:00
Scott Nonnenberg
b030001387 Scroll: Prevent paste of anything other than text 2025-01-20 17:25:13 +10:00
Scott Nonnenberg
a5abc5db27 On paste, apply existing selection's styles to incoming Delta 2025-01-20 17:25:13 +10:00
Scott Nonnenberg
c52143fdfc Clipboard: Prevent too many newlines in a row on paste 2025-01-20 17:04:11 +10:00
Scott Nonnenberg
194335b4d3 isLine: Add time element 2025-01-20 17:04:11 +10:00
Scott Nonnenberg
a826b90428 Keyboard: Fix crash when line is missing 2025-01-20 17:04:11 +10:00
Scott Nonnenberg
723daabb7d First move towards CJS 2025-01-20 17:04:11 +10:00
43 changed files with 303 additions and 140 deletions

View File

@ -1,3 +1,19 @@
## Note: This fork is for use in Signal Desktop
The biggest change is moving from ESM to CJS.
Beyond that, there are a few other changes:
- keyboard: fix a crash in keydown handler when line is missing
- clipboard: add `time` element to list of elements in `isLine`
- clipboard: update `matchNewline` to prevent too many newlines in a row
- clipboard: update `convert` to apply provided formats to both text and html inputs
- scroll: use `contenteditable="plaintext-only"` to fully prevent image paste
- clipboard: introduce two new options:
- `disableDefaultListeners` - if set to something truthy, disables the clipboard module's `cut`/`copy`/`paste` handlers
- `defaultMatchersOverride` - if set, overrides all the default clipboard matchers, giving full control of the list of installed matchers
It's published as `@signalapp/quill-cjs`.
<h1 align="center">
<a href="https://quilljs.com/" title="Quill">Quill Rich Text Editor</a>
</h1>

52
package-lock.json generated
View File

@ -5709,6 +5709,16 @@
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"dev": true
},
"node_modules/@signalapp/parchment-cjs": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@signalapp/parchment-cjs/-/parchment-cjs-3.0.1.tgz",
"integrity": "sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==",
"license": "BSD-3-Clause"
},
"node_modules/@signalapp/quill-cjs": {
"resolved": "packages/quill",
"link": true
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -6184,19 +6194,11 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
"license": "MIT"
},
"node_modules/@types/mdast": {
"version": "3.0.15",
@ -13233,13 +13235,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
@ -14840,11 +14836,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/parchment": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -15550,10 +15541,6 @@
}
]
},
"node_modules/quill": {
"resolved": "packages/quill",
"link": true
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
@ -19236,12 +19223,13 @@
}
},
"packages/quill": {
"version": "2.0.3",
"name": "@signalapp/quill-cjs",
"version": "2.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"@signalapp/parchment-cjs": "3.0.1",
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
"lodash": "^4.17.21",
"quill-delta": "^5.1.0"
},
"devDependencies": {
@ -19251,7 +19239,7 @@
"@babel/preset-typescript": "^7.23.3",
"@playwright/test": "1.44.1",
"@types/highlight.js": "^9.12.4",
"@types/lodash-es": "^4.17.12",
"@types/lodash": "^4.17.14",
"@types/node": "^20.10.0",
"@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^7.2.0",

View File

@ -1,3 +1,5 @@
## Note: This fork is for use in Signal Desktop
# Quill
This is the main package of Quill.

View File

@ -2,7 +2,7 @@ const pkg = require('./package.json');
module.exports = {
presets: [
['@babel/preset-env', { modules: false }],
['@babel/preset-env', { modules: 'cjs' }],
'@babel/preset-typescript',
],
plugins: [

View File

@ -1,15 +1,14 @@
{
"name": "quill",
"version": "2.0.3",
"name": "@signalapp/quill-cjs",
"version": "2.1.2",
"description": "Your powerful, rich text editor",
"author": "Jason Chen <jhchen7@gmail.com>",
"homepage": "https://quilljs.com",
"main": "quill.js",
"type": "module",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
"lodash": "^4.17.21",
"@signalapp/parchment-cjs": "3.0.1",
"quill-delta": "^5.1.0"
},
"devDependencies": {
@ -19,7 +18,7 @@
"@babel/preset-typescript": "^7.23.3",
"@playwright/test": "1.44.1",
"@types/highlight.js": "^9.12.4",
"@types/lodash-es": "^4.17.12",
"@types/lodash": "^4.17.14",
"@types/node": "^20.10.0",
"@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^7.2.0",

View File

@ -11,7 +11,7 @@ rm -rf $DIST
mkdir $DIST
mv $TMPDIR/src/* $DIST
rm -rf $TMPDIR
npx babel src --out-dir $DIST --copy-files --no-copy-ignored --extensions .ts --source-maps
npx babel src --out-dir $DIST --copy-files --no-copy-ignored --extensions .ts
npx webpack -- --mode $1
# https://github.com/webpack-contrib/mini-css-extract-plugin/issues/151
rm -rf $DIST/dist/*.css.js $DIST/dist/*.css.js.*

View File

@ -4,8 +4,8 @@ import {
EmbedBlot,
LeafBlot,
Scope,
} from 'parchment';
import type { Blot, Parent } from 'parchment';
} from '@signalapp/parchment-cjs';
import type { Blot, Parent } from '@signalapp/parchment-cjs';
import Delta from 'quill-delta';
import Break from './break.js';
import Inline from './inline.js';

View File

@ -1,4 +1,4 @@
import { EmbedBlot } from 'parchment';
import { EmbedBlot } from '@signalapp/parchment-cjs';
class Break extends EmbedBlot {
static value() {

View File

@ -1,4 +1,4 @@
import { ContainerBlot } from 'parchment';
import { ContainerBlot } from '@signalapp/parchment-cjs';
class Container extends ContainerBlot {}

View File

@ -1,5 +1,5 @@
import { EmbedBlot, Scope } from 'parchment';
import type { Parent, ScrollBlot } from 'parchment';
import { EmbedBlot, Scope } from '@signalapp/parchment-cjs';
import type { Parent, ScrollBlot } from '@signalapp/parchment-cjs';
import type Selection from '../core/selection.js';
import TextBlot from './text.js';
import type { EmbedContextRange } from './embed.js';

View File

@ -1,5 +1,5 @@
import type { ScrollBlot } from 'parchment';
import { EmbedBlot } from 'parchment';
import type { ScrollBlot } from '@signalapp/parchment-cjs';
import { EmbedBlot } from '@signalapp/parchment-cjs';
import TextBlot from './text.js';
const GUARD_TEXT = '\uFEFF';

View File

@ -1,5 +1,5 @@
import { EmbedBlot, InlineBlot, Scope } from 'parchment';
import type { BlotConstructor } from 'parchment';
import { EmbedBlot, InlineBlot, Scope } from '@signalapp/parchment-cjs';
import type { BlotConstructor } from '@signalapp/parchment-cjs';
import Break from './break.js';
import Text from './text.js';

View File

@ -1,5 +1,16 @@
import { ContainerBlot, LeafBlot, Scope, ScrollBlot } from 'parchment';
import type { Blot, Parent, EmbedBlot, ParentBlot, Registry } from 'parchment';
import {
ContainerBlot,
LeafBlot,
Scope,
ScrollBlot,
} from '@signalapp/parchment-cjs';
import type {
Blot,
Parent,
EmbedBlot,
ParentBlot,
Registry,
} from '@signalapp/parchment-cjs';
import Delta, { AttributeMap, Op } from 'quill-delta';
import Emitter from '../core/emitter.js';
import type { EmitterSource } from '../core/emitter.js';
@ -96,7 +107,10 @@ class Scroll extends ScrollBlot {
}
enable(enabled = true) {
this.domNode.setAttribute('contenteditable', enabled ? 'true' : 'false');
this.domNode.setAttribute(
'contenteditable',
enabled ? 'plaintext-only' : 'false',
);
}
formatAt(index: number, length: number, format: string, value: unknown) {
@ -210,7 +224,8 @@ class Scroll extends ScrollBlot {
}
isEnabled() {
return this.domNode.getAttribute('contenteditable') === 'true';
const value = this.domNode.getAttribute('contenteditable');
return value === 'true' || value === 'plaintext-only';
}
leaf(index: number): [LeafBlot | null, number] {

View File

@ -1,4 +1,4 @@
import { TextBlot } from 'parchment';
import { TextBlot } from '@signalapp/parchment-cjs';
class Text extends TextBlot {}

View File

@ -1,6 +1,11 @@
import { cloneDeep, isEqual, merge } from 'lodash-es';
import { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment';
import type { Blot } from 'parchment';
import { cloneDeep, isEqual, merge } from 'lodash';
import {
LeafBlot,
EmbedBlot,
Scope,
ParentBlot,
} from '@signalapp/parchment-cjs';
import type { Blot } from '@signalapp/parchment-cjs';
import Delta, { AttributeMap, Op } from 'quill-delta';
import Block, { BlockEmbed, bubbleFormats } from '../blots/block.js';
import Break from '../blots/break.js';

View File

@ -1,5 +1,5 @@
import { merge } from 'lodash-es';
import * as Parchment from 'parchment';
import { merge } from 'lodash';
import * as Parchment from '@signalapp/parchment-cjs';
import type { Op } from 'quill-delta';
import Delta from 'quill-delta';
import type { BlockEmbed } from '../blots/block.js';

View File

@ -1,5 +1,5 @@
import { LeafBlot, Scope } from 'parchment';
import { cloneDeep, isEqual } from 'lodash-es';
import { LeafBlot, Scope } from '@signalapp/parchment-cjs';
import { cloneDeep, isEqual } from 'lodash';
import Emitter from './emitter.js';
import type { EmitterSource } from './emitter.js';
import logger from './logger.js';

View File

@ -1,4 +1,4 @@
import { Registry } from 'parchment';
import { Registry } from '@signalapp/parchment-cjs';
const MAX_REGISTER_ITERATIONS = 100;
const CORE_FORMATS = ['block', 'break', 'cursor', 'inline', 'scroll', 'text'];

View File

@ -1,4 +1,9 @@
import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment';
import {
Attributor,
ClassAttributor,
Scope,
StyleAttributor,
} from '@signalapp/parchment-cjs';
const config = {
scope: Scope.BLOCK,

View File

@ -1,4 +1,4 @@
import { ClassAttributor, Scope } from 'parchment';
import { ClassAttributor, Scope } from '@signalapp/parchment-cjs';
import { ColorAttributor } from './color.js';
const BackgroundClass = new ClassAttributor('background', 'ql-bg', {

View File

@ -1,4 +1,8 @@
import { ClassAttributor, Scope, StyleAttributor } from 'parchment';
import {
ClassAttributor,
Scope,
StyleAttributor,
} from '@signalapp/parchment-cjs';
class ColorAttributor extends StyleAttributor {
value(domNode: HTMLElement) {

View File

@ -1,4 +1,9 @@
import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment';
import {
Attributor,
ClassAttributor,
Scope,
StyleAttributor,
} from '@signalapp/parchment-cjs';
const config = {
scope: Scope.BLOCK,

View File

@ -1,4 +1,8 @@
import { ClassAttributor, Scope, StyleAttributor } from 'parchment';
import {
ClassAttributor,
Scope,
StyleAttributor,
} from '@signalapp/parchment-cjs';
const config = {
scope: Scope.INLINE,

View File

@ -1,4 +1,4 @@
import { EmbedBlot } from 'parchment';
import { EmbedBlot } from '@signalapp/parchment-cjs';
import { sanitize } from './link.js';
const ATTRIBUTES = ['alt', 'height', 'width'];

View File

@ -1,4 +1,4 @@
import { ClassAttributor, Scope } from 'parchment';
import { ClassAttributor, Scope } from '@signalapp/parchment-cjs';
class IndentAttributor extends ClassAttributor {
add(node: HTMLElement, value: string | number) {

View File

@ -1,4 +1,8 @@
import { ClassAttributor, Scope, StyleAttributor } from 'parchment';
import {
ClassAttributor,
Scope,
StyleAttributor,
} from '@signalapp/parchment-cjs';
const SizeClass = new ClassAttributor('size', 'ql-size', {
scope: Scope.INLINE,

View File

@ -1,4 +1,4 @@
import type { LinkedList } from 'parchment';
import type { LinkedList } from '@signalapp/parchment-cjs';
import Block from '../blots/block.js';
import Container from '../blots/container.js';

View File

@ -1,4 +1,4 @@
import type { ScrollBlot } from 'parchment';
import type { ScrollBlot } from '@signalapp/parchment-cjs';
import {
Attributor,
BlockBlot,
@ -6,8 +6,9 @@ import {
EmbedBlot,
Scope,
StyleAttributor,
} from 'parchment';
} from '@signalapp/parchment-cjs';
import Delta from 'quill-delta';
import type { AttributeMap } from 'quill-delta';
import { BlockEmbed } from '../blots/block.js';
import type { EmitterSource } from '../core/emitter.js';
import logger from '../core/logger.js';
@ -27,7 +28,12 @@ import normalizeExternalHTML from './normalizeExternalHTML/index.js';
const debug = logger('quill:clipboard');
type Selector = string | Node['TEXT_NODE'] | Node['ELEMENT_NODE'];
type Matcher = (node: Node, delta: Delta, scroll: ScrollBlot) => Delta;
type Matcher = (
node: Node,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) => Delta;
const CLIPBOARD_CONFIG: [Selector, Matcher][] = [
[Node.TEXT_NODE, matchText],
@ -69,6 +75,8 @@ const STYLE_ATTRIBUTORS = [
interface ClipboardOptions {
matchers: [Selector, Matcher][];
defaultMatchersOverride?: [Selector, Matcher][];
disableDefaultListeners?: boolean;
}
class Clipboard extends Module<ClipboardOptions> {
@ -80,17 +88,24 @@ class Clipboard extends Module<ClipboardOptions> {
constructor(quill: Quill, options: Partial<ClipboardOptions>) {
super(quill, options);
this.quill.root.addEventListener('copy', (e) =>
this.onCaptureCopy(e, false),
);
this.quill.root.addEventListener('cut', (e) => this.onCaptureCopy(e, true));
this.quill.root.addEventListener('paste', this.onCapturePaste.bind(this));
if (!options.disableDefaultListeners) {
this.quill.root.addEventListener('copy', (e) =>
this.onCaptureCopy(e, false),
);
this.quill.root.addEventListener('cut', (e) =>
this.onCaptureCopy(e, true),
);
this.quill.root.addEventListener('paste', this.onCapturePaste.bind(this));
}
this.matchers = [];
CLIPBOARD_CONFIG.concat(this.options.matchers ?? []).forEach(
([selector, matcher]) => {
const baseMatchers = options.defaultMatchersOverride ?? CLIPBOARD_CONFIG;
baseMatchers
.concat(this.options.matchers ?? [])
.forEach(([selector, matcher]) => {
this.addMatcher(selector, matcher);
},
);
});
}
addMatcher(selector: Selector, matcher: Matcher) {
@ -109,7 +124,7 @@ class Clipboard extends Module<ClipboardOptions> {
if (!html) {
return new Delta().insert(text || '', formats);
}
const delta = this.convertHTML(html);
const delta = this.convertHTML(html, formats);
// Remove trailing newline
if (
deltaEndsWith(delta, '\n') &&
@ -124,7 +139,7 @@ class Clipboard extends Module<ClipboardOptions> {
normalizeExternalHTML(doc);
}
protected convertHTML(html: string) {
protected convertHTML(html: string, formats: Record<string, unknown> = {}) {
const doc = new DOMParser().parseFromString(html, 'text/html');
this.normalizeHTML(doc);
const container = doc.body;
@ -139,6 +154,7 @@ class Clipboard extends Module<ClipboardOptions> {
elementMatchers,
textMatchers,
nodeMatches,
formats,
);
}
@ -282,6 +298,7 @@ function applyFormat(
format: string,
value: unknown,
scroll: ScrollBlot,
attributes: AttributeMap,
): Delta {
if (!scroll.query(format)) {
return delta;
@ -290,10 +307,14 @@ function applyFormat(
return delta.reduce((newDelta, op) => {
if (!op.insert) return newDelta;
if (op.attributes && op.attributes[format]) {
return newDelta.push(op);
return newDelta.insert(op.insert, { ...op.attributes, ...attributes });
}
const formats = value ? { [format]: value } : {};
return newDelta.insert(op.insert, { ...formats, ...op.attributes });
return newDelta.insert(op.insert, {
...formats,
...op.attributes,
...attributes,
});
}, new Delta());
}
@ -349,6 +370,7 @@ function isLine(node: Node, scroll: ScrollBlot) {
'section',
'table',
'td',
'time',
'tr',
'ul',
'video',
@ -384,11 +406,12 @@ function traverse(
elementMatchers: Matcher[],
textMatchers: Matcher[],
nodeMatches: WeakMap<Node, Matcher[]>,
attributes: AttributeMap,
): Delta {
// Post-order
if (node.nodeType === node.TEXT_NODE) {
return textMatchers.reduce((delta: Delta, matcher) => {
return matcher(node, delta, scroll);
return matcher(node, delta, scroll, attributes);
}, new Delta());
}
if (node.nodeType === node.ELEMENT_NODE) {
@ -399,14 +422,20 @@ function traverse(
elementMatchers,
textMatchers,
nodeMatches,
attributes,
);
if (childNode.nodeType === node.ELEMENT_NODE) {
childrenDelta = elementMatchers.reduce((reducedDelta, matcher) => {
return matcher(childNode as HTMLElement, reducedDelta, scroll);
return matcher(
childNode as HTMLElement,
reducedDelta,
scroll,
attributes,
);
}, childrenDelta);
childrenDelta = (nodeMatches.get(childNode) || []).reduce(
(reducedDelta, matcher) => {
return matcher(childNode, reducedDelta, scroll);
return matcher(childNode, reducedDelta, scroll, attributes);
},
childrenDelta,
);
@ -417,13 +446,23 @@ function traverse(
return new Delta();
}
function createMatchAlias(format: string) {
return (_node: Element, delta: Delta, scroll: ScrollBlot) => {
return applyFormat(delta, format, true, scroll);
function createMatchAlias(format: string): Matcher {
return (
_node: Element,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) => {
return applyFormat(delta, format, true, scroll, attributes);
};
}
function matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
function matchAttributor(
node: HTMLElement,
delta: Delta,
scroll: ScrollBlot,
targetAttributes: AttributeMap,
) {
const attributes = Attributor.keys(node);
const classes = ClassAttributor.keys(node);
const styles = StyleAttributor.keys(node);
@ -449,12 +488,18 @@ function matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
});
return Object.entries(formats).reduce(
(newDelta, [name, value]) => applyFormat(newDelta, name, value, scroll),
(newDelta, [name, value]) =>
applyFormat(newDelta, name, value, scroll, targetAttributes),
delta,
);
}
function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchBlot(
node: Node,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
const match = scroll.query(node);
if (match == null) return delta;
// @ts-expect-error
@ -483,33 +528,49 @@ function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot) {
match.blotName,
match.formats(node, scroll),
scroll,
attributes,
);
}
}
return delta;
}
function matchBreak(node: Node, delta: Delta) {
function matchBreak(
node: Node,
delta: Delta,
_scroll: ScrollBlot,
attributes: AttributeMap,
) {
if (!deltaEndsWith(delta, '\n')) {
delta.insert('\n');
delta.insert('\n', attributes);
}
return delta;
}
function matchCodeBlock(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchCodeBlock(
node: Node,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
const match = scroll.query('code-block');
const language =
match && 'formats' in match && typeof match.formats === 'function'
? match.formats(node, scroll)
: true;
return applyFormat(delta, 'code-block', language, scroll);
return applyFormat(delta, 'code-block', language, scroll, attributes);
}
function matchIgnore() {
return new Delta();
}
function matchIndent(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchIndent(
node: Node,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
const match = scroll.query(node);
if (
match == null ||
@ -534,11 +595,20 @@ function matchIndent(node: Node, delta: Delta, scroll: ScrollBlot) {
if (op.attributes && typeof op.attributes.indent === 'number') {
return composed.push(op);
}
return composed.insert(op.insert, { indent, ...(op.attributes || {}) });
return composed.insert(op.insert, {
indent,
...(op.attributes || {}),
attributes,
});
}, new Delta());
}
function matchList(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchList(
node: Node,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
const element = node as Element;
let list = element.tagName === 'OL' ? 'ordered' : 'bullet';
@ -547,27 +617,48 @@ function matchList(node: Node, delta: Delta, scroll: ScrollBlot) {
list = checkedAttr === 'true' ? 'checked' : 'unchecked';
}
return applyFormat(delta, 'list', list, scroll);
return applyFormat(delta, 'list', list, scroll, attributes);
}
function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot) {
if (!deltaEndsWith(delta, '\n')) {
function deltaIs(delta: Delta, text: string): boolean {
let allText = '';
for (
let i = delta.ops.length - 1;
i >= 0 && allText.length <= text.length;
i -= 1
) {
const op = delta.ops[i];
if (typeof op.insert !== 'string') {
break;
}
allText = op.insert + allText;
}
return allText === text;
}
function matchNewline(
node: Node,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
if (!deltaIs(delta, '\n') && !deltaEndsWith(delta, '\n\n')) {
if (
isLine(node, scroll) &&
(node.childNodes.length > 0 || node instanceof HTMLParagraphElement)
) {
return delta.insert('\n');
return delta.insert('\n', attributes);
}
if (delta.length() > 0 && node.nextSibling) {
let nextSibling: Node | null = node.nextSibling;
while (nextSibling != null) {
if (isLine(nextSibling, scroll)) {
return delta.insert('\n');
return delta.insert('\n', attributes);
}
const match = scroll.query(nextSibling);
// @ts-expect-error
if (match && match.prototype instanceof BlockEmbed) {
return delta.insert('\n');
return delta.insert('\n', attributes);
}
nextSibling = nextSibling.firstChild;
}
@ -576,7 +667,12 @@ function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot) {
return delta;
}
function matchStyles(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
function matchStyles(
node: HTMLElement,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
const formats: Record<string, unknown> = {};
const style: Partial<CSSStyleDeclaration> = node.style || {};
if (style.fontStyle === 'italic') {
@ -596,13 +692,14 @@ function matchStyles(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
formats.bold = true;
}
delta = Object.entries(formats).reduce(
(newDelta, [name, value]) => applyFormat(newDelta, name, value, scroll),
(newDelta, [name, value]) =>
applyFormat(newDelta, name, value, scroll, attributes),
delta,
);
// @ts-expect-error
if (parseFloat(style.textIndent || 0) > 0) {
// Could be 0.5in
return new Delta().insert('\t').concat(delta);
return new Delta().insert('\t', attributes).concat(delta);
}
return delta;
}
@ -611,6 +708,7 @@ function matchTable(
node: HTMLTableRowElement,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
const table =
node.parentElement?.tagName === 'TABLE'
@ -619,17 +717,22 @@ function matchTable(
if (table != null) {
const rows = Array.from(table.querySelectorAll('tr'));
const row = rows.indexOf(node) + 1;
return applyFormat(delta, 'table', row, scroll);
return applyFormat(delta, 'table', row, scroll, attributes);
}
return delta;
}
function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
function matchText(
node: HTMLElement,
delta: Delta,
scroll: ScrollBlot,
attributes: AttributeMap,
) {
// @ts-expect-error
let text = node.data as string;
// Word represents empty line with <o:p>&nbsp;</o:p>
if (node.parentElement?.tagName === 'O:P') {
return delta.insert(text.trim());
return delta.insert(text.trim(), attributes);
}
if (!isPre(node)) {
if (
@ -665,13 +768,14 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
// done removing whitespace and can normalize all to regular space
text = text.replaceAll('\u00a0', ' ');
}
return delta.insert(text);
return delta.insert(text, attributes);
}
export {
Clipboard as default,
matchAttributor,
matchBlot,
matchBreak,
matchNewline,
matchText,
traverse,

View File

@ -1,4 +1,4 @@
import { Scope } from 'parchment';
import { Scope } from '@signalapp/parchment-cjs';
import type Delta from 'quill-delta';
import Module from '../core/module.js';
import Quill from '../core/quill.js';

View File

@ -1,7 +1,7 @@
import { cloneDeep, isEqual } from 'lodash-es';
import { cloneDeep, isEqual } from 'lodash';
import Delta, { AttributeMap } from 'quill-delta';
import { EmbedBlot, Scope, TextBlot } from 'parchment';
import type { Blot, BlockBlot } from 'parchment';
import { EmbedBlot, Scope, TextBlot } from '@signalapp/parchment-cjs';
import type { Blot, BlockBlot } from '@signalapp/parchment-cjs';
import Quill from '../core/quill.js';
import logger from '../core/logger.js';
import Module from '../core/module.js';
@ -206,8 +206,7 @@ class Keyboard extends Module<KeyboardOptions> {
leafEnd instanceof TextBlot ? leafEnd.value().slice(offsetEnd) : '';
const curContext = {
collapsed: range.length === 0,
// @ts-expect-error Fix me later
empty: range.length === 0 && line.length() <= 1,
empty: range.length === 0 && (line?.length() ?? 0) <= 1,
format: this.quill.getFormat(range),
line,
offset,
@ -351,6 +350,13 @@ class Keyboard extends Module<KeyboardOptions> {
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.quill.focus();
Object.keys(context.format).forEach((name) => {
if (lineFormats[name] != null) return;
if (Array.isArray(context.format[name])) return;
if (name === 'code' || name === 'link') return;
this.quill.format(name, context.format[name], Quill.sources.USER);
});
}
}

View File

@ -1,6 +1,6 @@
import Delta from 'quill-delta';
import { ClassAttributor, Scope } from 'parchment';
import type { Blot, ScrollBlot } from 'parchment';
import { ClassAttributor, Scope } from '@signalapp/parchment-cjs';
import type { Blot, ScrollBlot } from '@signalapp/parchment-cjs';
import Inline from '../blots/inline.js';
import Quill from '../core/quill.js';
import Module from '../core/module.js';
@ -302,6 +302,10 @@ class Syntax extends Module<SyntaxOptions> {
const container = this.quill.root.ownerDocument.createElement('div');
container.classList.add(CodeBlock.className);
container.innerHTML = highlight(this.options.hljs, language, text);
const attributes = this.quill.getFormat(
this.quill.selection.savedRange.index,
);
return traverse(
this.quill.scroll,
container,
@ -329,6 +333,7 @@ class Syntax extends Module<SyntaxOptions> {
},
],
new WeakMap(),
attributes,
);
}
}

View File

@ -1,5 +1,5 @@
import Delta from 'quill-delta';
import { EmbedBlot, Scope } from 'parchment';
import { EmbedBlot, Scope } from '@signalapp/parchment-cjs';
import Quill from '../core/quill.js';
import logger from '../core/logger.js';
import Module from '../core/module.js';

View File

@ -1,4 +1,4 @@
import { ParentBlot } from 'parchment';
import { ParentBlot } from '@signalapp/parchment-cjs';
import Module from '../core/module.js';
import Quill from '../core/quill.js';

View File

@ -1,4 +1,4 @@
import { merge } from 'lodash-es';
import { merge } from 'lodash';
import type Quill from '../core/quill.js';
import Emitter from '../core/emitter.js';
import Theme from '../core/theme.js';

View File

@ -1,4 +1,4 @@
import { merge } from 'lodash-es';
import { merge } from 'lodash';
import Emitter from '../core/emitter.js';
import BaseTheme, { BaseTooltip } from './base.js';
import { Range } from '../core/selection.js';

View File

@ -1,4 +1,4 @@
import { merge } from 'lodash-es';
import { merge } from 'lodash';
import Emitter from '../core/emitter.js';
import BaseTheme, { BaseTooltip } from './base.js';
import LinkBlot from '../formats/link.js';

View File

@ -3,7 +3,7 @@ import Quill, { Delta } from '../../src/quill.js';
import type { EmitterSource, Parchment, Range } from '../../src/quill.js';
import type { default as Block, BlockEmbed } from '../../src/blots/block.js';
import SnowTheme from '../../src/themes/snow.js';
import { LeafBlot } from 'parchment';
import { LeafBlot } from '@signalapp/parchment-cjs';
{
const Counter = (quill: Quill, options: { unit: string }) => {

View File

@ -1,5 +1,5 @@
import { Registry } from 'parchment';
import type { Attributor } from 'parchment';
import { Registry } from '@signalapp/parchment-cjs';
import type { Attributor } from '@signalapp/parchment-cjs';
import Block from '../../../src/blots/block.js';
import Break from '../../../src/blots/break.js';

View File

@ -3,7 +3,7 @@ import Editor from '../../../src/core/editor.js';
import Block from '../../../src/blots/block.js';
import { Range } from '../../../src/core/selection.js';
import Scroll from '../../../src/blots/scroll.js';
import { Registry } from 'parchment';
import { Registry } from '@signalapp/parchment-cjs';
import Text from '../../../src/blots/text.js';
import Emitter from '../../../src/core/emitter.js';
import Break from '../../../src/blots/break.js';

View File

@ -1,6 +1,6 @@
import '../../../src/quill.js';
import Delta from 'quill-delta';
import { LeafBlot, Registry } from 'parchment';
import { LeafBlot, Registry } from '@signalapp/parchment-cjs';
import { afterEach, beforeEach, describe, expect, test, vitest } from 'vitest';
import type { MockedFunction } from 'vitest';
import Emitter from '../../../src/core/emitter.js';

View File

@ -3,7 +3,7 @@ import { describe, expect, test, vitest } from 'vitest';
import createRegistryWithFormats from '../../../../src/core/utils/createRegistryWithFormats.js';
import { globalRegistry } from '../../../../src/core/quill.js';
import logger from '../../../../src/core/logger.js';
import { Registry } from 'parchment';
import { Registry } from '@signalapp/parchment-cjs';
import Inline from '../../../../src/blots/inline.js';
import Container from '../../../../src/blots/container.js';

View File

@ -17,7 +17,7 @@ import {
} from '../../../src/formats/table.js';
import Video from '../../../src/formats/video.js';
import { createRegistry } from '../__helpers__/factory.js';
import type { RegistryDefinition } from 'parchment';
import type { RegistryDefinition } from '@signalapp/parchment-cjs';
import {
DirectionAttribute,
DirectionClass,

View File

@ -11,11 +11,12 @@
"sourceMap": true,
"resolveJsonModule": true,
"declaration": false,
"module": "ES2020",
"moduleResolution": "bundler",
"module": "CommonJS",
"moduleResolution": "node",
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true
"noUnusedLocals": true,
"skipLibCheck": true
},
"include": ["src/**/*", "test/**/*"]
}