Skip to content

Commit 8764b6e

Browse files
committed
fix: tab keypress and softbreak on rich text
1 parent c8ad9fc commit 8764b6e

File tree

3 files changed

+149
-39
lines changed

3 files changed

+149
-39
lines changed

packages/pluggableWidgets/rich-text-web/src/components/EditorWrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
120120
}, [quillRef.current, onChange?.isExecuting]);
121121

122122
const onTextChange = useCallback(() => {
123+
console.log("onTextChange called", quillRef?.current?.getContents());
123124
if (stringAttribute.value !== quillRef?.current?.getSemanticHTML()) {
124125
setAttributeValueDebounce(quillRef?.current?.getSemanticHTML());
125126
}
Lines changed: 140 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,150 @@
1+
/*
2+
* Custom Clipboard module to override Quill's default clipboard behavior
3+
* to better handle pasting from various sources.
4+
* https://github.com/slab/quill/blob/main/packages/quill/src/modules/clipboard.ts
5+
*/
6+
7+
import { EmbedBlot, type ScrollBlot } from "parchment";
18
import Quill, { Delta } from "quill";
2-
import Clipboard from "quill/modules/clipboard";
9+
import Clipboard, { matchNewline } from "quill/modules/clipboard";
10+
11+
function isLine(node: Node, scroll: ScrollBlot): boolean {
12+
if (!(node instanceof Element)) return false;
13+
const match = scroll.query(node);
14+
// @ts-expect-error prototype does not exist on Blot
15+
if (match && match.prototype instanceof EmbedBlot) return false;
16+
17+
return [
18+
"address",
19+
"article",
20+
"blockquote",
21+
"canvas",
22+
"dd",
23+
"div",
24+
"dl",
25+
"dt",
26+
"fieldset",
27+
"figcaption",
28+
"figure",
29+
"footer",
30+
"form",
31+
"h1",
32+
"h2",
33+
"h3",
34+
"h4",
35+
"h5",
36+
"h6",
37+
"header",
38+
"iframe",
39+
"li",
40+
"main",
41+
"nav",
42+
"ol",
43+
"output",
44+
"p",
45+
"pre",
46+
"section",
47+
"table",
48+
"td",
49+
"tr",
50+
"ul",
51+
"video"
52+
].includes(node.tagName.toLowerCase());
53+
}
54+
55+
function isBetweenInlineElements(node: HTMLElement, scroll: ScrollBlot): boolean | null {
56+
return (
57+
node.previousElementSibling &&
58+
node.nextElementSibling &&
59+
!isLine(node.previousElementSibling, scroll) &&
60+
!isLine(node.nextElementSibling, scroll)
61+
);
62+
}
63+
64+
const preNodes = new WeakMap();
65+
function isPre(node: Node | null): any {
66+
if (node == null) return false;
67+
if (!preNodes.has(node)) {
68+
// @ts-expect-error tagName does not exist on Node
69+
if (node.tagName === "PRE") {
70+
preNodes.set(node, true);
71+
} else {
72+
preNodes.set(node, isPre(node.parentNode));
73+
}
74+
}
75+
return preNodes.get(node);
76+
}
77+
78+
// overrides matchText from Quill's Clipboard module
79+
// removing text replacements that interfere with adding \t (tab)
80+
function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot): Delta {
81+
// @ts-expect-error data does not exist on HTMLElement
82+
let text = node.data as string;
83+
// Word represents empty line with <o:p>&nbsp;</o:p>
84+
if (node.parentElement?.tagName === "O:P") {
85+
return delta.insert(text.trim());
86+
}
87+
if (!isPre(node)) {
88+
if (text.trim().length === 0 && text.includes("\n") && !isBetweenInlineElements(node, scroll)) {
89+
return delta;
90+
}
91+
text = text.replace(/ {2,}/g, " ");
92+
if (
93+
(node.nextSibling == null && node.parentElement != null && isLine(node.parentElement, scroll)) ||
94+
(node.nextSibling instanceof Element && isLine(node.nextSibling, scroll))
95+
) {
96+
// block structure means we don't need trailing space
97+
text = text.replace(/ $/, "");
98+
}
99+
}
100+
return delta.insert(text);
101+
}
102+
103+
function matchList(node: HTMLElement, delta: Delta, _scroll: ScrollBlot): Delta {
104+
const format = "list";
105+
let list = "ordered";
106+
const element = node as HTMLUListElement;
107+
const checkedAttr = element.getAttribute("data-checked");
108+
if (checkedAttr) {
109+
list = checkedAttr === "true" ? "checked" : "unchecked";
110+
} else {
111+
const listStyleType = element.style.listStyleType;
112+
if (listStyleType) {
113+
if (listStyleType === "disc") {
114+
// disc is standard list type, convert to bullet
115+
list = "bullet";
116+
} else if (listStyleType === "decimal") {
117+
// list decimal type is dependant on indent level, convert to standard ordered list
118+
list = "ordered";
119+
} else {
120+
list = listStyleType;
121+
}
122+
} else {
123+
list = element.tagName === "OL" ? "ordered" : "bullet";
124+
}
125+
}
126+
127+
// apply list format to delta
128+
return delta.reduce((newDelta, op) => {
129+
if (!op.insert) return newDelta;
130+
if (op.attributes && op.attributes[format]) {
131+
return newDelta.push(op);
132+
}
133+
const formats = list ? { [format]: list } : {};
134+
135+
return newDelta.insert(op.insert, { ...formats, ...op.attributes });
136+
}, new Delta());
137+
}
3138

4139
export default class CustomClipboard extends Clipboard {
5140
constructor(quill: Quill, options: any) {
6141
super(quill, options);
7142

8143
// remove default list matchers for ol and ul
9-
this.matchers = this.matchers.filter(matcher => matcher[0] !== "ol, ul");
10-
144+
this.matchers = this.matchers.filter(matcher => matcher[0] !== "ol, ul" && matcher[0] !== Node.TEXT_NODE);
145+
this.addMatcher(Node.TEXT_NODE, matchNewline);
146+
this.addMatcher(Node.TEXT_NODE, matchText);
11147
// add custom list matchers for ol and ul to allow custom list types (lower-alpha, lower-roman, etc.)
12-
this.addMatcher("ol, ul", (node, delta) => {
13-
const format = "list";
14-
let list = "ordered";
15-
const element = node as HTMLUListElement;
16-
const checkedAttr = element.getAttribute("data-checked");
17-
if (checkedAttr) {
18-
list = checkedAttr === "true" ? "checked" : "unchecked";
19-
} else {
20-
const listStyleType = element.style.listStyleType;
21-
if (listStyleType) {
22-
if (listStyleType === "disc") {
23-
// disc is standard list type, convert to bullet
24-
list = "bullet";
25-
} else if (listStyleType === "decimal") {
26-
// list decimal type is dependant on indent level, convert to standard ordered list
27-
list = "ordered";
28-
} else {
29-
list = listStyleType;
30-
}
31-
} else {
32-
list = element.tagName === "OL" ? "ordered" : "bullet";
33-
}
34-
}
35-
36-
// apply list format to delta
37-
return delta.reduce((newDelta, op) => {
38-
if (!op.insert) return newDelta;
39-
if (op.attributes && op.attributes[format]) {
40-
return newDelta.push(op);
41-
}
42-
const formats = list ? { [format]: list } : {};
43-
44-
return newDelta.insert(op.insert, { ...formats, ...op.attributes });
45-
}, new Delta());
46-
});
148+
this.addMatcher("ol, ul", matchList);
47149
}
48150
}

packages/pluggableWidgets/rich-text-web/src/utils/modules/toolbarHandlers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,15 @@ export function shiftEnterKeyKeyboardHandler(this: Keyboard, range: Range, conte
8080
if (context.format.table) {
8181
return true;
8282
}
83+
84+
if (context.suffix === "") {
85+
// if it is on the end of block
86+
// we need to insert two soft breaks to create a new line within the same block
87+
// this is to override /n behavior
88+
this.quill.insertEmbed(range.index, "softbreak", true, Quill.sources.USER);
89+
}
8390
this.quill.insertEmbed(range.index, "softbreak", true, Quill.sources.USER);
84-
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
91+
this.quill.setSelection(range.index + 2, Quill.sources.SILENT);
8592
return false;
8693
}
8794

0 commit comments

Comments
 (0)