@crystallize/design-system 1.5.0 → 1.7.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 +22 -0
- package/dist/index.css +85 -142
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1526 -4411
- package/dist/index.mjs +1391 -4294
- package/package.json +19 -19
- package/src/iconography/document.tsx +19 -0
- package/src/iconography/folder.tsx +30 -0
- package/src/iconography/index.ts +8 -2
- package/src/iconography/product.tsx +42 -0
- package/src/rich-text-editor/model/crystallize-to-lexical.ts +1 -1
- package/src/rich-text-editor/model/lexical-to-crystallize.ts +2 -2
- package/src/rich-text-editor/model/to-text.test.ts +97 -0
- package/src/rich-text-editor/model/to-text.ts +47 -0
- package/src/rich-text-editor/nodes/BaseNodes.ts +0 -3
- package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +14 -3
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +1 -1
- package/src/rich-text-editor/plugins/ComponentPickerPlugin/index.tsx +5 -5
- package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.tsx +29 -50
- package/src/rich-text-editor/plugins/MaxLengthPlugin/index.tsx +34 -23
- package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +8 -8
- package/src/rich-text-editor/rich-text-editor.css +3 -3
- package/src/rich-text-editor/rich-text-editor.stories.tsx +9 -1
- package/src/rich-text-editor/rich-text-editor.tsx +30 -24
- package/src/rich-text-editor/tests/rich-text-editor-basic-rendering.test.tsx +1 -1
- package/src/rich-text-editor/tests/rich-text-editor-model-basics.test.tsx +1 -1
- package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +1 -1
- package/src/rich-text-editor/tests/rich-text-editor-text-formats.test.tsx +1 -1
- package/src/rich-text-editor/themes/{PlaygroundEditorTheme.css → CrystallizeRTEditorTheme.css} +81 -85
- package/src/rich-text-editor/themes/CrystallizeRTEditorTheme.ts +113 -0
- package/dist/draggable-block-menu-KKHDNKJA.svg +0 -1
- package/src/rich-text-editor/appSettings.ts +0 -28
- package/src/rich-text-editor/context/SettingsContext.tsx +0 -71
- package/src/rich-text-editor/context/SharedAutocompleteContext.tsx +0 -60
- package/src/rich-text-editor/nodes/AutocompleteNode.tsx +0 -96
- package/src/rich-text-editor/plugins/AutocompletePlugin/index.tsx +0 -2536
- package/src/rich-text-editor/themes/PlaygroundEditorTheme.ts +0 -113
- /package/src/rich-text-editor/{model → types}/crystallize-rich-text-types/code.ts +0 -0
- /package/src/rich-text-editor/{model → types}/crystallize-rich-text-types/headings.ts +0 -0
- /package/src/rich-text-editor/{model → types}/crystallize-rich-text-types/index.ts +0 -0
- /package/src/rich-text-editor/{model → types}/crystallize-rich-text-types/link.ts +0 -0
- /package/src/rich-text-editor/{model → types}/crystallize-rich-text-types/table.ts +0 -0
- /package/src/rich-text-editor/{types.ts → types/types.ts} +0 -0
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
import './index.css';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {eventFiles} from '@lexical/rich-text';
|
|
12
|
-
import {mergeRegister} from '@lexical/utils';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import { DragEvent as ReactDragEvent, useEffect, useRef, useState } from 'react';
|
|
13
11
|
import {
|
|
14
12
|
$getNearestNodeFromDOMNode,
|
|
15
13
|
$getNodeByKey,
|
|
@@ -20,13 +18,14 @@ import {
|
|
|
20
18
|
DROP_COMMAND,
|
|
21
19
|
LexicalEditor,
|
|
22
20
|
} from 'lexical';
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
21
|
+
import { createPortal } from 'react-dom';
|
|
22
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
23
|
+
import { eventFiles } from '@lexical/rich-text';
|
|
24
|
+
import { mergeRegister } from '@lexical/utils';
|
|
26
25
|
|
|
27
|
-
import {isHTMLElement} from '../../utils/guard';
|
|
28
|
-
import {Point} from '../../utils/point';
|
|
29
|
-
import {Rect} from '../../utils/rect';
|
|
26
|
+
import { isHTMLElement } from '../../utils/guard';
|
|
27
|
+
import { Point } from '../../utils/point';
|
|
28
|
+
import { Rect } from '../../utils/rect';
|
|
30
29
|
|
|
31
30
|
const SPACE = 4;
|
|
32
31
|
const TARGET_LINE_HALF_HEIGHT = 2;
|
|
@@ -55,11 +54,7 @@ function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
|
|
|
55
54
|
return editor.getEditorState().read(() => $getRoot().getChildrenKeys());
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
function getBlockElement(
|
|
59
|
-
anchorElem: HTMLElement,
|
|
60
|
-
editor: LexicalEditor,
|
|
61
|
-
event: MouseEvent,
|
|
62
|
-
): HTMLElement | null {
|
|
57
|
+
function getBlockElement(anchorElem: HTMLElement, editor: LexicalEditor, event: MouseEvent): HTMLElement | null {
|
|
63
58
|
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
64
59
|
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
|
|
65
60
|
|
|
@@ -77,7 +72,7 @@ function getBlockElement(
|
|
|
77
72
|
}
|
|
78
73
|
const point = new Point(event.x, event.y);
|
|
79
74
|
const domRect = Rect.fromDOM(elem);
|
|
80
|
-
const {marginTop, marginBottom} = window.getComputedStyle(elem);
|
|
75
|
+
const { marginTop, marginBottom } = window.getComputedStyle(elem);
|
|
81
76
|
|
|
82
77
|
const rect = domRect.generateNewRect({
|
|
83
78
|
bottom: domRect.bottom + parseFloat(marginBottom),
|
|
@@ -88,7 +83,7 @@ function getBlockElement(
|
|
|
88
83
|
|
|
89
84
|
const {
|
|
90
85
|
result,
|
|
91
|
-
reason: {isOnTopSide, isOnBottomSide},
|
|
86
|
+
reason: { isOnTopSide, isOnBottomSide },
|
|
92
87
|
} = rect.contains(point);
|
|
93
88
|
|
|
94
89
|
if (result) {
|
|
@@ -119,11 +114,7 @@ function isOnMenu(element: HTMLElement): boolean {
|
|
|
119
114
|
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`);
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
function setMenuPosition(
|
|
123
|
-
targetElem: HTMLElement | null,
|
|
124
|
-
floatingElem: HTMLElement,
|
|
125
|
-
anchorElem: HTMLElement,
|
|
126
|
-
) {
|
|
117
|
+
function setMenuPosition(targetElem: HTMLElement | null, floatingElem: HTMLElement, anchorElem: HTMLElement) {
|
|
127
118
|
if (!targetElem) {
|
|
128
119
|
floatingElem.style.opacity = '0';
|
|
129
120
|
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
|
@@ -136,9 +127,7 @@ function setMenuPosition(
|
|
|
136
127
|
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
137
128
|
|
|
138
129
|
const top =
|
|
139
|
-
targetRect.top +
|
|
140
|
-
(parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 -
|
|
141
|
-
anchorElementRect.top;
|
|
130
|
+
targetRect.top + (parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 - anchorElementRect.top;
|
|
142
131
|
|
|
143
132
|
const left = SPACE;
|
|
144
133
|
|
|
@@ -146,11 +135,8 @@ function setMenuPosition(
|
|
|
146
135
|
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
147
136
|
}
|
|
148
137
|
|
|
149
|
-
function setDragImage(
|
|
150
|
-
|
|
151
|
-
draggableBlockElem: HTMLElement,
|
|
152
|
-
) {
|
|
153
|
-
const {transform} = draggableBlockElem.style;
|
|
138
|
+
function setDragImage(dataTransfer: DataTransfer, draggableBlockElem: HTMLElement) {
|
|
139
|
+
const { transform } = draggableBlockElem.style;
|
|
154
140
|
|
|
155
141
|
// Remove dragImage borders
|
|
156
142
|
draggableBlockElem.style.transform = 'translateZ(0)';
|
|
@@ -168,10 +154,8 @@ function setTargetLine(
|
|
|
168
154
|
anchorElem: HTMLElement,
|
|
169
155
|
) {
|
|
170
156
|
const targetStyle = window.getComputedStyle(targetBlockElem);
|
|
171
|
-
const {top: targetBlockElemTop, height: targetBlockElemHeight} =
|
|
172
|
-
|
|
173
|
-
const {top: anchorTop, width: anchorWidth} =
|
|
174
|
-
anchorElem.getBoundingClientRect();
|
|
157
|
+
const { top: targetBlockElemTop, height: targetBlockElemHeight } = targetBlockElem.getBoundingClientRect();
|
|
158
|
+
const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect();
|
|
175
159
|
|
|
176
160
|
let lineTop = targetBlockElemTop;
|
|
177
161
|
// At the bottom of the target
|
|
@@ -185,9 +169,7 @@ function setTargetLine(
|
|
|
185
169
|
const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
|
|
186
170
|
|
|
187
171
|
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
188
|
-
targetLineElem.style.width = `${
|
|
189
|
-
anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2
|
|
190
|
-
}px`;
|
|
172
|
+
targetLineElem.style.width = `${anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2}px`;
|
|
191
173
|
targetLineElem.style.opacity = '.4';
|
|
192
174
|
}
|
|
193
175
|
|
|
@@ -198,17 +180,12 @@ function hideTargetLine(targetLineElem: HTMLElement | null) {
|
|
|
198
180
|
}
|
|
199
181
|
}
|
|
200
182
|
|
|
201
|
-
function useDraggableBlockMenu(
|
|
202
|
-
editor: LexicalEditor,
|
|
203
|
-
anchorElem: HTMLElement,
|
|
204
|
-
isEditable: boolean,
|
|
205
|
-
): JSX.Element {
|
|
183
|
+
function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, isEditable: boolean): JSX.Element {
|
|
206
184
|
const scrollerElem = anchorElem.parentElement;
|
|
207
185
|
|
|
208
186
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
209
187
|
const targetLineRef = useRef<HTMLDivElement>(null);
|
|
210
|
-
const [draggableBlockElem, setDraggableBlockElem] =
|
|
211
|
-
useState<HTMLElement | null>(null);
|
|
188
|
+
const [draggableBlockElem, setDraggableBlockElem] = useState<HTMLElement | null>(null);
|
|
212
189
|
|
|
213
190
|
useEffect(() => {
|
|
214
191
|
function onMouseMove(event: MouseEvent) {
|
|
@@ -252,7 +229,7 @@ function useDraggableBlockMenu(
|
|
|
252
229
|
if (isFileTransfer) {
|
|
253
230
|
return false;
|
|
254
231
|
}
|
|
255
|
-
const {pageY, target} = event;
|
|
232
|
+
const { pageY, target } = event;
|
|
256
233
|
if (!isHTMLElement(target)) {
|
|
257
234
|
return false;
|
|
258
235
|
}
|
|
@@ -272,7 +249,7 @@ function useDraggableBlockMenu(
|
|
|
272
249
|
if (isFileTransfer) {
|
|
273
250
|
return false;
|
|
274
251
|
}
|
|
275
|
-
const {target, dataTransfer, pageY} = event;
|
|
252
|
+
const { target, dataTransfer, pageY } = event;
|
|
276
253
|
const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || '';
|
|
277
254
|
const draggedNode = $getNodeByKey(dragData);
|
|
278
255
|
if (!draggedNode) {
|
|
@@ -292,7 +269,7 @@ function useDraggableBlockMenu(
|
|
|
292
269
|
if (targetNode === draggedNode) {
|
|
293
270
|
return true;
|
|
294
271
|
}
|
|
295
|
-
const {top, height} = targetBlockElem.getBoundingClientRect();
|
|
272
|
+
const { top, height } = targetBlockElem.getBoundingClientRect();
|
|
296
273
|
const shouldInsertAfter = pageY - top > height / 2;
|
|
297
274
|
if (shouldInsertAfter) {
|
|
298
275
|
targetNode.insertAfter(draggedNode);
|
|
@@ -307,14 +284,14 @@ function useDraggableBlockMenu(
|
|
|
307
284
|
return mergeRegister(
|
|
308
285
|
editor.registerCommand(
|
|
309
286
|
DRAGOVER_COMMAND,
|
|
310
|
-
|
|
287
|
+
event => {
|
|
311
288
|
return onDragover(event);
|
|
312
289
|
},
|
|
313
290
|
COMMAND_PRIORITY_LOW,
|
|
314
291
|
),
|
|
315
292
|
editor.registerCommand(
|
|
316
293
|
DROP_COMMAND,
|
|
317
|
-
|
|
294
|
+
event => {
|
|
318
295
|
return onDrop(event);
|
|
319
296
|
},
|
|
320
297
|
COMMAND_PRIORITY_HIGH,
|
|
@@ -324,6 +301,7 @@ function useDraggableBlockMenu(
|
|
|
324
301
|
|
|
325
302
|
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
|
|
326
303
|
const dataTransfer = event.dataTransfer;
|
|
304
|
+
|
|
327
305
|
if (!dataTransfer || !draggableBlockElem) {
|
|
328
306
|
return;
|
|
329
307
|
}
|
|
@@ -349,7 +327,8 @@ function useDraggableBlockMenu(
|
|
|
349
327
|
ref={menuRef}
|
|
350
328
|
draggable={true}
|
|
351
329
|
onDragStart={onDragStart}
|
|
352
|
-
onDragEnd={onDragEnd}
|
|
330
|
+
onDragEnd={onDragEnd}
|
|
331
|
+
>
|
|
353
332
|
<div className={isEditable ? 'icon' : ''} />
|
|
354
333
|
</div>
|
|
355
334
|
<div className="draggable-block-target-line" ref={targetLineRef} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
-
import { $getSelection, $isRangeSelection, EditorState
|
|
2
|
+
import { $getSelection, $isRangeSelection, EditorState } from 'lexical';
|
|
3
3
|
/**
|
|
4
4
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
5
5
|
*
|
|
@@ -12,36 +12,47 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|
|
12
12
|
import { trimTextContentFromAnchor } from '@lexical/selection';
|
|
13
13
|
import { $restoreEditorState } from '@lexical/utils';
|
|
14
14
|
|
|
15
|
+
import { lexicalToCrystallizeRichText } from '../../model/lexical-to-crystallize';
|
|
16
|
+
import { toText } from '../../model/to-text';
|
|
17
|
+
|
|
15
18
|
export function MaxLengthPlugin({ maxLength }: { maxLength: number }): null {
|
|
16
19
|
const [editor] = useLexicalComposerContext();
|
|
17
20
|
|
|
18
21
|
useEffect(() => {
|
|
19
22
|
let lastRestoredEditorState: EditorState | null = null;
|
|
20
23
|
|
|
21
|
-
return editor.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
return editor.registerUpdateListener(({ editorState, prevEditorState }) => {
|
|
25
|
+
editor.update(() => {
|
|
26
|
+
const selection = $getSelection();
|
|
27
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const prevTextContent = toText(lexicalToCrystallizeRichText({ editorState: prevEditorState }));
|
|
32
|
+
|
|
33
|
+
// const editorState = rootNode.get
|
|
34
|
+
// createState
|
|
35
|
+
const textContent = toText(lexicalToCrystallizeRichText({ editorState }));
|
|
36
|
+
|
|
37
|
+
// const prevTextContent = prevEditorState.read(() => rootNode.getTextContent());
|
|
38
|
+
// const textContent = rootNode.getTextContent();
|
|
39
|
+
if (prevTextContent !== textContent) {
|
|
40
|
+
const textLength = textContent.length;
|
|
41
|
+
const delCount = textLength - maxLength;
|
|
42
|
+
const anchor = selection.anchor;
|
|
43
|
+
|
|
44
|
+
if (delCount > 0) {
|
|
45
|
+
// Restore the old editor state instead if the last
|
|
46
|
+
// text content was already at the limit.
|
|
47
|
+
if (prevTextContent.length === maxLength && lastRestoredEditorState !== prevEditorState) {
|
|
48
|
+
lastRestoredEditorState = prevEditorState;
|
|
49
|
+
$restoreEditorState(editor, prevEditorState);
|
|
50
|
+
} else {
|
|
51
|
+
trimTextContentFromAnchor(editor, anchor, delCount);
|
|
52
|
+
}
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
|
-
}
|
|
55
|
+
});
|
|
45
56
|
});
|
|
46
57
|
}, [editor, maxLength]);
|
|
47
58
|
|
|
@@ -45,7 +45,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|
|
45
45
|
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode';
|
|
46
46
|
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
47
47
|
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, HeadingTagType } from '@lexical/rich-text';
|
|
48
|
-
import { $selectAll, $
|
|
48
|
+
import { $selectAll, $setBlocksType } from '@lexical/selection';
|
|
49
49
|
import {
|
|
50
50
|
$findMatchingParent,
|
|
51
51
|
$getNearestBlockElementAncestorOrThrow,
|
|
@@ -58,7 +58,7 @@ import { Dialog } from '../../../dialog';
|
|
|
58
58
|
import { DropdownMenu } from '../../../dropdown-menu';
|
|
59
59
|
import { IconButton } from '../../../icon-button';
|
|
60
60
|
import { Icon } from '../../../iconography';
|
|
61
|
-
import type { CrystallizeRichTextActionMenuItem } from '../../types';
|
|
61
|
+
import type { CrystallizeRichTextActionMenuItem } from '../../types/types';
|
|
62
62
|
import { IS_APPLE } from '../../utils/environment';
|
|
63
63
|
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
64
64
|
import { sanitizeUrl } from '../../utils/url';
|
|
@@ -111,7 +111,7 @@ function BlockFormatDropDown({
|
|
|
111
111
|
editor.update(() => {
|
|
112
112
|
const selection = $getSelection();
|
|
113
113
|
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection))
|
|
114
|
-
$
|
|
114
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
};
|
|
@@ -121,7 +121,7 @@ function BlockFormatDropDown({
|
|
|
121
121
|
editor.update(() => {
|
|
122
122
|
const selection = $getSelection();
|
|
123
123
|
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
|
124
|
-
$
|
|
124
|
+
$setBlocksType(selection, () => $createHeadingNode(headingSize));
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
}
|
|
@@ -148,13 +148,13 @@ function BlockFormatDropDown({
|
|
|
148
148
|
editor.update(() => {
|
|
149
149
|
const selection = $getSelection();
|
|
150
150
|
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
|
151
|
-
$
|
|
151
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
152
152
|
} else {
|
|
153
153
|
/**
|
|
154
154
|
* Will select the entire editor, in case it is not selected. This is added
|
|
155
155
|
* to get the unit tests working
|
|
156
156
|
*/
|
|
157
|
-
$
|
|
157
|
+
$setBlocksType($getRoot().select(), () => $createQuoteNode());
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
}
|
|
@@ -167,7 +167,7 @@ function BlockFormatDropDown({
|
|
|
167
167
|
|
|
168
168
|
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
|
169
169
|
if (selection.isCollapsed()) {
|
|
170
|
-
$
|
|
170
|
+
$setBlocksType(selection, () => $createCodeNode());
|
|
171
171
|
} else {
|
|
172
172
|
const textContent = selection.getTextContent();
|
|
173
173
|
const codeNode = $createCodeNode();
|
|
@@ -180,7 +180,7 @@ function BlockFormatDropDown({
|
|
|
180
180
|
* Will select the entire editor, in case it is not selected. This is added
|
|
181
181
|
* to get the unit tests working
|
|
182
182
|
*/
|
|
183
|
-
$
|
|
183
|
+
$setBlocksType($getRoot().select(), () => $createCodeNode());
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
}
|
|
@@ -1333,17 +1333,17 @@
|
|
|
1333
1333
|
z-index: 3;
|
|
1334
1334
|
}
|
|
1335
1335
|
|
|
1336
|
-
.TableNode__contentEditable .
|
|
1336
|
+
.TableNode__contentEditable .CrystallizeRTEditorTheme__paragraph {
|
|
1337
1337
|
@apply mt-0;
|
|
1338
1338
|
}
|
|
1339
1339
|
|
|
1340
|
-
.
|
|
1340
|
+
.CrystallizeRTEditorTheme__blockCursor {
|
|
1341
1341
|
display: block;
|
|
1342
1342
|
pointer-events: none;
|
|
1343
1343
|
position: absolute;
|
|
1344
1344
|
}
|
|
1345
1345
|
|
|
1346
|
-
.
|
|
1346
|
+
.CrystallizeRTEditorTheme__blockCursor:after {
|
|
1347
1347
|
content: '';
|
|
1348
1348
|
display: block;
|
|
1349
1349
|
position: absolute;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
|
|
4
|
-
import type { CrystallizeRichText, CrystallizeRichTextNode } from './model/crystallize-rich-text-types';
|
|
5
4
|
import { RichTextEditor } from './rich-text-editor';
|
|
5
|
+
import type { CrystallizeRichText, CrystallizeRichTextNode } from './types/crystallize-rich-text-types';
|
|
6
6
|
|
|
7
7
|
const meta: Meta<typeof RichTextEditor> = {
|
|
8
8
|
title: 'Components/RichTextEditor',
|
|
@@ -218,6 +218,14 @@ export const BlankSheet: Story = {
|
|
|
218
218
|
},
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
+
export const MaxLength: Story = {
|
|
222
|
+
args: {
|
|
223
|
+
initialData: [],
|
|
224
|
+
placeholder: 'You can only write 10 characters in here',
|
|
225
|
+
maxLength: 10,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
221
229
|
export const CodeInlined: Story = {
|
|
222
230
|
args: {
|
|
223
231
|
initialData: [
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { EditorState } from 'lexical';
|
|
3
3
|
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
|
4
|
-
import { CharacterLimitPlugin } from '@lexical/react/LexicalCharacterLimitPlugin';
|
|
5
4
|
import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin';
|
|
6
5
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
7
6
|
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
|
@@ -14,20 +13,18 @@ import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin
|
|
|
14
13
|
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
|
15
14
|
|
|
16
15
|
import './rich-text-editor.css';
|
|
17
|
-
import {
|
|
16
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
17
|
+
|
|
18
18
|
import { SharedHistoryContext, useSharedHistoryContext } from './context/SharedHistoryContext';
|
|
19
|
-
import type { CrystallizeRichText } from './model/crystallize-rich-text-types';
|
|
20
19
|
import { composeInitialState } from './model/crystallize-to-lexical';
|
|
21
20
|
import { lexicalToCrystallizeRichText } from './model/lexical-to-crystallize';
|
|
22
21
|
import { BaseNodes } from './nodes/BaseNodes';
|
|
23
22
|
import { BaseNodes as TableCellNodes } from './nodes/TableCellNodes';
|
|
24
23
|
import AutoLinkPlugin from './plugins/AutoLinkPlugin';
|
|
25
|
-
import AutocompletePlugin from './plugins/AutocompletePlugin';
|
|
26
24
|
import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin';
|
|
27
25
|
import CodeHighlightPlugin from './plugins/CodeHighlightPlugin';
|
|
28
26
|
import ComponentPickerPlugin from './plugins/ComponentPickerPlugin';
|
|
29
27
|
import DragDropPaste from './plugins/DragDropPastePlugin';
|
|
30
|
-
import DraggableBlockPlugin from './plugins/DraggableBlockPlugin';
|
|
31
28
|
import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
|
|
32
29
|
import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin';
|
|
33
30
|
import LinkPlugin from './plugins/LinkPlugin';
|
|
@@ -40,13 +37,21 @@ import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
|
|
|
40
37
|
import TableCellResizer from './plugins/TableCellResizer';
|
|
41
38
|
import { TableContext, TablePlugin as NewTablePlugin } from './plugins/TablePlugin';
|
|
42
39
|
import ToolbarPlugin from './plugins/ToolbarPlugin';
|
|
43
|
-
import
|
|
44
|
-
import type {
|
|
40
|
+
import CrystallizeRTEditorTheme from './themes/CrystallizeRTEditorTheme';
|
|
41
|
+
import type { CrystallizeRichText } from './types/crystallize-rich-text-types';
|
|
42
|
+
import type { CrystallizeRichTextActionMenuItem } from './types/types';
|
|
45
43
|
import ContentEditable from './ui/ContentEditable';
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Currently disabling this as there is a conflict with someting within
|
|
47
|
+
* the app. @hk have done 15minutes of debugging without finding out what
|
|
48
|
+
* causes the conflict. We can re-visit this at some later time.
|
|
49
|
+
*/
|
|
50
|
+
// import DraggableBlockPlugin from './plugins/DraggableBlockPlugin';
|
|
51
|
+
|
|
52
|
+
// Export types
|
|
53
|
+
export type { CrystallizeRichText } from './types/crystallize-rich-text-types';
|
|
54
|
+
export * from './types/types';
|
|
50
55
|
|
|
51
56
|
type TRichTextBase = {
|
|
52
57
|
placeholder?: string;
|
|
@@ -56,6 +61,8 @@ type TRichTextBase = {
|
|
|
56
61
|
actionsMenuPrepend?: CrystallizeRichTextActionMenuItem[];
|
|
57
62
|
actionsMenuAppend?: CrystallizeRichTextActionMenuItem[];
|
|
58
63
|
slotPreContent?: ReactNode;
|
|
64
|
+
maxLength?: number;
|
|
65
|
+
editable?: boolean;
|
|
59
66
|
};
|
|
60
67
|
|
|
61
68
|
export function RichTextEditor({
|
|
@@ -64,7 +71,6 @@ export function RichTextEditor({
|
|
|
64
71
|
...rest
|
|
65
72
|
}: TRichTextBase & {
|
|
66
73
|
initialData?: CrystallizeRichText;
|
|
67
|
-
editable?: boolean;
|
|
68
74
|
}) {
|
|
69
75
|
return (
|
|
70
76
|
<LexicalComposer
|
|
@@ -75,7 +81,7 @@ export function RichTextEditor({
|
|
|
75
81
|
onError: (error: Error) => {
|
|
76
82
|
throw error;
|
|
77
83
|
},
|
|
78
|
-
theme:
|
|
84
|
+
theme: CrystallizeRTEditorTheme,
|
|
79
85
|
editorState: initialData
|
|
80
86
|
? composeInitialState({
|
|
81
87
|
richText: initialData,
|
|
@@ -86,7 +92,7 @@ export function RichTextEditor({
|
|
|
86
92
|
<SharedHistoryContext>
|
|
87
93
|
<TableContext>
|
|
88
94
|
<div className="c-rich-text-editor">
|
|
89
|
-
<RichTextEditorWithoutContext {...rest} />
|
|
95
|
+
<RichTextEditorWithoutContext {...rest} editable={editable} />
|
|
90
96
|
</div>
|
|
91
97
|
</TableContext>
|
|
92
98
|
</SharedHistoryContext>
|
|
@@ -102,17 +108,17 @@ function RichTextEditorWithoutContext({
|
|
|
102
108
|
actionsMenuPrepend,
|
|
103
109
|
actionsMenuAppend,
|
|
104
110
|
slotPreContent,
|
|
111
|
+
maxLength,
|
|
112
|
+
editable,
|
|
105
113
|
}: TRichTextBase): JSX.Element {
|
|
106
114
|
const { historyState } = useSharedHistoryContext();
|
|
107
|
-
const {
|
|
108
|
-
settings: { isAutocomplete, isMaxLength, isCharLimit, isCharLimitUtf8 },
|
|
109
|
-
} = useSettings();
|
|
110
115
|
const placeholder = (
|
|
111
116
|
<div className="text-[14px] font-normal text-gray-300-600 italic absolute overflow-hidden text-ellipsis top-0 left-6 right-10 select-none whitespace-nowrap inline-block pointer-events-none ">
|
|
112
117
|
{placeholderText ?? ''}
|
|
113
118
|
</div>
|
|
114
119
|
);
|
|
115
120
|
|
|
121
|
+
const [editor] = useLexicalComposerContext();
|
|
116
122
|
const [floatingAnchorElem, setFloatingAnchorElem] = useState<HTMLDivElement | null>(null);
|
|
117
123
|
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState<boolean>(false);
|
|
118
124
|
const firstOnChangeTriggeredRef = useRef(!autoFocus);
|
|
@@ -123,6 +129,11 @@ function RichTextEditorWithoutContext({
|
|
|
123
129
|
}
|
|
124
130
|
};
|
|
125
131
|
|
|
132
|
+
// Listen for editable change
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
editor.setEditable(editable || false);
|
|
135
|
+
}, [editable, editor]);
|
|
136
|
+
|
|
126
137
|
// Deal with different window sizes
|
|
127
138
|
useEffect(() => {
|
|
128
139
|
const updateViewPortWidth = () => {
|
|
@@ -156,7 +167,7 @@ function RichTextEditorWithoutContext({
|
|
|
156
167
|
<ToolbarPlugin actionsMenuPrepend={actionsMenuPrepend} actionsMenuAppend={actionsMenuAppend} />
|
|
157
168
|
{slotPreContent}
|
|
158
169
|
<div className="editor-container">
|
|
159
|
-
{
|
|
170
|
+
{maxLength != null ? <MaxLengthPlugin maxLength={maxLength} /> : null}
|
|
160
171
|
<DragDropPaste />
|
|
161
172
|
{!autoFocus ? null : <AutoFocusPlugin />}
|
|
162
173
|
<ClearEditorPlugin />
|
|
@@ -190,7 +201,7 @@ function RichTextEditorWithoutContext({
|
|
|
190
201
|
onError: (error: Error) => {
|
|
191
202
|
throw error;
|
|
192
203
|
},
|
|
193
|
-
theme:
|
|
204
|
+
theme: CrystallizeRTEditorTheme,
|
|
194
205
|
}}
|
|
195
206
|
>
|
|
196
207
|
{!autoFocus ? <></> : <AutoFocusPlugin />}
|
|
@@ -210,18 +221,13 @@ function RichTextEditorWithoutContext({
|
|
|
210
221
|
<TabIndentationPlugin />
|
|
211
222
|
{floatingAnchorElem && !isSmallWidthViewport && (
|
|
212
223
|
<>
|
|
213
|
-
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
|
224
|
+
{/* <DraggableBlockPlugin anchorElem={floatingAnchorElem} /> */}
|
|
214
225
|
<CodeActionMenuPlugin anchorElem={floatingAnchorElem} />
|
|
215
226
|
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
|
|
216
227
|
<TableCellActionMenuPlugin anchorElem={floatingAnchorElem} />
|
|
217
228
|
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
|
218
229
|
</>
|
|
219
230
|
)}
|
|
220
|
-
|
|
221
|
-
{(isCharLimit || isCharLimitUtf8) && (
|
|
222
|
-
<CharacterLimitPlugin charset={isCharLimit ? 'UTF-16' : 'UTF-8'} maxLength={5} />
|
|
223
|
-
)}
|
|
224
|
-
{isAutocomplete && <AutocompletePlugin />}
|
|
225
231
|
</div>
|
|
226
232
|
</>
|
|
227
233
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
|
|
4
|
-
import type { CrystallizeRichText } from '../model/crystallize-rich-text-types';
|
|
5
4
|
import { RichTextEditor } from '../rich-text-editor';
|
|
5
|
+
import type { CrystallizeRichText } from '../types/crystallize-rich-text-types';
|
|
6
6
|
import { sleep } from './utils';
|
|
7
7
|
|
|
8
8
|
const emptyParagraph: CrystallizeRichText = [{ kind: 'block', type: 'paragraph', textContent: 'rabbit' }];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
2
|
|
|
3
|
-
import type { CrystallizeRichText } from '../model/crystallize-rich-text-types';
|
|
4
3
|
import { RichTextEditor } from '../rich-text-editor';
|
|
4
|
+
import type { CrystallizeRichText } from '../types/crystallize-rich-text-types';
|
|
5
5
|
import { sleep } from './utils';
|
|
6
6
|
|
|
7
7
|
describe('RichTextEditor model basics', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
2
|
|
|
3
|
-
import type { CrystallizeRichText, CrystallizeRichTextNode } from '../model/crystallize-rich-text-types';
|
|
4
3
|
import { RichTextEditor } from '../rich-text-editor';
|
|
4
|
+
import type { CrystallizeRichText, CrystallizeRichTextNode } from '../types/crystallize-rich-text-types';
|
|
5
5
|
import { sleep } from './utils';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
|
|
5
|
-
import type { CrystallizeRichText } from '../model/crystallize-rich-text-types';
|
|
6
5
|
import { RichTextEditor } from '../rich-text-editor';
|
|
6
|
+
import type { CrystallizeRichText } from '../types/crystallize-rich-text-types';
|
|
7
7
|
import { selectTextInElement } from './utils';
|
|
8
8
|
|
|
9
9
|
describe('RichTextEditor text formats', () => {
|