@sveltia/ui 0.78.0 → 0.78.2
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/core.js +22 -14
- package/dist/components/text-editor/lexical-root.svelte +6 -5
- 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 +4 -5
- package/dist/components/text-editor/text-editor.svelte +6 -0
- package/dist/components/util/popup.svelte +47 -15
- package/dist/typedefs.d.ts +4 -4
- package/dist/typedefs.js +3 -3
- package/package.json +1 -1
|
@@ -876,20 +876,28 @@ export const convertMarkdownToLexical = async (editor, value, enabledTransformer
|
|
|
876
876
|
// Pad blank blockquote lines so they are not imported as literal `>` text
|
|
877
877
|
value = padBlankBlockquoteLines(value);
|
|
878
878
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
879
|
+
/** @type {unknown} */
|
|
880
|
+
let error;
|
|
881
|
+
|
|
882
|
+
editor.update(
|
|
883
|
+
() => {
|
|
884
|
+
try {
|
|
885
|
+
convertFromMarkdownString(value, enabledTransformers);
|
|
886
|
+
} catch (ex) {
|
|
887
|
+
error = ex;
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
// Tell the import from a change made by the user, and commit it right away, so the node
|
|
891
|
+
// transforms, e.g. the one giving a code block its default language, have run by the time the
|
|
892
|
+
// content is exported below
|
|
893
|
+
{ tag: IMPORT_UPDATE_TAG, discrete: true },
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
if (error) {
|
|
897
|
+
throw new Error('Failed to convert Markdown', { cause: error });
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return editor.read(() => exportMarkdown(enabledTransformers));
|
|
893
901
|
};
|
|
894
902
|
|
|
895
903
|
/**
|
|
@@ -92,12 +92,13 @@
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const { value:
|
|
95
|
+
const { value: exportedValue, selection } = /** @type {CustomEvent} */ (event).detail;
|
|
96
|
+
// Keep the value as given when the content is the one last imported, which the editor only
|
|
97
|
+
// writes in its own Markdown style, e.g. `_text_` for `*text*`: that’s not a change, whether
|
|
98
|
+
// the content is untouched or the user has undone their changes to it
|
|
99
|
+
const newValue = editorStore.getImportedValue(exportedValue) ?? exportedValue;
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
// `_text_` for `*text*`: that’s not a change, and the value would otherwise change as soon as
|
|
99
|
-
// it’s set
|
|
100
|
-
if (inputValue !== newValue && !editorStore.isRestyledImport(newValue)) {
|
|
101
|
+
if (inputValue !== newValue) {
|
|
101
102
|
// Temporarily disable rich text to prevent unnecessary Markdown conversion that resets
|
|
102
103
|
// Lexical nodes and selection, then restore the state
|
|
103
104
|
editorStore.useRichText = false;
|
|
@@ -179,13 +179,12 @@ export const createEditorStore = () => {
|
|
|
179
179
|
editorId,
|
|
180
180
|
convertMarkdown,
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
183
|
-
* the editor’s own Markdown style.
|
|
182
|
+
* Get the value last imported if the given value, exported by the editor, is only that value
|
|
183
|
+
* written in the editor’s own Markdown style.
|
|
184
184
|
* @param {string} value Exported value.
|
|
185
|
-
* @returns {
|
|
185
|
+
* @returns {string | undefined} Imported value, or `undefined` if the content has changed.
|
|
186
186
|
*/
|
|
187
|
-
|
|
188
|
-
!!lastImport && lastImport.source === inputValue && lastImport.exported === value,
|
|
187
|
+
getImportedValue: (value) => (lastImport?.exported === value ? lastImport.source : undefined),
|
|
189
188
|
/* eslint-enable jsdoc/require-jsdoc */
|
|
190
189
|
};
|
|
191
190
|
};
|
|
@@ -236,6 +236,12 @@
|
|
|
236
236
|
pending = editorStore.pending;
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// An editor removed right after a change never converts it, so don’t leave a bound `pending` set
|
|
240
|
+
// for good, which would hold up anything waiting for the change
|
|
241
|
+
$effect(() => () => {
|
|
242
|
+
pending = false;
|
|
243
|
+
});
|
|
244
|
+
|
|
239
245
|
$effect(() => {
|
|
240
246
|
// The root initializes the editor before these effects first run, and stays initialized
|
|
241
247
|
/* v8 ignore next */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
<script>
|
|
6
6
|
import { sleep } from '@sveltia/utils/misc';
|
|
7
|
-
import { onMount } from 'svelte';
|
|
7
|
+
import { onMount, tick } from 'svelte';
|
|
8
8
|
import { activatePopup } from '../../services/popup.svelte.js';
|
|
9
9
|
import Modal from './modal.svelte';
|
|
10
10
|
|
|
@@ -81,6 +81,41 @@
|
|
|
81
81
|
let popupInstance = $state();
|
|
82
82
|
let hoveredTimeout = 0;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Move the focus into the content. A composite widget keeps exactly one of its items in the tab
|
|
86
|
+
* order and takes its own root out of it, so the item is what should receive focus. Falling back
|
|
87
|
+
* to any focusable element covers the popups that hold plain content.
|
|
88
|
+
* @param {object} [options] Options.
|
|
89
|
+
* @param {boolean} [options.tabStopOnly] Whether to only focus an element in the tab order,
|
|
90
|
+
* without falling back to any other element.
|
|
91
|
+
* @returns {boolean} Whether an element has taken the focus.
|
|
92
|
+
*/
|
|
93
|
+
const focusContent = ({ tabStopOnly = false } = {}) => {
|
|
94
|
+
/* v8 ignore next 3 -- the callers check the content first */
|
|
95
|
+
if (!content) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const candidates = /** @type {HTMLElement[]} */ ([
|
|
100
|
+
...content.querySelectorAll('[tabindex="0"]:not([aria-disabled="true"])'),
|
|
101
|
+
...(tabStopOnly ? [] : content.querySelectorAll('[tabindex]:not([aria-disabled="true"])')),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
// Skip an element that can’t take the focus, e.g. a hidden one
|
|
105
|
+
const focused = candidates.some((element) => {
|
|
106
|
+
element.focus();
|
|
107
|
+
|
|
108
|
+
return document.activeElement === element;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!focused && !tabStopOnly) {
|
|
112
|
+
content.tabIndex = -1;
|
|
113
|
+
content.focus();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return focused;
|
|
117
|
+
};
|
|
118
|
+
|
|
84
119
|
// Keep the `open` prop and the instance in sync both ways. The instance opens the popup when
|
|
85
120
|
// the anchor is activated, and closes it when the backdrop or an item is clicked, or Escape is
|
|
86
121
|
// pressed; the prop is what a consumer such as `<Combobox>` or `<MenuItem>` toggles from its own
|
|
@@ -198,25 +233,22 @@
|
|
|
198
233
|
onOpen={async (event) => {
|
|
199
234
|
onOpen?.(event);
|
|
200
235
|
|
|
201
|
-
|
|
236
|
+
// Move the focus into the content as soon as it’s rendered, if it holds a control that’s
|
|
237
|
+
// already in the tab order, like the search box of a dropdown list: a key typed right after
|
|
238
|
+
// the popup opens would otherwise be lost
|
|
239
|
+
await tick();
|
|
202
240
|
|
|
203
|
-
if (!content) {
|
|
241
|
+
if (!content || focusContent({ tabStopOnly: true })) {
|
|
204
242
|
return;
|
|
205
243
|
}
|
|
206
244
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
const target = /** @type {HTMLElement} */ (
|
|
211
|
-
content.querySelector('[tabindex="0"]:not([aria-disabled="true"])') ??
|
|
212
|
-
content.querySelector('[tabindex]:not([aria-disabled="true"])')
|
|
213
|
-
);
|
|
245
|
+
// Otherwise wait for a composite widget, such as a menu, to put one of its items in the tab
|
|
246
|
+
// order, which it does a moment later
|
|
247
|
+
await sleep(100);
|
|
214
248
|
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
content.tabIndex = -1;
|
|
219
|
-
content.focus();
|
|
249
|
+
// Leave the focus alone if the user has already moved it within the content
|
|
250
|
+
if (content && !content.contains(document.activeElement)) {
|
|
251
|
+
focusContent();
|
|
220
252
|
}
|
|
221
253
|
}}
|
|
222
254
|
>
|
package/dist/typedefs.d.ts
CHANGED
|
@@ -1046,11 +1046,11 @@ export type TextEditorStore = {
|
|
|
1046
1046
|
*/
|
|
1047
1047
|
convertMarkdown: () => Promise<void>;
|
|
1048
1048
|
/**
|
|
1049
|
-
* Function to
|
|
1050
|
-
*
|
|
1051
|
-
* the editor’s own Markdown style, e.g. `_text_` for `*text
|
|
1049
|
+
* Function to get the
|
|
1050
|
+
* {@link TextEditorStore.inputValue} last imported if the given value, exported by the editor, is
|
|
1051
|
+
* only that value written in the editor’s own Markdown style, e.g. `_text_` for `*text*`.
|
|
1052
1052
|
*/
|
|
1053
|
-
|
|
1053
|
+
getImportedValue: (value: string) => string | undefined;
|
|
1054
1054
|
};
|
|
1055
1055
|
export type SelectedItemDetail = {
|
|
1056
1056
|
/**
|
package/dist/typedefs.js
CHANGED
|
@@ -445,9 +445,9 @@
|
|
|
445
445
|
* @property {boolean} pending Whether the user has changed the rich text content, and the editor
|
|
446
446
|
* has yet to convert it to Markdown and update {@link TextEditorStore.inputValue}.
|
|
447
447
|
* @property {() => Promise<void>} convertMarkdown Function to trigger the Lexical converter.
|
|
448
|
-
* @property {(value: string) =>
|
|
449
|
-
*
|
|
450
|
-
* the editor’s own Markdown style, e.g. `_text_` for `*text
|
|
448
|
+
* @property {(value: string) => string | undefined} getImportedValue Function to get the
|
|
449
|
+
* {@link TextEditorStore.inputValue} last imported if the given value, exported by the editor, is
|
|
450
|
+
* only that value written in the editor’s own Markdown style, e.g. `_text_` for `*text*`.
|
|
451
451
|
*/
|
|
452
452
|
|
|
453
453
|
/**
|