@sveltia/ui 0.23.2 → 0.24.1
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/listbox/listbox.svelte +1 -1
- package/dist/components/listbox/option.svelte +7 -2
- package/dist/components/text-editor/code-editor.svelte +121 -0
- package/dist/components/text-editor/code-editor.svelte.d.ts +106 -0
- package/dist/components/text-editor/core.d.ts +3 -4
- package/dist/components/text-editor/core.js +78 -33
- package/dist/components/text-editor/lexical-root.svelte +42 -19
- package/dist/components/text-editor/lexical-root.svelte.d.ts +1 -9
- package/dist/components/text-editor/store.svelte.d.ts +1 -0
- package/dist/components/text-editor/store.svelte.js +120 -0
- package/dist/components/text-editor/text-editor.svelte +21 -86
- package/dist/components/text-editor/toolbar/code-editor-toolbar.svelte +28 -0
- package/dist/components/text-editor/toolbar/{editor-toolbar.svelte.d.ts → code-editor-toolbar.svelte.d.ts} +3 -3
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +96 -0
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte.d.ts +17 -0
- package/dist/components/text-editor/toolbar/format-text-button.svelte +10 -10
- package/dist/components/text-editor/toolbar/insert-image-button.svelte +5 -8
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +27 -25
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte +4 -7
- package/dist/components/text-editor/toolbar/{editor-toolbar.svelte → text-editor-toolbar.svelte} +59 -87
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte.d.ts +37 -0
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +14 -17
- package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte +58 -0
- package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte.d.ts +37 -0
- package/dist/components/util/app-shell.svelte +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/locales/en.d.ts +3 -0
- package/dist/locales/en.js +3 -0
- package/dist/locales/ja.d.ts +3 -0
- package/dist/locales/ja.js +3 -0
- package/dist/styles/variables.scss +2 -2
- package/dist/typedefs.d.ts +59 -26
- package/dist/typedefs.js +26 -13
- package/package.json +7 -7
|
@@ -89,7 +89,6 @@
|
|
|
89
89
|
.option :global(button) {
|
|
90
90
|
flex: none;
|
|
91
91
|
display: flex;
|
|
92
|
-
justify-content: space-between;
|
|
93
92
|
gap: 4px;
|
|
94
93
|
margin: 0 !important;
|
|
95
94
|
border-radius: var(--sui-option-border-radius);
|
|
@@ -101,6 +100,12 @@
|
|
|
101
100
|
text-overflow: ellipsis;
|
|
102
101
|
white-space: nowrap;
|
|
103
102
|
}
|
|
103
|
+
.option :global(button) :global(*) {
|
|
104
|
+
flex: none;
|
|
105
|
+
}
|
|
106
|
+
.option :global(button) :global(.label) {
|
|
107
|
+
flex: auto;
|
|
108
|
+
}
|
|
104
109
|
.option.wrap :global(button) {
|
|
105
110
|
white-space: normal;
|
|
106
111
|
}
|
|
@@ -115,6 +120,6 @@
|
|
|
115
120
|
.option :global(.icon.check) {
|
|
116
121
|
margin: -2px;
|
|
117
122
|
}
|
|
118
|
-
.option :global(button[aria-selected="true"]) :global(.icon) {
|
|
123
|
+
.option :global(button[aria-selected="true"]) :global(.icon.check) {
|
|
119
124
|
color: var(--sui-primary-accent-color-text);
|
|
120
125
|
}</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A code editor based on Lexical.
|
|
4
|
+
-->
|
|
5
|
+
<script>
|
|
6
|
+
import { setContext, untrack } from 'svelte';
|
|
7
|
+
import { _ } from 'svelte-i18n';
|
|
8
|
+
import Alert from '../alert/alert.svelte';
|
|
9
|
+
import Toast from '../toast/toast.svelte';
|
|
10
|
+
import LexicalRoot from './lexical-root.svelte';
|
|
11
|
+
import { createEditorStore } from './store.svelte';
|
|
12
|
+
import CodeEditorToolbar from './toolbar/code-editor-toolbar.svelte';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} Props
|
|
16
|
+
* @property {string} [code] - Input value.
|
|
17
|
+
* @property {string} [lang] - Selected language.
|
|
18
|
+
* @property {boolean} [showLanguageSwitcher] - Whether to show the language selector.
|
|
19
|
+
* @property {boolean} [flex] - Make the text input container flexible.
|
|
20
|
+
* @property {string} [class] - The `class` attribute on the wrapper element.
|
|
21
|
+
* @property {boolean} [hidden] - Whether to hide the widget.
|
|
22
|
+
* @property {boolean} [disabled] - Whether to disable the widget. An alias of the `aria-disabled`
|
|
23
|
+
* attribute.
|
|
24
|
+
* @property {boolean} [readonly] - Whether to make the widget read-only. An alias of the
|
|
25
|
+
* `aria-readonly` attribute.
|
|
26
|
+
* @property {boolean} [required] - Whether to mark the widget required. An alias of the
|
|
27
|
+
* `aria-required` attribute.
|
|
28
|
+
* @property {boolean} [invalid] - Whether to mark the widget invalid. An alias of the
|
|
29
|
+
* `aria-invalid` attribute.
|
|
30
|
+
* @property {import('svelte').Snippet} [children] - Primary slot content.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @type {Props & Record<string, any>}
|
|
35
|
+
*/
|
|
36
|
+
let {
|
|
37
|
+
/* eslint-disable prefer-const */
|
|
38
|
+
code = $bindable(''),
|
|
39
|
+
lang = $bindable(''),
|
|
40
|
+
showLanguageSwitcher = false,
|
|
41
|
+
flex = false,
|
|
42
|
+
hidden = false,
|
|
43
|
+
disabled = false,
|
|
44
|
+
readonly = false,
|
|
45
|
+
required = false,
|
|
46
|
+
invalid = false,
|
|
47
|
+
children,
|
|
48
|
+
...restProps
|
|
49
|
+
/* eslint-enable prefer-const */
|
|
50
|
+
} = $props();
|
|
51
|
+
|
|
52
|
+
const backticks = '```';
|
|
53
|
+
const editorStore = createEditorStore();
|
|
54
|
+
|
|
55
|
+
editorStore.config.isCodeEditor = true;
|
|
56
|
+
editorStore.config.defaultLanguage = lang;
|
|
57
|
+
|
|
58
|
+
setContext('editorStore', editorStore);
|
|
59
|
+
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (!editorStore.initialized) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void code;
|
|
66
|
+
void lang;
|
|
67
|
+
|
|
68
|
+
untrack(() => {
|
|
69
|
+
const newValue = code
|
|
70
|
+
? `${backticks}${lang}\n${code}\n${backticks}`
|
|
71
|
+
: `${backticks}${lang}\n${backticks}`;
|
|
72
|
+
|
|
73
|
+
editorStore.inputValue = newValue;
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (!editorStore.initialized) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
void editorStore.inputValue;
|
|
83
|
+
|
|
84
|
+
untrack(() => {
|
|
85
|
+
const { lang: _lang = '', code: _code = '' } =
|
|
86
|
+
editorStore.inputValue.match(/^```(?<lang>\w+?)?\n(?:(?<code>.*)\n)?```/s)?.groups ?? {};
|
|
87
|
+
|
|
88
|
+
if (lang !== _lang) {
|
|
89
|
+
lang = _lang;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (code !== _code) {
|
|
93
|
+
code = _code;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<div {...restProps} role="none" class="sui code-editor" class:flex {hidden}>
|
|
100
|
+
{#if showLanguageSwitcher}
|
|
101
|
+
<CodeEditorToolbar {disabled} {readonly} />
|
|
102
|
+
{/if}
|
|
103
|
+
<LexicalRoot
|
|
104
|
+
hidden={!editorStore.useRichText || hidden}
|
|
105
|
+
{disabled}
|
|
106
|
+
{readonly}
|
|
107
|
+
{required}
|
|
108
|
+
{invalid}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{#if editorStore.showConverterError}
|
|
113
|
+
<Toast bind:show={editorStore.showConverterError}>
|
|
114
|
+
<Alert status="error">{$_('_sui.text_editor.converter_error')}</Alert>
|
|
115
|
+
</Toast>
|
|
116
|
+
{/if}
|
|
117
|
+
|
|
118
|
+
<style>.code-editor {
|
|
119
|
+
margin: var(--sui-focus-ring-width);
|
|
120
|
+
width: calc(100% - var(--sui-focus-ring-width) * 2);
|
|
121
|
+
}</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export default CodeEditor;
|
|
2
|
+
type CodeEditor = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<Props & Record<string, any>>): void;
|
|
5
|
+
};
|
|
6
|
+
/** A code editor based on Lexical. */
|
|
7
|
+
declare const CodeEditor: import("svelte").Component<{
|
|
8
|
+
/**
|
|
9
|
+
* - Input value.
|
|
10
|
+
*/
|
|
11
|
+
code?: string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* - Selected language.
|
|
14
|
+
*/
|
|
15
|
+
lang?: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* - Whether to show the language selector.
|
|
18
|
+
*/
|
|
19
|
+
showLanguageSwitcher?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* - Make the text input container flexible.
|
|
22
|
+
*/
|
|
23
|
+
flex?: boolean | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* - The `class` attribute on the wrapper element.
|
|
26
|
+
*/
|
|
27
|
+
class?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* - Whether to hide the widget.
|
|
30
|
+
*/
|
|
31
|
+
hidden?: boolean | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* - Whether to disable the widget. An alias of the `aria-disabled`
|
|
34
|
+
* attribute.
|
|
35
|
+
*/
|
|
36
|
+
disabled?: boolean | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* - Whether to make the widget read-only. An alias of the
|
|
39
|
+
* `aria-readonly` attribute.
|
|
40
|
+
*/
|
|
41
|
+
readonly?: boolean | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* - Whether to mark the widget required. An alias of the
|
|
44
|
+
* `aria-required` attribute.
|
|
45
|
+
*/
|
|
46
|
+
required?: boolean | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* - Whether to mark the widget invalid. An alias of the
|
|
49
|
+
* `aria-invalid` attribute.
|
|
50
|
+
*/
|
|
51
|
+
invalid?: boolean | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* - Primary slot content.
|
|
54
|
+
*/
|
|
55
|
+
children?: import("svelte").Snippet<[]> | undefined;
|
|
56
|
+
} & Record<string, any>, {}, "code" | "lang">;
|
|
57
|
+
type Props = {
|
|
58
|
+
/**
|
|
59
|
+
* - Input value.
|
|
60
|
+
*/
|
|
61
|
+
code?: string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* - Selected language.
|
|
64
|
+
*/
|
|
65
|
+
lang?: string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* - Whether to show the language selector.
|
|
68
|
+
*/
|
|
69
|
+
showLanguageSwitcher?: boolean | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* - Make the text input container flexible.
|
|
72
|
+
*/
|
|
73
|
+
flex?: boolean | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* - The `class` attribute on the wrapper element.
|
|
76
|
+
*/
|
|
77
|
+
class?: string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* - Whether to hide the widget.
|
|
80
|
+
*/
|
|
81
|
+
hidden?: boolean | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* - Whether to disable the widget. An alias of the `aria-disabled`
|
|
84
|
+
* attribute.
|
|
85
|
+
*/
|
|
86
|
+
disabled?: boolean | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* - Whether to make the widget read-only. An alias of the
|
|
89
|
+
* `aria-readonly` attribute.
|
|
90
|
+
*/
|
|
91
|
+
readonly?: boolean | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* - Whether to mark the widget required. An alias of the
|
|
94
|
+
* `aria-required` attribute.
|
|
95
|
+
*/
|
|
96
|
+
required?: boolean | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* - Whether to mark the widget invalid. An alias of the
|
|
99
|
+
* `aria-invalid` attribute.
|
|
100
|
+
*/
|
|
101
|
+
invalid?: boolean | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* - Primary slot content.
|
|
104
|
+
*/
|
|
105
|
+
children?: import("svelte").Snippet<[]> | undefined;
|
|
106
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export function initEditor({ components }
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export function convertMarkdown(editor: import("lexical").LexicalEditor, value: string): Promise<void>;
|
|
1
|
+
export function initEditor({ components, isCodeEditor, defaultLanguage }: import("../../typedefs").TextEditorConfig): import("lexical").LexicalEditor;
|
|
2
|
+
export function loadCodeHighlighter(lang: string): Promise<void>;
|
|
3
|
+
export function convertMarkdownToLexical(editor: import("lexical").LexicalEditor, value: string): Promise<void>;
|
|
5
4
|
export function focusEditor(editor: import("lexical").LexicalEditor): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CodeHighlightNode,
|
|
3
3
|
CodeNode,
|
|
4
|
+
$createCodeNode as createCodeNode,
|
|
4
5
|
$isCodeHighlightNode as isCodeHighlightNode,
|
|
5
6
|
$isCodeNode as isCodeNode,
|
|
6
7
|
registerCodeHighlighting,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
INSERT_PARAGRAPH_COMMAND,
|
|
46
47
|
OUTDENT_CONTENT_COMMAND,
|
|
47
48
|
createEditor,
|
|
49
|
+
$getRoot as getRoot,
|
|
48
50
|
$getSelection as getSelection,
|
|
49
51
|
$isRangeSelection as isRangeSelection,
|
|
50
52
|
} from 'lexical';
|
|
@@ -122,15 +124,18 @@ const editorConfig = {
|
|
|
122
124
|
};
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
|
-
* Get the current selection’s block and inline level types.
|
|
126
|
-
* @returns {
|
|
127
|
-
* inlineTypes: import('../../typedefs').TextEditorInlineType[] }} Types.
|
|
127
|
+
* Get the current selection’s block node key as well as block and inline level types.
|
|
128
|
+
* @returns {import('../../typedefs').TextEditorSelectionState} Current selection state.
|
|
128
129
|
*/
|
|
129
130
|
const getSelectionTypes = () => {
|
|
130
131
|
const selection = getSelection();
|
|
131
132
|
|
|
132
133
|
if (!isRangeSelection(selection)) {
|
|
133
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
blockNodeKey: null,
|
|
136
|
+
blockType: 'paragraph',
|
|
137
|
+
inlineTypes: [],
|
|
138
|
+
};
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
const anchor = selection.anchor.getNode();
|
|
@@ -184,7 +189,11 @@ const getSelectionTypes = () => {
|
|
|
184
189
|
})()
|
|
185
190
|
);
|
|
186
191
|
|
|
187
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
blockNodeKey: parent?.getKey() ?? null,
|
|
194
|
+
blockType,
|
|
195
|
+
inlineTypes,
|
|
196
|
+
};
|
|
188
197
|
};
|
|
189
198
|
|
|
190
199
|
/**
|
|
@@ -192,8 +201,6 @@ const getSelectionTypes = () => {
|
|
|
192
201
|
* @param {import('lexical').LexicalEditor} editor - Editor instance.
|
|
193
202
|
*/
|
|
194
203
|
const onEditorUpdate = (editor) => {
|
|
195
|
-
const { blockType, inlineTypes } = getSelectionTypes();
|
|
196
|
-
|
|
197
204
|
editor.getRootElement()?.dispatchEvent(
|
|
198
205
|
new CustomEvent('Update', {
|
|
199
206
|
detail: {
|
|
@@ -201,8 +208,7 @@ const onEditorUpdate = (editor) => {
|
|
|
201
208
|
// Use underscores for italic text in Markdown instead of asterisks
|
|
202
209
|
allTransformers.filter((/** @type {any} */ { tag }) => tag !== '*'),
|
|
203
210
|
),
|
|
204
|
-
|
|
205
|
-
selectionInlineTypes: inlineTypes,
|
|
211
|
+
selection: getSelectionTypes(),
|
|
206
212
|
},
|
|
207
213
|
}),
|
|
208
214
|
);
|
|
@@ -210,12 +216,11 @@ const onEditorUpdate = (editor) => {
|
|
|
210
216
|
|
|
211
217
|
/**
|
|
212
218
|
* Initialize the Lexical editor.
|
|
213
|
-
* @param {
|
|
214
|
-
* @param {import('../../typedefs').TextEditorComponent[]} [options.components] - Editor components.
|
|
219
|
+
* @param {import('../../typedefs').TextEditorConfig} config - Editor configuration.
|
|
215
220
|
* @returns {import('lexical').LexicalEditor} Editor instance.
|
|
216
221
|
*/
|
|
217
|
-
export const initEditor = ({ components
|
|
218
|
-
components
|
|
222
|
+
export const initEditor = ({ components = [], isCodeEditor = false, defaultLanguage = '' }) => {
|
|
223
|
+
components.forEach(({ node, transformer }) => {
|
|
219
224
|
/** @type {any[]} */ (editorConfig.nodes).unshift(node);
|
|
220
225
|
allTransformers.unshift(transformer);
|
|
221
226
|
});
|
|
@@ -227,10 +232,10 @@ export const initEditor = ({ components } = {}) => {
|
|
|
227
232
|
registerHistory(editor, createEmptyHistoryState(), 1000);
|
|
228
233
|
|
|
229
234
|
registerCodeHighlighting(editor, {
|
|
230
|
-
defaultLanguage
|
|
235
|
+
defaultLanguage,
|
|
231
236
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
232
|
-
tokenize: (code,
|
|
233
|
-
window.Prism.tokenize(code, window.Prism.languages[
|
|
237
|
+
tokenize: (code, lang = 'plain') =>
|
|
238
|
+
window.Prism.tokenize(code, window.Prism.languages[lang] ?? window.Prism.languages.plain),
|
|
234
239
|
});
|
|
235
240
|
|
|
236
241
|
editor.registerCommand(
|
|
@@ -270,12 +275,31 @@ export const initEditor = ({ components } = {}) => {
|
|
|
270
275
|
COMMAND_PRIORITY_NORMAL,
|
|
271
276
|
);
|
|
272
277
|
|
|
273
|
-
editor.registerUpdateListener((
|
|
278
|
+
editor.registerUpdateListener(async () => {
|
|
274
279
|
if (editor?.isComposing()) {
|
|
275
280
|
return;
|
|
276
281
|
}
|
|
277
282
|
|
|
278
|
-
|
|
283
|
+
await sleep(100);
|
|
284
|
+
|
|
285
|
+
editor.update(() => {
|
|
286
|
+
// Prevent CodeNode from being removed
|
|
287
|
+
if (isCodeEditor) {
|
|
288
|
+
const root = getRoot();
|
|
289
|
+
const children = root.getChildren();
|
|
290
|
+
|
|
291
|
+
if (children.length === 1 && !isCodeNode(children[0])) {
|
|
292
|
+
children[0].remove();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (children.length === 0) {
|
|
296
|
+
const node = createCodeNode();
|
|
297
|
+
|
|
298
|
+
node.setLanguage(defaultLanguage);
|
|
299
|
+
root.append(node);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
279
303
|
onEditorUpdate(editor);
|
|
280
304
|
});
|
|
281
305
|
});
|
|
@@ -316,6 +340,35 @@ export const initEditor = ({ components } = {}) => {
|
|
|
316
340
|
return editor;
|
|
317
341
|
};
|
|
318
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Load additional Prism syntax highlighter settings for the given programming language.
|
|
345
|
+
* @param {string} lang - Language name, like scss.
|
|
346
|
+
*/
|
|
347
|
+
export const loadCodeHighlighter = async (lang) => {
|
|
348
|
+
if (lang in window.Prism.languages) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const canonicalLang = Object.entries(prismComponents.languages).find(
|
|
353
|
+
// @ts-ignore
|
|
354
|
+
([key, { alias }]) =>
|
|
355
|
+
key === lang || (Array.isArray(alias) ? alias.includes(lang) : alias === lang),
|
|
356
|
+
)?.[0];
|
|
357
|
+
|
|
358
|
+
if (!canonicalLang) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
await import(
|
|
364
|
+
// eslint-disable-next-line jsdoc/no-bad-blocks
|
|
365
|
+
/* @vite-ignore */ `https://unpkg.com/prismjs@1.29.0/components/prism-${canonicalLang}.min.js`
|
|
366
|
+
);
|
|
367
|
+
} catch {
|
|
368
|
+
//
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
319
372
|
/**
|
|
320
373
|
* Convert Markdown to Lexical nodes.
|
|
321
374
|
* @param {import('lexical').LexicalEditor} editor - Editor instance.
|
|
@@ -323,28 +376,19 @@ export const initEditor = ({ components } = {}) => {
|
|
|
323
376
|
* @returns {Promise<void>} Nothing.
|
|
324
377
|
* @throws {Error} Failed to convert the value to Lexical nodes.
|
|
325
378
|
*/
|
|
326
|
-
export const
|
|
379
|
+
export const convertMarkdownToLexical = async (editor, value) => {
|
|
327
380
|
// Load Prism language support on demand; the `loadLanguages` Prism utility method cannot be used
|
|
328
381
|
await Promise.all(
|
|
329
|
-
[...value.matchAll(/^```(?<lang>.+?)\n/gm)].map(async ({ groups: { lang } = {} }) =>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await import(
|
|
333
|
-
// eslint-disable-next-line jsdoc/no-bad-blocks
|
|
334
|
-
/* @vite-ignore */ `https://unpkg.com/prismjs@1.29.0/components/prism-${lang}.min.js`
|
|
335
|
-
);
|
|
336
|
-
} catch {
|
|
337
|
-
//
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}),
|
|
382
|
+
[...value.matchAll(/^```(?<lang>.+?)\n/gm)].map(async ({ groups: { lang } = {} }) =>
|
|
383
|
+
loadCodeHighlighter(lang),
|
|
384
|
+
),
|
|
341
385
|
);
|
|
342
386
|
|
|
343
387
|
return new Promise((resolve, reject) => {
|
|
344
388
|
editor.update(() => {
|
|
345
389
|
try {
|
|
346
390
|
convertFromMarkdownString(value, allTransformers);
|
|
347
|
-
resolve(
|
|
391
|
+
resolve(undefined);
|
|
348
392
|
} catch (ex) {
|
|
349
393
|
reject(new Error('Failed to convert Markdown', { cause: ex }));
|
|
350
394
|
}
|
|
@@ -362,7 +406,8 @@ export const focusEditor = async (editor) => {
|
|
|
362
406
|
editor.getRootElement()?.focus();
|
|
363
407
|
|
|
364
408
|
return new Promise((resolve) => {
|
|
365
|
-
editor.focus(() => {
|
|
409
|
+
editor.focus(async () => {
|
|
410
|
+
await sleep(100);
|
|
366
411
|
resolve(undefined);
|
|
367
412
|
});
|
|
368
413
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext, onMount } from 'svelte';
|
|
3
|
+
import { initEditor } from './core';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {object} Props
|
|
6
|
-
* @property {string} [value] - Input value.
|
|
7
7
|
* @property {string} [class] - The `class` attribute on the wrapper element.
|
|
8
8
|
* @property {boolean} [hidden] - Whether to hide the widget.
|
|
9
9
|
* @property {boolean} [disabled] - Whether to disable the widget. An alias of the `aria-disabled`
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
*/
|
|
23
23
|
let {
|
|
24
24
|
/* eslint-disable prefer-const */
|
|
25
|
-
value = $bindable(),
|
|
26
25
|
class: className,
|
|
27
26
|
hidden = false,
|
|
28
27
|
disabled = false,
|
|
@@ -34,12 +33,8 @@
|
|
|
34
33
|
/* eslint-enable prefer-const */
|
|
35
34
|
} = $props();
|
|
36
35
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* @type {import('../../typedefs').TextEditorState}
|
|
40
|
-
*/
|
|
41
|
-
const { editor, editorId, selectionBlockType, selectionInlineTypes, hasConverterError } =
|
|
42
|
-
getContext('state');
|
|
36
|
+
/** @type {import('../../typedefs').TextEditorStore} */
|
|
37
|
+
const editorStore = getContext('editorStore');
|
|
43
38
|
|
|
44
39
|
/**
|
|
45
40
|
* Reference to the Lexical editor root element.
|
|
@@ -50,7 +45,7 @@
|
|
|
50
45
|
const editable = $derived(!(disabled || readonly));
|
|
51
46
|
|
|
52
47
|
$effect(() => {
|
|
53
|
-
|
|
48
|
+
editorStore.editor?.setEditable(editable);
|
|
54
49
|
});
|
|
55
50
|
|
|
56
51
|
/**
|
|
@@ -58,19 +53,31 @@
|
|
|
58
53
|
* @param {Event} event - `Update` custom event.
|
|
59
54
|
*/
|
|
60
55
|
const onUpdate = (event) => {
|
|
61
|
-
if (
|
|
56
|
+
if (editorStore.hasConverterError) {
|
|
62
57
|
return;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
const { detail } = /** @type {CustomEvent} */ (event);
|
|
66
61
|
const newValue = detail.value;
|
|
67
62
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
63
|
+
if (editorStore.inputValue !== newValue) {
|
|
64
|
+
const { useRichText } = editorStore;
|
|
65
|
+
|
|
66
|
+
if (useRichText) {
|
|
67
|
+
// Temporarily disable rich text to prevent unnecessary Markdown conversion that resets
|
|
68
|
+
// Lexical nodes and selection
|
|
69
|
+
editorStore.useRichText = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
editorStore.inputValue = newValue;
|
|
73
|
+
|
|
74
|
+
if (useRichText) {
|
|
75
|
+
// Restore the rich text state
|
|
76
|
+
editorStore.useRichText = true;
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
$selectionInlineTypes = detail.selectionInlineTypes;
|
|
80
|
+
editorStore.selection = detail.selection;
|
|
74
81
|
};
|
|
75
82
|
|
|
76
83
|
/**
|
|
@@ -84,6 +91,8 @@
|
|
|
84
91
|
};
|
|
85
92
|
|
|
86
93
|
onMount(() => {
|
|
94
|
+
editorStore.editor = initEditor(editorStore.config);
|
|
95
|
+
|
|
87
96
|
lexicalRoot?.addEventListener('Update', onUpdate);
|
|
88
97
|
lexicalRoot?.addEventListener('click', onClick);
|
|
89
98
|
|
|
@@ -94,8 +103,9 @@
|
|
|
94
103
|
});
|
|
95
104
|
|
|
96
105
|
$effect(() => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
106
|
+
if (editorStore.editor && lexicalRoot) {
|
|
107
|
+
editorStore.editor.setRootElement(lexicalRoot);
|
|
108
|
+
editorStore.initialized = true;
|
|
99
109
|
}
|
|
100
110
|
});
|
|
101
111
|
</script>
|
|
@@ -111,14 +121,16 @@
|
|
|
111
121
|
aria-required={required}
|
|
112
122
|
aria-invalid={invalid}
|
|
113
123
|
class="lexical-root"
|
|
114
|
-
|
|
124
|
+
class:code={editorStore.config.isCodeEditor}
|
|
125
|
+
id="{editorStore.editorId}-lexical-root"
|
|
115
126
|
contenteditable={editable}
|
|
116
127
|
{hidden}
|
|
117
128
|
></div>
|
|
118
129
|
|
|
119
130
|
<style>.lexical-root {
|
|
131
|
+
overflow: hidden;
|
|
120
132
|
border: 1px solid var(--sui-textbox-border-color);
|
|
121
|
-
border-radius:
|
|
133
|
+
border-radius: var(--sui-textbox-border-radius) !important;
|
|
122
134
|
padding: var(--sui-textbox-multiline-padding);
|
|
123
135
|
min-height: 8em;
|
|
124
136
|
color: var(--sui-textbox-foreground-color);
|
|
@@ -127,6 +139,17 @@
|
|
|
127
139
|
font-size: var(--sui-textbox-font-size);
|
|
128
140
|
line-height: var(--sui-textbox-multiline-line-height);
|
|
129
141
|
}
|
|
142
|
+
.lexical-root:not(:first-child) {
|
|
143
|
+
border-top-left-radius: 0 !important;
|
|
144
|
+
border-top-right-radius: 0 !important;
|
|
145
|
+
}
|
|
146
|
+
.lexical-root.code {
|
|
147
|
+
padding: 0;
|
|
148
|
+
}
|
|
149
|
+
.lexical-root.code :global(.code-block) {
|
|
150
|
+
border-radius: 0 !important;
|
|
151
|
+
min-height: 120px;
|
|
152
|
+
}
|
|
130
153
|
.lexical-root:focus-visible {
|
|
131
154
|
outline: 0;
|
|
132
155
|
}
|
|
@@ -166,7 +189,7 @@
|
|
|
166
189
|
padding: 8px;
|
|
167
190
|
min-width: 40px;
|
|
168
191
|
color: var(--sui-tertiary-foreground-color);
|
|
169
|
-
background-color: var(--sui-
|
|
192
|
+
background-color: var(--sui-tertiary-background-color);
|
|
170
193
|
text-align: right;
|
|
171
194
|
}
|
|
172
195
|
.lexical-root :global([data-lexical-text=true]) {
|
|
@@ -4,10 +4,6 @@ type LexicalRoot = {
|
|
|
4
4
|
$set?(props: Partial<Props & Record<string, any>>): void;
|
|
5
5
|
};
|
|
6
6
|
declare const LexicalRoot: import("svelte").Component<{
|
|
7
|
-
/**
|
|
8
|
-
* - Input value.
|
|
9
|
-
*/
|
|
10
|
-
value?: string | undefined;
|
|
11
7
|
/**
|
|
12
8
|
* - The `class` attribute on the wrapper element.
|
|
13
9
|
*/
|
|
@@ -40,12 +36,8 @@ declare const LexicalRoot: import("svelte").Component<{
|
|
|
40
36
|
* - Primary slot content.
|
|
41
37
|
*/
|
|
42
38
|
children?: import("svelte").Snippet<[]> | undefined;
|
|
43
|
-
} & Record<string, any>, {}, "
|
|
39
|
+
} & Record<string, any>, {}, "">;
|
|
44
40
|
type Props = {
|
|
45
|
-
/**
|
|
46
|
-
* - Input value.
|
|
47
|
-
*/
|
|
48
|
-
value?: string | undefined;
|
|
49
41
|
/**
|
|
50
42
|
* - The `class` attribute on the wrapper element.
|
|
51
43
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createEditorStore(): import("../../typedefs").TextEditorStore;
|