@gcforms/editor 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/package.json +5 -5
- package/src/Editor.tsx +93 -74
- package/src/config.ts +6 -3
- package/src/context/LocaleContext.tsx +31 -0
- package/src/context/ToolbarContext.tsx +86 -0
- package/src/hooks/useTranslation.tsx +19 -0
- package/src/i18n/en.json +22 -0
- package/src/i18n/fr.json +22 -0
- package/src/i18n/index.tsx +12 -0
- package/src/icons/CancelIcon.tsx +15 -0
- package/src/icons/CheckIcon.tsx +15 -0
- package/src/icons/DeleteIcon.tsx +15 -0
- package/src/icons/EditIcon.tsx +4 -3
- package/src/icons/LinkIcon.tsx +2 -2
- package/src/plugins/FloatingLinkEditorPlugin/index.css +50 -54
- package/src/plugins/FloatingLinkEditorPlugin/index.tsx +305 -138
- package/src/plugins/MaxLengthPlugin/index.tsx +56 -0
- package/src/plugins/ShortcutsPlugin/index.tsx +78 -0
- package/src/plugins/ShortcutsPlugin/shortcuts.ts +76 -0
- package/src/plugins/ToolbarPlugin/index.tsx +121 -149
- package/src/plugins/ToolbarPlugin/styles.css +4 -6
- package/src/plugins/ToolbarPlugin/utils.ts +172 -0
- package/src/styles.css +8 -8
- package/src/tests/Editor.cy.tsx +5 -3
- package/src/transformers/index.ts +18 -0
- package/src/ui/ContentEditable.css +2 -16
- package/src/ui/ContentEditable.tsx +2 -2
- package/src/ui/ToolTip.css +35 -0
- package/src/{ToolTip.tsx → ui/ToolTip.tsx} +4 -6
- package/src/utils/url.ts +7 -9
- package/src/themes/BasicTheme.ts +0 -69
|
@@ -9,24 +9,22 @@ import type { JSX } from "react";
|
|
|
9
9
|
|
|
10
10
|
import "./index.css";
|
|
11
11
|
|
|
12
|
-
import { $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
12
|
+
import { $createLinkNode, $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
13
13
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
14
14
|
import { $findMatchingParent, mergeRegister } from "@lexical/utils";
|
|
15
15
|
import {
|
|
16
16
|
$getSelection,
|
|
17
|
+
$isLineBreakNode,
|
|
18
|
+
$isNodeSelection,
|
|
17
19
|
$isRangeSelection,
|
|
18
|
-
BLUR_COMMAND,
|
|
19
20
|
BaseSelection,
|
|
21
|
+
CLICK_COMMAND,
|
|
20
22
|
COMMAND_PRIORITY_CRITICAL,
|
|
21
23
|
COMMAND_PRIORITY_HIGH,
|
|
22
24
|
COMMAND_PRIORITY_LOW,
|
|
23
|
-
COMMAND_PRIORITY_NORMAL,
|
|
24
25
|
getDOMSelection,
|
|
25
26
|
KEY_ESCAPE_COMMAND,
|
|
26
|
-
KEY_TAB_COMMAND,
|
|
27
27
|
LexicalEditor,
|
|
28
|
-
NodeSelection,
|
|
29
|
-
RangeSelection,
|
|
30
28
|
SELECTION_CHANGE_COMMAND,
|
|
31
29
|
} from "lexical";
|
|
32
30
|
import { Dispatch, useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -34,11 +32,19 @@ import * as React from "react";
|
|
|
34
32
|
import { createPortal } from "react-dom";
|
|
35
33
|
|
|
36
34
|
import { getSelectedNode } from "../../utils/getSelectedNode";
|
|
37
|
-
|
|
38
|
-
import { sanitizeUrl } from "../../utils/url";
|
|
39
35
|
import { setFloatingElemPositionForLinkEditor } from "../../utils/setFloatingElemPositionForLinkEditor";
|
|
36
|
+
import { isValidUrl, sanitizeUrl } from "../../utils/url";
|
|
37
|
+
import { DeleteIcon } from "../../icons/DeleteIcon";
|
|
40
38
|
import { EditIcon } from "../../icons/EditIcon";
|
|
41
|
-
import {
|
|
39
|
+
import { CheckIcon } from "../../icons/CheckIcon";
|
|
40
|
+
import { CancelIcon } from "../../icons/CancelIcon";
|
|
41
|
+
import { useTranslation } from "../../hooks/useTranslation";
|
|
42
|
+
|
|
43
|
+
function preventDefault(
|
|
44
|
+
event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLElement>
|
|
45
|
+
): void {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
}
|
|
42
48
|
|
|
43
49
|
function FloatingLinkEditor({
|
|
44
50
|
editor,
|
|
@@ -58,26 +64,44 @@ function FloatingLinkEditor({
|
|
|
58
64
|
const editorRef = useRef<HTMLDivElement | null>(null);
|
|
59
65
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
60
66
|
const [linkUrl, setLinkUrl] = useState("");
|
|
61
|
-
const [
|
|
62
|
-
|
|
63
|
-
>(null);
|
|
64
|
-
|
|
67
|
+
const [editedLinkUrl, setEditedLinkUrl] = useState("");
|
|
68
|
+
const [lastSelection, setLastSelection] = useState<BaseSelection | null>(null);
|
|
65
69
|
const { t } = useTranslation();
|
|
66
|
-
const popped = useRef(false);
|
|
67
70
|
|
|
68
|
-
const updateLinkEditor = useCallback(() => {
|
|
71
|
+
const $updateLinkEditor = useCallback(() => {
|
|
69
72
|
const selection = $getSelection();
|
|
70
73
|
if ($isRangeSelection(selection)) {
|
|
71
74
|
const node = getSelectedNode(selection);
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const linkParent = $findMatchingParent(node, $isLinkNode);
|
|
76
|
+
|
|
77
|
+
if (linkParent) {
|
|
78
|
+
setLinkUrl(linkParent.getURL());
|
|
75
79
|
} else if ($isLinkNode(node)) {
|
|
76
80
|
setLinkUrl(node.getURL());
|
|
77
81
|
} else {
|
|
78
82
|
setLinkUrl("");
|
|
79
83
|
}
|
|
84
|
+
if (isLinkEditMode) {
|
|
85
|
+
setEditedLinkUrl(linkUrl);
|
|
86
|
+
}
|
|
87
|
+
} else if ($isNodeSelection(selection)) {
|
|
88
|
+
const nodes = selection.getNodes();
|
|
89
|
+
if (nodes.length > 0) {
|
|
90
|
+
const node = nodes[0];
|
|
91
|
+
const parent = node.getParent();
|
|
92
|
+
if ($isLinkNode(parent)) {
|
|
93
|
+
setLinkUrl(parent.getURL());
|
|
94
|
+
} else if ($isLinkNode(node)) {
|
|
95
|
+
setLinkUrl(node.getURL());
|
|
96
|
+
} else {
|
|
97
|
+
setLinkUrl("");
|
|
98
|
+
}
|
|
99
|
+
if (isLinkEditMode) {
|
|
100
|
+
setEditedLinkUrl(linkUrl);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
80
103
|
}
|
|
104
|
+
|
|
81
105
|
const editorElem = editorRef.current;
|
|
82
106
|
const nativeSelection = getDOMSelection(editor._window);
|
|
83
107
|
const activeElement = document.activeElement;
|
|
@@ -88,22 +112,29 @@ function FloatingLinkEditor({
|
|
|
88
112
|
|
|
89
113
|
const rootElement = editor.getRootElement();
|
|
90
114
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
if (selection !== null && rootElement !== null && editor.isEditable()) {
|
|
116
|
+
let domRect: DOMRect | undefined;
|
|
117
|
+
|
|
118
|
+
if ($isNodeSelection(selection)) {
|
|
119
|
+
const nodes = selection.getNodes();
|
|
120
|
+
if (nodes.length > 0) {
|
|
121
|
+
const element = editor.getElementByKey(nodes[0].getKey());
|
|
122
|
+
if (element) {
|
|
123
|
+
domRect = element.getBoundingClientRect();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else if (nativeSelection !== null && rootElement.contains(nativeSelection.anchorNode)) {
|
|
127
|
+
domRect = nativeSelection.focusNode?.parentElement?.getBoundingClientRect();
|
|
128
|
+
}
|
|
129
|
+
|
|
100
130
|
if (domRect) {
|
|
101
|
-
|
|
131
|
+
domRect.y += 40;
|
|
132
|
+
setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem);
|
|
102
133
|
}
|
|
103
134
|
setLastSelection(selection);
|
|
104
135
|
} else if (!activeElement || activeElement.className !== "link-input") {
|
|
105
136
|
if (rootElement !== null) {
|
|
106
|
-
setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem
|
|
137
|
+
setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem);
|
|
107
138
|
}
|
|
108
139
|
setLastSelection(null);
|
|
109
140
|
setIsLinkEditMode(false);
|
|
@@ -111,14 +142,14 @@ function FloatingLinkEditor({
|
|
|
111
142
|
}
|
|
112
143
|
|
|
113
144
|
return true;
|
|
114
|
-
}, [anchorElem, editor, setIsLinkEditMode]);
|
|
145
|
+
}, [anchorElem, editor, setIsLinkEditMode, isLinkEditMode, linkUrl]);
|
|
115
146
|
|
|
116
147
|
useEffect(() => {
|
|
117
148
|
const scrollerElem = anchorElem.parentElement;
|
|
118
149
|
|
|
119
150
|
const update = () => {
|
|
120
151
|
editor.getEditorState().read(() => {
|
|
121
|
-
updateLinkEditor();
|
|
152
|
+
$updateLinkEditor();
|
|
122
153
|
});
|
|
123
154
|
};
|
|
124
155
|
|
|
@@ -135,25 +166,24 @@ function FloatingLinkEditor({
|
|
|
135
166
|
scrollerElem.removeEventListener("scroll", update);
|
|
136
167
|
}
|
|
137
168
|
};
|
|
138
|
-
}, [anchorElem.parentElement, editor, updateLinkEditor]);
|
|
169
|
+
}, [anchorElem.parentElement, editor, $updateLinkEditor]);
|
|
139
170
|
|
|
140
171
|
useEffect(() => {
|
|
141
172
|
return mergeRegister(
|
|
142
173
|
editor.registerUpdateListener(({ editorState }) => {
|
|
143
174
|
editorState.read(() => {
|
|
144
|
-
updateLinkEditor();
|
|
175
|
+
$updateLinkEditor();
|
|
145
176
|
});
|
|
146
177
|
}),
|
|
147
178
|
|
|
148
179
|
editor.registerCommand(
|
|
149
180
|
SELECTION_CHANGE_COMMAND,
|
|
150
181
|
() => {
|
|
151
|
-
updateLinkEditor();
|
|
182
|
+
$updateLinkEditor();
|
|
152
183
|
return true;
|
|
153
184
|
},
|
|
154
185
|
COMMAND_PRIORITY_LOW
|
|
155
186
|
),
|
|
156
|
-
// Hide link editor by pressing escape
|
|
157
187
|
editor.registerCommand(
|
|
158
188
|
KEY_ESCAPE_COMMAND,
|
|
159
189
|
() => {
|
|
@@ -164,97 +194,185 @@ function FloatingLinkEditor({
|
|
|
164
194
|
return false;
|
|
165
195
|
},
|
|
166
196
|
COMMAND_PRIORITY_HIGH
|
|
167
|
-
),
|
|
168
|
-
// Hide link editor when editor loses focus
|
|
169
|
-
editor.registerCommand(
|
|
170
|
-
BLUR_COMMAND,
|
|
171
|
-
() => {
|
|
172
|
-
if (isLink && !popped.current) {
|
|
173
|
-
setIsLink(false);
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
popped.current = false;
|
|
177
|
-
return false;
|
|
178
|
-
},
|
|
179
|
-
COMMAND_PRIORITY_NORMAL
|
|
180
|
-
),
|
|
181
|
-
// Don't hide link editor when tabbing into it
|
|
182
|
-
editor.registerCommand(
|
|
183
|
-
KEY_TAB_COMMAND,
|
|
184
|
-
() => {
|
|
185
|
-
if (isLink) {
|
|
186
|
-
popped.current = true;
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
return false;
|
|
190
|
-
},
|
|
191
|
-
COMMAND_PRIORITY_NORMAL
|
|
192
197
|
)
|
|
193
198
|
);
|
|
194
|
-
}, [editor, updateLinkEditor, setIsLink, isLink
|
|
199
|
+
}, [editor, $updateLinkEditor, setIsLink, isLink]);
|
|
195
200
|
|
|
196
201
|
useEffect(() => {
|
|
197
202
|
editor.getEditorState().read(() => {
|
|
198
|
-
updateLinkEditor();
|
|
203
|
+
$updateLinkEditor();
|
|
199
204
|
});
|
|
200
|
-
}, [editor, updateLinkEditor]);
|
|
205
|
+
}, [editor, $updateLinkEditor]);
|
|
201
206
|
|
|
202
207
|
useEffect(() => {
|
|
203
208
|
if (isLinkEditMode && inputRef.current) {
|
|
204
209
|
inputRef.current.focus();
|
|
205
210
|
}
|
|
206
|
-
}, [isLinkEditMode]);
|
|
211
|
+
}, [isLinkEditMode, isLink]);
|
|
212
|
+
|
|
213
|
+
const monitorInputInteraction = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
214
|
+
if (event.key === "Enter") {
|
|
215
|
+
if (isValidUrl(editedLinkUrl)) {
|
|
216
|
+
handleLinkSubmission(event as React.KeyboardEvent<HTMLInputElement>);
|
|
217
|
+
}
|
|
218
|
+
} else if (event.key === "Escape") {
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
setIsLinkEditMode(false);
|
|
221
|
+
if (linkUrl === "") {
|
|
222
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleLinkSubmission = (
|
|
228
|
+
event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLElement>
|
|
229
|
+
) => {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
if (lastSelection !== null) {
|
|
232
|
+
if (editedLinkUrl !== "") {
|
|
233
|
+
editor.update(() => {
|
|
234
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl));
|
|
235
|
+
const selection = $getSelection();
|
|
236
|
+
if ($isRangeSelection(selection)) {
|
|
237
|
+
const parent = getSelectedNode(selection).getParent();
|
|
238
|
+
if ($isAutoLinkNode(parent)) {
|
|
239
|
+
const linkNode = $createLinkNode(parent.getURL(), {
|
|
240
|
+
rel: parent.__rel,
|
|
241
|
+
target: parent.__target,
|
|
242
|
+
title: parent.__title,
|
|
243
|
+
});
|
|
244
|
+
parent.replace(linkNode, true);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
setEditedLinkUrl("");
|
|
250
|
+
setIsLinkEditMode(false);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
207
253
|
|
|
208
254
|
return (
|
|
209
|
-
<div ref={editorRef} className="gc-link-editor" data-testid="link-editor">
|
|
210
|
-
{isLinkEditMode ? (
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
event
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
<div ref={editorRef} className="gc-link-editor" data-testid="gc-link-editor">
|
|
256
|
+
{!isLink ? null : isLinkEditMode ? (
|
|
257
|
+
<div className="gc-link-editor-container">
|
|
258
|
+
<input
|
|
259
|
+
ref={inputRef}
|
|
260
|
+
value={editedLinkUrl}
|
|
261
|
+
placeholder="http://"
|
|
262
|
+
onChange={(event) => {
|
|
263
|
+
setEditedLinkUrl(event.target.value);
|
|
264
|
+
}}
|
|
265
|
+
onKeyDown={(event) => {
|
|
266
|
+
monitorInputInteraction(event);
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
<div className="gc-link-editor-actions">
|
|
270
|
+
<button
|
|
271
|
+
onMouseDown={preventDefault}
|
|
272
|
+
onClick={() => {
|
|
225
273
|
setIsLinkEditMode(false);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
274
|
+
if (linkUrl === "") {
|
|
275
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
276
|
+
}
|
|
277
|
+
}}
|
|
278
|
+
onKeyDown={(event) => {
|
|
279
|
+
if (event.key === "Enter") {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
setIsLinkEditMode(false);
|
|
282
|
+
if (linkUrl === "") {
|
|
283
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<span className="sr-only">{t("cancelEditLink")}</span>
|
|
289
|
+
<CancelIcon />
|
|
290
|
+
</button>
|
|
291
|
+
|
|
233
292
|
<button
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
293
|
+
className={!isValidUrl(editedLinkUrl) ? "gc-link-editor-button-disabled" : ""}
|
|
294
|
+
disabled={!isValidUrl(editedLinkUrl)}
|
|
295
|
+
onMouseDown={preventDefault}
|
|
296
|
+
onClick={handleLinkSubmission}
|
|
238
297
|
onKeyDown={(event) => {
|
|
298
|
+
if (event.key === "Enter") {
|
|
299
|
+
handleLinkSubmission(event as React.KeyboardEvent<HTMLInputElement>);
|
|
300
|
+
}
|
|
239
301
|
if (event.key === "Escape") {
|
|
240
302
|
event.preventDefault();
|
|
241
|
-
|
|
303
|
+
setIsLinkEditMode(false);
|
|
304
|
+
}
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
<span className="sr-only">{t("saveLink")}</span>
|
|
308
|
+
<CheckIcon />
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
) : (
|
|
313
|
+
<div className="gc-link-editor-container">
|
|
314
|
+
<button
|
|
315
|
+
className="link"
|
|
316
|
+
tabIndex={0}
|
|
317
|
+
onKeyDown={(event) => {
|
|
318
|
+
if (event.key === "Enter") {
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
setEditedLinkUrl(linkUrl);
|
|
321
|
+
setIsLinkEditMode(true);
|
|
322
|
+
}
|
|
323
|
+
}}
|
|
324
|
+
onClick={(event) => {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
setEditedLinkUrl(linkUrl);
|
|
327
|
+
setIsLinkEditMode(true);
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<span className="sr-only">{t("pressEnterToEditLink")}</span>
|
|
331
|
+
{linkUrl}
|
|
332
|
+
</button>
|
|
333
|
+
<div className="gc-link-editor-actions">
|
|
334
|
+
<button
|
|
335
|
+
onMouseDown={preventDefault}
|
|
336
|
+
onClick={(event) => {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
setEditedLinkUrl(linkUrl);
|
|
339
|
+
setIsLinkEditMode(true);
|
|
340
|
+
}}
|
|
341
|
+
onKeyDown={(event) => {
|
|
342
|
+
if (event.key === "Enter") {
|
|
343
|
+
setEditedLinkUrl(linkUrl);
|
|
344
|
+
setIsLinkEditMode(true);
|
|
242
345
|
}
|
|
243
|
-
if (event.key === "
|
|
244
|
-
|
|
346
|
+
if (event.key === "Escape") {
|
|
347
|
+
event.preventDefault();
|
|
348
|
+
setIsLinkEditMode(false);
|
|
245
349
|
}
|
|
246
350
|
}}
|
|
351
|
+
>
|
|
352
|
+
<span className="sr-only">{t("editLink")}</span>
|
|
353
|
+
<EditIcon />
|
|
354
|
+
</button>
|
|
355
|
+
<button
|
|
356
|
+
onMouseDown={preventDefault}
|
|
247
357
|
onClick={() => {
|
|
248
|
-
|
|
249
|
-
|
|
358
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
359
|
+
}}
|
|
360
|
+
onKeyDown={(event) => {
|
|
361
|
+
if (event.key === "Enter") {
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
364
|
+
}
|
|
365
|
+
if (event.key === "Escape") {
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
setIsLinkEditMode(false);
|
|
368
|
+
}
|
|
250
369
|
}}
|
|
251
370
|
>
|
|
252
|
-
{
|
|
253
|
-
<
|
|
371
|
+
<span className="sr-only">{t("removeLink")}</span>
|
|
372
|
+
<DeleteIcon />
|
|
254
373
|
</button>
|
|
255
374
|
</div>
|
|
256
|
-
|
|
257
|
-
</>
|
|
375
|
+
</div>
|
|
258
376
|
)}
|
|
259
377
|
</div>
|
|
260
378
|
);
|
|
@@ -269,47 +387,96 @@ function useFloatingLinkEditorToolbar(
|
|
|
269
387
|
const [activeEditor, setActiveEditor] = useState(editor);
|
|
270
388
|
const [isLink, setIsLink] = useState(false);
|
|
271
389
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
function $updateToolbar() {
|
|
392
|
+
const selection = $getSelection();
|
|
393
|
+
if ($isRangeSelection(selection)) {
|
|
394
|
+
const focusNode = getSelectedNode(selection);
|
|
395
|
+
const focusLinkNode = $findMatchingParent(focusNode, $isLinkNode);
|
|
396
|
+
const focusAutoLinkNode = $findMatchingParent(focusNode, $isAutoLinkNode);
|
|
397
|
+
if (!(focusLinkNode || focusAutoLinkNode)) {
|
|
398
|
+
setIsLink(false);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const badNode = selection
|
|
402
|
+
.getNodes()
|
|
403
|
+
.filter((node) => !$isLineBreakNode(node))
|
|
404
|
+
.find((node) => {
|
|
405
|
+
const linkNode = $findMatchingParent(node, $isLinkNode);
|
|
406
|
+
const autoLinkNode = $findMatchingParent(node, $isAutoLinkNode);
|
|
407
|
+
return (
|
|
408
|
+
(focusLinkNode && !focusLinkNode.is(linkNode)) ||
|
|
409
|
+
(linkNode && !linkNode.is(focusLinkNode)) ||
|
|
410
|
+
(focusAutoLinkNode && !focusAutoLinkNode.is(autoLinkNode)) ||
|
|
411
|
+
(autoLinkNode &&
|
|
412
|
+
(!autoLinkNode.is(focusAutoLinkNode) || autoLinkNode.getIsUnlinked()))
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
if (!badNode) {
|
|
416
|
+
setIsLink(true);
|
|
417
|
+
} else {
|
|
418
|
+
setIsLink(false);
|
|
419
|
+
}
|
|
420
|
+
} else if ($isNodeSelection(selection)) {
|
|
421
|
+
const nodes = selection.getNodes();
|
|
422
|
+
if (nodes.length === 0) {
|
|
423
|
+
setIsLink(false);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const node = nodes[0];
|
|
427
|
+
const parent = node.getParent();
|
|
428
|
+
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
429
|
+
setIsLink(true);
|
|
430
|
+
} else {
|
|
431
|
+
setIsLink(false);
|
|
432
|
+
}
|
|
284
433
|
}
|
|
285
434
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
435
|
+
return mergeRegister(
|
|
436
|
+
editor.registerUpdateListener(({ editorState }) => {
|
|
437
|
+
editorState.read(() => {
|
|
438
|
+
$updateToolbar();
|
|
439
|
+
});
|
|
440
|
+
}),
|
|
441
|
+
editor.registerCommand(
|
|
442
|
+
SELECTION_CHANGE_COMMAND,
|
|
443
|
+
(_payload, newEditor) => {
|
|
444
|
+
$updateToolbar();
|
|
445
|
+
setActiveEditor(newEditor);
|
|
446
|
+
return false;
|
|
447
|
+
},
|
|
448
|
+
COMMAND_PRIORITY_CRITICAL
|
|
449
|
+
),
|
|
450
|
+
editor.registerCommand(
|
|
451
|
+
CLICK_COMMAND,
|
|
452
|
+
(payload) => {
|
|
453
|
+
const selection = $getSelection();
|
|
454
|
+
if ($isRangeSelection(selection)) {
|
|
455
|
+
const node = getSelectedNode(selection);
|
|
456
|
+
const linkNode = $findMatchingParent(node, $isLinkNode);
|
|
457
|
+
if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) {
|
|
458
|
+
window.open(linkNode.getURL(), "_blank");
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
},
|
|
464
|
+
COMMAND_PRIORITY_LOW
|
|
311
465
|
)
|
|
312
|
-
|
|
466
|
+
);
|
|
467
|
+
}, [editor]);
|
|
468
|
+
|
|
469
|
+
return createPortal(
|
|
470
|
+
<FloatingLinkEditor
|
|
471
|
+
editor={activeEditor}
|
|
472
|
+
isLink={isLink}
|
|
473
|
+
anchorElem={anchorElem}
|
|
474
|
+
setIsLink={setIsLink}
|
|
475
|
+
isLinkEditMode={isLinkEditMode}
|
|
476
|
+
setIsLinkEditMode={setIsLinkEditMode}
|
|
477
|
+
/>,
|
|
478
|
+
anchorElem
|
|
479
|
+
);
|
|
313
480
|
}
|
|
314
481
|
|
|
315
482
|
export default function FloatingLinkEditorPlugin({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
10
|
+
import { $trimTextContentFromAnchor } from "@lexical/selection";
|
|
11
|
+
import { $restoreEditorState } from "@lexical/utils";
|
|
12
|
+
import { $getSelection, $isRangeSelection, EditorState, RootNode } from "lexical";
|
|
13
|
+
import { useEffect } from "react";
|
|
14
|
+
|
|
15
|
+
export function MaxLengthPlugin({
|
|
16
|
+
maxLength,
|
|
17
|
+
setContentLength,
|
|
18
|
+
}: {
|
|
19
|
+
maxLength: number;
|
|
20
|
+
setContentLength: (length: number) => void;
|
|
21
|
+
}): null {
|
|
22
|
+
const [editor] = useLexicalComposerContext();
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let lastRestoredEditorState: EditorState | null = null;
|
|
26
|
+
|
|
27
|
+
return editor.registerNodeTransform(RootNode, (rootNode: RootNode) => {
|
|
28
|
+
const selection = $getSelection();
|
|
29
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const prevEditorState = editor.getEditorState();
|
|
33
|
+
const prevTextContentSize = prevEditorState.read(() => rootNode.getTextContentSize());
|
|
34
|
+
const textContentSize = rootNode.getTextContentSize();
|
|
35
|
+
setContentLength(textContentSize);
|
|
36
|
+
|
|
37
|
+
if (prevTextContentSize !== textContentSize) {
|
|
38
|
+
const delCount = textContentSize - maxLength;
|
|
39
|
+
const anchor = selection.anchor;
|
|
40
|
+
|
|
41
|
+
if (delCount > 0) {
|
|
42
|
+
// Restore the old editor state instead if the last
|
|
43
|
+
// text content was already at the limit.
|
|
44
|
+
if (prevTextContentSize === maxLength && lastRestoredEditorState !== prevEditorState) {
|
|
45
|
+
lastRestoredEditorState = prevEditorState;
|
|
46
|
+
$restoreEditorState(editor, prevEditorState);
|
|
47
|
+
} else {
|
|
48
|
+
$trimTextContentFromAnchor(editor, anchor, delCount);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}, [editor, maxLength, setContentLength]);
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|