@gcforms/editor 0.0.1 → 1.0.1

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.
@@ -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 { useTranslation } from "@i18n/client"; // @TODO: inject i18n
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 [lastSelection, setLastSelection] = useState<
62
- RangeSelection | NodeSelection | BaseSelection | null
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 parent = node.getParent();
73
- if ($isLinkNode(parent)) {
74
- setLinkUrl(parent.getURL());
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
- selection !== null &&
93
- nativeSelection !== null &&
94
- rootElement !== null &&
95
- rootElement.contains(nativeSelection.anchorNode) &&
96
- editor.isEditable()
97
- ) {
98
- const domRect: DOMRect | undefined =
99
- nativeSelection.focusNode?.parentElement?.getBoundingClientRect();
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
- setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem, -30, -10);
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, -30, -10);
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, popped]);
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
- <input
212
- ref={inputRef}
213
- className="link-input"
214
- value={linkUrl}
215
- onChange={(event) => {
216
- setLinkUrl(event.target.value);
217
- }}
218
- onKeyDown={(event) => {
219
- if (event.key === "Enter" || event.key === "Escape" || event.key === "Tab") {
220
- event.preventDefault();
221
- if (lastSelection !== null) {
222
- if (linkUrl !== "") {
223
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(linkUrl));
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
- <div className="link-input">
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
- title={t("editLink")}
235
- aria-label={t("editLink")}
236
- className="relative w-full truncate pr-5"
237
- onMouseDown={(event) => event.preventDefault()}
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
- editor.focus();
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 === "Tab") {
244
- setIsLink(false);
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
- popped.current = true;
249
- setIsLinkEditMode(true);
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
- {linkUrl}
253
- <EditIcon title={t("editLink")} className="absolute right-0 inline-block size-5" />
371
+ <span className="sr-only">{t("removeLink")}</span>
372
+ <DeleteIcon />
254
373
  </button>
255
374
  </div>
256
- {/* <LinkPreview url={linkUrl} /> */}
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
- const updateToolbar = useCallback(() => {
273
- const selection = $getSelection();
274
- if ($isRangeSelection(selection)) {
275
- const node = getSelectedNode(selection);
276
- const linkParent = $findMatchingParent(node, $isLinkNode);
277
- const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode);
278
-
279
- // We don't want this menu to open for auto links.
280
- if (linkParent != null && autoLinkParent == null) {
281
- setIsLink(true);
282
- } else {
283
- setIsLink(false);
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
- useEffect(() => {
289
- return editor.registerCommand(
290
- SELECTION_CHANGE_COMMAND,
291
- (_payload, newEditor) => {
292
- updateToolbar();
293
- setActiveEditor(newEditor);
294
- return false;
295
- },
296
- COMMAND_PRIORITY_CRITICAL
297
- );
298
- }, [editor, updateToolbar]);
299
-
300
- return isLink
301
- ? createPortal(
302
- <FloatingLinkEditor
303
- editor={activeEditor}
304
- isLink={isLink}
305
- anchorElem={anchorElem}
306
- setIsLink={setIsLink}
307
- isLinkEditMode={isLinkEditMode}
308
- setIsLinkEditMode={setIsLinkEditMode}
309
- />,
310
- anchorElem
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
- : null;
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,73 @@
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 { JSX, useEffect, useState } from "react";
14
+ import "./styles.css";
15
+
16
+ const MaxLengthIndicator = ({
17
+ maxLength,
18
+ contentLength,
19
+ }: {
20
+ maxLength?: number;
21
+ contentLength: number;
22
+ }) => {
23
+ if (!maxLength) {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ // Only show if contentLength reaches 80% of maxLength
29
+ contentLength >= 0.8 * maxLength && (
30
+ <div className="gc-editor-max-length">
31
+ {contentLength >= maxLength ? maxLength : contentLength}/{maxLength}
32
+ </div>
33
+ )
34
+ );
35
+ };
36
+
37
+ export function MaxLengthPlugin({ maxLength }: { maxLength: number }): JSX.Element {
38
+ const [editor] = useLexicalComposerContext();
39
+ const [contentLength, setContentLength] = useState(0);
40
+
41
+ useEffect(() => {
42
+ let lastRestoredEditorState: EditorState | null = null;
43
+
44
+ return editor.registerNodeTransform(RootNode, (rootNode: RootNode) => {
45
+ const selection = $getSelection();
46
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
47
+ return;
48
+ }
49
+ const prevEditorState = editor.getEditorState();
50
+ const prevTextContentSize = prevEditorState.read(() => rootNode.getTextContentSize());
51
+ const textContentSize = rootNode.getTextContentSize();
52
+ setContentLength(textContentSize);
53
+
54
+ if (prevTextContentSize !== textContentSize) {
55
+ const delCount = textContentSize - maxLength;
56
+ const anchor = selection.anchor;
57
+
58
+ if (delCount > 0) {
59
+ // Restore the old editor state instead if the last
60
+ // text content was already at the limit.
61
+ if (prevTextContentSize === maxLength && lastRestoredEditorState !== prevEditorState) {
62
+ lastRestoredEditorState = prevEditorState;
63
+ $restoreEditorState(editor, prevEditorState);
64
+ } else {
65
+ $trimTextContentFromAnchor(editor, anchor, delCount);
66
+ }
67
+ }
68
+ }
69
+ });
70
+ }, [editor, maxLength, setContentLength]);
71
+
72
+ return <MaxLengthIndicator contentLength={contentLength} maxLength={maxLength} />;
73
+ }
@@ -0,0 +1,7 @@
1
+ .gc-editor-max-length {
2
+ position: absolute;
3
+ bottom: 0;
4
+ right: 8px;
5
+ font-size: 0.8rem;
6
+ color: #999;
7
+ }