@sveltia/ui 0.78.4 → 0.79.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.
- package/dist/components/alert/alert-content.svelte +51 -0
- package/dist/components/alert/alert-content.svelte.d.ts +39 -0
- package/dist/components/alert/alert.d.ts +3 -0
- package/dist/components/alert/alert.js +21 -0
- package/dist/components/alert/alert.svelte +6 -30
- package/dist/components/alert/infobar.svelte +7 -21
- package/dist/components/alert/infobar.svelte.d.ts +2 -2
- package/dist/components/button/button.svelte +11 -11
- package/dist/components/button/button.svelte.d.ts +2 -2
- package/dist/components/button/select-button.svelte +1 -1
- package/dist/components/button/select-button.svelte.d.ts +2 -2
- package/dist/components/button/split-button.svelte +1 -1
- package/dist/components/button/split-button.svelte.d.ts +3 -3
- package/dist/components/calendar/calendar.d.ts +2 -0
- package/dist/components/calendar/calendar.js +34 -0
- package/dist/components/calendar/calendar.svelte +49 -5
- package/dist/components/checkbox/checkbox.svelte +11 -4
- package/dist/components/checkbox/checkbox.svelte.d.ts +2 -2
- package/dist/components/dialog/alert-dialog.svelte +1 -1
- package/dist/components/dialog/alert-dialog.svelte.d.ts +2 -2
- package/dist/components/dialog/confirmation-dialog.svelte +1 -1
- package/dist/components/dialog/confirmation-dialog.svelte.d.ts +2 -2
- package/dist/components/dialog/dialog.svelte +8 -18
- package/dist/components/dialog/dialog.svelte.d.ts +2 -2
- package/dist/components/dialog/modal-close-button.svelte +50 -0
- package/dist/components/dialog/modal-close-button.svelte.d.ts +43 -0
- package/dist/components/dialog/prompt-dialog.svelte +1 -1
- package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -4
- package/dist/components/disclosure/disclosure.svelte +1 -1
- package/dist/components/drawer/drawer.svelte +12 -34
- package/dist/components/drawer/drawer.svelte.d.ts +1 -1
- package/dist/components/emoji/caret.d.ts +1 -1
- package/dist/components/emoji/caret.js +1 -1
- package/dist/components/emoji/emoji-suggestions.svelte +1 -1
- package/dist/components/emoji/emoji-suggestions.svelte.d.ts +3 -3
- package/dist/components/emoji/emoji.d.ts +2 -2
- package/dist/components/emoji/emoji.js +1 -1
- package/dist/components/emoji/position.d.ts +2 -2
- package/dist/components/emoji/position.js +1 -1
- package/dist/components/grid/grid-body.svelte +6 -2
- package/dist/components/grid/grid-cell.svelte +3 -9
- package/dist/components/grid/grid-col-header.svelte +3 -9
- package/dist/components/grid/grid-foot.svelte +4 -11
- package/dist/components/grid/grid-head.svelte +4 -11
- package/dist/components/grid/grid-row-header.svelte +3 -10
- package/dist/components/grid/grid-row.svelte +3 -16
- package/dist/components/listbox/listbox.svelte +1 -1
- package/dist/components/listbox/listbox.svelte.d.ts +1 -1
- package/dist/components/listbox/option.svelte +1 -1
- package/dist/components/listbox/option.svelte.d.ts +2 -2
- package/dist/components/menu/menu-button.svelte +2 -2
- package/dist/components/menu/menu-button.svelte.d.ts +4 -4
- package/dist/components/menu/menu-item-checkable.svelte +71 -0
- package/dist/components/menu/menu-item-checkable.svelte.d.ts +25 -0
- package/dist/components/menu/menu-item-checkbox.svelte +3 -36
- package/dist/components/menu/menu-item-checkbox.svelte.d.ts +3 -3
- package/dist/components/menu/menu-item-group.svelte +8 -5
- package/dist/components/menu/menu-item-group.svelte.d.ts +9 -1
- package/dist/components/menu/menu-item-radio.svelte +3 -36
- package/dist/components/menu/menu-item-radio.svelte.d.ts +3 -3
- package/dist/components/menu/menu-item.svelte +22 -6
- package/dist/components/menu/menu-item.svelte.d.ts +4 -4
- package/dist/components/radio/radio.svelte +12 -1
- package/dist/components/resizable-pane/resizable-handle.svelte +14 -18
- package/dist/components/resizable-pane/resizable-pane-group.svelte +9 -6
- package/dist/components/resizable-pane/resizable-pane.svelte +4 -4
- package/dist/components/resizable-pane/resizable-pane.svelte.d.ts +7 -6
- package/dist/components/resizable-pane/sizing.d.ts +5 -4
- package/dist/components/resizable-pane/sizing.js +84 -25
- package/dist/components/scroll/infinite-scroll.svelte +16 -21
- package/dist/components/select/combobox.svelte +43 -52
- package/dist/components/select/combobox.svelte.d.ts +2 -2
- package/dist/components/select/select-tags.svelte +7 -1
- package/dist/components/select/select.svelte +2 -22
- package/dist/components/select/select.svelte.d.ts +1 -1
- package/dist/components/slider/slider.d.ts +6 -0
- package/dist/components/slider/slider.js +55 -2
- package/dist/components/slider/slider.svelte +63 -32
- package/dist/components/table/table-cell.svelte +3 -9
- package/dist/components/table/table-col-header.svelte +3 -9
- package/dist/components/table/table-foot.svelte +4 -11
- package/dist/components/table/table-head.svelte +4 -11
- package/dist/components/table/table-part.svelte +69 -0
- package/dist/components/table/table-part.svelte.d.ts +48 -0
- package/dist/components/table/table-row-header.svelte +3 -10
- package/dist/components/table/table-row.svelte +3 -10
- package/dist/components/tabs/tab-list.svelte +12 -0
- package/dist/components/tabs/tab.svelte +1 -1
- package/dist/components/tabs/tab.svelte.d.ts +2 -2
- package/dist/components/text-editor/constants.d.ts +5 -5
- package/dist/components/text-editor/constants.js +1 -1
- package/dist/components/text-editor/core.d.ts +4 -5
- package/dist/components/text-editor/core.js +372 -285
- package/dist/components/text-editor/emoji-autocomplete.svelte +1 -1
- package/dist/components/text-editor/lexical-root.svelte +4 -2
- package/dist/components/text-editor/raw-markdown.d.ts +6 -33
- package/dist/components/text-editor/raw-markdown.js +63 -8
- package/dist/components/text-editor/shiki/facade.d.ts +1 -1
- package/dist/components/text-editor/shiki/facade.js +63 -48
- package/dist/components/text-editor/shiki/generated.d.ts +1 -1
- package/dist/components/text-editor/shiki/generated.js +1 -1
- package/dist/components/text-editor/shiki/highlighter.d.ts +6 -1
- package/dist/components/text-editor/shiki/highlighter.js +3 -4
- package/dist/components/text-editor/shiki/loader.d.ts +1 -1
- package/dist/components/text-editor/shiki/loader.js +1 -1
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-editor/store.svelte.d.ts +1 -1
- package/dist/components/text-editor/store.svelte.js +29 -2
- package/dist/components/text-editor/text-editor.svelte +2 -2
- package/dist/components/text-editor/text-editor.svelte.d.ts +3 -3
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -16
- package/dist/components/text-editor/toolbar/format-text-button.svelte +7 -31
- package/dist/components/text-editor/toolbar/format-text-button.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/insert-component.d.ts +3 -0
- package/dist/components/text-editor/toolbar/insert-component.js +34 -0
- package/dist/components/text-editor/toolbar/insert-item-button.svelte +4 -24
- package/dist/components/text-editor/toolbar/insert-item-button.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +9 -25
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte +4 -24
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +1 -1
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +31 -35
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte +0 -10
- package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte.d.ts +1 -11
- package/dist/components/text-editor/transformers/table.d.ts +0 -1
- package/dist/components/text-editor/transformers/table.js +1 -2
- package/dist/components/text-field/emoji-autocomplete.svelte +1 -1
- package/dist/components/text-field/masked-input.svelte +179 -0
- package/dist/components/text-field/masked-input.svelte.d.ts +42 -0
- package/dist/components/text-field/number-input.js +7 -1
- package/dist/components/text-field/number-input.svelte +13 -5
- package/dist/components/text-field/number-input.svelte.d.ts +3 -3
- package/dist/components/text-field/password-input.svelte +3 -116
- package/dist/components/text-field/password-input.svelte.d.ts +3 -3
- package/dist/components/text-field/search-bar.svelte +1 -1
- package/dist/components/text-field/search-bar.svelte.d.ts +3 -3
- package/dist/components/text-field/secret-input.svelte +3 -119
- package/dist/components/text-field/secret-input.svelte.d.ts +3 -3
- package/dist/components/text-field/text-area.svelte +1 -1
- package/dist/components/text-field/text-area.svelte.d.ts +3 -3
- package/dist/components/text-field/text-input.svelte +1 -1
- package/dist/components/text-field/text-input.svelte.d.ts +3 -3
- package/dist/components/toast/toast.svelte +11 -20
- package/dist/components/toast/toast.svelte.d.ts +1 -1
- package/dist/components/tree/tree-item.svelte +1 -1
- package/dist/components/tree/tree-item.svelte.d.ts +1 -1
- package/dist/components/tree/tree.svelte +1 -1
- package/dist/components/tree/tree.svelte.d.ts +1 -1
- package/dist/components/util/font-links.svelte +1 -0
- package/dist/components/util/modal.svelte +37 -1
- package/dist/components/util/modal.svelte.d.ts +1 -1
- package/dist/components/util/popup.svelte +2 -5
- package/dist/components/util/popup.svelte.d.ts +2 -2
- package/dist/components/util/visibility-observer.svelte +1 -0
- package/dist/locales/ar.yaml +1 -1
- package/dist/locales/sk.yaml +136 -0
- package/dist/services/group.svelte.d.ts +10 -1
- package/dist/services/group.svelte.js +361 -251
- package/dist/services/navigation.d.ts +19 -0
- package/dist/services/navigation.js +84 -0
- package/dist/services/popup-position.d.ts +90 -0
- package/dist/services/popup-position.js +275 -0
- package/dist/services/popup.svelte.d.ts +16 -17
- package/dist/services/popup.svelte.js +25 -169
- package/dist/services/scroll.d.ts +1 -0
- package/dist/services/scroll.js +17 -0
- package/dist/services/select.svelte.d.ts +2 -2
- package/dist/services/select.svelte.js +1 -1
- package/dist/services/text.d.ts +2 -0
- package/dist/services/text.js +28 -0
- package/dist/services/tree.svelte.d.ts +3 -11
- package/dist/services/tree.svelte.js +35 -77
- package/dist/services/type-ahead.d.ts +0 -4
- package/dist/services/type-ahead.js +3 -1
- package/dist/services/widget.d.ts +8 -0
- package/dist/services/widget.js +39 -0
- package/dist/shiki-engine.js +15 -15
- package/dist/styles/mixins.scss +126 -0
- package/dist/typedefs.d.ts +5 -0
- package/dist/typedefs.js +2 -0
- package/package.json +24 -20
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
$getSelection as getSelection,
|
|
53
53
|
INDENT_CONTENT_COMMAND,
|
|
54
54
|
INSERT_PARAGRAPH_COMMAND,
|
|
55
|
+
KEY_ENTER_COMMAND,
|
|
55
56
|
$insertNodes as insertNodes,
|
|
56
57
|
$isDecoratorNode as isDecoratorNode,
|
|
57
58
|
$isElementNode as isElementNode,
|
|
@@ -89,7 +90,8 @@ import { HR } from './transformers/hr.js';
|
|
|
89
90
|
import { TABLE } from './transformers/table.js';
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
|
-
* @import { LexicalEditor } from 'lexical';
|
|
93
|
+
* @import { LexicalEditor, LexicalNode } from 'lexical';
|
|
94
|
+
* @import { ListType } from '@lexical/list';
|
|
93
95
|
* @import { Transformer } from '@lexical/markdown';
|
|
94
96
|
* @import {
|
|
95
97
|
* TextEditorBlockType,
|
|
@@ -98,7 +100,7 @@ import { TABLE } from './transformers/table.js';
|
|
|
98
100
|
* TextEditorInlineType,
|
|
99
101
|
* TextEditorNodeType,
|
|
100
102
|
* TextEditorSelectionState,
|
|
101
|
-
* } from '../../typedefs';
|
|
103
|
+
* } from '../../typedefs.js';
|
|
102
104
|
*/
|
|
103
105
|
|
|
104
106
|
/**
|
|
@@ -162,7 +164,7 @@ export const isStaticDecoratorContent = (target) => {
|
|
|
162
164
|
* block, or a new paragraph if the adjacent nodes are decorators, where nothing can be typed.
|
|
163
165
|
* @returns {boolean} Whether the selection has been moved.
|
|
164
166
|
*/
|
|
165
|
-
|
|
167
|
+
const $moveCaretIntoBlock = () => {
|
|
166
168
|
const selection = getSelection();
|
|
167
169
|
|
|
168
170
|
if (
|
|
@@ -205,17 +207,29 @@ export const $moveCaretIntoBlock = () => {
|
|
|
205
207
|
};
|
|
206
208
|
|
|
207
209
|
/**
|
|
208
|
-
* Move the focus to the editor, and the
|
|
209
|
-
*
|
|
210
|
+
* Move the focus to the editor, and run the given function with the node of the given element
|
|
211
|
+
* within an editor update.
|
|
210
212
|
* @param {LexicalEditor} editor Editor instance.
|
|
211
|
-
* @param {Element}
|
|
213
|
+
* @param {Element} element Element of the node.
|
|
214
|
+
* @param {(node: ReturnType<typeof getNearestNodeFromDOMNode>) => void} callback Function to be
|
|
215
|
+
* called with the node.
|
|
212
216
|
*/
|
|
213
|
-
const
|
|
217
|
+
const updateNodeWithFocus = (editor, element, callback) => {
|
|
214
218
|
editor.getRootElement()?.focus({ preventScroll: true });
|
|
215
219
|
|
|
216
220
|
editor.update(() => {
|
|
217
|
-
|
|
221
|
+
callback(getNearestNodeFromDOMNode(element));
|
|
222
|
+
});
|
|
223
|
+
};
|
|
218
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Move the focus to the editor, and the caret to right after the given decorator node, like an
|
|
227
|
+
* editor component: the start of the next block, or a new paragraph if there’s none to type in.
|
|
228
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
229
|
+
* @param {Element} decorator Element of the decorator node.
|
|
230
|
+
*/
|
|
231
|
+
const placeCaretAfterDecorator = (editor, decorator) => {
|
|
232
|
+
updateNodeWithFocus(editor, decorator, (node) => {
|
|
219
233
|
// The element has been found by its `data-lexical-decorator` attribute
|
|
220
234
|
/* v8 ignore next 3 */
|
|
221
235
|
if (!isDecoratorNode(node)) {
|
|
@@ -233,11 +247,7 @@ const placeCaretAfterDecorator = (editor, decorator) => {
|
|
|
233
247
|
* @param {Element} block Element of the block node.
|
|
234
248
|
*/
|
|
235
249
|
const placeCaretAtBlockEnd = (editor, block) => {
|
|
236
|
-
editor
|
|
237
|
-
|
|
238
|
-
editor.update(() => {
|
|
239
|
-
const node = getNearestNodeFromDOMNode(block);
|
|
240
|
-
|
|
250
|
+
updateNodeWithFocus(editor, block, (node) => {
|
|
241
251
|
/* v8 ignore next 3 */
|
|
242
252
|
if (!isElementNode(node)) {
|
|
243
253
|
return;
|
|
@@ -354,6 +364,14 @@ export const isSafeLinkURL = (url) => {
|
|
|
354
364
|
return !scheme || SAFE_LINK_URL_SCHEMES.includes(scheme.toLowerCase());
|
|
355
365
|
};
|
|
356
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Get the given node itself if it’s an element node, or its nearest element node ancestor.
|
|
369
|
+
* @param {LexicalNode} node Node.
|
|
370
|
+
* @returns {ElementNode | null} Element node.
|
|
371
|
+
*/
|
|
372
|
+
const getNearestElementNode = (node) =>
|
|
373
|
+
node instanceof ElementNode ? node : getNearestNodeOfType(node, ElementNode);
|
|
374
|
+
|
|
357
375
|
/**
|
|
358
376
|
* Get the current selection’s block node key as well as block and inline level types.
|
|
359
377
|
* @internal
|
|
@@ -377,7 +395,7 @@ export const getSelectionTypes = () => {
|
|
|
377
395
|
const inlineTypes = TEXT_FORMAT_BUTTON_TYPES.filter((type) => selection.hasFormat(type));
|
|
378
396
|
|
|
379
397
|
if (anchor.getType() !== 'root') {
|
|
380
|
-
parent =
|
|
398
|
+
parent = getNearestElementNode(anchor);
|
|
381
399
|
|
|
382
400
|
if (isLinkNode(parent)) {
|
|
383
401
|
inlineTypes.push('link');
|
|
@@ -531,264 +549,208 @@ export const getComponentMarkdown = ({ node, createNode, transformer, createMark
|
|
|
531
549
|
};
|
|
532
550
|
|
|
533
551
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* @
|
|
552
|
+
* Combine the given cleanup handlers into one, which calls them in the given order. Missing
|
|
553
|
+
* handlers, like those of the features that are not enabled, are skipped.
|
|
554
|
+
* @param {...((() => void) | undefined | null)} unregisters Cleanup handlers.
|
|
555
|
+
* @returns {() => void} Combined cleanup handler.
|
|
537
556
|
*/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
components = [],
|
|
541
|
-
useMarkdownShortcuts,
|
|
542
|
-
isCodeEditor = false,
|
|
543
|
-
defaultLanguage = 'plain',
|
|
544
|
-
}) => {
|
|
545
|
-
const hasCodeBlock = enabledButtons.includes('code-block') || isCodeEditor;
|
|
546
|
-
|
|
547
|
-
const editorExtension = defineExtension({
|
|
548
|
-
name: '@sveltia/ui/editor',
|
|
549
|
-
namespace: 'editor',
|
|
550
|
-
// `CodeNode` expects the editor to be built with `CodeExtension`, which also registers the
|
|
551
|
-
// `CodeNode` and `CodeHighlightNode` nodes
|
|
552
|
-
dependencies: hasCodeBlock ? [CodeExtension] : [],
|
|
553
|
-
nodes: [
|
|
554
|
-
...components.map(({ node }) => node),
|
|
555
|
-
...new Set(
|
|
556
|
-
Object.entries(NODE_MAP)
|
|
557
|
-
.filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
|
|
558
|
-
.flatMap(([, nodes]) => nodes),
|
|
559
|
-
),
|
|
560
|
-
...(isCodeEditor
|
|
561
|
-
? [CodeNode, CodeHighlightNode]
|
|
562
|
-
: // We haven’t implemented buttons for horizontal rules and tables yet, but we still want
|
|
563
|
-
// to support them in Markdown, so always include them in the node list
|
|
564
|
-
[HorizontalRuleNode, TableNode, TableCellNode, TableRowNode]),
|
|
565
|
-
],
|
|
566
|
-
theme: EDITOR_THEME,
|
|
567
|
-
/**
|
|
568
|
-
* Log an error like `createEditor` does by default, instead of throwing it.
|
|
569
|
-
* @param {Error} error Error.
|
|
570
|
-
*/
|
|
571
|
-
onError: (error) => {
|
|
572
|
-
// eslint-disable-next-line no-console
|
|
573
|
-
console.error(error);
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
/** @type {Transformer[]} */
|
|
578
|
-
const enabledTransformers = [
|
|
579
|
-
...components.map(({ transformer }) => transformer),
|
|
580
|
-
...new Set(
|
|
581
|
-
Object.entries(TRANSFORMER_MAP)
|
|
582
|
-
.filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
|
|
583
|
-
.flatMap(([, transformers]) => transformers),
|
|
584
|
-
),
|
|
585
|
-
...(isCodeEditor
|
|
586
|
-
? [CODE]
|
|
587
|
-
: // See the comment above for why we always include horizontal rules and tables
|
|
588
|
-
[HR, TABLE]),
|
|
589
|
-
];
|
|
557
|
+
const mergeUnregisters = (...unregisters) => {
|
|
558
|
+
const handlers = unregisters.filter((unregister) => typeof unregister === 'function');
|
|
590
559
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const unregisters = [];
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Add a cleanup handler if it is defined.
|
|
597
|
-
* @param {(() => void) | undefined | null} unregister Cleanup handler.
|
|
598
|
-
*/
|
|
599
|
-
const addUnregister = (unregister) => {
|
|
600
|
-
/* v8 ignore next */
|
|
601
|
-
if (typeof unregister === 'function') {
|
|
602
|
-
unregisters.push(unregister);
|
|
603
|
-
}
|
|
560
|
+
return () => {
|
|
561
|
+
handlers.forEach((unregister) => unregister());
|
|
604
562
|
};
|
|
563
|
+
};
|
|
605
564
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
undefined,
|
|
616
|
-
HISTORY_MAX_DEPTH,
|
|
617
|
-
),
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
if (isCodeEditor) {
|
|
565
|
+
/**
|
|
566
|
+
* Register the commands and transform specific to the code editor, which only holds a single code
|
|
567
|
+
* block.
|
|
568
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
569
|
+
* @param {string} defaultLanguage Default language of the code block.
|
|
570
|
+
* @returns {() => void} Cleanup handler.
|
|
571
|
+
*/
|
|
572
|
+
const registerCodeEditorCommands = (editor, defaultLanguage) =>
|
|
573
|
+
mergeUnregisters(
|
|
621
574
|
// Pressing Backspace at the very beginning of a code block converts it to a paragraph by
|
|
622
575
|
// default (`CodeNode.collapseAtStart()`), which makes no sense when the editor only holds a
|
|
623
576
|
// single code block, so ignore it
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
(
|
|
628
|
-
const selection = getSelection();
|
|
629
|
-
|
|
630
|
-
if (!isBackward || !isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const { offset } = selection.anchor;
|
|
635
|
-
const node = selection.anchor.getNode();
|
|
636
|
-
const codeNode = isCodeNode(node) ? node : getNearestNodeOfType(node, CodeNode);
|
|
637
|
-
|
|
638
|
-
return (
|
|
639
|
-
!!codeNode &&
|
|
640
|
-
offset === 0 &&
|
|
641
|
-
(node.is(codeNode) || !!codeNode.getFirstDescendant()?.is(node))
|
|
642
|
-
);
|
|
643
|
-
},
|
|
644
|
-
COMMAND_PRIORITY_LOW,
|
|
645
|
-
),
|
|
646
|
-
);
|
|
577
|
+
editor.registerCommand(
|
|
578
|
+
DELETE_CHARACTER_COMMAND,
|
|
579
|
+
(isBackward) => {
|
|
580
|
+
const selection = getSelection();
|
|
647
581
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
addUnregister(
|
|
651
|
-
editor.registerNodeTransform(RootNode, (root) => {
|
|
652
|
-
const children = root.getChildren();
|
|
653
|
-
|
|
654
|
-
if (children.length === 1 && isCodeNode(children[0])) {
|
|
655
|
-
return;
|
|
582
|
+
if (!isBackward || !isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
583
|
+
return false;
|
|
656
584
|
}
|
|
657
585
|
|
|
658
|
-
const
|
|
586
|
+
const { offset } = selection.anchor;
|
|
587
|
+
const node = selection.anchor.getNode();
|
|
588
|
+
const codeNode = isCodeNode(node) ? node : getNearestNodeOfType(node, CodeNode);
|
|
659
589
|
|
|
660
|
-
|
|
661
|
-
|
|
590
|
+
return (
|
|
591
|
+
!!codeNode &&
|
|
592
|
+
offset === 0 &&
|
|
593
|
+
(node.is(codeNode) || !!codeNode.getFirstDescendant()?.is(node))
|
|
594
|
+
);
|
|
595
|
+
},
|
|
596
|
+
COMMAND_PRIORITY_LOW,
|
|
597
|
+
),
|
|
598
|
+
// Pressing Enter after two blank lines at the end of a code block exits it by default
|
|
599
|
+
// (`CodeExtension`), adding a paragraph after it, which would be left out of the code. Insert a
|
|
600
|
+
// new line instead, which `CodeNode.insertNewAfter()` does without exiting when the extension
|
|
601
|
+
// is in place. This has to take priority over the extension’s own handler
|
|
602
|
+
editor.registerCommand(
|
|
603
|
+
KEY_ENTER_COMMAND,
|
|
604
|
+
(event) => {
|
|
605
|
+
const selection = getSelection();
|
|
606
|
+
|
|
607
|
+
if (!isRangeSelection(selection)) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
662
610
|
|
|
663
|
-
|
|
664
|
-
firstChild.replace(node, true);
|
|
665
|
-
} else if (children.length === 0) {
|
|
666
|
-
const node = createCodeNode(defaultLanguage);
|
|
611
|
+
event?.preventDefault();
|
|
667
612
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
613
|
+
return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
|
|
614
|
+
},
|
|
615
|
+
COMMAND_PRIORITY_NORMAL,
|
|
616
|
+
),
|
|
617
|
+
// Make sure the editor always has a single code block. This runs within the same update that
|
|
618
|
+
// made the change, so the user never gets a chance to type in anything else
|
|
619
|
+
editor.registerNodeTransform(RootNode, (root) => {
|
|
620
|
+
const children = root.getChildren();
|
|
674
621
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
622
|
+
if (children.length === 1 && isCodeNode(children[0])) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
678
625
|
|
|
679
|
-
|
|
680
|
-
addUnregister(
|
|
681
|
-
registerCodeHighlighting(editor, {
|
|
682
|
-
...shikiTokenizer,
|
|
683
|
-
defaultLanguage,
|
|
684
|
-
defaultTheme: getCodeTheme(),
|
|
685
|
-
}),
|
|
686
|
-
);
|
|
626
|
+
const [firstChild] = children;
|
|
687
627
|
|
|
688
|
-
|
|
689
|
-
|
|
628
|
+
if (children.length === 1 && isElementNode(firstChild)) {
|
|
629
|
+
const node = createCodeNode(defaultLanguage);
|
|
690
630
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
TOGGLE_LINK_COMMAND,
|
|
696
|
-
(payload) => {
|
|
697
|
-
// Ignore an unsafe URL rather than linking to it
|
|
698
|
-
if (typeof payload === 'string' && !isSafeLinkURL(payload)) {
|
|
699
|
-
return true;
|
|
700
|
-
}
|
|
631
|
+
// Keep the content and the selection
|
|
632
|
+
firstChild.replace(node, true);
|
|
633
|
+
} else if (children.length === 0) {
|
|
634
|
+
const node = createCodeNode(defaultLanguage);
|
|
701
635
|
|
|
702
|
-
|
|
636
|
+
root.append(node);
|
|
637
|
+
node.selectStart();
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
703
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Register the link command, and the paste handler that turns a pasted URL into a link.
|
|
644
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
645
|
+
* @returns {() => void} Cleanup handler.
|
|
646
|
+
* @see https://github.com/facebook/lexical/blob/main/packages/lexical-link/src/LexicalLinkExtension.ts
|
|
647
|
+
*/
|
|
648
|
+
const registerLinkCommands = (editor) =>
|
|
649
|
+
mergeUnregisters(
|
|
650
|
+
editor.registerCommand(
|
|
651
|
+
TOGGLE_LINK_COMMAND,
|
|
652
|
+
(payload) => {
|
|
653
|
+
// Ignore an unsafe URL rather than linking to it
|
|
654
|
+
if (typeof payload === 'string' && !isSafeLinkURL(payload)) {
|
|
704
655
|
return true;
|
|
705
|
-
}
|
|
706
|
-
COMMAND_PRIORITY_NORMAL,
|
|
707
|
-
),
|
|
708
|
-
);
|
|
656
|
+
}
|
|
709
657
|
|
|
710
|
-
|
|
711
|
-
editor.registerCommand(
|
|
712
|
-
PASTE_COMMAND,
|
|
713
|
-
(event) => {
|
|
714
|
-
const selection = getSelection();
|
|
658
|
+
toggleLink(typeof payload === 'string' ? payload : null);
|
|
715
659
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
660
|
+
return true;
|
|
661
|
+
},
|
|
662
|
+
COMMAND_PRIORITY_NORMAL,
|
|
663
|
+
),
|
|
664
|
+
editor.registerCommand(
|
|
665
|
+
PASTE_COMMAND,
|
|
666
|
+
(event) => {
|
|
667
|
+
const selection = getSelection();
|
|
668
|
+
|
|
669
|
+
if (
|
|
670
|
+
!isRangeSelection(selection) ||
|
|
671
|
+
!objectKlassEquals(event, ClipboardEvent) ||
|
|
672
|
+
!event.clipboardData ||
|
|
673
|
+
/** @type {HTMLElement} */ (event.target).matches('input, textarea')
|
|
674
|
+
) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
724
677
|
|
|
725
|
-
|
|
678
|
+
const clipboardText = event.clipboardData.getData('text').trim();
|
|
726
679
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
680
|
+
// Paste an unsafe URL as plain text rather than as a link
|
|
681
|
+
if (!isURL(clipboardText) || !isSafeLinkURL(clipboardText)) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
731
684
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
685
|
+
if (selection.isCollapsed()) {
|
|
686
|
+
insertNodes([createTextNode(clipboardText)]);
|
|
687
|
+
}
|
|
735
688
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
689
|
+
if (
|
|
690
|
+
!selection
|
|
691
|
+
.getNodes()
|
|
692
|
+
.some((node) => isElementNode(node) || (isTextNode(node) && !node.isSimpleText()))
|
|
693
|
+
) {
|
|
694
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, clipboardText);
|
|
695
|
+
event.preventDefault();
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
745
698
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
699
|
+
return false;
|
|
700
|
+
},
|
|
701
|
+
COMMAND_PRIORITY_LOW,
|
|
702
|
+
),
|
|
703
|
+
);
|
|
752
704
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
705
|
+
/**
|
|
706
|
+
* List buttons, along with the command to insert the list and its type.
|
|
707
|
+
* @type {[TextEditorBlockType, typeof INSERT_UNORDERED_LIST_COMMAND, ListType][]}
|
|
708
|
+
*/
|
|
709
|
+
const LIST_COMMANDS = [
|
|
710
|
+
['bulleted-list', INSERT_UNORDERED_LIST_COMMAND, 'bullet'],
|
|
711
|
+
['numbered-list', INSERT_ORDERED_LIST_COMMAND, 'number'],
|
|
712
|
+
];
|
|
759
713
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
714
|
+
/**
|
|
715
|
+
* Register the commands for the enabled list buttons.
|
|
716
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
717
|
+
* @param {TextEditorConfig['enabledButtons']} enabledButtons Enabled buttons.
|
|
718
|
+
* @returns {() => void} Cleanup handler.
|
|
719
|
+
*/
|
|
720
|
+
const registerListCommands = (editor, enabledButtons = []) => {
|
|
721
|
+
const enabledLists = LIST_COMMANDS.filter(([button]) => enabledButtons.includes(button));
|
|
766
722
|
|
|
767
|
-
|
|
768
|
-
|
|
723
|
+
return mergeUnregisters(
|
|
724
|
+
...enabledLists.map(([, command, listType]) =>
|
|
769
725
|
editor.registerCommand(
|
|
770
|
-
|
|
726
|
+
command,
|
|
771
727
|
() => {
|
|
772
|
-
insertList(
|
|
728
|
+
insertList(listType);
|
|
773
729
|
|
|
774
730
|
return true;
|
|
775
731
|
},
|
|
776
732
|
COMMAND_PRIORITY_NORMAL,
|
|
777
733
|
),
|
|
778
|
-
)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (enabledButtons.includes('bulleted-list') || enabledButtons.includes('numbered-list')) {
|
|
734
|
+
),
|
|
782
735
|
// https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/shared/useList.ts
|
|
783
|
-
|
|
784
|
-
editor.registerCommand(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
736
|
+
enabledLists.length
|
|
737
|
+
? editor.registerCommand(
|
|
738
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
739
|
+
() => handleListInsertParagraph(),
|
|
740
|
+
COMMAND_PRIORITY_NORMAL,
|
|
741
|
+
)
|
|
742
|
+
: undefined,
|
|
743
|
+
);
|
|
744
|
+
};
|
|
791
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Register an update listener that triggers the Update event with the Markdown value, debounced so
|
|
748
|
+
* only the latest update in a burst triggers it.
|
|
749
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
750
|
+
* @param {Transformer[]} enabledTransformers Enabled Markdown transformers.
|
|
751
|
+
* @returns {() => void} Cleanup handler.
|
|
752
|
+
*/
|
|
753
|
+
const registerUpdateEvent = (editor, enabledTransformers) => {
|
|
792
754
|
/** Incremented on every update, so only the latest one in a burst triggers the Update event. */
|
|
793
755
|
let updateCount = 0;
|
|
794
756
|
/** Whether the content has changed since the Update event was last triggered. */
|
|
@@ -797,11 +759,10 @@ export const initEditor = ({
|
|
|
797
759
|
let lastValue = undefined;
|
|
798
760
|
let disposed = false;
|
|
799
761
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
addUnregister(
|
|
762
|
+
return mergeUnregisters(
|
|
763
|
+
() => {
|
|
764
|
+
disposed = true;
|
|
765
|
+
},
|
|
805
766
|
editor.registerUpdateListener(({ dirtyElements, dirtyLeaves } = /** @type {any} */ ({})) => {
|
|
806
767
|
// An update that only moves the selection leaves no dirty nodes. Remember whether any update
|
|
807
768
|
// in the burst changed the content, so the document is only converted again when needed.
|
|
@@ -835,52 +796,147 @@ export const initEditor = ({
|
|
|
835
796
|
})();
|
|
836
797
|
}),
|
|
837
798
|
);
|
|
799
|
+
};
|
|
838
800
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const onKeydown = (event) => {
|
|
852
|
-
editor.update(() => {
|
|
853
|
-
if (event.key === 'Tab') {
|
|
854
|
-
const selection = getSelection();
|
|
801
|
+
/**
|
|
802
|
+
* Register a handler for the Tab key that indents a list item, or unindents it with Shift.
|
|
803
|
+
* `editor.registerCommand(KEY_TAB_COMMAND, listener, priority)` doesn’t work for some reason, so
|
|
804
|
+
* listen to the `keydown` event on the root element instead.
|
|
805
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
806
|
+
* @returns {() => void} Cleanup handler.
|
|
807
|
+
*/
|
|
808
|
+
const registerTabIndentation = (editor) =>
|
|
809
|
+
editor.registerRootListener((root) => {
|
|
810
|
+
if (!root) {
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
855
813
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
814
|
+
/**
|
|
815
|
+
* Handle Tab indentation shortcuts.
|
|
816
|
+
* @param {KeyboardEvent} event Keydown event.
|
|
817
|
+
*/
|
|
818
|
+
const onKeydown = (event) => {
|
|
819
|
+
editor.update(() => {
|
|
820
|
+
if (event.key === 'Tab') {
|
|
821
|
+
const selection = getSelection();
|
|
859
822
|
|
|
860
|
-
|
|
823
|
+
if (!isRangeSelection(selection)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
861
826
|
|
|
862
|
-
|
|
863
|
-
anchor instanceof ElementNode ? anchor : getNearestNodeOfType(anchor, ElementNode);
|
|
827
|
+
const parent = getNearestElementNode(selection.anchor.getNode());
|
|
864
828
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
829
|
+
if (isListItemNode(parent) && parent.canIndent()) {
|
|
830
|
+
if (!event.shiftKey) {
|
|
831
|
+
event.preventDefault();
|
|
832
|
+
editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);
|
|
833
|
+
} else if (parent.getIndent() > 0) {
|
|
834
|
+
event.preventDefault();
|
|
835
|
+
editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
|
|
873
836
|
}
|
|
874
837
|
}
|
|
875
|
-
}
|
|
876
|
-
};
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
};
|
|
877
841
|
|
|
878
|
-
|
|
842
|
+
root.addEventListener('keydown', onKeydown);
|
|
879
843
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
844
|
+
return () => {
|
|
845
|
+
root.removeEventListener('keydown', onKeydown);
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Initialize the Lexical editor.
|
|
851
|
+
* @param {TextEditorConfig} config Editor configuration.
|
|
852
|
+
* @returns {InitEditorResult} Editor instance and cleanup.
|
|
853
|
+
*/
|
|
854
|
+
export const initEditor = ({
|
|
855
|
+
enabledButtons = [],
|
|
856
|
+
components = [],
|
|
857
|
+
useMarkdownShortcuts,
|
|
858
|
+
isCodeEditor = false,
|
|
859
|
+
defaultLanguage = 'plain',
|
|
860
|
+
}) => {
|
|
861
|
+
const hasCodeBlock = enabledButtons.includes('code-block') || isCodeEditor;
|
|
862
|
+
|
|
863
|
+
const editorExtension = defineExtension({
|
|
864
|
+
name: '@sveltia/ui/editor',
|
|
865
|
+
namespace: 'editor',
|
|
866
|
+
// `CodeNode` expects the editor to be built with `CodeExtension`, which also registers the
|
|
867
|
+
// `CodeNode` and `CodeHighlightNode` nodes
|
|
868
|
+
dependencies: hasCodeBlock ? [CodeExtension] : [],
|
|
869
|
+
// Start with an empty editor state like `createEditor` does, instead of a blank paragraph. The
|
|
870
|
+
// Markdown import then becomes the first state in the history, so undoing right after the
|
|
871
|
+
// editor loads doesn’t revert it to the blank paragraph and clear the value
|
|
872
|
+
$initialEditorState: null,
|
|
873
|
+
nodes: [
|
|
874
|
+
...components.map(({ node }) => node),
|
|
875
|
+
...new Set(
|
|
876
|
+
Object.entries(NODE_MAP)
|
|
877
|
+
.filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
|
|
878
|
+
.flatMap(([, nodes]) => nodes),
|
|
879
|
+
),
|
|
880
|
+
...(isCodeEditor
|
|
881
|
+
? [CodeNode, CodeHighlightNode]
|
|
882
|
+
: // We haven’t implemented buttons for horizontal rules and tables yet, but we still want
|
|
883
|
+
// to support them in Markdown, so always include them in the node list
|
|
884
|
+
[HorizontalRuleNode, TableNode, TableCellNode, TableRowNode]),
|
|
885
|
+
],
|
|
886
|
+
theme: EDITOR_THEME,
|
|
887
|
+
/**
|
|
888
|
+
* Log an error like `createEditor` does by default, instead of throwing it.
|
|
889
|
+
* @param {Error} error Error.
|
|
890
|
+
*/
|
|
891
|
+
onError: (error) => {
|
|
892
|
+
// eslint-disable-next-line no-console
|
|
893
|
+
console.error(error);
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
/** @type {Transformer[]} */
|
|
898
|
+
const enabledTransformers = [
|
|
899
|
+
...components.map(({ transformer }) => transformer),
|
|
900
|
+
...new Set(
|
|
901
|
+
Object.entries(TRANSFORMER_MAP)
|
|
902
|
+
.filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
|
|
903
|
+
.flatMap(([, transformers]) => transformers),
|
|
904
|
+
),
|
|
905
|
+
...(isCodeEditor
|
|
906
|
+
? [CODE]
|
|
907
|
+
: // See the comment above for why we always include horizontal rules and tables
|
|
908
|
+
[HR, TABLE]),
|
|
909
|
+
];
|
|
910
|
+
|
|
911
|
+
const editor = buildEditorFromExtensions(editorExtension);
|
|
912
|
+
|
|
913
|
+
// The order matters, as listeners with the same priority are called in the registration order
|
|
914
|
+
const unregister = mergeUnregisters(
|
|
915
|
+
registerRichText(editor),
|
|
916
|
+
registerDragonSupport(editor),
|
|
917
|
+
// Cap the undo stack, as each entry holds a snapshot of the whole document
|
|
918
|
+
registerHistory(
|
|
919
|
+
editor,
|
|
920
|
+
createEmptyHistoryState(),
|
|
921
|
+
1000,
|
|
922
|
+
undefined,
|
|
923
|
+
undefined,
|
|
924
|
+
HISTORY_MAX_DEPTH,
|
|
925
|
+
),
|
|
926
|
+
isCodeEditor ? registerCodeEditorCommands(editor, defaultLanguage) : undefined,
|
|
927
|
+
useMarkdownShortcuts ? registerMarkdownShortcuts(editor, enabledTransformers) : undefined,
|
|
928
|
+
hasCodeBlock
|
|
929
|
+
? registerCodeHighlighting(editor, {
|
|
930
|
+
...shikiTokenizer,
|
|
931
|
+
defaultLanguage,
|
|
932
|
+
defaultTheme: getCodeTheme(),
|
|
933
|
+
})
|
|
934
|
+
: undefined,
|
|
935
|
+
hasCodeBlock ? observeCodeTheme(editor) : undefined,
|
|
936
|
+
enabledButtons.includes('link') ? registerLinkCommands(editor) : undefined,
|
|
937
|
+
registerListCommands(editor, enabledButtons),
|
|
938
|
+
registerUpdateEvent(editor, enabledTransformers),
|
|
939
|
+
registerTabIndentation(editor),
|
|
884
940
|
);
|
|
885
941
|
|
|
886
942
|
return {
|
|
@@ -890,7 +946,7 @@ export const initEditor = ({
|
|
|
890
946
|
* Remove all registered Lexical listeners.
|
|
891
947
|
*/
|
|
892
948
|
dispose: () => {
|
|
893
|
-
|
|
949
|
+
unregister();
|
|
894
950
|
editor.dispose();
|
|
895
951
|
},
|
|
896
952
|
};
|
|
@@ -914,23 +970,54 @@ export const loadCodeHighlighter = async (lang) => {
|
|
|
914
970
|
await Promise.all([loadCodeLanguage(normalizeCodeLanguage(lang)), loadCodeTheme(getCodeTheme())]);
|
|
915
971
|
};
|
|
916
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Number of the latest Markdown import started for each editor.
|
|
975
|
+
* @type {WeakMap<LexicalEditor, number>}
|
|
976
|
+
*/
|
|
977
|
+
const latestImports = new WeakMap();
|
|
978
|
+
|
|
917
979
|
/**
|
|
918
980
|
* Convert Markdown to Lexical nodes.
|
|
919
981
|
* @param {LexicalEditor} editor Editor instance.
|
|
920
982
|
* @param {string} value Current Markdown value.
|
|
921
983
|
* @param {Transformer[]} enabledTransformers List of enabled Markdown transformers.
|
|
922
|
-
* @returns {Promise<string>} The value as the editor exports it once imported, which
|
|
923
|
-
* style from the given value, e.g. `_text_` for `*text*`.
|
|
984
|
+
* @returns {Promise<string | undefined>} The value as the editor exports it once imported, which
|
|
985
|
+
* can differ in style from the given value, e.g. `_text_` for `*text*`. `undefined` if another
|
|
986
|
+
* import has been started for the same editor in the meantime, in which case this value is skipped.
|
|
924
987
|
* @throws {Error} Failed to convert the value to Lexical nodes.
|
|
925
988
|
*/
|
|
926
989
|
export const convertMarkdownToLexical = async (editor, value, enabledTransformers) => {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
990
|
+
const importId = (latestImports.get(editor) ?? 0) + 1;
|
|
991
|
+
|
|
992
|
+
latestImports.set(editor, importId);
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Check if a newer import has been started while waiting for the highlighter. Imports can finish
|
|
996
|
+
* out of order, as only those with code blocks have to wait, and an older value must not
|
|
997
|
+
* overwrite a newer one.
|
|
998
|
+
* @returns {boolean} Result.
|
|
999
|
+
*/
|
|
1000
|
+
const isSuperseded = () => latestImports.get(editor) !== importId;
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
// Preload the highlighter for every language used in the document, so code blocks are
|
|
1004
|
+
// highlighted as soon as they appear rather than a moment later
|
|
1005
|
+
await Promise.all(
|
|
1006
|
+
[...value.matchAll(/^```(?<lang>.+?)\n/gm)].map(async ({ groups: { lang = 'plain' } = {} }) =>
|
|
1007
|
+
loadCodeHighlighter(lang),
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
1010
|
+
} catch (ex) {
|
|
1011
|
+
if (isSuperseded()) {
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
throw ex;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (isSuperseded()) {
|
|
1019
|
+
return undefined;
|
|
1020
|
+
}
|
|
934
1021
|
|
|
935
1022
|
// Split multiline formatting into separate lines to prevent Markdown parsing issues
|
|
936
1023
|
value = splitMultilineFormatting(value);
|