@crystallize/design-system 1.6.1 → 1.8.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 +13 -0
- package/dist/index.css +53 -332
- package/dist/index.d.ts +15 -8
- package/dist/index.js +1736 -4555
- package/dist/index.mjs +1655 -2339
- package/package.json +1 -1
- package/src/action-menu/action-item-separator.tsx +14 -0
- package/src/action-menu/action-item.tsx +2 -2
- package/src/action-menu/action-menu.css +8 -0
- package/src/action-menu/action-menu.tsx +2 -1
- package/src/dropdown-menu/dropdown-menu-root.tsx +3 -1
- package/src/dropdown-menu/index.ts +5 -2
- package/src/iconography/subscription-contracts.tsx +4 -4
- package/src/iconography/subscription-plans.tsx +5 -5
- package/src/rich-text-editor/model/crystallize-to-lexical.ts +12 -1
- package/src/rich-text-editor/model/lexical-to-crystallize.ts +38 -38
- package/src/rich-text-editor/nodes/BaseNodes.ts +0 -7
- package/src/rich-text-editor/nodes/TableCellNodes.ts +0 -7
- package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +13 -2
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +3 -2
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +0 -1
- package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.css +17 -17
- package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +1 -1
- package/src/rich-text-editor/plugins/MaxLengthPlugin/index.tsx +2 -7
- package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +80 -149
- package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +2 -2
- package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +55 -0
- package/src/rich-text-editor/rich-text-editor.css +10 -322
- package/src/rich-text-editor/rich-text-editor.stories.tsx +35 -5
- package/src/rich-text-editor/rich-text-editor.tsx +6 -39
- package/src/rich-text-editor/tests/rich-text-editor-code.test.tsx +10 -6
- package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +19 -7
- package/src/rich-text-editor/themes/CrystallizeRTEditorTheme.css +3 -11
- package/dist/TableComponent-I2YOOYOU.css +0 -281
- package/dist/TableComponent-QINOO453.mjs +0 -1377
- package/dist/chevron-down-3FRWSIKS.svg +0 -1
- package/dist/chunk-VUXQZRSP.mjs +0 -737
- package/dist/markdown-4BGQNLLT.svg +0 -1
- package/src/rich-text-editor/nodes/KeywordNode.ts +0 -73
- package/src/rich-text-editor/nodes/TableComponent.tsx +0 -1547
- package/src/rich-text-editor/nodes/TableNode.tsx +0 -398
- package/src/rich-text-editor/plugins/ComponentPickerPlugin/index.tsx +0 -320
- package/src/rich-text-editor/plugins/DragDropPastePlugin/index.ts +0 -40
- package/src/rich-text-editor/plugins/MarkdownShortcutPlugin/index.tsx +0 -16
- package/src/rich-text-editor/plugins/MarkdownTransformers/index.ts +0 -195
- package/src/rich-text-editor/plugins/SpeechToTextPlugin/index.ts +0 -113
- package/src/rich-text-editor/plugins/TableCellResizer/index.css +0 -12
- package/src/rich-text-editor/plugins/TableCellResizer/index.tsx +0 -386
- package/src/rich-text-editor/plugins/TablePlugin.tsx +0 -190
- package/src/rich-text-editor/plugins/TreeViewPlugin/index.tsx +0 -25
- package/src/rich-text-editor/plugins/TypingPerfPlugin/index.ts +0 -117
|
@@ -1,320 +0,0 @@
|
|
|
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 { useCallback, useMemo, useState } from 'react';
|
|
10
|
-
import * as React from 'react';
|
|
11
|
-
import { $createParagraphNode, $getSelection, $isRangeSelection, FORMAT_ELEMENT_COMMAND, TextNode } from 'lexical';
|
|
12
|
-
import * as ReactDOM from 'react-dom';
|
|
13
|
-
import { $createCodeNode } from '@lexical/code';
|
|
14
|
-
import { INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list';
|
|
15
|
-
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
16
|
-
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
17
|
-
import {
|
|
18
|
-
LexicalTypeaheadMenuPlugin,
|
|
19
|
-
TypeaheadOption,
|
|
20
|
-
useBasicTypeaheadTriggerMatch,
|
|
21
|
-
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
|
22
|
-
import { $createHeadingNode, $createQuoteNode } from '@lexical/rich-text';
|
|
23
|
-
import { $setBlocksType } from '@lexical/selection';
|
|
24
|
-
import { INSERT_TABLE_COMMAND } from '@lexical/table';
|
|
25
|
-
|
|
26
|
-
// import { InsertNewTableDialog } from '../TablePlugin';
|
|
27
|
-
|
|
28
|
-
class ComponentPickerOption extends TypeaheadOption {
|
|
29
|
-
// What shows up in the editor
|
|
30
|
-
title: string;
|
|
31
|
-
// Icon for display
|
|
32
|
-
icon?: JSX.Element;
|
|
33
|
-
// For extra searching.
|
|
34
|
-
keywords: Array<string>;
|
|
35
|
-
// TBD
|
|
36
|
-
keyboardShortcut?: string;
|
|
37
|
-
// What happens when you select this option?
|
|
38
|
-
onSelect: (queryString: string) => void;
|
|
39
|
-
|
|
40
|
-
constructor(
|
|
41
|
-
title: string,
|
|
42
|
-
options: {
|
|
43
|
-
icon?: JSX.Element;
|
|
44
|
-
keywords?: Array<string>;
|
|
45
|
-
keyboardShortcut?: string;
|
|
46
|
-
onSelect: (queryString: string) => void;
|
|
47
|
-
},
|
|
48
|
-
) {
|
|
49
|
-
super(title);
|
|
50
|
-
this.title = title;
|
|
51
|
-
this.keywords = options.keywords || [];
|
|
52
|
-
this.icon = options.icon;
|
|
53
|
-
this.keyboardShortcut = options.keyboardShortcut;
|
|
54
|
-
this.onSelect = options.onSelect.bind(this);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function ComponentPickerMenuItem({
|
|
59
|
-
index,
|
|
60
|
-
isSelected,
|
|
61
|
-
onClick,
|
|
62
|
-
onMouseEnter,
|
|
63
|
-
option,
|
|
64
|
-
}: {
|
|
65
|
-
index: number;
|
|
66
|
-
isSelected: boolean;
|
|
67
|
-
onClick: () => void;
|
|
68
|
-
onMouseEnter: () => void;
|
|
69
|
-
option: ComponentPickerOption;
|
|
70
|
-
}) {
|
|
71
|
-
let className = 'item';
|
|
72
|
-
if (isSelected) {
|
|
73
|
-
className += ' selected';
|
|
74
|
-
}
|
|
75
|
-
return (
|
|
76
|
-
<li
|
|
77
|
-
key={option.key}
|
|
78
|
-
tabIndex={-1}
|
|
79
|
-
className={className}
|
|
80
|
-
ref={option.setRefElement}
|
|
81
|
-
role="option"
|
|
82
|
-
aria-selected={isSelected}
|
|
83
|
-
id={'typeahead-item-' + index}
|
|
84
|
-
onMouseEnter={onMouseEnter}
|
|
85
|
-
onClick={onClick}
|
|
86
|
-
>
|
|
87
|
-
{option.icon}
|
|
88
|
-
<span className="text">{option.title}</span>
|
|
89
|
-
</li>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export default function ComponentPickerMenuPlugin(): JSX.Element {
|
|
94
|
-
const [editor] = useLexicalComposerContext();
|
|
95
|
-
const [queryString, setQueryString] = useState<string | null>(null);
|
|
96
|
-
|
|
97
|
-
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
|
98
|
-
minLength: 0,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const getDynamicOptions = useCallback(() => {
|
|
102
|
-
const options: Array<ComponentPickerOption> = [];
|
|
103
|
-
|
|
104
|
-
if (queryString == null) {
|
|
105
|
-
return options;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const fullTableRegex = new RegExp(/^([1-9]|10)x([1-9]|10)$/);
|
|
109
|
-
const partialTableRegex = new RegExp(/^([1-9]|10)x?$/);
|
|
110
|
-
|
|
111
|
-
const fullTableMatch = fullTableRegex.exec(queryString);
|
|
112
|
-
const partialTableMatch = partialTableRegex.exec(queryString);
|
|
113
|
-
|
|
114
|
-
if (fullTableMatch) {
|
|
115
|
-
const [rows, columns] = fullTableMatch[0].split('x').map((n: string) => parseInt(n, 10));
|
|
116
|
-
|
|
117
|
-
options.push(
|
|
118
|
-
new ComponentPickerOption(`${rows}x${columns} Table`, {
|
|
119
|
-
icon: <i className="icon table" />,
|
|
120
|
-
keywords: ['table'],
|
|
121
|
-
onSelect: () =>
|
|
122
|
-
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
|
123
|
-
editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns, rows }),
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
} else if (partialTableMatch) {
|
|
127
|
-
const rows = parseInt(partialTableMatch[0], 10);
|
|
128
|
-
|
|
129
|
-
options.push(
|
|
130
|
-
...Array.from({ length: 5 }, (_, i) => i + 1).map(
|
|
131
|
-
columns =>
|
|
132
|
-
new ComponentPickerOption(`${rows}x${columns} Table`, {
|
|
133
|
-
icon: <i className="icon table" />,
|
|
134
|
-
keywords: ['table'],
|
|
135
|
-
onSelect: () =>
|
|
136
|
-
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
|
137
|
-
editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns, rows }),
|
|
138
|
-
}),
|
|
139
|
-
),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return options;
|
|
144
|
-
}, [editor, queryString]);
|
|
145
|
-
|
|
146
|
-
const options = useMemo(() => {
|
|
147
|
-
const baseOptions = [
|
|
148
|
-
new ComponentPickerOption('Paragraph', {
|
|
149
|
-
icon: <i className="icon paragraph" />,
|
|
150
|
-
keywords: ['normal', 'paragraph', 'p', 'text'],
|
|
151
|
-
onSelect: () =>
|
|
152
|
-
editor.update(() => {
|
|
153
|
-
const selection = $getSelection();
|
|
154
|
-
if ($isRangeSelection(selection)) {
|
|
155
|
-
$setBlocksType(selection, () => $createParagraphNode());
|
|
156
|
-
}
|
|
157
|
-
}),
|
|
158
|
-
}),
|
|
159
|
-
...Array.from({ length: 3 }, (_, i) => i + 1).map(
|
|
160
|
-
n =>
|
|
161
|
-
new ComponentPickerOption(`Heading ${n}`, {
|
|
162
|
-
icon: <i className={`icon h${n}`} />,
|
|
163
|
-
keywords: ['heading', 'header', `h${n}`],
|
|
164
|
-
onSelect: () =>
|
|
165
|
-
editor.update(() => {
|
|
166
|
-
const selection = $getSelection();
|
|
167
|
-
if ($isRangeSelection(selection)) {
|
|
168
|
-
$setBlocksType(selection, () =>
|
|
169
|
-
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
|
170
|
-
$createHeadingNode(`h${n}`),
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
}),
|
|
174
|
-
}),
|
|
175
|
-
),
|
|
176
|
-
// new ComponentPickerOption('Table', {
|
|
177
|
-
// icon: <i className="icon table" />,
|
|
178
|
-
// keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
|
|
179
|
-
// onSelect: () =>
|
|
180
|
-
// showModal('Insert Table', onClose => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
|
181
|
-
// }),
|
|
182
|
-
// new ComponentPickerOption('Table (Experimental)', {
|
|
183
|
-
// icon: <i className="icon table" />,
|
|
184
|
-
// keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
|
|
185
|
-
// onSelect: () =>
|
|
186
|
-
// showModal('Insert Table', onClose => <InsertNewTableDialog activeEditor={editor} onClose={onClose} />),
|
|
187
|
-
// }),
|
|
188
|
-
new ComponentPickerOption('Numbered List', {
|
|
189
|
-
icon: <i className="icon number" />,
|
|
190
|
-
keywords: ['numbered list', 'ordered list', 'ol'],
|
|
191
|
-
onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
|
|
192
|
-
}),
|
|
193
|
-
new ComponentPickerOption('Bulleted List', {
|
|
194
|
-
icon: <i className="icon bullet" />,
|
|
195
|
-
keywords: ['bulleted list', 'unordered list', 'ul'],
|
|
196
|
-
onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
|
|
197
|
-
}),
|
|
198
|
-
new ComponentPickerOption('Check List', {
|
|
199
|
-
icon: <i className="icon check" />,
|
|
200
|
-
keywords: ['check list', 'todo list'],
|
|
201
|
-
onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
|
|
202
|
-
}),
|
|
203
|
-
new ComponentPickerOption('Quote', {
|
|
204
|
-
icon: <i className="icon quote" />,
|
|
205
|
-
keywords: ['block quote'],
|
|
206
|
-
onSelect: () =>
|
|
207
|
-
editor.update(() => {
|
|
208
|
-
const selection = $getSelection();
|
|
209
|
-
if ($isRangeSelection(selection)) {
|
|
210
|
-
$setBlocksType(selection, () => $createQuoteNode());
|
|
211
|
-
}
|
|
212
|
-
}),
|
|
213
|
-
}),
|
|
214
|
-
new ComponentPickerOption('Code', {
|
|
215
|
-
icon: <i className="icon code" />,
|
|
216
|
-
keywords: ['javascript', 'python', 'js', 'codeblock'],
|
|
217
|
-
onSelect: () =>
|
|
218
|
-
editor.update(() => {
|
|
219
|
-
const selection = $getSelection();
|
|
220
|
-
|
|
221
|
-
if ($isRangeSelection(selection)) {
|
|
222
|
-
if (selection.isCollapsed()) {
|
|
223
|
-
$setBlocksType(selection, () => $createCodeNode());
|
|
224
|
-
} else {
|
|
225
|
-
// Will this ever happen?
|
|
226
|
-
const textContent = selection.getTextContent();
|
|
227
|
-
const codeNode = $createCodeNode();
|
|
228
|
-
selection.insertNodes([codeNode]);
|
|
229
|
-
selection.insertRawText(textContent);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}),
|
|
233
|
-
}),
|
|
234
|
-
new ComponentPickerOption('Divider', {
|
|
235
|
-
icon: <i className="icon horizontal-rule" />,
|
|
236
|
-
keywords: ['horizontal rule', 'divider', 'hr'],
|
|
237
|
-
onSelect: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
|
|
238
|
-
}),
|
|
239
|
-
|
|
240
|
-
...['left', 'center', 'right', 'justify'].map(
|
|
241
|
-
alignment =>
|
|
242
|
-
new ComponentPickerOption(`Align ${alignment}`, {
|
|
243
|
-
icon: <i className={`icon ${alignment}-align`} />,
|
|
244
|
-
keywords: ['align', 'justify', alignment],
|
|
245
|
-
onSelect: () =>
|
|
246
|
-
// @ts-ignore Correct types, but since they're dynamic TS doesn't like it.
|
|
247
|
-
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment),
|
|
248
|
-
}),
|
|
249
|
-
),
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
const dynamicOptions = getDynamicOptions();
|
|
253
|
-
|
|
254
|
-
return queryString
|
|
255
|
-
? [
|
|
256
|
-
...dynamicOptions,
|
|
257
|
-
...baseOptions.filter(option => {
|
|
258
|
-
return new RegExp(queryString, 'gi').exec(option.title) || option.keywords != null
|
|
259
|
-
? option.keywords.some(keyword => new RegExp(queryString, 'gi').exec(keyword))
|
|
260
|
-
: false;
|
|
261
|
-
}),
|
|
262
|
-
]
|
|
263
|
-
: baseOptions;
|
|
264
|
-
}, [editor, getDynamicOptions, queryString]);
|
|
265
|
-
|
|
266
|
-
const onSelectOption = useCallback(
|
|
267
|
-
(
|
|
268
|
-
selectedOption: ComponentPickerOption,
|
|
269
|
-
nodeToRemove: TextNode | null,
|
|
270
|
-
closeMenu: () => void,
|
|
271
|
-
matchingString: string,
|
|
272
|
-
) => {
|
|
273
|
-
editor.update(() => {
|
|
274
|
-
if (nodeToRemove) {
|
|
275
|
-
nodeToRemove.remove();
|
|
276
|
-
}
|
|
277
|
-
selectedOption.onSelect(matchingString);
|
|
278
|
-
closeMenu();
|
|
279
|
-
});
|
|
280
|
-
},
|
|
281
|
-
[editor],
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
return (
|
|
285
|
-
<>
|
|
286
|
-
<LexicalTypeaheadMenuPlugin<ComponentPickerOption>
|
|
287
|
-
onQueryChange={setQueryString}
|
|
288
|
-
onSelectOption={onSelectOption}
|
|
289
|
-
triggerFn={checkForTriggerMatch}
|
|
290
|
-
options={options}
|
|
291
|
-
menuRenderFn={(anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) =>
|
|
292
|
-
anchorElementRef.current && options.length
|
|
293
|
-
? ReactDOM.createPortal(
|
|
294
|
-
<div className="typeahead-popover component-picker-menu">
|
|
295
|
-
<ul>
|
|
296
|
-
{options.map((option, i: number) => (
|
|
297
|
-
<ComponentPickerMenuItem
|
|
298
|
-
index={i}
|
|
299
|
-
isSelected={selectedIndex === i}
|
|
300
|
-
onClick={() => {
|
|
301
|
-
setHighlightedIndex(i);
|
|
302
|
-
selectOptionAndCleanUp(option);
|
|
303
|
-
}}
|
|
304
|
-
onMouseEnter={() => {
|
|
305
|
-
setHighlightedIndex(i);
|
|
306
|
-
}}
|
|
307
|
-
key={option.key}
|
|
308
|
-
option={option}
|
|
309
|
-
/>
|
|
310
|
-
))}
|
|
311
|
-
</ul>
|
|
312
|
-
</div>,
|
|
313
|
-
anchorElementRef.current,
|
|
314
|
-
)
|
|
315
|
-
: null
|
|
316
|
-
}
|
|
317
|
-
/>
|
|
318
|
-
</>
|
|
319
|
-
);
|
|
320
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
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 { DRAG_DROP_PASTE } from '@lexical/rich-text';
|
|
11
|
-
import { isMimeType, mediaFileReader } from '@lexical/utils';
|
|
12
|
-
import { COMMAND_PRIORITY_LOW } from 'lexical';
|
|
13
|
-
import { useEffect } from 'react';
|
|
14
|
-
|
|
15
|
-
const ACCEPTABLE_IMAGE_TYPES = ['image/', 'image/heic', 'image/heif', 'image/gif', 'image/webp'];
|
|
16
|
-
|
|
17
|
-
export default function DragDropPaste(): null {
|
|
18
|
-
const [editor] = useLexicalComposerContext();
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
return editor.registerCommand(
|
|
21
|
-
DRAG_DROP_PASTE,
|
|
22
|
-
files => {
|
|
23
|
-
(async () => {
|
|
24
|
-
const filesResult = await mediaFileReader(
|
|
25
|
-
files,
|
|
26
|
-
[ACCEPTABLE_IMAGE_TYPES].flatMap(x => x),
|
|
27
|
-
);
|
|
28
|
-
for (const { file, result } of filesResult) {
|
|
29
|
-
if (isMimeType(file, ACCEPTABLE_IMAGE_TYPES)) {
|
|
30
|
-
console.log({ file, name: file.name });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
})();
|
|
34
|
-
return true;
|
|
35
|
-
},
|
|
36
|
-
COMMAND_PRIORITY_LOW,
|
|
37
|
-
);
|
|
38
|
-
}, [editor]);
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
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 * as React from 'react';
|
|
10
|
-
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
|
11
|
-
|
|
12
|
-
import { PLAYGROUND_TRANSFORMERS } from '../MarkdownTransformers';
|
|
13
|
-
|
|
14
|
-
export default function MarkdownPlugin(): JSX.Element {
|
|
15
|
-
return <MarkdownShortcutPlugin transformers={PLAYGROUND_TRANSFORMERS} />;
|
|
16
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
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 {
|
|
10
|
-
$createParagraphNode,
|
|
11
|
-
$createTextNode,
|
|
12
|
-
$isElementNode,
|
|
13
|
-
$isParagraphNode,
|
|
14
|
-
$isTextNode,
|
|
15
|
-
type ElementNode,
|
|
16
|
-
type LexicalNode,
|
|
17
|
-
} from 'lexical';
|
|
18
|
-
import {
|
|
19
|
-
CHECK_LIST,
|
|
20
|
-
ELEMENT_TRANSFORMERS,
|
|
21
|
-
TEXT_FORMAT_TRANSFORMERS,
|
|
22
|
-
TEXT_MATCH_TRANSFORMERS,
|
|
23
|
-
type ElementTransformer,
|
|
24
|
-
type Transformer,
|
|
25
|
-
} from '@lexical/markdown';
|
|
26
|
-
import {
|
|
27
|
-
$createHorizontalRuleNode,
|
|
28
|
-
$isHorizontalRuleNode,
|
|
29
|
-
HorizontalRuleNode,
|
|
30
|
-
} from '@lexical/react/LexicalHorizontalRuleNode';
|
|
31
|
-
import {
|
|
32
|
-
$createTableCellNode,
|
|
33
|
-
$createTableNode,
|
|
34
|
-
$createTableRowNode,
|
|
35
|
-
$isTableNode,
|
|
36
|
-
$isTableRowNode,
|
|
37
|
-
TableCellHeaderStates,
|
|
38
|
-
TableCellNode,
|
|
39
|
-
TableNode,
|
|
40
|
-
TableRowNode,
|
|
41
|
-
} from '@lexical/table';
|
|
42
|
-
|
|
43
|
-
export const HR: ElementTransformer = {
|
|
44
|
-
dependencies: [HorizontalRuleNode],
|
|
45
|
-
export: (node: LexicalNode) => {
|
|
46
|
-
return $isHorizontalRuleNode(node) ? '***' : null;
|
|
47
|
-
},
|
|
48
|
-
regExp: /^(---|\*\*\*|___)\s?$/,
|
|
49
|
-
replace: (parentNode, _1, _2, isImport) => {
|
|
50
|
-
const line = $createHorizontalRuleNode();
|
|
51
|
-
|
|
52
|
-
// TODO: Get rid of isImport flag
|
|
53
|
-
if (isImport || parentNode.getNextSibling() != null) {
|
|
54
|
-
parentNode.replace(line);
|
|
55
|
-
} else {
|
|
56
|
-
parentNode.insertBefore(line);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
line.selectNext();
|
|
60
|
-
},
|
|
61
|
-
type: 'element',
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Very primitive table setup
|
|
65
|
-
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
|
|
66
|
-
|
|
67
|
-
export const TABLE: ElementTransformer = {
|
|
68
|
-
// TODO: refactor transformer for new TableNode
|
|
69
|
-
dependencies: [TableNode, TableRowNode, TableCellNode],
|
|
70
|
-
export: (node: LexicalNode, exportChildren: (elementNode: ElementNode) => string) => {
|
|
71
|
-
if (!$isTableNode(node)) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const output = [];
|
|
76
|
-
|
|
77
|
-
for (const row of node.getChildren()) {
|
|
78
|
-
const rowOutput = [];
|
|
79
|
-
|
|
80
|
-
if ($isTableRowNode(row)) {
|
|
81
|
-
for (const cell of row.getChildren()) {
|
|
82
|
-
// It's TableCellNode (hence ElementNode) so it's just to make flow happy
|
|
83
|
-
if ($isElementNode(cell)) {
|
|
84
|
-
rowOutput.push(exportChildren(cell));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
output.push(`| ${rowOutput.join(' | ')} |`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return output.join('\n');
|
|
93
|
-
},
|
|
94
|
-
regExp: TABLE_ROW_REG_EXP,
|
|
95
|
-
replace: (parentNode, _1, match) => {
|
|
96
|
-
const matchCells = mapToTableCells(match[0]);
|
|
97
|
-
|
|
98
|
-
if (matchCells == null) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const rows = [matchCells];
|
|
103
|
-
let sibling = parentNode.getPreviousSibling();
|
|
104
|
-
let maxCells = matchCells.length;
|
|
105
|
-
|
|
106
|
-
while (sibling) {
|
|
107
|
-
if (!$isParagraphNode(sibling)) {
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (sibling.getChildrenSize() !== 1) {
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const firstChild = sibling.getFirstChild();
|
|
116
|
-
|
|
117
|
-
if (!$isTextNode(firstChild)) {
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const cells = mapToTableCells(firstChild.getTextContent());
|
|
122
|
-
|
|
123
|
-
if (cells == null) {
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
maxCells = Math.max(maxCells, cells.length);
|
|
128
|
-
rows.unshift(cells);
|
|
129
|
-
const previousSibling = sibling.getPreviousSibling();
|
|
130
|
-
sibling.remove();
|
|
131
|
-
sibling = previousSibling;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const table = $createTableNode();
|
|
135
|
-
|
|
136
|
-
for (const cells of rows) {
|
|
137
|
-
const tableRow = $createTableRowNode();
|
|
138
|
-
table.append(tableRow);
|
|
139
|
-
|
|
140
|
-
for (let i = 0; i < maxCells; i++) {
|
|
141
|
-
tableRow.append(i < cells.length ? cells[i] : createTableCell(null));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const previousSibling = parentNode.getPreviousSibling();
|
|
146
|
-
if ($isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) {
|
|
147
|
-
previousSibling.append(...table.getChildren());
|
|
148
|
-
parentNode.remove();
|
|
149
|
-
} else {
|
|
150
|
-
parentNode.replace(table);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
table.selectEnd();
|
|
154
|
-
},
|
|
155
|
-
type: 'element',
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
function getTableColumnsSize(table: TableNode) {
|
|
159
|
-
const row = table.getFirstChild();
|
|
160
|
-
return $isTableRowNode(row) ? row.getChildrenSize() : 0;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const createTableCell = (textContent: string | null | undefined): TableCellNode => {
|
|
164
|
-
const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
|
|
165
|
-
const paragraph = $createParagraphNode();
|
|
166
|
-
|
|
167
|
-
if (textContent != null) {
|
|
168
|
-
paragraph.append($createTextNode(textContent.trim()));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
cell.append(paragraph);
|
|
172
|
-
return cell;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const mapToTableCells = (textContent: string): Array<TableCellNode> | null => {
|
|
176
|
-
// TODO:
|
|
177
|
-
// For now plain text, single node. Can be expanded to more complex content
|
|
178
|
-
// including formatted text
|
|
179
|
-
const match = textContent.match(TABLE_ROW_REG_EXP);
|
|
180
|
-
|
|
181
|
-
if (!match || !match[1]) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return match[1].split('|').map(text => createTableCell(text));
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export const PLAYGROUND_TRANSFORMERS: Array<Transformer> = [
|
|
189
|
-
TABLE,
|
|
190
|
-
HR,
|
|
191
|
-
CHECK_LIST,
|
|
192
|
-
...ELEMENT_TRANSFORMERS,
|
|
193
|
-
...TEXT_FORMAT_TRANSFORMERS,
|
|
194
|
-
...TEXT_MATCH_TRANSFORMERS,
|
|
195
|
-
];
|
|
@@ -1,113 +0,0 @@
|
|
|
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 type { LexicalCommand, LexicalEditor, RangeSelection } from 'lexical';
|
|
10
|
-
|
|
11
|
-
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
12
|
-
import {
|
|
13
|
-
$getSelection,
|
|
14
|
-
$isRangeSelection,
|
|
15
|
-
COMMAND_PRIORITY_EDITOR,
|
|
16
|
-
createCommand,
|
|
17
|
-
REDO_COMMAND,
|
|
18
|
-
UNDO_COMMAND,
|
|
19
|
-
} from 'lexical';
|
|
20
|
-
import { useEffect, useRef, useState } from 'react';
|
|
21
|
-
|
|
22
|
-
import useReport from '../../hooks/useReport';
|
|
23
|
-
|
|
24
|
-
export const SPEECH_TO_TEXT_COMMAND: LexicalCommand<boolean> = createCommand('SPEECH_TO_TEXT_COMMAND');
|
|
25
|
-
|
|
26
|
-
const VOICE_COMMANDS: Readonly<Record<string, (arg0: { editor: LexicalEditor; selection: RangeSelection }) => void>> = {
|
|
27
|
-
'\n': ({ selection }) => {
|
|
28
|
-
selection.insertParagraph();
|
|
29
|
-
},
|
|
30
|
-
redo: ({ editor }) => {
|
|
31
|
-
editor.dispatchCommand(REDO_COMMAND, undefined);
|
|
32
|
-
},
|
|
33
|
-
undo: ({ editor }) => {
|
|
34
|
-
editor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const SUPPORT_SPEECH_RECOGNITION: boolean =
|
|
39
|
-
typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window);
|
|
40
|
-
|
|
41
|
-
function SpeechToTextPlugin(): null {
|
|
42
|
-
const [editor] = useLexicalComposerContext();
|
|
43
|
-
const [isEnabled, setIsEnabled] = useState<boolean>(false);
|
|
44
|
-
const SpeechRecognition =
|
|
45
|
-
// @ts-ignore
|
|
46
|
-
window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
47
|
-
const recognition = useRef<typeof SpeechRecognition | null>(null);
|
|
48
|
-
const report = useReport();
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (isEnabled && recognition.current === null) {
|
|
52
|
-
recognition.current = new SpeechRecognition();
|
|
53
|
-
recognition.current.continuous = true;
|
|
54
|
-
recognition.current.interimResults = true;
|
|
55
|
-
recognition.current.addEventListener('result', (event: typeof SpeechRecognition) => {
|
|
56
|
-
const resultItem = event.results.item(event.resultIndex);
|
|
57
|
-
const { transcript } = resultItem.item(0);
|
|
58
|
-
report(transcript);
|
|
59
|
-
|
|
60
|
-
if (!resultItem.isFinal) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
editor.update(() => {
|
|
65
|
-
const selection = $getSelection();
|
|
66
|
-
|
|
67
|
-
if ($isRangeSelection(selection)) {
|
|
68
|
-
const command = VOICE_COMMANDS[transcript.toLowerCase().trim()];
|
|
69
|
-
|
|
70
|
-
if (command) {
|
|
71
|
-
command({
|
|
72
|
-
editor,
|
|
73
|
-
selection,
|
|
74
|
-
});
|
|
75
|
-
} else if (transcript.match(/\s*\n\s*/)) {
|
|
76
|
-
selection.insertParagraph();
|
|
77
|
-
} else {
|
|
78
|
-
selection.insertText(transcript);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (recognition.current) {
|
|
86
|
-
if (isEnabled) {
|
|
87
|
-
recognition.current.start();
|
|
88
|
-
} else {
|
|
89
|
-
recognition.current.stop();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
if (recognition.current !== null) {
|
|
95
|
-
recognition.current.stop();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
}, [SpeechRecognition, editor, isEnabled, report]);
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
return editor.registerCommand(
|
|
101
|
-
SPEECH_TO_TEXT_COMMAND,
|
|
102
|
-
(_isEnabled: boolean) => {
|
|
103
|
-
setIsEnabled(_isEnabled);
|
|
104
|
-
return true;
|
|
105
|
-
},
|
|
106
|
-
COMMAND_PRIORITY_EDITOR,
|
|
107
|
-
);
|
|
108
|
-
}, [editor]);
|
|
109
|
-
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default (SUPPORT_SPEECH_RECOGNITION ? SpeechToTextPlugin : () => null) as () => null;
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
|
|
10
|
-
.TableCellResizer__resizer {
|
|
11
|
-
position: absolute;
|
|
12
|
-
}
|