@sveltia/ui 0.23.2 → 0.24.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/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 +5 -5
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { generateElementId } from '@sveltia/utils/element';
|
|
2
|
+
import { convertMarkdownToLexical } from './core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create an editor editor store that contains all the states and configuration.
|
|
6
|
+
* @returns {import('../../typedefs').TextEditorStore} Store.
|
|
7
|
+
*/
|
|
8
|
+
export const createEditorStore = () => {
|
|
9
|
+
/** @type {string} */
|
|
10
|
+
const editorId = generateElementId('editor');
|
|
11
|
+
/** @type {boolean} */
|
|
12
|
+
let initialized = $state(false);
|
|
13
|
+
/** @type {import('lexical').LexicalEditor | undefined} */
|
|
14
|
+
let editor = $state();
|
|
15
|
+
/** @type {import('../../typedefs').TextEditorConfig} */
|
|
16
|
+
let config = $state({ modes: [], enabledButtons: [], components: [], isCodeEditor: false });
|
|
17
|
+
/** @type {string} */
|
|
18
|
+
let inputValue = $state('');
|
|
19
|
+
/** @type {import('../../typedefs').TextEditorSelectionState} */
|
|
20
|
+
let selection = $state({ blockNodeKey: null, blockType: 'paragraph', inlineTypes: [] });
|
|
21
|
+
/** @type {boolean} */
|
|
22
|
+
let useRichText = $state(true);
|
|
23
|
+
/** @type {boolean} */
|
|
24
|
+
let hasConverterError = $state(false);
|
|
25
|
+
/** @type {boolean} */
|
|
26
|
+
let showConverterError = $state(false);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert the Markdown {@link inputValue} to Lexical nodes. Disable the rich text mode and
|
|
30
|
+
* restore the original value when there is an error while conversion.
|
|
31
|
+
*/
|
|
32
|
+
const convertMarkdown = async () => {
|
|
33
|
+
if (!editor || !initialized) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const originalValue = inputValue;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// We should avoid an empty editor; there should be at least one `<p>`, so give it an empty
|
|
41
|
+
// string if the `value` is `undefined`
|
|
42
|
+
// @see https://github.com/facebook/lexical/issues/2308
|
|
43
|
+
await convertMarkdownToLexical(editor, inputValue || '');
|
|
44
|
+
} catch (ex) {
|
|
45
|
+
hasConverterError = true;
|
|
46
|
+
inputValue = originalValue;
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.error(ex);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
54
|
+
get editor() {
|
|
55
|
+
return editor;
|
|
56
|
+
},
|
|
57
|
+
set editor(newValue) {
|
|
58
|
+
editor = newValue;
|
|
59
|
+
},
|
|
60
|
+
get initialized() {
|
|
61
|
+
return initialized;
|
|
62
|
+
},
|
|
63
|
+
set initialized(newValue) {
|
|
64
|
+
initialized = newValue;
|
|
65
|
+
},
|
|
66
|
+
get config() {
|
|
67
|
+
return config;
|
|
68
|
+
},
|
|
69
|
+
set config(newValue) {
|
|
70
|
+
config = newValue;
|
|
71
|
+
useRichText = newValue.modes[0] === 'rich-text';
|
|
72
|
+
},
|
|
73
|
+
get inputValue() {
|
|
74
|
+
return inputValue;
|
|
75
|
+
},
|
|
76
|
+
set inputValue(newValue) {
|
|
77
|
+
const hasChange = inputValue !== newValue;
|
|
78
|
+
|
|
79
|
+
if (hasChange) {
|
|
80
|
+
inputValue = newValue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (useRichText && (hasChange || editor?.getEditorState().isEmpty())) {
|
|
84
|
+
convertMarkdown();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
get selection() {
|
|
88
|
+
return selection;
|
|
89
|
+
},
|
|
90
|
+
set selection(newValue) {
|
|
91
|
+
selection = newValue;
|
|
92
|
+
},
|
|
93
|
+
get useRichText() {
|
|
94
|
+
return useRichText;
|
|
95
|
+
},
|
|
96
|
+
set useRichText(newValue) {
|
|
97
|
+
useRichText = newValue;
|
|
98
|
+
},
|
|
99
|
+
get hasConverterError() {
|
|
100
|
+
return hasConverterError;
|
|
101
|
+
},
|
|
102
|
+
set hasConverterError(newValue) {
|
|
103
|
+
hasConverterError = newValue;
|
|
104
|
+
|
|
105
|
+
if (hasConverterError) {
|
|
106
|
+
useRichText = false;
|
|
107
|
+
showConverterError = true;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
get showConverterError() {
|
|
111
|
+
return showConverterError;
|
|
112
|
+
},
|
|
113
|
+
set showConverterError(newValue) {
|
|
114
|
+
showConverterError = newValue;
|
|
115
|
+
},
|
|
116
|
+
editorId,
|
|
117
|
+
convertMarkdown,
|
|
118
|
+
/* eslint-enable jsdoc/require-jsdoc */
|
|
119
|
+
};
|
|
120
|
+
};
|
|
@@ -3,17 +3,15 @@
|
|
|
3
3
|
A rich text editor based on Lexical.
|
|
4
4
|
-->
|
|
5
5
|
<script>
|
|
6
|
-
import {
|
|
7
|
-
import { onMount, setContext, untrack } from 'svelte';
|
|
6
|
+
import { setContext, untrack } from 'svelte';
|
|
8
7
|
import { _ } from 'svelte-i18n';
|
|
9
|
-
import { writable } from 'svelte/store';
|
|
10
8
|
import { blockButtonTypes, inlineButtonTypes } from '.';
|
|
11
9
|
import Alert from '../alert/alert.svelte';
|
|
12
10
|
import TextArea from '../text-field/text-area.svelte';
|
|
13
11
|
import Toast from '../toast/toast.svelte';
|
|
14
|
-
import { convertMarkdown as convertMarkdownToLexical, initEditor } from './core';
|
|
15
12
|
import LexicalRoot from './lexical-root.svelte';
|
|
16
|
-
import
|
|
13
|
+
import { createEditorStore } from './store.svelte';
|
|
14
|
+
import TextEditorToolbar from './toolbar/text-editor-toolbar.svelte';
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* @typedef {object} Props
|
|
@@ -56,108 +54,45 @@
|
|
|
56
54
|
/* eslint-enable prefer-const */
|
|
57
55
|
} = $props();
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
const editor = writable();
|
|
61
|
-
const selectionBlockType = writable('paragraph');
|
|
62
|
-
const selectionInlineTypes = writable([]);
|
|
63
|
-
const editorId = writable(generateElementId('editor'));
|
|
64
|
-
const useRichText = writable(modes[0] === 'rich-text');
|
|
65
|
-
const hasConverterError = writable(false);
|
|
66
|
-
let inputValue = $state('');
|
|
67
|
-
let showConverterError = $state(false);
|
|
57
|
+
const editorStore = createEditorStore();
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*/
|
|
73
|
-
const convertMarkdown = async () => {
|
|
74
|
-
if (!$editor?.getRootElement()) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const originalValue = inputValue;
|
|
59
|
+
editorStore.config.modes = modes;
|
|
60
|
+
editorStore.config.enabledButtons = buttons;
|
|
61
|
+
editorStore.config.components = components;
|
|
79
62
|
|
|
80
|
-
|
|
81
|
-
// We should avoid an empty editor; there should be at least one `<p>`, so give it an empty
|
|
82
|
-
// string if the `value` is `undefined`
|
|
83
|
-
// @see https://github.com/facebook/lexical/issues/2308
|
|
84
|
-
await convertMarkdownToLexical($editor, inputValue ?? '');
|
|
85
|
-
} catch (ex) {
|
|
86
|
-
$hasConverterError = true;
|
|
87
|
-
inputValue = originalValue;
|
|
88
|
-
// eslint-disable-next-line no-console
|
|
89
|
-
console.error(ex);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
63
|
+
setContext('editorStore', editorStore);
|
|
92
64
|
|
|
93
65
|
$effect(() => {
|
|
94
|
-
|
|
66
|
+
if (!editorStore.initialized) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
95
69
|
|
|
96
70
|
const newValue = value;
|
|
97
71
|
|
|
98
|
-
// Avoid a cycle dependency & infinite loop
|
|
99
72
|
untrack(() => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (hasChange) {
|
|
103
|
-
inputValue = newValue ?? '';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if ($useRichText && (hasChange || $editor?.getEditorState().isEmpty())) {
|
|
107
|
-
convertMarkdown();
|
|
108
|
-
}
|
|
73
|
+
editorStore.inputValue = newValue;
|
|
109
74
|
});
|
|
110
75
|
});
|
|
111
76
|
|
|
112
77
|
$effect(() => {
|
|
113
|
-
if (
|
|
78
|
+
if (!editorStore.initialized) {
|
|
114
79
|
return;
|
|
115
80
|
}
|
|
116
81
|
|
|
117
|
-
const newValue = inputValue;
|
|
82
|
+
const newValue = editorStore.inputValue;
|
|
118
83
|
|
|
119
|
-
// Avoid a cycle dependency & infinite loop
|
|
120
84
|
untrack(() => {
|
|
121
85
|
if (value !== newValue) {
|
|
122
86
|
value = newValue;
|
|
123
87
|
}
|
|
124
88
|
});
|
|
125
89
|
});
|
|
126
|
-
|
|
127
|
-
$effect(() => {
|
|
128
|
-
if ($hasConverterError) {
|
|
129
|
-
$useRichText = false;
|
|
130
|
-
showConverterError = true;
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// The editor has to be initialized in the browser
|
|
135
|
-
onMount(() => {
|
|
136
|
-
$editor = initEditor({ components });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
setContext(
|
|
140
|
-
'state',
|
|
141
|
-
/** @type {import('../../typedefs').TextEditorState} */ ({
|
|
142
|
-
editor,
|
|
143
|
-
editorId,
|
|
144
|
-
selectionBlockType,
|
|
145
|
-
selectionInlineTypes,
|
|
146
|
-
modes,
|
|
147
|
-
useRichText,
|
|
148
|
-
hasConverterError,
|
|
149
|
-
enabledButtons: buttons,
|
|
150
|
-
components,
|
|
151
|
-
convertMarkdown,
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
154
90
|
</script>
|
|
155
91
|
|
|
156
|
-
<div {...restProps} role="none" class="sui text-editor" {hidden}>
|
|
157
|
-
<
|
|
92
|
+
<div {...restProps} role="none" class="sui text-editor" class:flex {hidden}>
|
|
93
|
+
<TextEditorToolbar {disabled} {readonly} />
|
|
158
94
|
<LexicalRoot
|
|
159
|
-
|
|
160
|
-
hidden={!$useRichText || hidden}
|
|
95
|
+
hidden={!editorStore.useRichText || hidden}
|
|
161
96
|
{disabled}
|
|
162
97
|
{readonly}
|
|
163
98
|
{required}
|
|
@@ -165,9 +100,9 @@
|
|
|
165
100
|
/>
|
|
166
101
|
<TextArea
|
|
167
102
|
autoResize={true}
|
|
168
|
-
bind:value={inputValue}
|
|
103
|
+
bind:value={editorStore.inputValue}
|
|
169
104
|
{flex}
|
|
170
|
-
hidden={
|
|
105
|
+
hidden={editorStore.useRichText || hidden}
|
|
171
106
|
{disabled}
|
|
172
107
|
{readonly}
|
|
173
108
|
{required}
|
|
@@ -175,8 +110,8 @@
|
|
|
175
110
|
/>
|
|
176
111
|
</div>
|
|
177
112
|
|
|
178
|
-
{#if showConverterError}
|
|
179
|
-
<Toast bind:show={showConverterError}>
|
|
113
|
+
{#if editorStore.showConverterError}
|
|
114
|
+
<Toast bind:show={editorStore.showConverterError}>
|
|
180
115
|
<Alert status="error">{$_('_sui.text_editor.converter_error')}</Alert>
|
|
181
116
|
</Toast>
|
|
182
117
|
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { _ } from 'svelte-i18n';
|
|
3
|
+
import CodeLanguageSwitcher from './code-language-switcher.svelte';
|
|
4
|
+
import ToolbarWrapper from './toolbar-wrapper.svelte';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} Props
|
|
8
|
+
* @property {boolean} [hidden] - Whether to hide the widget.
|
|
9
|
+
* @property {boolean} [disabled] - Whether to disable the widget. An alias of the `aria-disabled`
|
|
10
|
+
* attribute.
|
|
11
|
+
* @property {boolean} [readonly] - Whether to make the widget read-only. An alias of the
|
|
12
|
+
* `aria-readonly` attribute.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {Props & Record<string, any>}
|
|
17
|
+
*/
|
|
18
|
+
let {
|
|
19
|
+
/* eslint-disable prefer-const */
|
|
20
|
+
disabled = false,
|
|
21
|
+
readonly = false,
|
|
22
|
+
/* eslint-enable prefer-const */
|
|
23
|
+
} = $props();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<ToolbarWrapper disabled={disabled || readonly} aria-label={$_('_sui.text_editor.code_editor')}>
|
|
27
|
+
<CodeLanguageSwitcher disabled={disabled || readonly} />
|
|
28
|
+
</ToolbarWrapper>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export default
|
|
2
|
-
type
|
|
1
|
+
export default CodeEditorToolbar;
|
|
2
|
+
type CodeEditorToolbar = {
|
|
3
3
|
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
4
|
$set?(props: Partial<Props & Record<string, any>>): void;
|
|
5
5
|
};
|
|
6
|
-
declare const
|
|
6
|
+
declare const CodeEditorToolbar: import("svelte").Component<{
|
|
7
7
|
/**
|
|
8
8
|
* - Whether to hide the widget.
|
|
9
9
|
*/
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { $isCodeNode as isCodeNode } from '@lexical/code';
|
|
3
|
+
import { $getNodeByKey as getNodeByKey, $getRoot as getRoot } from 'lexical';
|
|
4
|
+
import prismComponents from 'prismjs/components';
|
|
5
|
+
import { getContext } from 'svelte';
|
|
6
|
+
import { _ } from 'svelte-i18n';
|
|
7
|
+
import Option from '../../listbox/option.svelte';
|
|
8
|
+
import Select from '../../select/select.svelte';
|
|
9
|
+
import { focusEditor, loadCodeHighlighter } from '../core';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} Props
|
|
13
|
+
* @property {boolean} [disabled] - Whether to disable the switcher.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {Props} */
|
|
17
|
+
let {
|
|
18
|
+
/* eslint-disable prefer-const */
|
|
19
|
+
disabled = false,
|
|
20
|
+
/* eslint-enable prefer-const */
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
/** @type {{ key: string, label: string, aliases: string[] }[]} */
|
|
24
|
+
const codeLanguages = Object.entries(prismComponents.languages)
|
|
25
|
+
.filter(([, config]) => 'title' in config)
|
|
26
|
+
.map(([key, val]) => {
|
|
27
|
+
const { title: label, aliasTitles, alias } = /** @type {Record<string, any>} */ (val);
|
|
28
|
+
let aliases = [];
|
|
29
|
+
|
|
30
|
+
if (alias && !aliasTitles) {
|
|
31
|
+
aliases = Array.isArray(alias) ? alias : [alias];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
{ key, label, aliases },
|
|
36
|
+
...Object.entries(aliasTitles ?? {}).map(([k, v]) => ({ key: k, label: v, aliases: [] })),
|
|
37
|
+
];
|
|
38
|
+
})
|
|
39
|
+
.flat(1)
|
|
40
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
41
|
+
|
|
42
|
+
/** @type {import('../../../typedefs').TextEditorStore} */
|
|
43
|
+
const editorStore = getContext('editorStore');
|
|
44
|
+
|
|
45
|
+
let selectedLanguage = $state('');
|
|
46
|
+
|
|
47
|
+
$effect(() => {
|
|
48
|
+
void editorStore.selection.blockNodeKey;
|
|
49
|
+
|
|
50
|
+
editorStore.editor?.read(() => {
|
|
51
|
+
const node = editorStore.config.isCodeEditor
|
|
52
|
+
? getRoot().getChildren()[0]
|
|
53
|
+
: getNodeByKey(/** @type {string} */ (editorStore.selection.blockNodeKey));
|
|
54
|
+
|
|
55
|
+
if (isCodeNode(node)) {
|
|
56
|
+
selectedLanguage = node.getLanguage() ?? editorStore.config.defaultLanguage ?? '';
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<Select
|
|
63
|
+
{disabled}
|
|
64
|
+
aria-label={$_('_sui.text_editor.language')}
|
|
65
|
+
value={selectedLanguage}
|
|
66
|
+
onChange={async ({ detail: { value: lang } }) => {
|
|
67
|
+
if (!editorStore.editor || selectedLanguage === lang) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await focusEditor(editorStore.editor);
|
|
72
|
+
|
|
73
|
+
if (editorStore.selection?.blockNodeKey) {
|
|
74
|
+
await loadCodeHighlighter(lang);
|
|
75
|
+
|
|
76
|
+
editorStore.editor.update(() => {
|
|
77
|
+
// https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx#L713
|
|
78
|
+
const node = getNodeByKey(/** @type {string} */ (editorStore.selection.blockNodeKey));
|
|
79
|
+
|
|
80
|
+
if (isCodeNode(node)) {
|
|
81
|
+
node.setLanguage(lang);
|
|
82
|
+
selectedLanguage = lang;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<Option label={$_('_sui.text_editor.plain_text')} value="" />
|
|
89
|
+
{#each codeLanguages as { key, label, aliases } (key)}
|
|
90
|
+
<Option
|
|
91
|
+
{label}
|
|
92
|
+
value={key}
|
|
93
|
+
selected={key === selectedLanguage || aliases.includes(selectedLanguage)}
|
|
94
|
+
/>
|
|
95
|
+
{/each}
|
|
96
|
+
</Select>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default CodeLanguageSwitcher;
|
|
2
|
+
type CodeLanguageSwitcher = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<Props>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const CodeLanguageSwitcher: import("svelte").Component<{
|
|
7
|
+
/**
|
|
8
|
+
* - Whether to disable the switcher.
|
|
9
|
+
*/
|
|
10
|
+
disabled?: boolean | undefined;
|
|
11
|
+
}, {}, "">;
|
|
12
|
+
type Props = {
|
|
13
|
+
/**
|
|
14
|
+
* - Whether to disable the switcher.
|
|
15
|
+
*/
|
|
16
|
+
disabled?: boolean | undefined;
|
|
17
|
+
};
|
|
@@ -21,22 +21,22 @@
|
|
|
21
21
|
/* eslint-enable prefer-const */
|
|
22
22
|
} = $props();
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*/
|
|
28
|
-
const { editor, editorId, selectionInlineTypes, useRichText } = getContext('state');
|
|
24
|
+
/** @type {import('../../../typedefs').TextEditorStore} */
|
|
25
|
+
const editorStore = getContext('editorStore');
|
|
26
|
+
const selectionTypeMatches = $derived(editorStore.selection.inlineTypes.includes(type));
|
|
29
27
|
</script>
|
|
30
28
|
|
|
31
29
|
<Button
|
|
32
30
|
iconic
|
|
33
31
|
aria-label={$_(`_sui.text_editor.${availableButtons[type].labelKey}`)}
|
|
34
|
-
aria-controls="{
|
|
35
|
-
disabled={
|
|
36
|
-
pressed={
|
|
32
|
+
aria-controls="{editorStore.editorId}-lexical-root"
|
|
33
|
+
disabled={!editorStore.useRichText}
|
|
34
|
+
pressed={selectionTypeMatches}
|
|
37
35
|
onclick={async () => {
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
if (editorStore.editor) {
|
|
37
|
+
await focusEditor(editorStore.editor);
|
|
38
|
+
editorStore.editor.dispatchCommand(FORMAT_TEXT_COMMAND, type);
|
|
39
|
+
}
|
|
40
40
|
}}
|
|
41
41
|
>
|
|
42
42
|
{#snippet startIcon()}
|
|
@@ -19,20 +19,17 @@
|
|
|
19
19
|
/* eslint-enable prefer-const */
|
|
20
20
|
} = $props();
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* @type {import('../../../typedefs').TextEditorState}
|
|
25
|
-
*/
|
|
26
|
-
const { editor, editorId, useRichText } = getContext('state');
|
|
22
|
+
/** @type {import('../../../typedefs').TextEditorStore} */
|
|
23
|
+
const editorStore = getContext('editorStore');
|
|
27
24
|
</script>
|
|
28
25
|
|
|
29
26
|
<Button
|
|
30
27
|
iconic
|
|
31
28
|
aria-label={component.label}
|
|
32
|
-
aria-controls="{
|
|
33
|
-
disabled={
|
|
29
|
+
aria-controls="{editorStore.editorId}-lexical-root"
|
|
30
|
+
disabled={!editorStore.useRichText}
|
|
34
31
|
onclick={() => {
|
|
35
|
-
|
|
32
|
+
editorStore.editor?.update(() => {
|
|
36
33
|
insertNodes([component.createNode(), createParagraphNode()]);
|
|
37
34
|
});
|
|
38
35
|
}}
|
|
@@ -31,11 +31,9 @@
|
|
|
31
31
|
*/
|
|
32
32
|
const type = 'link';
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*/
|
|
38
|
-
const { editor, editorId, selectionInlineTypes, useRichText } = getContext('state');
|
|
34
|
+
/** @type {import('../../../typedefs').TextEditorStore} */
|
|
35
|
+
const editorStore = getContext('editorStore');
|
|
36
|
+
const selectionTypeMatches = $derived(editorStore.selection.inlineTypes.includes(type));
|
|
39
37
|
|
|
40
38
|
let openDialog = $state(false);
|
|
41
39
|
/** @type {'create' | 'update' | 'remove'} */
|
|
@@ -48,7 +46,7 @@
|
|
|
48
46
|
* Create a new link by showing a dialog to accept a URL and optionally text.
|
|
49
47
|
*/
|
|
50
48
|
const createLink = () => {
|
|
51
|
-
|
|
49
|
+
editorStore.editor?.getEditorState().read(() => {
|
|
52
50
|
const textContent = getTextContent().trim();
|
|
53
51
|
|
|
54
52
|
anchorURL = textContent;
|
|
@@ -62,18 +60,18 @@
|
|
|
62
60
|
* Remove an existing link.
|
|
63
61
|
*/
|
|
64
62
|
const removeLink = () => {
|
|
65
|
-
|
|
63
|
+
editorStore.editor?.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
66
64
|
};
|
|
67
65
|
|
|
68
66
|
/**
|
|
69
67
|
* Update an existing link.
|
|
70
68
|
*/
|
|
71
69
|
const updateLink = () => {
|
|
72
|
-
|
|
73
|
-
const
|
|
70
|
+
editorStore.editor?.getEditorState().read(() => {
|
|
71
|
+
const _selection = getSelection();
|
|
74
72
|
|
|
75
|
-
if (isRangeSelection(
|
|
76
|
-
const anchor =
|
|
73
|
+
if (isRangeSelection(_selection)) {
|
|
74
|
+
const anchor = _selection.anchor.getNode();
|
|
77
75
|
const parent = anchor instanceof LinkNode ? anchor : getNearestNodeOfType(anchor, LinkNode);
|
|
78
76
|
const url = parent?.getURL();
|
|
79
77
|
|
|
@@ -97,7 +95,7 @@
|
|
|
97
95
|
* create a new link.
|
|
98
96
|
*/
|
|
99
97
|
const onButtonClick = () => {
|
|
100
|
-
if (
|
|
98
|
+
if (selectionTypeMatches) {
|
|
101
99
|
updateLink();
|
|
102
100
|
} else {
|
|
103
101
|
createLink();
|
|
@@ -121,19 +119,23 @@
|
|
|
121
119
|
*/
|
|
122
120
|
const onDialogClose = async (event) => {
|
|
123
121
|
if (event.detail.returnValue !== 'cancel' && dialogMode !== 'remove') {
|
|
122
|
+
if (!editorStore.editor) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
124
126
|
await new Promise((resolve) => {
|
|
125
|
-
|
|
126
|
-
let
|
|
127
|
+
editorStore.editor?.update(async () => {
|
|
128
|
+
let _selection = getSelection() ?? getPreviousSelection()?.clone();
|
|
127
129
|
|
|
128
|
-
if (!isRangeSelection(
|
|
129
|
-
|
|
130
|
+
if (!isRangeSelection(_selection)) {
|
|
131
|
+
_selection = createRangeSelection();
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
if (!hasAnchor) {
|
|
133
135
|
anchorText = anchorText.trim();
|
|
134
136
|
anchorText ||= anchorURL;
|
|
135
137
|
|
|
136
|
-
const { anchor, focus } = /** @type {import('lexical').RangeSelection} */ (
|
|
138
|
+
const { anchor, focus } = /** @type {import('lexical').RangeSelection} */ (_selection);
|
|
137
139
|
const node = createTextNode(anchorText);
|
|
138
140
|
const key = node.getKey();
|
|
139
141
|
|
|
@@ -142,13 +144,13 @@
|
|
|
142
144
|
focus.set(key, 0, 'text');
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
setSelection(
|
|
147
|
+
setSelection(_selection);
|
|
146
148
|
resolve(undefined);
|
|
147
149
|
});
|
|
148
150
|
});
|
|
149
151
|
|
|
150
|
-
await focusEditor(
|
|
151
|
-
|
|
152
|
+
await focusEditor(editorStore.editor);
|
|
153
|
+
editorStore.editor.dispatchCommand(TOGGLE_LINK_COMMAND, anchorURL);
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
anchorURL = '';
|
|
@@ -159,7 +161,7 @@
|
|
|
159
161
|
* Open the dialog with a keyboard shortcut: Accel+K.
|
|
160
162
|
*/
|
|
161
163
|
const _registerCommand = () => {
|
|
162
|
-
|
|
164
|
+
editorStore.editor?.registerCommand(
|
|
163
165
|
KEY_DOWN_COMMAND,
|
|
164
166
|
(event) => {
|
|
165
167
|
if (matchShortcuts(event, isMac() ? 'Meta+K' : 'Ctrl+K')) {
|
|
@@ -174,7 +176,7 @@
|
|
|
174
176
|
};
|
|
175
177
|
|
|
176
178
|
$effect(() => {
|
|
177
|
-
if (
|
|
179
|
+
if (editorStore.editor) {
|
|
178
180
|
_registerCommand();
|
|
179
181
|
}
|
|
180
182
|
});
|
|
@@ -183,9 +185,9 @@
|
|
|
183
185
|
<Button
|
|
184
186
|
iconic
|
|
185
187
|
aria-label={$_(`_sui.text_editor.${availableButtons[type].labelKey}`)}
|
|
186
|
-
aria-controls="{
|
|
187
|
-
disabled={
|
|
188
|
-
pressed={
|
|
188
|
+
aria-controls="{editorStore.editorId}-lexical-root"
|
|
189
|
+
disabled={!editorStore.useRichText}
|
|
190
|
+
pressed={selectionTypeMatches}
|
|
189
191
|
onclick={() => {
|
|
190
192
|
onButtonClick();
|
|
191
193
|
}}
|
|
@@ -19,14 +19,11 @@
|
|
|
19
19
|
/* eslint-enable prefer-const */
|
|
20
20
|
} = $props();
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* @type {import('../../../typedefs').TextEditorState}
|
|
25
|
-
*/
|
|
26
|
-
const { editor, useRichText } = getContext('state');
|
|
22
|
+
/** @type {import('../../../typedefs').TextEditorStore} */
|
|
23
|
+
const editorStore = getContext('editorStore');
|
|
27
24
|
</script>
|
|
28
25
|
|
|
29
|
-
<MenuButton disabled={
|
|
26
|
+
<MenuButton disabled={!editorStore.useRichText} label={$_('_sui.insert')}>
|
|
30
27
|
{#snippet endIcon()}
|
|
31
28
|
<Icon name="arrow_drop_down" class="small-arrow" />
|
|
32
29
|
{/snippet}
|
|
@@ -36,7 +33,7 @@
|
|
|
36
33
|
<MenuItem
|
|
37
34
|
{label}
|
|
38
35
|
onclick={() => {
|
|
39
|
-
|
|
36
|
+
editorStore.editor?.update(() => {
|
|
40
37
|
insertNodes([createNode()]);
|
|
41
38
|
});
|
|
42
39
|
}}
|