@sveltia/ui 0.20.1 → 0.21.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/package/components/text-editor/core.d.ts +3 -1
- package/package/components/text-editor/core.js +34 -14
- package/package/components/text-editor/lexical-root.svelte +0 -6
- package/package/components/text-editor/text-editor.svelte +9 -6
- package/package/components/text-editor/text-editor.svelte.d.ts +8 -0
- package/package/components/text-editor/toolbar/editor-toolbar.svelte +15 -0
- package/package/components/text-editor/toolbar/insert-image-button.svelte +43 -0
- package/package/components/text-editor/toolbar/insert-image-button.svelte.d.ts +17 -0
- package/package/components/text-editor/toolbar/insert-menu-button.svelte +53 -0
- package/package/components/text-editor/toolbar/insert-menu-button.svelte.d.ts +17 -0
- package/package/components/util/app-shell.svelte +469 -2
- package/package/styles/core.scss +151 -149
- package/package/styles/variables.scss +198 -196
- package/package/typedefs.d.ts +31 -0
- package/package/typedefs.js +12 -0
- package/package.json +7 -7
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export function initEditor(
|
|
1
|
+
export function initEditor({ components }?: {
|
|
2
|
+
components?: import("../../typedefs").TextEditorComponent[] | undefined;
|
|
3
|
+
} | undefined): import("lexical").LexicalEditor;
|
|
2
4
|
export function convertMarkdown(editor: import("lexical").LexicalEditor, value: string): Promise<void>;
|
|
3
5
|
export function focusEditor(editor: import("lexical").LexicalEditor): Promise<void>;
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
} from 'lexical';
|
|
44
44
|
import { blockButtonTypes, textFormatButtonTypes } from '.';
|
|
45
45
|
|
|
46
|
+
const allTransformers = [...TRANSFORMERS];
|
|
47
|
+
|
|
46
48
|
/**
|
|
47
49
|
* Lexical editor configuration.
|
|
48
50
|
* @type {import('lexical').CreateEditorArgs}
|
|
@@ -78,27 +80,28 @@ const editorConfig = {
|
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @
|
|
83
|
+
* Get the current selection’s block and inline level types.
|
|
84
|
+
* @returns {{ blockType: import('../../typedefs').TextEditorBlockType,
|
|
85
|
+
* inlineTypes: import('../../typedefs').TextEditorInlineType[] }} Types.
|
|
83
86
|
*/
|
|
84
|
-
const
|
|
87
|
+
const getSelectionTypes = () => {
|
|
85
88
|
const selection = getSelection();
|
|
86
89
|
|
|
87
90
|
if (!isRangeSelection(selection)) {
|
|
88
|
-
return;
|
|
91
|
+
return { blockType: 'paragraph', inlineTypes: [] };
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
const anchor = selection.anchor.getNode();
|
|
92
95
|
/** @type {ElementNode | null} */
|
|
93
96
|
let parent = null;
|
|
94
97
|
/** @type {import('../../typedefs').TextEditorInlineType[]} */
|
|
95
|
-
const
|
|
98
|
+
const inlineTypes = textFormatButtonTypes.filter((type) => selection.hasFormat(type));
|
|
96
99
|
|
|
97
100
|
if (anchor.getType() !== 'root') {
|
|
98
101
|
parent = anchor instanceof ElementNode ? anchor : getNearestNodeOfType(anchor, ElementNode);
|
|
99
102
|
|
|
100
103
|
if (isLinkNode(parent)) {
|
|
101
|
-
|
|
104
|
+
inlineTypes.push('link');
|
|
102
105
|
parent = getNearestNodeOfType(parent, ElementNode);
|
|
103
106
|
}
|
|
104
107
|
|
|
@@ -107,7 +110,7 @@ const onEditorUpdate = (editor) => {
|
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
const
|
|
113
|
+
const blockType = /** @type {import('../../typedefs').TextEditorBlockType} */ (
|
|
111
114
|
(() => {
|
|
112
115
|
if (!parent) {
|
|
113
116
|
return 'paragraph';
|
|
@@ -135,15 +138,25 @@ const onEditorUpdate = (editor) => {
|
|
|
135
138
|
})()
|
|
136
139
|
);
|
|
137
140
|
|
|
141
|
+
return { blockType, inlineTypes };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Listen to changes made on the editor and trigger the Update event.
|
|
146
|
+
* @param {import('lexical').LexicalEditor} editor - Editor instance.
|
|
147
|
+
*/
|
|
148
|
+
const onEditorUpdate = (editor) => {
|
|
149
|
+
const { blockType, inlineTypes } = getSelectionTypes();
|
|
150
|
+
|
|
138
151
|
editor.getRootElement()?.dispatchEvent(
|
|
139
152
|
new CustomEvent('Update', {
|
|
140
153
|
detail: {
|
|
141
154
|
value: convertToMarkdownString(
|
|
142
155
|
// Use underscores for italic text in Markdown instead of asterisks
|
|
143
|
-
|
|
156
|
+
allTransformers.filter((/** @type {any} */ { tag }) => tag !== '*'),
|
|
144
157
|
),
|
|
145
|
-
selectionBlockType,
|
|
146
|
-
selectionInlineTypes,
|
|
158
|
+
selectionBlockType: blockType,
|
|
159
|
+
selectionInlineTypes: inlineTypes,
|
|
147
160
|
},
|
|
148
161
|
}),
|
|
149
162
|
);
|
|
@@ -151,9 +164,16 @@ const onEditorUpdate = (editor) => {
|
|
|
151
164
|
|
|
152
165
|
/**
|
|
153
166
|
* Initialize the Lexical editor.
|
|
167
|
+
* @param {object} [options] - Options.
|
|
168
|
+
* @param {import('../../typedefs').TextEditorComponent[]} [options.components] - Editor components.
|
|
154
169
|
* @returns {import('lexical').LexicalEditor} Editor instance.
|
|
155
170
|
*/
|
|
156
|
-
export const initEditor = () => {
|
|
171
|
+
export const initEditor = ({ components } = {}) => {
|
|
172
|
+
components?.forEach(({ node, transformer }) => {
|
|
173
|
+
/** @type {any[]} */ (editorConfig.nodes).unshift(node);
|
|
174
|
+
allTransformers.unshift(transformer);
|
|
175
|
+
});
|
|
176
|
+
|
|
157
177
|
const editor = createEditor(editorConfig);
|
|
158
178
|
|
|
159
179
|
registerRichText(editor);
|
|
@@ -254,10 +274,10 @@ export const convertMarkdown = async (editor, value) =>
|
|
|
254
274
|
new Promise((resolve, reject) => {
|
|
255
275
|
editor.update(() => {
|
|
256
276
|
try {
|
|
257
|
-
convertFromMarkdownString(value,
|
|
277
|
+
convertFromMarkdownString(value, allTransformers);
|
|
258
278
|
resolve(void 0);
|
|
259
|
-
} catch {
|
|
260
|
-
reject(new Error('Failed to convert Markdown'));
|
|
279
|
+
} catch (ex) {
|
|
280
|
+
reject(new Error('Failed to convert Markdown', { cause: ex }));
|
|
261
281
|
}
|
|
262
282
|
});
|
|
263
283
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { $createParagraphNode as createParagraphNode, $getRoot as getRoot } from 'lexical';
|
|
3
2
|
import { getContext, onMount } from 'svelte';
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -97,11 +96,6 @@
|
|
|
97
96
|
$effect(() => {
|
|
98
97
|
if ($editor && lexicalRoot) {
|
|
99
98
|
$editor.setRootElement(lexicalRoot);
|
|
100
|
-
// We should avoid an empty editor; there should be at least one `<p>`
|
|
101
|
-
// @see https://github.com/facebook/lexical/issues/2308
|
|
102
|
-
$editor.update(() => {
|
|
103
|
-
getRoot().append(createParagraphNode());
|
|
104
|
-
});
|
|
105
99
|
}
|
|
106
100
|
});
|
|
107
101
|
</script>
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* @property {import('../../typedefs').TextEditorMode[]} [modes] - Enabled modes.
|
|
23
23
|
* @property {(import('../../typedefs').TextEditorBlockType |
|
|
24
24
|
* import('../../typedefs').TextEditorInlineType)[]} [buttons] - Enabled buttons.
|
|
25
|
+
* @property {import('../../typedefs').TextEditorComponent[]} [components] - Editor components.
|
|
25
26
|
* @property {string} [class] - The `class` attribute on the wrapper element.
|
|
26
27
|
* @property {boolean} [hidden] - Whether to hide the widget.
|
|
27
28
|
* @property {boolean} [disabled] - Whether to disable the widget. An alias of the `aria-disabled`
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
flex = false,
|
|
45
46
|
modes = ['rich-text', 'plain-text'],
|
|
46
47
|
buttons = [...inlineButtonTypes, ...blockButtonTypes],
|
|
48
|
+
components = [],
|
|
47
49
|
hidden = false,
|
|
48
50
|
disabled = false,
|
|
49
51
|
readonly = false,
|
|
@@ -69,7 +71,7 @@
|
|
|
69
71
|
* restore the original value when there is an error while conversion.
|
|
70
72
|
*/
|
|
71
73
|
const convertMarkdown = async () => {
|
|
72
|
-
if (!$editor) {
|
|
74
|
+
if (!$editor?.getRootElement()) {
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -80,16 +82,16 @@
|
|
|
80
82
|
// string if the `value` is `undefined`
|
|
81
83
|
// @see https://github.com/facebook/lexical/issues/2308
|
|
82
84
|
await convertMarkdownToLexical($editor, inputValue ?? '');
|
|
83
|
-
} catch {
|
|
85
|
+
} catch (ex) {
|
|
84
86
|
$hasConverterError = true;
|
|
85
87
|
inputValue = originalValue;
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.error(ex);
|
|
86
90
|
}
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
$effect(() => {
|
|
90
|
-
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
94
|
+
void $editor;
|
|
93
95
|
|
|
94
96
|
const newValue = value;
|
|
95
97
|
|
|
@@ -129,7 +131,7 @@
|
|
|
129
131
|
|
|
130
132
|
// The editor has to be initialized in the browser
|
|
131
133
|
onMount(() => {
|
|
132
|
-
$editor = initEditor();
|
|
134
|
+
$editor = initEditor({ components });
|
|
133
135
|
});
|
|
134
136
|
|
|
135
137
|
setContext(
|
|
@@ -143,6 +145,7 @@
|
|
|
143
145
|
useRichText,
|
|
144
146
|
hasConverterError,
|
|
145
147
|
enabledButtons: buttons,
|
|
148
|
+
components,
|
|
146
149
|
convertMarkdown,
|
|
147
150
|
}),
|
|
148
151
|
);
|
|
@@ -21,6 +21,10 @@ declare const TextEditor: import("svelte").Component<{
|
|
|
21
21
|
* - Enabled buttons.
|
|
22
22
|
*/
|
|
23
23
|
buttons?: (import("../../typedefs").TextEditorBlockType | import("../../typedefs").TextEditorInlineType)[] | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* - Editor components.
|
|
26
|
+
*/
|
|
27
|
+
components?: import("../../typedefs").TextEditorComponent[] | undefined;
|
|
24
28
|
/**
|
|
25
29
|
* - The `class` attribute on the wrapper element.
|
|
26
30
|
*/
|
|
@@ -71,6 +75,10 @@ type Props = {
|
|
|
71
75
|
* - Enabled buttons.
|
|
72
76
|
*/
|
|
73
77
|
buttons?: (import("../../typedefs").TextEditorBlockType | import("../../typedefs").TextEditorInlineType)[] | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* - Editor components.
|
|
80
|
+
*/
|
|
81
|
+
components?: import("../../typedefs").TextEditorComponent[] | undefined;
|
|
74
82
|
/**
|
|
75
83
|
* - The `class` attribute on the wrapper element.
|
|
76
84
|
*/
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import Menu from '../../menu/menu.svelte';
|
|
13
13
|
import Toolbar from '../../toolbar/toolbar.svelte';
|
|
14
14
|
import FormatTextButton from './format-text-button.svelte';
|
|
15
|
+
import InsertImageButton from './insert-image-button.svelte';
|
|
15
16
|
import InsertLinkButton from './insert-link-button.svelte';
|
|
17
|
+
import InsertMenuButton from './insert-menu-button.svelte';
|
|
16
18
|
import ToggleBlockMenuItem from './toggle-block-menu-item.svelte';
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -45,9 +47,13 @@
|
|
|
45
47
|
useRichText,
|
|
46
48
|
hasConverterError,
|
|
47
49
|
enabledButtons,
|
|
50
|
+
components,
|
|
48
51
|
convertMarkdown,
|
|
49
52
|
} = getContext('state');
|
|
50
53
|
|
|
54
|
+
const imageComponent = $derived(components.find(({ id }) => id === 'image'));
|
|
55
|
+
const otherComponents = $derived(components.filter(({ id }) => id !== 'image'));
|
|
56
|
+
|
|
51
57
|
/**
|
|
52
58
|
* Enabled block level buttons.
|
|
53
59
|
* @type {import('../../../typedefs').TextEditorBlockType[]}
|
|
@@ -100,6 +106,15 @@
|
|
|
100
106
|
{/each}
|
|
101
107
|
</ButtonGroup>
|
|
102
108
|
{/if}
|
|
109
|
+
{#if components.length}
|
|
110
|
+
<Divider orientation="vertical" />
|
|
111
|
+
{#if imageComponent}
|
|
112
|
+
<InsertImageButton component={imageComponent} />
|
|
113
|
+
{/if}
|
|
114
|
+
{#if otherComponents.length}
|
|
115
|
+
<InsertMenuButton components={otherComponents} />
|
|
116
|
+
{/if}
|
|
117
|
+
{/if}
|
|
103
118
|
<Spacer flex />
|
|
104
119
|
{#if modes.length > 1}
|
|
105
120
|
<Button
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import {
|
|
3
|
+
$createParagraphNode as createParagraphNode,
|
|
4
|
+
$insertNodes as insertNodes,
|
|
5
|
+
} from 'lexical';
|
|
6
|
+
import { getContext } from 'svelte';
|
|
7
|
+
import Button from '../../button/button.svelte';
|
|
8
|
+
import Icon from '../../icon/icon.svelte';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} Props
|
|
12
|
+
* @property {import('../../../typedefs').TextEditorComponent} component - Image editor component.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {Props} */
|
|
16
|
+
let {
|
|
17
|
+
/* eslint-disable prefer-const */
|
|
18
|
+
component,
|
|
19
|
+
/* eslint-enable prefer-const */
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Text editor state.
|
|
24
|
+
* @type {import('../../../typedefs').TextEditorState}
|
|
25
|
+
*/
|
|
26
|
+
const { editor, editorId, useRichText } = getContext('state');
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Button
|
|
30
|
+
iconic
|
|
31
|
+
aria-label={component.label}
|
|
32
|
+
aria-controls="{$editorId}-lexical-root"
|
|
33
|
+
disabled={!$useRichText}
|
|
34
|
+
onclick={() => {
|
|
35
|
+
$editor.update(() => {
|
|
36
|
+
insertNodes([component.createNode(), createParagraphNode()]);
|
|
37
|
+
});
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{#snippet startIcon()}
|
|
41
|
+
<Icon name={component.icon} />
|
|
42
|
+
{/snippet}
|
|
43
|
+
</Button>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default InsertImageButton;
|
|
2
|
+
type InsertImageButton = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<Props>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const InsertImageButton: import("svelte").Component<{
|
|
7
|
+
/**
|
|
8
|
+
* - Image editor component.
|
|
9
|
+
*/
|
|
10
|
+
component: import("../../../typedefs").TextEditorComponent;
|
|
11
|
+
}, {}, "">;
|
|
12
|
+
type Props = {
|
|
13
|
+
/**
|
|
14
|
+
* - Image editor component.
|
|
15
|
+
*/
|
|
16
|
+
component: import("../../../typedefs").TextEditorComponent;
|
|
17
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { $insertNodes as insertNodes } from 'lexical';
|
|
3
|
+
import { getContext } from 'svelte';
|
|
4
|
+
import { _ } from 'svelte-i18n';
|
|
5
|
+
import Icon from '../../icon/icon.svelte';
|
|
6
|
+
import MenuButton from '../../menu/menu-button.svelte';
|
|
7
|
+
import MenuItem from '../../menu/menu-item.svelte';
|
|
8
|
+
import Menu from '../../menu/menu.svelte';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} Props
|
|
12
|
+
* @property {import('../../../typedefs').TextEditorComponent[]} components - Editor components.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {Props} */
|
|
16
|
+
let {
|
|
17
|
+
/* eslint-disable prefer-const */
|
|
18
|
+
components,
|
|
19
|
+
/* eslint-enable prefer-const */
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Text editor state.
|
|
24
|
+
* @type {import('../../../typedefs').TextEditorState}
|
|
25
|
+
*/
|
|
26
|
+
const { editor, useRichText } = getContext('state');
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<MenuButton disabled={!$useRichText} label={$_('_sui.insert')}>
|
|
30
|
+
{#snippet endIcon()}
|
|
31
|
+
<Icon name="arrow_drop_down" class="small-arrow" />
|
|
32
|
+
{/snippet}
|
|
33
|
+
{#snippet popup()}
|
|
34
|
+
<Menu>
|
|
35
|
+
{#each components as { id, label, icon, createNode } (id)}
|
|
36
|
+
<MenuItem
|
|
37
|
+
{label}
|
|
38
|
+
onclick={() => {
|
|
39
|
+
$editor.update(() => {
|
|
40
|
+
insertNodes([createNode()]);
|
|
41
|
+
});
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{#snippet startIcon()}
|
|
45
|
+
{#if icon}
|
|
46
|
+
<Icon name={icon} />
|
|
47
|
+
{/if}
|
|
48
|
+
{/snippet}
|
|
49
|
+
</MenuItem>
|
|
50
|
+
{/each}
|
|
51
|
+
</Menu>
|
|
52
|
+
{/snippet}
|
|
53
|
+
</MenuButton>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default InsertMenuButton;
|
|
2
|
+
type InsertMenuButton = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<Props>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const InsertMenuButton: import("svelte").Component<{
|
|
7
|
+
/**
|
|
8
|
+
* - Editor components.
|
|
9
|
+
*/
|
|
10
|
+
components: import("../../../typedefs").TextEditorComponent[];
|
|
11
|
+
}, {}, "">;
|
|
12
|
+
type Props = {
|
|
13
|
+
/**
|
|
14
|
+
* - Editor components.
|
|
15
|
+
*/
|
|
16
|
+
components: import("../../../typedefs").TextEditorComponent[];
|
|
17
|
+
};
|