@sveltia/ui 0.79.2 → 0.79.4
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/text-editor/background-update.d.ts +3 -0
- package/dist/components/text-editor/background-update.js +18 -0
- package/dist/components/text-editor/code-editor.svelte +14 -0
- package/dist/components/text-editor/code-editor.svelte.d.ts +13 -1
- package/dist/components/text-editor/core.js +6 -1
- package/dist/components/text-editor/shiki/facade.js +2 -1
- package/dist/components/text-editor/shiki/highlighter.js +2 -1
- package/dist/components/text-editor/shiki/theme.js +2 -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.js +27 -1
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +43 -17
- package/dist/typedefs.d.ts +8 -1
- package/dist/typedefs.js +5 -1
- package/package.json +3 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SKIP_DOM_SELECTION_TAG } from 'lexical';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { LexicalEditor, UpdateTag } from 'lexical';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the tags of an editor update the user didn’t make, such as one that highlights the code once
|
|
9
|
+
* a grammar has loaded. While the focus is elsewhere, the update leaves the DOM selection alone,
|
|
10
|
+
* which would otherwise move the focus back to the editor, taking over what the user types next.
|
|
11
|
+
* @param {LexicalEditor} editor Editor instance.
|
|
12
|
+
* @param {UpdateTag[]} [tags] Other tags of the update.
|
|
13
|
+
* @returns {UpdateTag[]} Tags.
|
|
14
|
+
*/
|
|
15
|
+
export const getBackgroundUpdateTags = (editor, tags = []) =>
|
|
16
|
+
editor.getRootElement()?.contains(document.activeElement)
|
|
17
|
+
? tags
|
|
18
|
+
: [...tags, SKIP_DOM_SELECTION_TAG];
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* @typedef {object} Props
|
|
21
21
|
* @property {string} [code] Input value.
|
|
22
22
|
* @property {string} [lang] Selected language.
|
|
23
|
+
* @property {boolean} [pending] Whether the user has changed the code, and the editor has yet to
|
|
24
|
+
* update {@link code}, which it does a moment later. Bind it to wait for the change before
|
|
25
|
+
* reading the code, for example to save it. Read-only.
|
|
23
26
|
* @property {boolean} [showLanguageSwitcher] Whether to show the language selector.
|
|
24
27
|
* @property {boolean} [flex] Make the text input container flexible.
|
|
25
28
|
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
/* eslint-disable prefer-const */
|
|
48
51
|
code = $bindable(''),
|
|
49
52
|
lang = $bindable('plain'),
|
|
53
|
+
pending = $bindable(false),
|
|
50
54
|
showLanguageSwitcher = false,
|
|
51
55
|
flex = false,
|
|
52
56
|
hidden = false,
|
|
@@ -86,6 +90,16 @@
|
|
|
86
90
|
|
|
87
91
|
setContext('editorStore', editorStore);
|
|
88
92
|
|
|
93
|
+
$effect(() => {
|
|
94
|
+
pending = editorStore.pending;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// An editor removed right after a change never converts it, so don’t leave a bound `pending` set
|
|
98
|
+
// for good, which would hold up anything waiting for the change
|
|
99
|
+
$effect(() => () => {
|
|
100
|
+
pending = false;
|
|
101
|
+
});
|
|
102
|
+
|
|
89
103
|
$effect(() => {
|
|
90
104
|
// The root initializes the editor before these effects first run, and stays initialized
|
|
91
105
|
/* v8 ignore next */
|
|
@@ -13,6 +13,12 @@ declare const CodeEditor: import("svelte").Component<{
|
|
|
13
13
|
* Selected language.
|
|
14
14
|
*/
|
|
15
15
|
lang?: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the user has changed the code, and the editor has yet to
|
|
18
|
+
* update {@link code}, which it does a moment later. Bind it to wait for the change before
|
|
19
|
+
* reading the code, for example to save it. Read-only.
|
|
20
|
+
*/
|
|
21
|
+
pending?: boolean | undefined;
|
|
16
22
|
/**
|
|
17
23
|
* Whether to show the language selector.
|
|
18
24
|
*/
|
|
@@ -67,7 +73,7 @@ declare const CodeEditor: import("svelte").Component<{
|
|
|
67
73
|
* Primary slot content.
|
|
68
74
|
*/
|
|
69
75
|
children?: Snippet<[]> | undefined;
|
|
70
|
-
} & Record<string, any>, {}, "code" | "lang">;
|
|
76
|
+
} & Record<string, any>, {}, "code" | "lang" | "pending">;
|
|
71
77
|
type Props = {
|
|
72
78
|
/**
|
|
73
79
|
* Input value.
|
|
@@ -77,6 +83,12 @@ type Props = {
|
|
|
77
83
|
* Selected language.
|
|
78
84
|
*/
|
|
79
85
|
lang?: string | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the user has changed the code, and the editor has yet to
|
|
88
|
+
* update {@link code}, which it does a moment later. Bind it to wait for the change before
|
|
89
|
+
* reading the code, for example to save it. Read-only.
|
|
90
|
+
*/
|
|
91
|
+
pending?: boolean | undefined;
|
|
80
92
|
/**
|
|
81
93
|
* Whether to show the language selector.
|
|
82
94
|
*/
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
$getNearestNodeFromDOMNode as getNearestNodeFromDOMNode,
|
|
51
51
|
$getRoot as getRoot,
|
|
52
52
|
$getSelection as getSelection,
|
|
53
|
+
HISTORY_MERGE_TAG,
|
|
53
54
|
INDENT_CONTENT_COMMAND,
|
|
54
55
|
INSERT_PARAGRAPH_COMMAND,
|
|
55
56
|
KEY_ENTER_COMMAND,
|
|
@@ -1033,6 +1034,10 @@ export const convertMarkdownToLexical = async (editor, value, enabledTransformer
|
|
|
1033
1034
|
// Pad blank blockquote lines so they are not imported as literal `>` text
|
|
1034
1035
|
value = padBlankBlockquoteLines(value);
|
|
1035
1036
|
|
|
1037
|
+
// Lexical commits an empty state when the root element is attached, and the history keeps it as
|
|
1038
|
+
// the current entry. An empty state can’t be restored, so undoing to it throws an error. Merge
|
|
1039
|
+
// the first import into that entry instead of pushing it onto the undo stack
|
|
1040
|
+
const isEmpty = editor.getEditorState().isEmpty();
|
|
1036
1041
|
/** @type {unknown} */
|
|
1037
1042
|
let error;
|
|
1038
1043
|
|
|
@@ -1047,7 +1052,7 @@ export const convertMarkdownToLexical = async (editor, value, enabledTransformer
|
|
|
1047
1052
|
// Tell the import from a change made by the user, and commit it right away, so the node
|
|
1048
1053
|
// transforms, e.g. the one giving a code block its default language, have run by the time the
|
|
1049
1054
|
// content is exported below
|
|
1050
|
-
{ tag: IMPORT_UPDATE_TAG, discrete: true },
|
|
1055
|
+
{ tag: isEmpty ? [IMPORT_UPDATE_TAG, HISTORY_MERGE_TAG] : IMPORT_UPDATE_TAG, discrete: true },
|
|
1051
1056
|
);
|
|
1052
1057
|
|
|
1053
1058
|
if (error) {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
HISTORY_MERGE_TAG,
|
|
22
22
|
tokenizeRawText,
|
|
23
23
|
} from 'lexical';
|
|
24
|
+
import { getBackgroundUpdateTags } from '../background-update.js';
|
|
24
25
|
import { cachePayload, getCachedPayload } from './cache.js';
|
|
25
26
|
import { LANGUAGES, THEMES } from './generated.js';
|
|
26
27
|
import { getCodeHighlighterLoaders } from './loader.js';
|
|
@@ -123,7 +124,7 @@ const refreshCodeNode = (editor, codeNodeKey) => {
|
|
|
123
124
|
|
|
124
125
|
codeNode.markDirty();
|
|
125
126
|
},
|
|
126
|
-
{ tag: HISTORY_MERGE_TAG },
|
|
127
|
+
{ tag: getBackgroundUpdateTags(editor, [HISTORY_MERGE_TAG]) },
|
|
127
128
|
);
|
|
128
129
|
};
|
|
129
130
|
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
HISTORY_MERGE_TAG,
|
|
34
34
|
TextNode,
|
|
35
35
|
} from 'lexical';
|
|
36
|
+
import { getBackgroundUpdateTags } from '../background-update.js';
|
|
36
37
|
import {
|
|
37
38
|
getHighlightNodes,
|
|
38
39
|
isCodeLanguageLoaded,
|
|
@@ -368,7 +369,7 @@ const codeNodeTransform = (editor, tokenizer, transformState, node) => {
|
|
|
368
369
|
staleNode.markDirty();
|
|
369
370
|
}
|
|
370
371
|
},
|
|
371
|
-
{ tag: HISTORY_MERGE_TAG },
|
|
372
|
+
{ tag: getBackgroundUpdateTags(editor, [HISTORY_MERGE_TAG]) },
|
|
372
373
|
);
|
|
373
374
|
});
|
|
374
375
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { $isCodeNode as isCodeNode } from '@lexical/code-core';
|
|
2
2
|
import { $getRoot as getRoot, HISTORY_MERGE_TAG } from 'lexical';
|
|
3
|
+
import { getBackgroundUpdateTags } from '../background-update.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @import { LexicalEditor } from 'lexical';
|
|
@@ -85,6 +86,6 @@ export const observeCodeTheme = (editor) =>
|
|
|
85
86
|
}
|
|
86
87
|
});
|
|
87
88
|
},
|
|
88
|
-
{ tag: HISTORY_MERGE_TAG },
|
|
89
|
+
{ tag: getBackgroundUpdateTags(editor, [HISTORY_MERGE_TAG]) },
|
|
89
90
|
);
|
|
90
91
|
});
|
|
@@ -45,6 +45,11 @@ export const createEditorStore = () => {
|
|
|
45
45
|
let showConverterError = $state(false);
|
|
46
46
|
/** @type {boolean} */
|
|
47
47
|
let pending = $state(false);
|
|
48
|
+
/**
|
|
49
|
+
* Number of editor operations started by the user that have yet to update the editor.
|
|
50
|
+
* @type {number}
|
|
51
|
+
*/
|
|
52
|
+
let runningOperations = $state(0);
|
|
48
53
|
/**
|
|
49
54
|
* Value last imported to the Lexical editor, and the Markdown the editor exports for it.
|
|
50
55
|
* @type {{ source: string, exported: string } | undefined}
|
|
@@ -195,7 +200,7 @@ export const createEditorStore = () => {
|
|
|
195
200
|
showConverterError = newValue;
|
|
196
201
|
},
|
|
197
202
|
get pending() {
|
|
198
|
-
return pending;
|
|
203
|
+
return pending || runningOperations > 0;
|
|
199
204
|
},
|
|
200
205
|
set pending(newValue) {
|
|
201
206
|
pending = newValue;
|
|
@@ -205,6 +210,27 @@ export const createEditorStore = () => {
|
|
|
205
210
|
},
|
|
206
211
|
editorId,
|
|
207
212
|
convertMarkdown,
|
|
213
|
+
/**
|
|
214
|
+
* Mark an operation the user started as running, which makes the content count as
|
|
215
|
+
* {@link TextEditorStore.pending} until it’s done. Use it for an operation that only reaches
|
|
216
|
+
* the editor after an `await`, such as a language change waiting for the highlighter to load:
|
|
217
|
+
* the flag the editor sets for itself is cleared by every update it makes in the meantime,
|
|
218
|
+
* including the one the focus move fires, so it can’t cover the wait.
|
|
219
|
+
* @returns {() => void} Function to call once the operation has updated the editor. Calling it
|
|
220
|
+
* more than once has no further effect.
|
|
221
|
+
*/
|
|
222
|
+
startOperation: () => {
|
|
223
|
+
runningOperations += 1;
|
|
224
|
+
|
|
225
|
+
let done = false;
|
|
226
|
+
|
|
227
|
+
return () => {
|
|
228
|
+
if (!done) {
|
|
229
|
+
done = true;
|
|
230
|
+
runningOperations -= 1;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
},
|
|
208
234
|
/**
|
|
209
235
|
* Get the value last imported if the given value, exported by the editor, is only that value
|
|
210
236
|
* written in the editor’s own Markdown style.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { getContext } from 'svelte';
|
|
6
6
|
import Option from '../../listbox/option.svelte';
|
|
7
7
|
import Select from '../../select/select.svelte';
|
|
8
|
+
import { getBackgroundUpdateTags } from '../background-update.js';
|
|
8
9
|
import { focusEditor, loadCodeHighlighter } from '../core.js';
|
|
9
10
|
import { LANGUAGES } from '../shiki/generated.js';
|
|
10
11
|
|
|
@@ -65,6 +66,46 @@
|
|
|
65
66
|
return isCodeNode(node) ? node : null;
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Give the code block the given language, once the highlighter for it has loaded.
|
|
71
|
+
* @param {string} lang Language ID.
|
|
72
|
+
*/
|
|
73
|
+
const changeLanguage = async (lang) => {
|
|
74
|
+
const { editor } = editorStore;
|
|
75
|
+
|
|
76
|
+
if (!editor || selectedKey === lang) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The change only reaches the editor, and anything bound to it, once the highlighter has
|
|
81
|
+
// loaded, so report the content as pending until then: the flag the editor sets for itself is
|
|
82
|
+
// cleared by the update the focus move below fires
|
|
83
|
+
const endOperation = editorStore.startOperation();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await focusEditor(editor);
|
|
87
|
+
await loadCodeHighlighter(lang);
|
|
88
|
+
|
|
89
|
+
// The user may have moved on to another field while the highlighter was loading
|
|
90
|
+
editor.update(
|
|
91
|
+
() => {
|
|
92
|
+
// https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx#L713
|
|
93
|
+
const node = getCodeNode();
|
|
94
|
+
|
|
95
|
+
if (node) {
|
|
96
|
+
node.setLanguage(lang);
|
|
97
|
+
selectedLanguage = lang;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{ tag: getBackgroundUpdateTags(editor) },
|
|
101
|
+
);
|
|
102
|
+
} finally {
|
|
103
|
+
// The editor has flagged the export that follows as pending by now, so the hand-off leaves
|
|
104
|
+
// no gap
|
|
105
|
+
endOperation();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
68
109
|
$effect(() => {
|
|
69
110
|
void editorStore.selection.blockNodeKey;
|
|
70
111
|
|
|
@@ -85,23 +126,8 @@
|
|
|
85
126
|
{disabled}
|
|
86
127
|
ariaLabel={_('_sui.text_editor.language')}
|
|
87
128
|
value={selectedKey}
|
|
88
|
-
onChange={
|
|
89
|
-
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
await focusEditor(editorStore.editor);
|
|
94
|
-
await loadCodeHighlighter(lang);
|
|
95
|
-
|
|
96
|
-
editorStore.editor.update(() => {
|
|
97
|
-
// https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx#L713
|
|
98
|
-
const node = getCodeNode();
|
|
99
|
-
|
|
100
|
-
if (node) {
|
|
101
|
-
node.setLanguage(lang);
|
|
102
|
-
selectedLanguage = lang;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
129
|
+
onChange={({ detail: { value: lang } }) => {
|
|
130
|
+
changeLanguage(lang);
|
|
105
131
|
}}
|
|
106
132
|
>
|
|
107
133
|
<Option label={_('_sui.text_editor.plain_text')} value="plain" dir="ltr" />
|
package/dist/typedefs.d.ts
CHANGED
|
@@ -1045,7 +1045,8 @@ export type TextEditorStore = {
|
|
|
1045
1045
|
showConverterError: boolean;
|
|
1046
1046
|
/**
|
|
1047
1047
|
* Whether the user has changed the rich text content, and the editor
|
|
1048
|
-
* has yet to convert it to Markdown and update {@link TextEditorStore.inputValue}.
|
|
1048
|
+
* has yet to convert it to Markdown and update {@link TextEditorStore.inputValue}. It’s also set
|
|
1049
|
+
* while an operation started with {@link TextEditorStore.startOperation} is running.
|
|
1049
1050
|
*/
|
|
1050
1051
|
pending: boolean;
|
|
1051
1052
|
/**
|
|
@@ -1057,6 +1058,12 @@ export type TextEditorStore = {
|
|
|
1057
1058
|
* Function to trigger the Lexical converter.
|
|
1058
1059
|
*/
|
|
1059
1060
|
convertMarkdown: () => Promise<void>;
|
|
1061
|
+
/**
|
|
1062
|
+
* Function marking an operation the user started, which
|
|
1063
|
+
* only updates the editor after an `await`, as running. It returns a function to call once the
|
|
1064
|
+
* operation is done. The content counts as {@link TextEditorStore.pending} meanwhile.
|
|
1065
|
+
*/
|
|
1066
|
+
startOperation: () => () => void;
|
|
1060
1067
|
/**
|
|
1061
1068
|
* Function to get the
|
|
1062
1069
|
* {@link TextEditorStore.inputValue} last imported if the given value, exported by the editor, is
|
package/dist/typedefs.js
CHANGED
|
@@ -447,10 +447,14 @@
|
|
|
447
447
|
* Lexical nodes.
|
|
448
448
|
* @property {boolean} showConverterError Whether to show a converter error in the UI.
|
|
449
449
|
* @property {boolean} pending Whether the user has changed the rich text content, and the editor
|
|
450
|
-
* has yet to convert it to Markdown and update {@link TextEditorStore.inputValue}.
|
|
450
|
+
* has yet to convert it to Markdown and update {@link TextEditorStore.inputValue}. It’s also set
|
|
451
|
+
* while an operation started with {@link TextEditorStore.startOperation} is running.
|
|
451
452
|
* @property {boolean} importing Whether the latest {@link TextEditorStore.inputValue} is still
|
|
452
453
|
* being imported to the Lexical editor.
|
|
453
454
|
* @property {() => Promise<void>} convertMarkdown Function to trigger the Lexical converter.
|
|
455
|
+
* @property {() => () => void} startOperation Function marking an operation the user started, which
|
|
456
|
+
* only updates the editor after an `await`, as running. It returns a function to call once the
|
|
457
|
+
* operation is done. The content counts as {@link TextEditorStore.pending} meanwhile.
|
|
454
458
|
* @property {(value: string) => string | undefined} getImportedValue Function to get the
|
|
455
459
|
* {@link TextEditorStore.inputValue} last imported if the given value, exported by the editor, is
|
|
456
460
|
* only that value written in the editor’s own Markdown style, e.g. `_text_` for `*text*`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sveltia/ui",
|
|
3
|
-
"version": "0.79.
|
|
3
|
+
"version": "0.79.4",
|
|
4
4
|
"description": "A collection of Svelte components and utilities for building user interfaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
"eslint-plugin-svelte": "^3.23.0",
|
|
86
86
|
"globals": "^17.13.0",
|
|
87
87
|
"happy-dom": "^20.14.5",
|
|
88
|
-
"oxlint": "^1.
|
|
88
|
+
"oxlint": "^1.87.0",
|
|
89
89
|
"playwright": "^1.63.0",
|
|
90
|
-
"postcss": "^8.5.
|
|
90
|
+
"postcss": "^8.5.29",
|
|
91
91
|
"postcss-html": "^2.0.0",
|
|
92
92
|
"prettier": "^3.9.9",
|
|
93
93
|
"prettier-plugin-svelte": "^4.1.1",
|