@sveltia/ui 0.79.3 → 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.
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
3
3
  */
4
- export const UI_VERSION: "0.79.3";
4
+ export const UI_VERSION: "0.79.4";
@@ -3,4 +3,4 @@
3
3
  /**
4
4
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
5
5
  */
6
- export const UI_VERSION = '0.79.3';
6
+ export const UI_VERSION = '0.79.4';
@@ -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.
@@ -66,6 +66,46 @@
66
66
  return isCodeNode(node) ? node : null;
67
67
  };
68
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
+
69
109
  $effect(() => {
70
110
  void editorStore.selection.blockNodeKey;
71
111
 
@@ -86,29 +126,8 @@
86
126
  {disabled}
87
127
  ariaLabel={_('_sui.text_editor.language')}
88
128
  value={selectedKey}
89
- onChange={async ({ detail: { value: lang } }) => {
90
- if (!editorStore.editor || selectedKey === lang) {
91
- return;
92
- }
93
-
94
- const { editor } = editorStore;
95
-
96
- await focusEditor(editor);
97
- await loadCodeHighlighter(lang);
98
-
99
- // The user may have moved on to another field while the highlighter was loading
100
- editor.update(
101
- () => {
102
- // https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx#L713
103
- const node = getCodeNode();
104
-
105
- if (node) {
106
- node.setLanguage(lang);
107
- selectedLanguage = lang;
108
- }
109
- },
110
- { tag: getBackgroundUpdateTags(editor) },
111
- );
129
+ onChange={({ detail: { value: lang } }) => {
130
+ changeLanguage(lang);
112
131
  }}
113
132
  >
114
133
  <Option label={_('_sui.text_editor.plain_text')} value="plain" dir="ltr" />
@@ -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",
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.86.0",
88
+ "oxlint": "^1.87.0",
89
89
  "playwright": "^1.63.0",
90
- "postcss": "^8.5.28",
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",