@sveltia/ui 0.77.3 → 0.78.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/dialog/dialog.svelte +7 -4
- package/dist/components/text-editor/constants.d.ts +5 -0
- package/dist/components/text-editor/constants.js +6 -0
- package/dist/components/text-editor/core.d.ts +6 -2
- package/dist/components/text-editor/core.js +316 -46
- package/dist/components/text-editor/lexical-root.svelte +39 -3
- package/dist/components/text-editor/raw-markdown.d.ts +90 -0
- package/dist/components/text-editor/raw-markdown.js +471 -0
- package/dist/components/text-editor/shiki/facade.d.ts +4 -0
- package/dist/components/text-editor/shiki/facade.js +62 -0
- package/dist/components/text-editor/shiki/theme.d.ts +1 -0
- package/dist/components/text-editor/shiki/theme.js +31 -27
- 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 +39 -1
- package/dist/components/text-editor/text-editor.svelte +137 -1
- package/dist/components/text-editor/text-editor.svelte.d.ts +19 -3
- package/dist/components/text-editor/toolbar/component-editor.svelte +96 -0
- package/dist/components/text-editor/toolbar/component-editor.svelte.d.ts +35 -0
- package/dist/components/text-editor/toolbar/format-text-button.svelte +69 -9
- package/dist/components/text-editor/toolbar/insert-component-dialog.svelte +92 -0
- package/dist/components/text-editor/toolbar/insert-component-dialog.svelte.d.ts +30 -0
- package/dist/components/text-editor/toolbar/insert-item-button.svelte +13 -2
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +89 -10
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte +21 -2
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +4 -4
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +13 -0
- package/dist/components/text-field/text-area.svelte +119 -4
- package/dist/components/text-field/text-area.svelte.d.ts +17 -0
- package/dist/components/util/font-links.svelte +1 -1
- package/dist/components/util/fonts.js +1 -1
- package/dist/self-hosted/fonts/material-symbols-outlined-latin-wght-normal.woff2 +0 -0
- package/dist/services/group.svelte.d.ts +1 -1
- package/dist/services/group.svelte.js +14 -6
- package/dist/services/tree.svelte.d.ts +6 -1
- package/dist/services/tree.svelte.js +23 -7
- package/dist/typedefs.d.ts +49 -1
- package/dist/typedefs.js +22 -1
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { $getNearestNodeOfType as getNearestNodeOfType } from '@lexical/utils';
|
|
4
4
|
import { _ } from '@sveltia/i18n';
|
|
5
5
|
import { isMac, matchesShortcuts } from '@sveltia/utils/events';
|
|
6
|
+
import { isURL } from '@sveltia/utils/string';
|
|
6
7
|
import {
|
|
7
8
|
COMMAND_PRIORITY_NORMAL,
|
|
8
9
|
KEY_DOWN_COMMAND,
|
|
@@ -22,6 +23,12 @@
|
|
|
22
23
|
import TextInput from '../../text-field/text-input.svelte';
|
|
23
24
|
import { AVAILABLE_BUTTONS } from '../constants.js';
|
|
24
25
|
import { focusEditor, isSafeLinkURL } from '../core.js';
|
|
26
|
+
import {
|
|
27
|
+
applyRawTextEdit,
|
|
28
|
+
getRawTextState,
|
|
29
|
+
insertLink,
|
|
30
|
+
isRawTextEditable,
|
|
31
|
+
} from '../raw-markdown.js';
|
|
25
32
|
|
|
26
33
|
/**
|
|
27
34
|
* @import { TextEditorStore } from '../../../typedefs';
|
|
@@ -50,17 +57,39 @@
|
|
|
50
57
|
*/
|
|
51
58
|
const isValidURL = $derived(!!anchorURL.trim() && isSafeLinkURL(anchorURL));
|
|
52
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Open the dialog to create a new link from the given selected text.
|
|
62
|
+
* @param {string} textContent Selected text.
|
|
63
|
+
*/
|
|
64
|
+
const openCreateDialog = (textContent) => {
|
|
65
|
+
// Prefill the URL field with the selected text only if it’s a URL that can be linked to.
|
|
66
|
+
// Otherwise, it’s just the link text, like a word or phrase.
|
|
67
|
+
anchorURL = isURL(textContent) && isSafeLinkURL(textContent) ? textContent : '';
|
|
68
|
+
hasAnchor = !!textContent;
|
|
69
|
+
dialogMode = 'create';
|
|
70
|
+
openDialog = true;
|
|
71
|
+
};
|
|
72
|
+
|
|
53
73
|
/**
|
|
54
74
|
* Create a new link by showing a dialog to accept a URL and optionally text.
|
|
55
75
|
*/
|
|
56
76
|
const createLink = () => {
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
const { textArea, useRichText } = editorStore;
|
|
78
|
+
|
|
79
|
+
if (!useRichText) {
|
|
80
|
+
// The button is only enabled while the `<textarea>` is there
|
|
81
|
+
/* v8 ignore else */
|
|
82
|
+
if (textArea) {
|
|
83
|
+
const { value, start, end } = getRawTextState(textArea);
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
openCreateDialog(value.slice(start, end).trim());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
editorStore.editor?.getEditorState().read(() => {
|
|
92
|
+
openCreateDialog(getTextContent().trim());
|
|
64
93
|
});
|
|
65
94
|
};
|
|
66
95
|
|
|
@@ -109,7 +138,8 @@
|
|
|
109
138
|
* create a new link.
|
|
110
139
|
*/
|
|
111
140
|
const onButtonClick = () => {
|
|
112
|
-
|
|
141
|
+
// Links are not detected in the plain text mode, so a new one is always created there
|
|
142
|
+
if (selectionTypeMatches && editorStore.useRichText) {
|
|
113
143
|
updateLink();
|
|
114
144
|
} else {
|
|
115
145
|
createLink();
|
|
@@ -132,6 +162,31 @@
|
|
|
132
162
|
* @see https://github.com/facebook/lexical/discussions/3013
|
|
133
163
|
*/
|
|
134
164
|
const onDialogClose = async (event) => {
|
|
165
|
+
const { textArea, useRichText } = editorStore;
|
|
166
|
+
|
|
167
|
+
if (!useRichText) {
|
|
168
|
+
// The dialog can only be opened from the button, which needs the `<textarea>`
|
|
169
|
+
/* v8 ignore else */
|
|
170
|
+
if (textArea) {
|
|
171
|
+
if (event.detail.returnValue !== 'cancel') {
|
|
172
|
+
applyRawTextEdit(
|
|
173
|
+
textArea,
|
|
174
|
+
insertLink(getRawTextState(textArea), {
|
|
175
|
+
url: anchorURL.trim(),
|
|
176
|
+
text: hasAnchor ? undefined : anchorText,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
textArea.focus();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
anchorURL = '';
|
|
185
|
+
anchorText = '';
|
|
186
|
+
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
135
190
|
if (event.detail.returnValue !== 'cancel' && dialogMode !== 'remove') {
|
|
136
191
|
// The dialog can only be opened from the button, which needs the editor
|
|
137
192
|
/* v8 ignore next */
|
|
@@ -187,6 +242,31 @@
|
|
|
187
242
|
COMMAND_PRIORITY_NORMAL,
|
|
188
243
|
);
|
|
189
244
|
|
|
245
|
+
$effect(() => {
|
|
246
|
+
const { textArea } = editorStore;
|
|
247
|
+
|
|
248
|
+
if (!textArea) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handle the keyboard shortcut in the plain text mode.
|
|
254
|
+
* @param {KeyboardEvent} event `keydown` event.
|
|
255
|
+
*/
|
|
256
|
+
const onKeyDown = (event) => {
|
|
257
|
+
if (isRawTextEditable(textArea) && matchesShortcuts(event, isMac() ? 'Meta+K' : 'Ctrl+K')) {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
onButtonClick();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
textArea.addEventListener('keydown', onKeyDown);
|
|
264
|
+
|
|
265
|
+
return () => {
|
|
266
|
+
textArea.removeEventListener('keydown', onKeyDown);
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
|
|
190
270
|
$effect(() => {
|
|
191
271
|
if (editorStore.editor) {
|
|
192
272
|
// Unregister on unmount, e.g. when the toolbar swaps the button out while the caret is in a
|
|
@@ -201,9 +281,8 @@
|
|
|
201
281
|
<Button
|
|
202
282
|
iconic
|
|
203
283
|
aria-label={_(`_sui.text_editor.${AVAILABLE_BUTTONS[type].labelKey}`)}
|
|
204
|
-
aria-controls={
|
|
205
|
-
|
|
206
|
-
pressed={selectionTypeMatches}
|
|
284
|
+
aria-controls={editorStore.controlId}
|
|
285
|
+
pressed={selectionTypeMatches && editorStore.useRichText}
|
|
207
286
|
onclick={() => {
|
|
208
287
|
onButtonClick();
|
|
209
288
|
}}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import MenuButton from '../../menu/menu-button.svelte';
|
|
10
10
|
import MenuItem from '../../menu/menu-item.svelte';
|
|
11
11
|
import Menu from '../../menu/menu.svelte';
|
|
12
|
+
import InsertComponentDialog from './insert-component-dialog.svelte';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @import { TextEditorComponent, TextEditorStore } from '../../../typedefs';
|
|
@@ -28,18 +29,34 @@
|
|
|
28
29
|
|
|
29
30
|
/** @type {TextEditorStore} */
|
|
30
31
|
const editorStore = getContext('editorStore');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Component to insert with the dialog in the plain text mode.
|
|
35
|
+
* @type {TextEditorComponent | undefined}
|
|
36
|
+
*/
|
|
37
|
+
let dialogComponent = $state();
|
|
38
|
+
let openDialog = $state(false);
|
|
31
39
|
</script>
|
|
32
40
|
|
|
33
|
-
<MenuButton
|
|
41
|
+
<MenuButton label={_('_sui.insert')}>
|
|
34
42
|
{#snippet endIcon()}
|
|
35
43
|
<Icon name="arrow_drop_down" class="small-arrow" />
|
|
36
44
|
{/snippet}
|
|
37
45
|
{#snippet popup()}
|
|
38
46
|
<Menu>
|
|
39
|
-
{#each components as
|
|
47
|
+
{#each components as component (component.id)}
|
|
48
|
+
{@const { label, icon, createNode } = component}
|
|
40
49
|
<MenuItem
|
|
41
50
|
{label}
|
|
42
51
|
onclick={() => {
|
|
52
|
+
// The plain text mode needs a dialog to fill in the component’s fields
|
|
53
|
+
if (!editorStore.useRichText) {
|
|
54
|
+
dialogComponent = component;
|
|
55
|
+
openDialog = true;
|
|
56
|
+
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
editorStore.editor?.update(() => {
|
|
44
61
|
// Add an additional paragraph for easier editing
|
|
45
62
|
insertNodes([createNode(), createParagraphNode()]);
|
|
@@ -56,3 +73,5 @@
|
|
|
56
73
|
</Menu>
|
|
57
74
|
{/snippet}
|
|
58
75
|
</MenuButton>
|
|
76
|
+
|
|
77
|
+
<InsertComponentDialog component={dialogComponent} bind:open={openDialog} />
|
|
@@ -80,9 +80,8 @@
|
|
|
80
80
|
<ToolbarWrapper disabled={disabled || readonly} ariaLabel={_('_sui.text_editor.text_editor')}>
|
|
81
81
|
{#if blockLevelButtons.length > 1}
|
|
82
82
|
<MenuButton
|
|
83
|
-
disabled={!editorStore.useRichText}
|
|
84
83
|
aria-label={_('_sui.text_editor.show_text_style_options')}
|
|
85
|
-
aria-controls={
|
|
84
|
+
aria-controls={editorStore.controlId}
|
|
86
85
|
>
|
|
87
86
|
{#snippet startIcon()}
|
|
88
87
|
<Icon
|
|
@@ -98,9 +97,10 @@
|
|
|
98
97
|
{/snippet}
|
|
99
98
|
</MenuButton>
|
|
100
99
|
{/if}
|
|
101
|
-
|
|
100
|
+
<!-- The language switcher only works with the rich text editor -->
|
|
101
|
+
{#if editorStore.useRichText && editorStore.selection.blockType === 'code-block'}
|
|
102
102
|
<Divider orientation="vertical" />
|
|
103
|
-
<CodeLanguageSwitcher
|
|
103
|
+
<CodeLanguageSwitcher />
|
|
104
104
|
{:else}
|
|
105
105
|
{#if inlineLevelButtons.length}
|
|
106
106
|
<Divider orientation="vertical" />
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import MenuItemCheckbox from '../../menu/menu-item-checkbox.svelte';
|
|
17
17
|
import { AVAILABLE_BUTTONS } from '../constants.js';
|
|
18
18
|
import { focusEditor } from '../core.js';
|
|
19
|
+
import { applyRawTextEdit, getRawTextState, setBlockType } from '../raw-markdown.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* @import { TextEditorBlockType, TextEditorStore } from '../../../typedefs';
|
|
@@ -44,6 +45,18 @@
|
|
|
44
45
|
* Change the current selection’s type to {@link type}.
|
|
45
46
|
*/
|
|
46
47
|
const changeBlockType = async () => {
|
|
48
|
+
if (!editorStore.useRichText) {
|
|
49
|
+
const { textArea } = editorStore;
|
|
50
|
+
|
|
51
|
+
// The item is only clickable while the `<textarea>` is there
|
|
52
|
+
/* v8 ignore else */
|
|
53
|
+
if (textArea) {
|
|
54
|
+
applyRawTextEdit(textArea, setBlockType(getRawTextState(textArea), type));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
// The item is only clickable while the editor is there
|
|
48
61
|
/* v8 ignore next */
|
|
49
62
|
if (!editorStore.editor) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @import { Snippet } from 'svelte';
|
|
13
|
-
* @import { CommonEventHandlers, InputEventHandlers } from '../../typedefs';
|
|
13
|
+
* @import { CommonEventHandlers, HighlightedToken, InputEventHandlers } from '../../typedefs';
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* content.
|
|
25
25
|
* @property {boolean} [useEmojiAutocomplete] Whether to autocomplete emojis when the user
|
|
26
26
|
* types a shortcode, like `:smi`.
|
|
27
|
+
* @property {(value: string) => HighlightedToken[][] | undefined} [highlight] Function to
|
|
28
|
+
* tokenize the value for syntax highlighting, returning one array of tokens per line, or
|
|
29
|
+
* `undefined` to show the text as is, for example while a grammar is still loading. The tokens
|
|
30
|
+
* are painted behind the text, which is made transparent, so the `<textarea>` keeps working as
|
|
31
|
+
* usual. Only styles that keep the text width can be applied, so the glyphs stay aligned.
|
|
27
32
|
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
28
33
|
* @property {boolean} [hidden] Whether to hide the widget.
|
|
29
34
|
* @property {boolean} [disabled] Whether to disable the widget. An alias of the `aria-disabled`
|
|
@@ -49,6 +54,7 @@
|
|
|
49
54
|
name = undefined,
|
|
50
55
|
autoResize = false,
|
|
51
56
|
useEmojiAutocomplete = false,
|
|
57
|
+
highlight = undefined,
|
|
52
58
|
class: className,
|
|
53
59
|
hidden = false,
|
|
54
60
|
disabled = false,
|
|
@@ -67,6 +73,51 @@
|
|
|
67
73
|
* @type {string}
|
|
68
74
|
*/
|
|
69
75
|
const cloneValue = $derived(`${value}\n`);
|
|
76
|
+
/**
|
|
77
|
+
* Syntax-highlighted tokens of the value, flattened with line breaks in between, plus a trailing
|
|
78
|
+
* one like {@link cloneValue}. They’re only used when they add up to the value exactly, since
|
|
79
|
+
* stale tokens would show text different from what’s being edited.
|
|
80
|
+
* @type {HighlightedToken[] | undefined}
|
|
81
|
+
*/
|
|
82
|
+
const highlightedTokens = $derived.by(() => {
|
|
83
|
+
const lines = highlight?.(value ?? '');
|
|
84
|
+
|
|
85
|
+
if (!lines) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tokens = lines.flatMap((line) => [...line, { content: '\n' }]);
|
|
90
|
+
|
|
91
|
+
return tokens.map(({ content }) => content).join('') === cloneValue ? tokens : undefined;
|
|
92
|
+
});
|
|
93
|
+
/** A reference to the clone element. */
|
|
94
|
+
let cloneElement = $state();
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
// Keep the highlighted text scrolled along with the `<textarea>`. It never scrolls when it’s
|
|
98
|
+
// auto-resized
|
|
99
|
+
if (!element || !cloneElement || autoResize) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const textarea = element;
|
|
104
|
+
const clone = cloneElement;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sync the scroll position.
|
|
108
|
+
*/
|
|
109
|
+
const onScroll = () => {
|
|
110
|
+
clone.scrollTop = textarea.scrollTop;
|
|
111
|
+
clone.scrollLeft = textarea.scrollLeft;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
onScroll();
|
|
115
|
+
textarea.addEventListener('scroll', onScroll);
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
textarea.removeEventListener('scroll', onScroll);
|
|
119
|
+
};
|
|
120
|
+
});
|
|
70
121
|
</script>
|
|
71
122
|
|
|
72
123
|
<div
|
|
@@ -75,6 +126,7 @@
|
|
|
75
126
|
class:flex
|
|
76
127
|
class:disabled
|
|
77
128
|
class:readonly
|
|
129
|
+
class:highlighted={!!highlightedTokens}
|
|
78
130
|
{hidden}
|
|
79
131
|
>
|
|
80
132
|
<textarea
|
|
@@ -91,8 +143,24 @@
|
|
|
91
143
|
aria-required={required}
|
|
92
144
|
aria-invalid={invalid}
|
|
93
145
|
class:auto-resize={autoResize}></textarea>
|
|
94
|
-
{#if
|
|
95
|
-
|
|
146
|
+
{#if highlightedTokens}
|
|
147
|
+
<!-- Line breaks are only allowed within tags, as any whitespace would end up in the text -->
|
|
148
|
+
<!-- prettier-ignore -->
|
|
149
|
+
<div
|
|
150
|
+
bind:this={cloneElement}
|
|
151
|
+
class="clone"
|
|
152
|
+
class:auto-resize={autoResize}
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
{dir}
|
|
155
|
+
>{#each highlightedTokens as token, index (index)}<span
|
|
156
|
+
style:color={token.color}
|
|
157
|
+
class:bold={token.bold}
|
|
158
|
+
class:underline={token.underline}
|
|
159
|
+
class:strikethrough={token.strikethrough}
|
|
160
|
+
>{token.content}</span
|
|
161
|
+
>{/each}</div>
|
|
162
|
+
{:else if autoResize}
|
|
163
|
+
<div class="clone auto-resize" aria-hidden="true" {dir}>{cloneValue}</div>
|
|
96
164
|
{/if}
|
|
97
165
|
{#if useEmojiAutocomplete && !disabled && !readonly}
|
|
98
166
|
<EmojiAutocomplete {element} />
|
|
@@ -100,6 +168,7 @@
|
|
|
100
168
|
</div>
|
|
101
169
|
|
|
102
170
|
<style>.text-area {
|
|
171
|
+
position: relative;
|
|
103
172
|
display: inline-grid;
|
|
104
173
|
margin: var(--sui-focus-ring-width);
|
|
105
174
|
min-width: var(--sui-textbox-multiline-min-width);
|
|
@@ -145,7 +214,7 @@
|
|
|
145
214
|
color: var(--sui-textbox-foreground-color-focus, var(--sui-textbox-foreground-color));
|
|
146
215
|
background-color: var(--sui-textbox-background-color-focus, var(--sui-textbox-background-color));
|
|
147
216
|
}
|
|
148
|
-
:is(textarea, .clone):is(:disabled, :read-only) {
|
|
217
|
+
:is(textarea, .clone):is(:disabled, :read-only):not(.clone) {
|
|
149
218
|
background-color: var(--sui-disabled-background-color);
|
|
150
219
|
}
|
|
151
220
|
|
|
@@ -164,6 +233,52 @@ textarea[aria-invalid=true] {
|
|
|
164
233
|
overflow: hidden;
|
|
165
234
|
visibility: hidden;
|
|
166
235
|
}
|
|
236
|
+
.clone:not(.auto-resize) {
|
|
237
|
+
position: absolute;
|
|
238
|
+
inset: 0;
|
|
239
|
+
min-height: 0;
|
|
240
|
+
scroll-behavior: auto;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.highlighted textarea {
|
|
244
|
+
z-index: 1;
|
|
245
|
+
caret-color: var(--sui-textbox-foreground-color);
|
|
246
|
+
color: transparent !important;
|
|
247
|
+
background-color: transparent !important;
|
|
248
|
+
}
|
|
249
|
+
.highlighted textarea:not(.auto-resize) {
|
|
250
|
+
scrollbar-gutter: stable;
|
|
251
|
+
}
|
|
252
|
+
.highlighted textarea::placeholder {
|
|
253
|
+
color: var(--sui-textbox-placeholder-foreground-color, var(--sui-textbox-foreground-color));
|
|
254
|
+
opacity: var(--sui-textbox-placeholder-opacity, 1);
|
|
255
|
+
}
|
|
256
|
+
.highlighted .clone {
|
|
257
|
+
visibility: visible;
|
|
258
|
+
pointer-events: none;
|
|
259
|
+
}
|
|
260
|
+
.highlighted .clone .bold {
|
|
261
|
+
-webkit-text-stroke: 0.03em currentColor;
|
|
262
|
+
}
|
|
263
|
+
.highlighted .clone .underline {
|
|
264
|
+
text-decoration-line: underline;
|
|
265
|
+
}
|
|
266
|
+
.highlighted .clone .strikethrough {
|
|
267
|
+
text-decoration-line: line-through;
|
|
268
|
+
}
|
|
269
|
+
.highlighted .clone .underline.strikethrough {
|
|
270
|
+
text-decoration-line: underline line-through;
|
|
271
|
+
}
|
|
272
|
+
.highlighted .clone:not(.auto-resize) {
|
|
273
|
+
scrollbar-gutter: stable;
|
|
274
|
+
}
|
|
275
|
+
.highlighted:focus-within .clone {
|
|
276
|
+
color: var(--sui-textbox-foreground-color-focus, var(--sui-textbox-foreground-color));
|
|
277
|
+
background-color: var(--sui-textbox-background-color-focus, var(--sui-textbox-background-color));
|
|
278
|
+
}
|
|
279
|
+
.highlighted:is(.disabled, .readonly) .clone {
|
|
280
|
+
background-color: var(--sui-disabled-background-color);
|
|
281
|
+
}
|
|
167
282
|
|
|
168
283
|
textarea,
|
|
169
284
|
.clone {
|
|
@@ -40,6 +40,14 @@ declare const TextArea: import("svelte").Component<import("../../typedefs").Keyb
|
|
|
40
40
|
* types a shortcode, like `:smi`.
|
|
41
41
|
*/
|
|
42
42
|
useEmojiAutocomplete?: boolean | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Function to
|
|
45
|
+
* tokenize the value for syntax highlighting, returning one array of tokens per line, or
|
|
46
|
+
* `undefined` to show the text as is, for example while a grammar is still loading. The tokens
|
|
47
|
+
* are painted behind the text, which is made transparent, so the `<textarea>` keeps working as
|
|
48
|
+
* usual. Only styles that keep the text width can be applied, so the glyphs stay aligned.
|
|
49
|
+
*/
|
|
50
|
+
highlight?: ((value: string) => HighlightedToken[][] | undefined) | undefined;
|
|
43
51
|
/**
|
|
44
52
|
* The `class` attribute on the wrapper element.
|
|
45
53
|
*/
|
|
@@ -104,6 +112,14 @@ type Props = {
|
|
|
104
112
|
* types a shortcode, like `:smi`.
|
|
105
113
|
*/
|
|
106
114
|
useEmojiAutocomplete?: boolean | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Function to
|
|
117
|
+
* tokenize the value for syntax highlighting, returning one array of tokens per line, or
|
|
118
|
+
* `undefined` to show the text as is, for example while a grammar is still loading. The tokens
|
|
119
|
+
* are painted behind the text, which is made transparent, so the `<textarea>` keeps working as
|
|
120
|
+
* usual. Only styles that keep the text width can be applied, so the glyphs stay aligned.
|
|
121
|
+
*/
|
|
122
|
+
highlight?: ((value: string) => HighlightedToken[][] | undefined) | undefined;
|
|
107
123
|
/**
|
|
108
124
|
* The `class` attribute on the wrapper element.
|
|
109
125
|
*/
|
|
@@ -138,4 +154,5 @@ type Props = {
|
|
|
138
154
|
children?: Snippet<[]> | undefined;
|
|
139
155
|
};
|
|
140
156
|
import type { InputEventHandlers } from '../../typedefs';
|
|
157
|
+
import type { HighlightedToken } from '../../typedefs';
|
|
141
158
|
import type { Snippet } from 'svelte';
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
font-style: normal;
|
|
38
38
|
font-display: block;
|
|
39
39
|
font-weight: 400;
|
|
40
|
-
src: url(https://cdn.jsdelivr.net/fontsource/fonts/material-symbols-outlined:vf@5.3.
|
|
40
|
+
src: url(https://cdn.jsdelivr.net/fontsource/fonts/material-symbols-outlined:vf@5.3.8/latin-wght-normal.woff2)
|
|
41
41
|
format('woff2-variations');
|
|
42
42
|
unicode-range:
|
|
43
43
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
|
@@ -69,7 +69,7 @@ export const FONTS = [
|
|
|
69
69
|
{
|
|
70
70
|
family: 'Material Symbols Outlined',
|
|
71
71
|
packageName: '@fontsource-variable/material-symbols-outlined',
|
|
72
|
-
version: '5.3.
|
|
72
|
+
version: '5.3.8',
|
|
73
73
|
cdnId: 'material-symbols-outlined:vf',
|
|
74
74
|
subset: 'latin-wght-normal',
|
|
75
75
|
format: 'woff2-variations',
|
|
Binary file
|
|
@@ -199,7 +199,7 @@ export class Group {
|
|
|
199
199
|
activated = false;
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* Whether {@link destroy} has run, so a pending {@link activate} must not
|
|
202
|
+
* Whether {@link destroy} has run, so a pending {@link activate} must not run.
|
|
203
203
|
* @type {boolean}
|
|
204
204
|
*/
|
|
205
205
|
#destroyed = false;
|
|
@@ -284,12 +284,14 @@ export class Group {
|
|
|
284
284
|
// eslint-disable-next-line jsdoc/require-description
|
|
285
285
|
/** @type {(event: MouseEvent) => void} */
|
|
286
286
|
this._onClick = (event) => {
|
|
287
|
+
this.activate();
|
|
287
288
|
this.onClick(event);
|
|
288
289
|
};
|
|
289
290
|
|
|
290
291
|
// eslint-disable-next-line jsdoc/require-description
|
|
291
292
|
/** @type {(event: KeyboardEvent) => void} */
|
|
292
293
|
this._onKeyDown = (event) => {
|
|
294
|
+
this.activate();
|
|
293
295
|
this.onKeyDown(event);
|
|
294
296
|
};
|
|
295
297
|
|
|
@@ -352,12 +354,16 @@ export class Group {
|
|
|
352
354
|
attributeFilter: ['aria-disabled', 'aria-hidden'],
|
|
353
355
|
});
|
|
354
356
|
|
|
357
|
+
// Listen right away: a click or a key press that comes before the activation below activates
|
|
358
|
+
// the members first, rather than being lost
|
|
359
|
+
parent.addEventListener('click', this._onClick);
|
|
360
|
+
parent.addEventListener('keydown', this._onKeyDown);
|
|
361
|
+
|
|
355
362
|
// Wait a bit before the relevant components, including the `aria-controls` target are mounted
|
|
356
363
|
(async () => {
|
|
357
364
|
await sleep(100);
|
|
358
365
|
|
|
359
|
-
// The widget may have been unmounted in the meantime
|
|
360
|
-
// would then never be removed
|
|
366
|
+
// The widget may have been unmounted in the meantime
|
|
361
367
|
if (!this.#destroyed) {
|
|
362
368
|
this.activate();
|
|
363
369
|
}
|
|
@@ -365,9 +371,13 @@ export class Group {
|
|
|
365
371
|
}
|
|
366
372
|
|
|
367
373
|
/**
|
|
368
|
-
* Activate the members.
|
|
374
|
+
* Activate the members, unless they already are.
|
|
369
375
|
*/
|
|
370
376
|
activate() {
|
|
377
|
+
if (this.activated) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
371
381
|
const { parent, allMembers, selected: defaultSelected } = this;
|
|
372
382
|
|
|
373
383
|
this.activated = true;
|
|
@@ -408,8 +418,6 @@ export class Group {
|
|
|
408
418
|
});
|
|
409
419
|
|
|
410
420
|
this.updateTabStop();
|
|
411
|
-
parent.addEventListener('click', this._onClick);
|
|
412
|
-
parent.addEventListener('keydown', this._onKeyDown);
|
|
413
421
|
parent.dispatchEvent(new CustomEvent('Initialized'));
|
|
414
422
|
}
|
|
415
423
|
|
|
@@ -45,6 +45,11 @@ export class Tree {
|
|
|
45
45
|
* @type {ReturnType<typeof globalThis.setTimeout> | undefined}
|
|
46
46
|
*/
|
|
47
47
|
typeAheadTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Whether {@link activate} has run.
|
|
50
|
+
* @type {boolean}
|
|
51
|
+
*/
|
|
52
|
+
activated: boolean;
|
|
48
53
|
/**
|
|
49
54
|
* Whether {@link destroy} has run, so a pending {@link activate} must not set anything up.
|
|
50
55
|
* @type {boolean}
|
|
@@ -58,7 +63,7 @@ export class Tree {
|
|
|
58
63
|
_onFocusIn: (event: FocusEvent) => void;
|
|
59
64
|
observer: MutationObserver;
|
|
60
65
|
/**
|
|
61
|
-
* Activate the items.
|
|
66
|
+
* Activate the items, unless they already are.
|
|
62
67
|
*/
|
|
63
68
|
activate(): void;
|
|
64
69
|
/**
|
|
@@ -67,6 +67,12 @@ export class Tree {
|
|
|
67
67
|
*/
|
|
68
68
|
this.typeAheadTimer = undefined;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Whether {@link activate} has run.
|
|
72
|
+
* @type {boolean}
|
|
73
|
+
*/
|
|
74
|
+
this.activated = false;
|
|
75
|
+
|
|
70
76
|
/**
|
|
71
77
|
* Whether {@link destroy} has run, so a pending {@link activate} must not set anything up.
|
|
72
78
|
* @type {boolean}
|
|
@@ -76,18 +82,21 @@ export class Tree {
|
|
|
76
82
|
// eslint-disable-next-line jsdoc/require-description
|
|
77
83
|
/** @type {(event: MouseEvent) => void} */
|
|
78
84
|
this._onClick = (event) => {
|
|
85
|
+
this.activate();
|
|
79
86
|
this.onClick(event);
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
// eslint-disable-next-line jsdoc/require-description
|
|
83
90
|
/** @type {(event: KeyboardEvent) => void} */
|
|
84
91
|
this._onKeyDown = (event) => {
|
|
92
|
+
this.activate();
|
|
85
93
|
this.onKeyDown(event);
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
// eslint-disable-next-line jsdoc/require-description
|
|
89
97
|
/** @type {(event: FocusEvent) => void} */
|
|
90
98
|
this._onFocusIn = (event) => {
|
|
99
|
+
this.activate();
|
|
91
100
|
this.onFocusIn(event);
|
|
92
101
|
};
|
|
93
102
|
|
|
@@ -99,12 +108,18 @@ export class Tree {
|
|
|
99
108
|
// The widget root itself is never part of the tab order; one of the items always is
|
|
100
109
|
this.parent.tabIndex = -1;
|
|
101
110
|
|
|
111
|
+
// Listen right away: a click, a key press or a focus that comes before the activation below
|
|
112
|
+
// activates the items first, rather than being lost
|
|
113
|
+
parent.addEventListener('click', this._onClick);
|
|
114
|
+
parent.addEventListener('keydown', this._onKeyDown);
|
|
115
|
+
parent.addEventListener('focusin', this._onFocusIn);
|
|
116
|
+
|
|
102
117
|
// Wait a bit before the child components are mounted
|
|
103
118
|
(async () => {
|
|
104
119
|
await sleep(100);
|
|
105
120
|
|
|
106
|
-
// The widget may have been unmounted in the meantime, and the
|
|
107
|
-
//
|
|
121
|
+
// The widget may have been unmounted in the meantime, and the observer set up by
|
|
122
|
+
// `activate()` would then never be disconnected
|
|
108
123
|
if (!this.destroyed) {
|
|
109
124
|
this.activate();
|
|
110
125
|
}
|
|
@@ -112,16 +127,17 @@ export class Tree {
|
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
/**
|
|
115
|
-
* Activate the items.
|
|
130
|
+
* Activate the items, unless they already are.
|
|
116
131
|
*/
|
|
117
132
|
activate() {
|
|
133
|
+
if (this.activated) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
const { parent } = this;
|
|
119
138
|
|
|
139
|
+
this.activated = true;
|
|
120
140
|
this.update();
|
|
121
|
-
|
|
122
|
-
parent.addEventListener('click', this._onClick);
|
|
123
|
-
parent.addEventListener('keydown', this._onKeyDown);
|
|
124
|
-
parent.addEventListener('focusin', this._onFocusIn);
|
|
125
141
|
this.observer.observe(parent, { childList: true, subtree: true });
|
|
126
142
|
parent.dispatchEvent(new CustomEvent('Initialized'));
|
|
127
143
|
}
|