@crystallize/design-system 1.9.0 → 1.11.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 +26 -0
- package/dist/index.css +15 -17
- package/dist/index.d.ts +10 -3
- package/dist/index.js +528 -470
- package/dist/index.mjs +477 -423
- package/package.json +1 -1
- package/src/rich-text-editor/i18n/i18n.test.ts +14 -0
- package/src/rich-text-editor/i18n/index.tsx +64 -0
- package/src/rich-text-editor/i18n/translations/en.ts +66 -0
- package/src/rich-text-editor/i18n/types.ts +62 -0
- package/src/rich-text-editor/model/crystallize-to-lexical.ts +29 -1
- package/src/rich-text-editor/model/lexical-to-crystallize.ts +54 -29
- package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +5 -22
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +4 -1
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +11 -1
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +2 -1
- package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +23 -5
- package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +21 -10
- package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +4 -12
- package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +23 -14
- package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +33 -33
- package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +6 -4
- package/src/rich-text-editor/rich-text-editor.css +6 -0
- package/src/rich-text-editor/rich-text-editor.stories.tsx +38 -0
- package/src/rich-text-editor/rich-text-editor.tsx +15 -9
- package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +23 -1
- package/src/rich-text-editor/types/crystallize-rich-text-types/index.ts +2 -4
- package/src/rich-text-editor/ui/LinkPreview.tsx +3 -1
- package/src/rich-text-editor/ui/ContentEditable.css +0 -13
- package/src/rich-text-editor/ui/ContentEditable.tsx +0 -15
|
@@ -25,6 +25,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|
|
25
25
|
import { mergeRegister } from '@lexical/utils';
|
|
26
26
|
|
|
27
27
|
import { IconButton } from '../../../icon-button';
|
|
28
|
+
import { useTr } from '../../i18n';
|
|
29
|
+
import { IS_APPLE } from '../../utils/environment';
|
|
28
30
|
import { getDOMRangeRect } from '../../utils/getDOMRangeRect';
|
|
29
31
|
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
30
32
|
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
|
|
@@ -53,6 +55,7 @@ function TextFormatFloatingToolbar({
|
|
|
53
55
|
isUnderline: boolean;
|
|
54
56
|
}): JSX.Element {
|
|
55
57
|
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
|
58
|
+
const tr = useTr();
|
|
56
59
|
|
|
57
60
|
const insertLink = useCallback(() => {
|
|
58
61
|
if (!isLink) {
|
|
@@ -139,7 +142,8 @@ function TextFormatFloatingToolbar({
|
|
|
139
142
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
140
143
|
}}
|
|
141
144
|
style={{ padding: 0, overflow: 'hidden' }}
|
|
142
|
-
|
|
145
|
+
title={tr(IS_APPLE ? 'actionFormatAsStrongTitleApple' : 'actionFormatAsStrongTitle')}
|
|
146
|
+
aria-label={tr('actionFormatAsStrongLabel')}
|
|
143
147
|
>
|
|
144
148
|
<i
|
|
145
149
|
className={`format bold w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -152,7 +156,8 @@ function TextFormatFloatingToolbar({
|
|
|
152
156
|
onClick={() => {
|
|
153
157
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
154
158
|
}}
|
|
155
|
-
|
|
159
|
+
title={tr('actionFormatAsEmphasizedTitle')}
|
|
160
|
+
aria-label={tr('actionFormatAsEmphasizedLabel')}
|
|
156
161
|
>
|
|
157
162
|
<i
|
|
158
163
|
className={`format italic w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -165,7 +170,8 @@ function TextFormatFloatingToolbar({
|
|
|
165
170
|
onClick={() => {
|
|
166
171
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
|
167
172
|
}}
|
|
168
|
-
|
|
173
|
+
title={tr('actionFormatAsUnderlinedTitle')}
|
|
174
|
+
aria-label={tr('actionFormatAsUnderlinedLabel')}
|
|
169
175
|
>
|
|
170
176
|
<i
|
|
171
177
|
className={`format underline w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -178,7 +184,8 @@ function TextFormatFloatingToolbar({
|
|
|
178
184
|
onClick={() => {
|
|
179
185
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
180
186
|
}}
|
|
181
|
-
|
|
187
|
+
title={tr('actionFormatWithStrikethroughTitle')}
|
|
188
|
+
aria-label={tr('actionFormatWithStrikethroughLabel')}
|
|
182
189
|
>
|
|
183
190
|
<i
|
|
184
191
|
className={`format strikethrough w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -191,8 +198,8 @@ function TextFormatFloatingToolbar({
|
|
|
191
198
|
onClick={() => {
|
|
192
199
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
|
193
200
|
}}
|
|
194
|
-
title=
|
|
195
|
-
aria-label=
|
|
201
|
+
title={tr('actionFormatWithSubscriptTitle')}
|
|
202
|
+
aria-label={tr('actionFormatWithSubscriptLabel')}
|
|
196
203
|
>
|
|
197
204
|
<i
|
|
198
205
|
className={`format subscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -205,8 +212,8 @@ function TextFormatFloatingToolbar({
|
|
|
205
212
|
onClick={() => {
|
|
206
213
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
|
|
207
214
|
}}
|
|
208
|
-
title=
|
|
209
|
-
aria-label=
|
|
215
|
+
title={tr('actionFormatWithSuperscriptTitle')}
|
|
216
|
+
aria-label={tr('actionFormatWithSuperscriptLabel')}
|
|
210
217
|
>
|
|
211
218
|
<i
|
|
212
219
|
className={`format superscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -219,7 +226,7 @@ function TextFormatFloatingToolbar({
|
|
|
219
226
|
onClick={() => {
|
|
220
227
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
221
228
|
}}
|
|
222
|
-
aria-label=
|
|
229
|
+
aria-label={tr('actionInsertCodeBlock')}
|
|
223
230
|
>
|
|
224
231
|
<i
|
|
225
232
|
className={`format code w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -227,7 +234,11 @@ function TextFormatFloatingToolbar({
|
|
|
227
234
|
}`}
|
|
228
235
|
/>
|
|
229
236
|
</IconButton>
|
|
230
|
-
<IconButton
|
|
237
|
+
<IconButton
|
|
238
|
+
style={{ padding: 0, overflow: 'hidden' }}
|
|
239
|
+
onClick={insertLink}
|
|
240
|
+
aria-label={tr('actionInsertlink')}
|
|
241
|
+
>
|
|
231
242
|
<i
|
|
232
243
|
className={`format link w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
233
244
|
isLink ? 'bg-purple-50-900 opacity-100' : 'opacity-60'
|
|
@@ -6,14 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
$isRangeSelection,
|
|
13
|
-
$setSelection,
|
|
14
|
-
FOCUS_COMMAND,
|
|
15
|
-
} from 'lexical';
|
|
16
|
-
import {useEffect} from 'react';
|
|
9
|
+
import { useEffect } from 'react';
|
|
10
|
+
import { $getSelection, $isRangeSelection, $setSelection, FOCUS_COMMAND } from 'lexical';
|
|
11
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
17
12
|
|
|
18
13
|
const COMMAND_PRIORITY_LOW = 1;
|
|
19
14
|
const TAB_TO_FOCUS_INTERVAL = 100;
|
|
@@ -48,10 +43,7 @@ export default function TabFocusPlugin(): null {
|
|
|
48
43
|
(event: FocusEvent) => {
|
|
49
44
|
const selection = $getSelection();
|
|
50
45
|
if ($isRangeSelection(selection)) {
|
|
51
|
-
if (
|
|
52
|
-
lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL >
|
|
53
|
-
event.timeStamp
|
|
54
|
-
) {
|
|
46
|
+
if (lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL > event.timeStamp) {
|
|
55
47
|
$setSelection(selection.clone());
|
|
56
48
|
}
|
|
57
49
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { DropdownMenu } from '../../../dropdown-menu';
|
|
34
34
|
import { IconButton } from '../../../icon-button';
|
|
35
35
|
import { Icon } from '../../../iconography';
|
|
36
|
+
import { useTr } from '../../i18n';
|
|
36
37
|
|
|
37
38
|
type TableStats = {
|
|
38
39
|
rows: number;
|
|
@@ -51,6 +52,7 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
|
|
|
51
52
|
columns: 1,
|
|
52
53
|
rows: 1,
|
|
53
54
|
});
|
|
55
|
+
const tr = useTr();
|
|
54
56
|
|
|
55
57
|
useEffect(() => {
|
|
56
58
|
return editor.registerMutationListener(TableCellNode, nodeMutations => {
|
|
@@ -254,41 +256,48 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
|
|
|
254
256
|
return (
|
|
255
257
|
<>
|
|
256
258
|
<DropdownMenu.Item onSelect={() => insertTableRowAtSelection(false)}>
|
|
257
|
-
|
|
259
|
+
{tr('actionTableInsertRowsAbove', selectionCounts.rows)}
|
|
258
260
|
</DropdownMenu.Item>
|
|
259
261
|
<DropdownMenu.Item onSelect={() => insertTableRowAtSelection(true)}>
|
|
260
|
-
|
|
262
|
+
{tr('actionTableInsertRowsBelow', selectionCounts.rows)}
|
|
261
263
|
</DropdownMenu.Item>
|
|
262
264
|
<DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(false)}>
|
|
263
|
-
|
|
265
|
+
{tr('actionTableInsertColumnsBefore', selectionCounts.columns)}
|
|
264
266
|
</DropdownMenu.Item>
|
|
265
267
|
<DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(true)}>
|
|
266
|
-
|
|
268
|
+
{tr('actionTableInsertColumnsAfter', selectionCounts.columns)}
|
|
267
269
|
</DropdownMenu.Item>
|
|
268
270
|
<DropdownMenu.Item onSelect={() => toggleTableRowIsHeader()}>
|
|
269
|
-
{(
|
|
270
|
-
|
|
271
|
+
{tr(
|
|
272
|
+
(tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW
|
|
273
|
+
? 'actionTableRemoveRowHeader'
|
|
274
|
+
: 'actionTableAddRowHeader',
|
|
275
|
+
)}
|
|
271
276
|
</DropdownMenu.Item>
|
|
272
277
|
<DropdownMenu.Item onSelect={() => toggleTableColumnIsHeader()}>
|
|
273
|
-
{(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
{tr(
|
|
279
|
+
(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
|
|
280
|
+
? 'actionTableRemoveColumnHeader'
|
|
281
|
+
: 'actionTableAddColumnHeader',
|
|
282
|
+
)}
|
|
277
283
|
</DropdownMenu.Item>
|
|
278
284
|
<DropdownMenu.Separator />
|
|
279
285
|
{tableStats.columns > 1 && (
|
|
280
|
-
<DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>
|
|
286
|
+
<DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>
|
|
287
|
+
{tr('actionTableDeleteColumn')}
|
|
288
|
+
</DropdownMenu.Item>
|
|
281
289
|
)}
|
|
282
290
|
{tableStats.rows > 1 && (
|
|
283
|
-
<DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>
|
|
291
|
+
<DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>{tr('actionTableDeleteRow')}</DropdownMenu.Item>
|
|
284
292
|
)}
|
|
285
|
-
<DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>
|
|
293
|
+
<DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>{tr('actionTableDeleteTable')}</DropdownMenu.Item>
|
|
286
294
|
</>
|
|
287
295
|
);
|
|
288
296
|
}
|
|
289
297
|
|
|
290
298
|
function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }) {
|
|
291
299
|
const [editor] = useLexicalComposerContext();
|
|
300
|
+
const tr = useTr();
|
|
292
301
|
|
|
293
302
|
const menuButtonRef = useRef(null);
|
|
294
303
|
|
|
@@ -393,7 +402,7 @@ function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement
|
|
|
393
402
|
onOpenChange={isOpen => setIsMenuOpen(isOpen)}
|
|
394
403
|
content={<TableActionMenu tableCellNode={tableCellNode} tableStats={tableStats} />}
|
|
395
404
|
>
|
|
396
|
-
<IconButton size="xs" className="table-cell-action-button">
|
|
405
|
+
<IconButton size="xs" className="table-cell-action-button" aria-label={tr('actionTableOpenOptions')}>
|
|
397
406
|
<Icon.Arrow />
|
|
398
407
|
</IconButton>
|
|
399
408
|
</DropdownMenu.Root>
|
|
@@ -58,6 +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 { useTr } from '../../i18n';
|
|
61
62
|
import type { CrystallizeRichTextActionMenuItem } from '../../types/types';
|
|
62
63
|
import { IS_APPLE } from '../../utils/environment';
|
|
63
64
|
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
@@ -301,6 +302,7 @@ export default function ToolbarPlugin({
|
|
|
301
302
|
const [isCode, setIsCode] = useState(false);
|
|
302
303
|
const [canUndo, setCanUndo] = useState(false);
|
|
303
304
|
const [canRedo, setCanRedo] = useState(false);
|
|
305
|
+
const tr = useTr();
|
|
304
306
|
|
|
305
307
|
const [codeLanguage, setCodeLanguage] = useState<string>('');
|
|
306
308
|
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
|
|
@@ -453,9 +455,9 @@ export default function ToolbarPlugin({
|
|
|
453
455
|
onClick={() => {
|
|
454
456
|
activeEditor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
455
457
|
}}
|
|
456
|
-
title={IS_APPLE ? 'Undo (⌘Z)' : 'Undo (Ctrl+Z)'}
|
|
457
458
|
type="button"
|
|
458
|
-
|
|
459
|
+
title={tr(IS_APPLE ? 'actionUndoTitleApple' : 'actionUndoTitle')}
|
|
460
|
+
aria-label={tr('actionUndoLabel')}
|
|
459
461
|
>
|
|
460
462
|
<i
|
|
461
463
|
className={`format icon undo border w-4 h-6 bg-no-repeat bg-center bg-[length:17px_17px] ${
|
|
@@ -469,9 +471,9 @@ export default function ToolbarPlugin({
|
|
|
469
471
|
onClick={() => {
|
|
470
472
|
activeEditor.dispatchCommand(REDO_COMMAND, undefined);
|
|
471
473
|
}}
|
|
472
|
-
title={IS_APPLE ? 'Redo (⌘Y)' : 'Redo (Ctrl+Y)'}
|
|
473
474
|
type="button"
|
|
474
|
-
|
|
475
|
+
title={tr(IS_APPLE ? 'actionRedoTitleApple' : 'actionRedoTitle')}
|
|
476
|
+
aria-label={tr('actionRedoLabel')}
|
|
475
477
|
>
|
|
476
478
|
<i
|
|
477
479
|
className={`format icon redo border w-4 h-6 bg-no-repeat bg-center bg-[length:17px_17px] ${
|
|
@@ -515,7 +517,7 @@ export default function ToolbarPlugin({
|
|
|
515
517
|
</>
|
|
516
518
|
}
|
|
517
519
|
>
|
|
518
|
-
<Button aria-label=
|
|
520
|
+
<Button aria-label={tr('codeSelectLanguage')} append={<Icon.Arrow />}>
|
|
519
521
|
<span className="font-medium text-sm">{getLanguageFriendlyName(codeLanguage)}</span>
|
|
520
522
|
</Button>
|
|
521
523
|
</DropdownMenu.Root>
|
|
@@ -525,10 +527,10 @@ export default function ToolbarPlugin({
|
|
|
525
527
|
<div className="flex gap-1">
|
|
526
528
|
<IconButton
|
|
527
529
|
disabled={!isEditable}
|
|
528
|
-
title={IS_APPLE ? 'Bold (⌘B)' : 'Bold (Ctrl+B)'}
|
|
529
530
|
className={`${isBold ? 'opacity-100 !bg-purple-50-900' : 'opacity-60'}`}
|
|
530
531
|
type="button"
|
|
531
|
-
|
|
532
|
+
title={tr(IS_APPLE ? 'actionFormatAsStrongTitleApple' : 'actionFormatAsStrongTitle')}
|
|
533
|
+
aria-label={tr('actionFormatAsStrongLabel')}
|
|
532
534
|
data-testid="toggle-format-bold"
|
|
533
535
|
onClick={() => {
|
|
534
536
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
@@ -542,9 +544,9 @@ export default function ToolbarPlugin({
|
|
|
542
544
|
onClick={() => {
|
|
543
545
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
544
546
|
}}
|
|
545
|
-
title={IS_APPLE ? 'Italic (⌘I)' : 'Italic (Ctrl+I)'}
|
|
546
547
|
type="button"
|
|
547
|
-
|
|
548
|
+
title={tr(IS_APPLE ? 'actionFormatAsEmphasizedTitleApple' : 'actionFormatAsEmphasizedTitle')}
|
|
549
|
+
aria-label={tr('actionFormatAsEmphasizedLabel')}
|
|
548
550
|
data-testid="toggle-format-emphasized"
|
|
549
551
|
>
|
|
550
552
|
<i className={`format icon italic border w-full h-full bg-no-repeat bg-center bg-[length:18px_18px]`} />
|
|
@@ -556,9 +558,9 @@ export default function ToolbarPlugin({
|
|
|
556
558
|
onClick={() => {
|
|
557
559
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
|
558
560
|
}}
|
|
559
|
-
title={IS_APPLE ? 'Underline (⌘U)' : 'Underline (Ctrl+U)'}
|
|
560
561
|
type="button"
|
|
561
|
-
|
|
562
|
+
title={tr(IS_APPLE ? 'actionFormatAsUnderlinedTitleApple' : 'actionFormatAsUnderlinedTitle')}
|
|
563
|
+
aria-label={tr('actionFormatAsUnderlinedLabel')}
|
|
562
564
|
data-testid="toggle-format-underlined"
|
|
563
565
|
>
|
|
564
566
|
<i
|
|
@@ -572,9 +574,9 @@ export default function ToolbarPlugin({
|
|
|
572
574
|
onClick={() => {
|
|
573
575
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
574
576
|
}}
|
|
575
|
-
title="Insert code block"
|
|
576
577
|
type="button"
|
|
577
|
-
|
|
578
|
+
title={tr('actionInsertCodeBlock')}
|
|
579
|
+
aria-label={tr('actionInsertCodeBlock')}
|
|
578
580
|
>
|
|
579
581
|
<i className={`format icon code border w-full h-full bg-no-repeat bg-center bg-[length:18px_18px]`} />
|
|
580
582
|
</IconButton>
|
|
@@ -583,9 +585,9 @@ export default function ToolbarPlugin({
|
|
|
583
585
|
className={`${isLink ? 'opacity-100 !bg-purple-50-900' : 'opacity-60'}`}
|
|
584
586
|
disabled={!isEditable}
|
|
585
587
|
onClick={insertLink}
|
|
586
|
-
aria-label="Insert link"
|
|
587
|
-
title="Insert link"
|
|
588
588
|
type="button"
|
|
589
|
+
aria-label={tr('actionInsertlink')}
|
|
590
|
+
title={tr('actionInsertlink')}
|
|
589
591
|
>
|
|
590
592
|
<i className={`format icon link border w-full h-full bg-no-repeat bg-center bg-[length:18px_18px]`} />
|
|
591
593
|
</IconButton>
|
|
@@ -599,8 +601,8 @@ export default function ToolbarPlugin({
|
|
|
599
601
|
onClick={() => {
|
|
600
602
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
601
603
|
}}
|
|
602
|
-
title=
|
|
603
|
-
aria-label=
|
|
604
|
+
title={tr('actionFormatWithStrikethroughTitle')}
|
|
605
|
+
aria-label={tr('actionFormatWithStrikethroughLabel')}
|
|
604
606
|
>
|
|
605
607
|
<i
|
|
606
608
|
className={`icon w-6 h-6 strikethrough border bg-no-repeat bg-center bg-[length:16px_16px] rounded-sm ${
|
|
@@ -608,15 +610,15 @@ export default function ToolbarPlugin({
|
|
|
608
610
|
}`}
|
|
609
611
|
/>
|
|
610
612
|
<span className={`px-3 text-sm font-sans ${isStrikethrough ? 'font-medium' : 'font-normal'}`}>
|
|
611
|
-
|
|
613
|
+
{tr('actionFormatAsStrongTitle')}
|
|
612
614
|
</span>
|
|
613
615
|
</DropdownMenu.Item>
|
|
614
616
|
<DropdownMenu.Item
|
|
615
617
|
onClick={() => {
|
|
616
618
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
|
617
619
|
}}
|
|
618
|
-
title=
|
|
619
|
-
aria-label=
|
|
620
|
+
title={tr('actionFormatWithSubscriptTitle')}
|
|
621
|
+
aria-label={tr('actionFormatWithSubscriptLabel')}
|
|
620
622
|
>
|
|
621
623
|
<i
|
|
622
624
|
className={`icon w-6 h-6 subscript border bg-no-repeat bg-center bg-[length:16px_16px] rounded-sm ${
|
|
@@ -624,15 +626,15 @@ export default function ToolbarPlugin({
|
|
|
624
626
|
}`}
|
|
625
627
|
/>
|
|
626
628
|
<span className={`px-3 text-sm font-sans ${isSubscript ? 'font-medium' : 'font-normal'}`}>
|
|
627
|
-
|
|
629
|
+
{tr('actionFormatWithSubscriptTitle')}
|
|
628
630
|
</span>
|
|
629
631
|
</DropdownMenu.Item>
|
|
630
632
|
<DropdownMenu.Item
|
|
631
633
|
onClick={() => {
|
|
632
634
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
|
|
633
635
|
}}
|
|
634
|
-
title=
|
|
635
|
-
aria-label=
|
|
636
|
+
title={tr('actionFormatWithSuperscriptTitle')}
|
|
637
|
+
aria-label={tr('actionFormatWithSuperscriptLabel')}
|
|
636
638
|
>
|
|
637
639
|
<i
|
|
638
640
|
className={`icon w-6 h-6 superscript border bg-no-repeat bg-center bg-[length:16px_16px] rounded-sm ${
|
|
@@ -640,14 +642,14 @@ export default function ToolbarPlugin({
|
|
|
640
642
|
}`}
|
|
641
643
|
/>
|
|
642
644
|
<span className={`px-3 text-sm font-sans ${isSuperscript ? 'bg-purple-50-900' : 'font-normal'}`}>
|
|
643
|
-
|
|
645
|
+
{tr('actionFormatWithSuperscriptTitle')}
|
|
644
646
|
</span>
|
|
645
647
|
</DropdownMenu.Item>
|
|
646
648
|
<DropdownMenu.Item
|
|
647
649
|
onClick={clearFormatting}
|
|
648
650
|
className="item"
|
|
649
|
-
title=
|
|
650
|
-
aria-label=
|
|
651
|
+
title={tr('actionClearTextFormatting')}
|
|
652
|
+
aria-label={tr('actionClearTextFormatting')}
|
|
651
653
|
>
|
|
652
654
|
<i className="icon w-6 h-6 clear border bg-no-repeat bg-center bg-[length:16px_16px] opacity-60" />
|
|
653
655
|
<span className="px-3 text-sm text-pink-600-300 font-sans font-normal">Clear Formatting</span>
|
|
@@ -657,7 +659,7 @@ export default function ToolbarPlugin({
|
|
|
657
659
|
>
|
|
658
660
|
<Button
|
|
659
661
|
style={{ backgroundColor: 'transparent', padding: '0 8px' }}
|
|
660
|
-
aria-label=
|
|
662
|
+
aria-label={tr('actionTextFormattingOptions')}
|
|
661
663
|
>
|
|
662
664
|
<i className={`icon dropdown-more border bg-no-repeat bg-center bg-[length:18px_18px] w-6 h-6`} />
|
|
663
665
|
<Icon.Arrow />
|
|
@@ -676,14 +678,14 @@ export default function ToolbarPlugin({
|
|
|
676
678
|
>
|
|
677
679
|
<div className="flex items-center font-sans font-normal">
|
|
678
680
|
<i className="icon w-5 h-5 horizontal-rule border bg-no-repeat bg-center bg-[length:16px_16px] opacity-60" />
|
|
679
|
-
<span className="px-3 text-sm">
|
|
681
|
+
<span className="px-3 text-sm">{tr('horizontalRule')}</span>
|
|
680
682
|
</div>
|
|
681
683
|
</DropdownMenu.Item>
|
|
682
684
|
<DropdownMenu.Item>
|
|
683
685
|
<Dialog.Trigger asChild>
|
|
684
686
|
<div className="flex items-center font-sans font-normal">
|
|
685
687
|
<i className="icon w-5 h-5 table border bg-no-repeat bg-center bg-[length:16px_16px] opacity-60" />
|
|
686
|
-
<span className="px-3 text-sm">
|
|
688
|
+
<span className="px-3 text-sm">{tr('table')}</span>
|
|
687
689
|
</div>
|
|
688
690
|
</Dialog.Trigger>
|
|
689
691
|
</DropdownMenu.Item>
|
|
@@ -695,10 +697,8 @@ export default function ToolbarPlugin({
|
|
|
695
697
|
</IconButton>
|
|
696
698
|
</DropdownMenu.Root>
|
|
697
699
|
<Dialog.Content>
|
|
698
|
-
<Dialog.Title>
|
|
699
|
-
<Dialog.Description>
|
|
700
|
-
Define your starting point of a table, you can add and remove columns and rows after creation.
|
|
701
|
-
</Dialog.Description>
|
|
700
|
+
<Dialog.Title>{tr('insertTableTitle')}</Dialog.Title>
|
|
701
|
+
<Dialog.Description>{tr('insertTableDescription')}</Dialog.Description>
|
|
702
702
|
<div className="items-center justify-between">
|
|
703
703
|
<InsertTableDialog activeEditor={activeEditor} />
|
|
704
704
|
</div>
|
|
@@ -5,10 +5,12 @@ import { INSERT_TABLE_COMMAND } from '@lexical/table';
|
|
|
5
5
|
import { Button } from '../../../button';
|
|
6
6
|
import { Dialog } from '../../../dialog';
|
|
7
7
|
import { InputWithLabel } from '../../../input-with-label';
|
|
8
|
+
import { useTr } from '../../i18n';
|
|
8
9
|
|
|
9
10
|
export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEditor }) {
|
|
10
11
|
const [rows, setRows] = useState('5');
|
|
11
12
|
const [columns, setColumns] = useState('5');
|
|
13
|
+
const tr = useTr();
|
|
12
14
|
|
|
13
15
|
const onClick = () => {
|
|
14
16
|
if (parseInt(rows) < 1 || parseInt(columns) < 1) {
|
|
@@ -28,7 +30,7 @@ export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEdito
|
|
|
28
30
|
<>
|
|
29
31
|
<div className="grid grid-cols-[1fr_1px_1fr] border border-gray-100-800 border-solid shadow-sm rounded-md ">
|
|
30
32
|
<InputWithLabel
|
|
31
|
-
label=
|
|
33
|
+
label={tr('insertTableRows')}
|
|
32
34
|
value={rows}
|
|
33
35
|
placeholder="0"
|
|
34
36
|
type="text"
|
|
@@ -38,7 +40,7 @@ export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEdito
|
|
|
38
40
|
<span className="h-full bg-gray-100-800" />
|
|
39
41
|
<InputWithLabel
|
|
40
42
|
type="text"
|
|
41
|
-
label=
|
|
43
|
+
label={tr('insertTableColumns')}
|
|
42
44
|
placeholder="0"
|
|
43
45
|
value={columns}
|
|
44
46
|
inputMode="numeric"
|
|
@@ -46,8 +48,8 @@ export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEdito
|
|
|
46
48
|
/>
|
|
47
49
|
</div>
|
|
48
50
|
<div className="flex justify-end mt-3">
|
|
49
|
-
<Button as={Dialog.Close} size="sm" intent="action" aria-label=
|
|
50
|
-
|
|
51
|
+
<Button as={Dialog.Close} size="sm" intent="action" aria-label={tr('insertTableCommit')} onClick={onClick}>
|
|
52
|
+
{tr('insertTableCommit')}
|
|
51
53
|
</Button>
|
|
52
54
|
</div>
|
|
53
55
|
</>
|
|
@@ -997,6 +997,12 @@
|
|
|
997
997
|
user-select: none;
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
+
.ContentEditable__root {
|
|
1001
|
+
tab-size: 1;
|
|
1002
|
+
min-height: calc(100% - 16px);
|
|
1003
|
+
@apply relative block border-0 px-6 py-2 !pt-0 text-sm outline-0;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1000
1006
|
.TableNode__contentEditable {
|
|
1001
1007
|
min-height: 20px;
|
|
1002
1008
|
border: 0px;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
|
|
4
|
+
import { labelTranslationEn } from './i18n';
|
|
4
5
|
import { RichTextEditor } from './rich-text-editor';
|
|
5
6
|
import type { CrystallizeRichText, CrystallizeRichTextNode } from './types/crystallize-rich-text-types';
|
|
6
7
|
|
|
@@ -421,3 +422,40 @@ export const Quote: Story = {
|
|
|
421
422
|
},
|
|
422
423
|
},
|
|
423
424
|
};
|
|
425
|
+
|
|
426
|
+
export const MultipleInlineFormats: Story = {
|
|
427
|
+
args: {
|
|
428
|
+
initialData: [
|
|
429
|
+
{
|
|
430
|
+
type: 'paragraph',
|
|
431
|
+
kind: 'block',
|
|
432
|
+
children: [
|
|
433
|
+
{
|
|
434
|
+
kind: 'inline',
|
|
435
|
+
type: 'strong',
|
|
436
|
+
children: [
|
|
437
|
+
{
|
|
438
|
+
kind: 'inline',
|
|
439
|
+
textContent: 'placed',
|
|
440
|
+
type: 'emphasized',
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
onChange(data) {
|
|
448
|
+
console.log('onChange');
|
|
449
|
+
console.log(JSON.stringify(data, null, 1));
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
export const CustomTranslation: Story = {
|
|
455
|
+
args: {
|
|
456
|
+
labelTranslations: {
|
|
457
|
+
...labelTranslationEn,
|
|
458
|
+
actionClear: '💥 Kaboom',
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
};
|
|
@@ -14,8 +14,11 @@ import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
|
|
14
14
|
|
|
15
15
|
import './rich-text-editor.css';
|
|
16
16
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
17
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
17
18
|
|
|
18
19
|
import { SharedHistoryContext, useSharedHistoryContext } from './context/SharedHistoryContext';
|
|
20
|
+
import { I18nProvider, SupportedLanguages } from './i18n';
|
|
21
|
+
import type { I18N } from './i18n/types';
|
|
19
22
|
import { composeInitialState } from './model/crystallize-to-lexical';
|
|
20
23
|
import { lexicalToCrystallizeRichText } from './model/lexical-to-crystallize';
|
|
21
24
|
import { BaseNodes } from './nodes/BaseNodes';
|
|
@@ -33,7 +36,6 @@ import ToolbarPlugin from './plugins/ToolbarPlugin';
|
|
|
33
36
|
import CrystallizeRTEditorTheme from './themes/CrystallizeRTEditorTheme';
|
|
34
37
|
import type { CrystallizeRichText } from './types/crystallize-rich-text-types';
|
|
35
38
|
import type { CrystallizeRichTextActionMenuItem } from './types/types';
|
|
36
|
-
import ContentEditable from './ui/ContentEditable';
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* Currently disabling this as there is a conflict with someting within
|
|
@@ -56,11 +58,15 @@ type TRichTextBase = {
|
|
|
56
58
|
slotPreContent?: ReactNode;
|
|
57
59
|
maxLength?: number;
|
|
58
60
|
editable?: boolean;
|
|
61
|
+
language?: SupportedLanguages;
|
|
62
|
+
labelTranslations?: I18N;
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
export function RichTextEditor({
|
|
62
66
|
initialData,
|
|
63
67
|
editable = true,
|
|
68
|
+
language = 'en',
|
|
69
|
+
labelTranslations,
|
|
64
70
|
...rest
|
|
65
71
|
}: TRichTextBase & {
|
|
66
72
|
initialData?: CrystallizeRichText;
|
|
@@ -82,13 +88,13 @@ export function RichTextEditor({
|
|
|
82
88
|
: undefined,
|
|
83
89
|
}}
|
|
84
90
|
>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</
|
|
91
|
+
<I18nProvider language={language} labelTranslations={labelTranslations}>
|
|
92
|
+
<SharedHistoryContext>
|
|
93
|
+
<div className="c-rich-text-editor">
|
|
94
|
+
<RichTextEditorWithoutContext {...rest} editable={editable} />
|
|
95
|
+
</div>
|
|
96
|
+
</SharedHistoryContext>
|
|
97
|
+
</I18nProvider>
|
|
92
98
|
</LexicalComposer>
|
|
93
99
|
);
|
|
94
100
|
}
|
|
@@ -171,7 +177,7 @@ function RichTextEditorWithoutContext({
|
|
|
171
177
|
contentEditable={
|
|
172
178
|
<div className="editor-scroller">
|
|
173
179
|
<div className="editor" ref={onRef}>
|
|
174
|
-
<ContentEditable />
|
|
180
|
+
<ContentEditable className="ContentEditable__root" />
|
|
175
181
|
</div>
|
|
176
182
|
</div>
|
|
177
183
|
}
|
|
@@ -189,11 +189,33 @@ const dataTypes: { name: string; model: CrystallizeRichText }[] = [
|
|
|
189
189
|
},
|
|
190
190
|
],
|
|
191
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: 'Multiple inline formats',
|
|
194
|
+
model: [
|
|
195
|
+
{
|
|
196
|
+
type: 'paragraph',
|
|
197
|
+
kind: 'block',
|
|
198
|
+
children: [
|
|
199
|
+
{
|
|
200
|
+
kind: 'inline',
|
|
201
|
+
type: 'strong',
|
|
202
|
+
children: [
|
|
203
|
+
{
|
|
204
|
+
kind: 'inline',
|
|
205
|
+
textContent: 'placed',
|
|
206
|
+
type: 'emphasized',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
192
214
|
];
|
|
193
215
|
|
|
194
216
|
describe('RichTextEditor model conversions', () => {
|
|
195
217
|
dataTypes.forEach(dataType => {
|
|
196
|
-
it('correctly converts
|
|
218
|
+
it('correctly converts ' + dataType.name, async () => {
|
|
197
219
|
const onChange = vi.fn();
|
|
198
220
|
|
|
199
221
|
render(<RichTextEditor initialData={dataType.model} onChange={onChange} triggerOnChangeOnAutoFocus autoFocus />);
|
|
@@ -34,10 +34,8 @@ type CrystallizeRichTextGenericNode = {
|
|
|
34
34
|
| 'aside'
|
|
35
35
|
| 'container'
|
|
36
36
|
| 'details'
|
|
37
|
-
| 'emphasized'
|
|
38
37
|
| 'figcaption'
|
|
39
38
|
| 'figure'
|
|
40
|
-
| 'highlight'
|
|
41
39
|
| 'image'
|
|
42
40
|
| 'line-break'
|
|
43
41
|
| 'unordered-list'
|
|
@@ -51,9 +49,9 @@ type CrystallizeRichTextGenericNode = {
|
|
|
51
49
|
| 'title-of-a-work';
|
|
52
50
|
};
|
|
53
51
|
|
|
54
|
-
type CrystallizeRichTextInlineFormattedNodes = {
|
|
52
|
+
export type CrystallizeRichTextInlineFormattedNodes = {
|
|
55
53
|
kind: 'inline';
|
|
56
|
-
type: 'deleted' | 'strong' | 'subscripted' | 'superscripted' | 'underlined';
|
|
54
|
+
type: 'deleted' | 'strong' | 'subscripted' | 'superscripted' | 'underlined' | 'emphasized' | 'code' | 'highlight';
|
|
57
55
|
};
|
|
58
56
|
|
|
59
57
|
export type CrystallizeRichTextNode = CrystallizeRichTextNodeBase &
|