@sveltia/ui 0.78.3 → 0.79.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/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 +376 -273
- package/dist/components/text-editor/emoji-autocomplete.svelte +1 -1
- package/dist/components/text-editor/lexical-root.svelte +1 -1
- 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 +5 -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/package.json +38 -34
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CodeExtension,
|
|
2
3
|
CodeHighlightNode,
|
|
3
4
|
CodeNode,
|
|
4
5
|
$createCodeNode as createCodeNode,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
$isCodeNode as isCodeNode,
|
|
7
8
|
} from '@lexical/code-core';
|
|
8
9
|
import { registerDragonSupport } from '@lexical/dragon';
|
|
9
|
-
import { HorizontalRuleNode } from '@lexical/extension';
|
|
10
|
+
import { buildEditorFromExtensions, HorizontalRuleNode } from '@lexical/extension';
|
|
10
11
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
11
12
|
import {
|
|
12
13
|
$isLinkNode as isLinkNode,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
COMMAND_PRIORITY_LOW,
|
|
42
43
|
COMMAND_PRIORITY_NORMAL,
|
|
43
44
|
createEditor,
|
|
45
|
+
defineExtension,
|
|
44
46
|
DELETE_CHARACTER_COMMAND,
|
|
45
47
|
$createParagraphNode as createParagraphNode,
|
|
46
48
|
$createTextNode as createTextNode,
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
$getSelection as getSelection,
|
|
51
53
|
INDENT_CONTENT_COMMAND,
|
|
52
54
|
INSERT_PARAGRAPH_COMMAND,
|
|
55
|
+
KEY_ENTER_COMMAND,
|
|
53
56
|
$insertNodes as insertNodes,
|
|
54
57
|
$isDecoratorNode as isDecoratorNode,
|
|
55
58
|
$isElementNode as isElementNode,
|
|
@@ -87,7 +90,8 @@ import { HR } from './transformers/hr.js';
|
|
|
87
90
|
import { TABLE } from './transformers/table.js';
|
|
88
91
|
|
|
89
92
|
/**
|
|
90
|
-
* @import {
|
|
93
|
+
* @import { LexicalEditor, LexicalNode } from 'lexical';
|
|
94
|
+
* @import { ListType } from '@lexical/list';
|
|
91
95
|
* @import { Transformer } from '@lexical/markdown';
|
|
92
96
|
* @import {
|
|
93
97
|
* TextEditorBlockType,
|
|
@@ -96,7 +100,7 @@ import { TABLE } from './transformers/table.js';
|
|
|
96
100
|
* TextEditorInlineType,
|
|
97
101
|
* TextEditorNodeType,
|
|
98
102
|
* TextEditorSelectionState,
|
|
99
|
-
* } from '../../typedefs';
|
|
103
|
+
* } from '../../typedefs.js';
|
|
100
104
|
*/
|
|
101
105
|
|
|
102
106
|
/**
|
|
@@ -160,7 +164,7 @@ export const isStaticDecoratorContent = (target) => {
|
|
|
160
164
|
* block, or a new paragraph if the adjacent nodes are decorators, where nothing can be typed.
|
|
161
165
|
* @returns {boolean} Whether the selection has been moved.
|
|
162
166
|
*/
|
|
163
|
-
|
|
167
|
+
const $moveCaretIntoBlock = () => {
|
|
164
168
|
const selection = getSelection();
|
|
165
169
|
|
|
166
170
|
if (
|
|
@@ -203,17 +207,29 @@ export const $moveCaretIntoBlock = () => {
|
|
|
203
207
|
};
|
|
204
208
|
|
|
205
209
|
/**
|
|
206
|
-
* Move the focus to the editor, and the
|
|
207
|
-
*
|
|
210
|
+
* Move the focus to the editor, and run the given function with the node of the given element
|
|
211
|
+
* within an editor update.
|
|
208
212
|
* @param {LexicalEditor} editor Editor instance.
|
|
209
|
-
* @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.
|
|
210
216
|
*/
|
|
211
|
-
const
|
|
217
|
+
const updateNodeWithFocus = (editor, element, callback) => {
|
|
212
218
|
editor.getRootElement()?.focus({ preventScroll: true });
|
|
213
219
|
|
|
214
220
|
editor.update(() => {
|
|
215
|
-
|
|
221
|
+
callback(getNearestNodeFromDOMNode(element));
|
|
222
|
+
});
|
|
223
|
+
};
|
|
216
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) => {
|
|
217
233
|
// The element has been found by its `data-lexical-decorator` attribute
|
|
218
234
|
/* v8 ignore next 3 */
|
|
219
235
|
if (!isDecoratorNode(node)) {
|
|
@@ -231,11 +247,7 @@ const placeCaretAfterDecorator = (editor, decorator) => {
|
|
|
231
247
|
* @param {Element} block Element of the block node.
|
|
232
248
|
*/
|
|
233
249
|
const placeCaretAtBlockEnd = (editor, block) => {
|
|
234
|
-
editor
|
|
235
|
-
|
|
236
|
-
editor.update(() => {
|
|
237
|
-
const node = getNearestNodeFromDOMNode(block);
|
|
238
|
-
|
|
250
|
+
updateNodeWithFocus(editor, block, (node) => {
|
|
239
251
|
/* v8 ignore next 3 */
|
|
240
252
|
if (!isElementNode(node)) {
|
|
241
253
|
return;
|
|
@@ -352,6 +364,14 @@ export const isSafeLinkURL = (url) => {
|
|
|
352
364
|
return !scheme || SAFE_LINK_URL_SCHEMES.includes(scheme.toLowerCase());
|
|
353
365
|
};
|
|
354
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
|
+
|
|
355
375
|
/**
|
|
356
376
|
* Get the current selection’s block node key as well as block and inline level types.
|
|
357
377
|
* @internal
|
|
@@ -375,7 +395,7 @@ export const getSelectionTypes = () => {
|
|
|
375
395
|
const inlineTypes = TEXT_FORMAT_BUTTON_TYPES.filter((type) => selection.hasFormat(type));
|
|
376
396
|
|
|
377
397
|
if (anchor.getType() !== 'root') {
|
|
378
|
-
parent =
|
|
398
|
+
parent = getNearestElementNode(anchor);
|
|
379
399
|
|
|
380
400
|
if (isLinkNode(parent)) {
|
|
381
401
|
inlineTypes.push('link');
|
|
@@ -529,251 +549,208 @@ export const getComponentMarkdown = ({ node, createNode, transformer, createMark
|
|
|
529
549
|
};
|
|
530
550
|
|
|
531
551
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* @
|
|
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.
|
|
535
556
|
*/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
components = [],
|
|
539
|
-
useMarkdownShortcuts,
|
|
540
|
-
isCodeEditor = false,
|
|
541
|
-
defaultLanguage = 'plain',
|
|
542
|
-
}) => {
|
|
543
|
-
/** @type {CreateEditorArgs} */
|
|
544
|
-
const editorConfig = {
|
|
545
|
-
namespace: 'editor',
|
|
546
|
-
nodes: [
|
|
547
|
-
...components.map(({ node }) => node),
|
|
548
|
-
...new Set(
|
|
549
|
-
Object.entries(NODE_MAP)
|
|
550
|
-
.filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
|
|
551
|
-
.flatMap(([, nodes]) => nodes),
|
|
552
|
-
),
|
|
553
|
-
...(isCodeEditor
|
|
554
|
-
? [CodeNode, CodeHighlightNode]
|
|
555
|
-
: // We haven’t implemented buttons for horizontal rules and tables yet, but we still want
|
|
556
|
-
// to support them in Markdown, so always include them in the node list
|
|
557
|
-
[HorizontalRuleNode, TableNode, TableCellNode, TableRowNode]),
|
|
558
|
-
],
|
|
559
|
-
theme: EDITOR_THEME,
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
/** @type {Transformer[]} */
|
|
563
|
-
const enabledTransformers = [
|
|
564
|
-
...components.map(({ transformer }) => transformer),
|
|
565
|
-
...new Set(
|
|
566
|
-
Object.entries(TRANSFORMER_MAP)
|
|
567
|
-
.filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
|
|
568
|
-
.flatMap(([, transformers]) => transformers),
|
|
569
|
-
),
|
|
570
|
-
...(isCodeEditor
|
|
571
|
-
? [CODE]
|
|
572
|
-
: // See the comment above for why we always include horizontal rules and tables
|
|
573
|
-
[HR, TABLE]),
|
|
574
|
-
];
|
|
575
|
-
|
|
576
|
-
const editor = createEditor(editorConfig);
|
|
577
|
-
/** @type {Array<() => void>} */
|
|
578
|
-
const unregisters = [];
|
|
557
|
+
const mergeUnregisters = (...unregisters) => {
|
|
558
|
+
const handlers = unregisters.filter((unregister) => typeof unregister === 'function');
|
|
579
559
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
* @param {(() => void) | undefined | null} unregister Cleanup handler.
|
|
583
|
-
*/
|
|
584
|
-
const addUnregister = (unregister) => {
|
|
585
|
-
/* v8 ignore next */
|
|
586
|
-
if (typeof unregister === 'function') {
|
|
587
|
-
unregisters.push(unregister);
|
|
588
|
-
}
|
|
560
|
+
return () => {
|
|
561
|
+
handlers.forEach((unregister) => unregister());
|
|
589
562
|
};
|
|
563
|
+
};
|
|
590
564
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
undefined,
|
|
601
|
-
HISTORY_MAX_DEPTH,
|
|
602
|
-
),
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
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(
|
|
606
574
|
// Pressing Backspace at the very beginning of a code block converts it to a paragraph by
|
|
607
575
|
// default (`CodeNode.collapseAtStart()`), which makes no sense when the editor only holds a
|
|
608
576
|
// single code block, so ignore it
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
(
|
|
613
|
-
const selection = getSelection();
|
|
614
|
-
|
|
615
|
-
if (!isBackward || !isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const { offset } = selection.anchor;
|
|
620
|
-
const node = selection.anchor.getNode();
|
|
621
|
-
const codeNode = isCodeNode(node) ? node : getNearestNodeOfType(node, CodeNode);
|
|
622
|
-
|
|
623
|
-
return (
|
|
624
|
-
!!codeNode &&
|
|
625
|
-
offset === 0 &&
|
|
626
|
-
(node.is(codeNode) || !!codeNode.getFirstDescendant()?.is(node))
|
|
627
|
-
);
|
|
628
|
-
},
|
|
629
|
-
COMMAND_PRIORITY_LOW,
|
|
630
|
-
),
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
// Make sure the editor always has a single code block. This runs within the same update that
|
|
634
|
-
// made the change, so the user never gets a chance to type in anything else
|
|
635
|
-
addUnregister(
|
|
636
|
-
editor.registerNodeTransform(RootNode, (root) => {
|
|
637
|
-
const children = root.getChildren();
|
|
577
|
+
editor.registerCommand(
|
|
578
|
+
DELETE_CHARACTER_COMMAND,
|
|
579
|
+
(isBackward) => {
|
|
580
|
+
const selection = getSelection();
|
|
638
581
|
|
|
639
|
-
if (
|
|
640
|
-
return;
|
|
582
|
+
if (!isBackward || !isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
583
|
+
return false;
|
|
641
584
|
}
|
|
642
585
|
|
|
643
|
-
const
|
|
586
|
+
const { offset } = selection.anchor;
|
|
587
|
+
const node = selection.anchor.getNode();
|
|
588
|
+
const codeNode = isCodeNode(node) ? node : getNearestNodeOfType(node, CodeNode);
|
|
644
589
|
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
}
|
|
647
610
|
|
|
648
|
-
|
|
649
|
-
firstChild.replace(node, true);
|
|
650
|
-
} else if (children.length === 0) {
|
|
651
|
-
const node = createCodeNode(defaultLanguage);
|
|
611
|
+
event?.preventDefault();
|
|
652
612
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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();
|
|
659
621
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
622
|
+
if (children.length === 1 && isCodeNode(children[0])) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
663
625
|
|
|
664
|
-
|
|
665
|
-
addUnregister(
|
|
666
|
-
registerCodeHighlighting(editor, {
|
|
667
|
-
...shikiTokenizer,
|
|
668
|
-
defaultLanguage,
|
|
669
|
-
defaultTheme: getCodeTheme(),
|
|
670
|
-
}),
|
|
671
|
-
);
|
|
626
|
+
const [firstChild] = children;
|
|
672
627
|
|
|
673
|
-
|
|
674
|
-
|
|
628
|
+
if (children.length === 1 && isElementNode(firstChild)) {
|
|
629
|
+
const node = createCodeNode(defaultLanguage);
|
|
675
630
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
TOGGLE_LINK_COMMAND,
|
|
681
|
-
(payload) => {
|
|
682
|
-
// Ignore an unsafe URL rather than linking to it
|
|
683
|
-
if (typeof payload === 'string' && !isSafeLinkURL(payload)) {
|
|
684
|
-
return true;
|
|
685
|
-
}
|
|
631
|
+
// Keep the content and the selection
|
|
632
|
+
firstChild.replace(node, true);
|
|
633
|
+
} else if (children.length === 0) {
|
|
634
|
+
const node = createCodeNode(defaultLanguage);
|
|
686
635
|
|
|
687
|
-
|
|
636
|
+
root.append(node);
|
|
637
|
+
node.selectStart();
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
688
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)) {
|
|
689
655
|
return true;
|
|
690
|
-
}
|
|
691
|
-
COMMAND_PRIORITY_NORMAL,
|
|
692
|
-
),
|
|
693
|
-
);
|
|
656
|
+
}
|
|
694
657
|
|
|
695
|
-
|
|
696
|
-
editor.registerCommand(
|
|
697
|
-
PASTE_COMMAND,
|
|
698
|
-
(event) => {
|
|
699
|
-
const selection = getSelection();
|
|
658
|
+
toggleLink(typeof payload === 'string' ? payload : null);
|
|
700
659
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
+
}
|
|
709
677
|
|
|
710
|
-
|
|
678
|
+
const clipboardText = event.clipboardData.getData('text').trim();
|
|
711
679
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
680
|
+
// Paste an unsafe URL as plain text rather than as a link
|
|
681
|
+
if (!isURL(clipboardText) || !isSafeLinkURL(clipboardText)) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
716
684
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
685
|
+
if (selection.isCollapsed()) {
|
|
686
|
+
insertNodes([createTextNode(clipboardText)]);
|
|
687
|
+
}
|
|
720
688
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
+
}
|
|
730
698
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
699
|
+
return false;
|
|
700
|
+
},
|
|
701
|
+
COMMAND_PRIORITY_LOW,
|
|
702
|
+
),
|
|
703
|
+
);
|
|
737
704
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
+
];
|
|
744
713
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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));
|
|
751
722
|
|
|
752
|
-
|
|
753
|
-
|
|
723
|
+
return mergeUnregisters(
|
|
724
|
+
...enabledLists.map(([, command, listType]) =>
|
|
754
725
|
editor.registerCommand(
|
|
755
|
-
|
|
726
|
+
command,
|
|
756
727
|
() => {
|
|
757
|
-
insertList(
|
|
728
|
+
insertList(listType);
|
|
758
729
|
|
|
759
730
|
return true;
|
|
760
731
|
},
|
|
761
732
|
COMMAND_PRIORITY_NORMAL,
|
|
762
733
|
),
|
|
763
|
-
)
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (enabledButtons.includes('bulleted-list') || enabledButtons.includes('numbered-list')) {
|
|
734
|
+
),
|
|
767
735
|
// https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/shared/useList.ts
|
|
768
|
-
|
|
769
|
-
editor.registerCommand(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
736
|
+
enabledLists.length
|
|
737
|
+
? editor.registerCommand(
|
|
738
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
739
|
+
() => handleListInsertParagraph(),
|
|
740
|
+
COMMAND_PRIORITY_NORMAL,
|
|
741
|
+
)
|
|
742
|
+
: undefined,
|
|
743
|
+
);
|
|
744
|
+
};
|
|
776
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) => {
|
|
777
754
|
/** Incremented on every update, so only the latest one in a burst triggers the Update event. */
|
|
778
755
|
let updateCount = 0;
|
|
779
756
|
/** Whether the content has changed since the Update event was last triggered. */
|
|
@@ -782,11 +759,10 @@ export const initEditor = ({
|
|
|
782
759
|
let lastValue = undefined;
|
|
783
760
|
let disposed = false;
|
|
784
761
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
addUnregister(
|
|
762
|
+
return mergeUnregisters(
|
|
763
|
+
() => {
|
|
764
|
+
disposed = true;
|
|
765
|
+
},
|
|
790
766
|
editor.registerUpdateListener(({ dirtyElements, dirtyLeaves } = /** @type {any} */ ({})) => {
|
|
791
767
|
// An update that only moves the selection leaves no dirty nodes. Remember whether any update
|
|
792
768
|
// in the burst changed the content, so the document is only converted again when needed.
|
|
@@ -820,52 +796,147 @@ export const initEditor = ({
|
|
|
820
796
|
})();
|
|
821
797
|
}),
|
|
822
798
|
);
|
|
799
|
+
};
|
|
823
800
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const onKeydown = (event) => {
|
|
837
|
-
editor.update(() => {
|
|
838
|
-
if (event.key === 'Tab') {
|
|
839
|
-
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
|
+
}
|
|
840
813
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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();
|
|
844
822
|
|
|
845
|
-
|
|
823
|
+
if (!isRangeSelection(selection)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
846
826
|
|
|
847
|
-
|
|
848
|
-
anchor instanceof ElementNode ? anchor : getNearestNodeOfType(anchor, ElementNode);
|
|
827
|
+
const parent = getNearestElementNode(selection.anchor.getNode());
|
|
849
828
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
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);
|
|
858
836
|
}
|
|
859
837
|
}
|
|
860
|
-
}
|
|
861
|
-
};
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
};
|
|
862
841
|
|
|
863
|
-
|
|
842
|
+
root.addEventListener('keydown', onKeydown);
|
|
864
843
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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),
|
|
869
940
|
);
|
|
870
941
|
|
|
871
942
|
return {
|
|
@@ -875,7 +946,8 @@ export const initEditor = ({
|
|
|
875
946
|
* Remove all registered Lexical listeners.
|
|
876
947
|
*/
|
|
877
948
|
dispose: () => {
|
|
878
|
-
|
|
949
|
+
unregister();
|
|
950
|
+
editor.dispose();
|
|
879
951
|
},
|
|
880
952
|
};
|
|
881
953
|
};
|
|
@@ -898,23 +970,54 @@ export const loadCodeHighlighter = async (lang) => {
|
|
|
898
970
|
await Promise.all([loadCodeLanguage(normalizeCodeLanguage(lang)), loadCodeTheme(getCodeTheme())]);
|
|
899
971
|
};
|
|
900
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
|
+
|
|
901
979
|
/**
|
|
902
980
|
* Convert Markdown to Lexical nodes.
|
|
903
981
|
* @param {LexicalEditor} editor Editor instance.
|
|
904
982
|
* @param {string} value Current Markdown value.
|
|
905
983
|
* @param {Transformer[]} enabledTransformers List of enabled Markdown transformers.
|
|
906
|
-
* @returns {Promise<string>} The value as the editor exports it once imported, which
|
|
907
|
-
* 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.
|
|
908
987
|
* @throws {Error} Failed to convert the value to Lexical nodes.
|
|
909
988
|
*/
|
|
910
989
|
export const convertMarkdownToLexical = async (editor, value, enabledTransformers) => {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
+
}
|
|
918
1021
|
|
|
919
1022
|
// Split multiline formatting into separate lines to prevent Markdown parsing issues
|
|
920
1023
|
value = splitMultilineFormatting(value);
|