@sveltia/ui 0.78.1 → 0.78.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/text-editor/core.d.ts +2 -0
- package/dist/components/text-editor/core.js +71 -14
- package/dist/components/text-editor/lexical-root.svelte +8 -7
- package/dist/components/text-editor/raw-markdown.d.ts +7 -7
- package/dist/components/text-editor/raw-markdown.js +32 -3
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-editor/store.svelte.js +4 -5
- package/dist/components/text-editor/text-editor.svelte +6 -0
- package/dist/components/text-editor/toolbar/insert-item-button.svelte +10 -8
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte +10 -14
- package/dist/components/util/popup.svelte +14 -9
- package/dist/typedefs.d.ts +11 -4
- package/dist/typedefs.js +7 -3
- package/package.json +1 -1
- package/dist/components/text-editor/toolbar/component-editor.svelte +0 -96
- package/dist/components/text-editor/toolbar/component-editor.svelte.d.ts +0 -35
- package/dist/components/text-editor/toolbar/insert-component-dialog.svelte +0 -92
- package/dist/components/text-editor/toolbar/insert-component-dialog.svelte.d.ts +0 -30
|
@@ -5,6 +5,7 @@ export function isSafeLinkURL(url: string): boolean;
|
|
|
5
5
|
export function getSelectionTypes(): TextEditorSelectionState;
|
|
6
6
|
export function exportMarkdown(enabledTransformers: Transformer[]): string;
|
|
7
7
|
export function onEditorUpdate(editor: LexicalEditor, enabledTransformers: Transformer[], cachedValue?: string): string;
|
|
8
|
+
export function getComponentMarkdown({ node, createNode, transformer, createMarkdown }: TextEditorComponent): string;
|
|
8
9
|
export function initEditor({ enabledButtons, components, useMarkdownShortcuts, isCodeEditor, defaultLanguage, }: TextEditorConfig): InitEditorResult;
|
|
9
10
|
export function loadCodeHighlighter(lang: string): Promise<void>;
|
|
10
11
|
export function convertMarkdownToLexical(editor: LexicalEditor, value: string, enabledTransformers: Transformer[]): Promise<string>;
|
|
@@ -26,4 +27,5 @@ export type InitEditorResult = {
|
|
|
26
27
|
import type { LexicalEditor } from 'lexical';
|
|
27
28
|
import type { TextEditorSelectionState } from '../../typedefs';
|
|
28
29
|
import type { Transformer } from '@lexical/markdown';
|
|
30
|
+
import type { TextEditorComponent } from '../../typedefs';
|
|
29
31
|
import type { TextEditorConfig } from '../../typedefs';
|
|
@@ -91,6 +91,7 @@ import { TABLE } from './transformers/table.js';
|
|
|
91
91
|
* @import { Transformer } from '@lexical/markdown';
|
|
92
92
|
* @import {
|
|
93
93
|
* TextEditorBlockType,
|
|
94
|
+
* TextEditorComponent,
|
|
94
95
|
* TextEditorConfig,
|
|
95
96
|
* TextEditorInlineType,
|
|
96
97
|
* TextEditorNodeType,
|
|
@@ -479,6 +480,54 @@ export const onEditorUpdate = (editor, enabledTransformers, cachedValue) => {
|
|
|
479
480
|
return value;
|
|
480
481
|
};
|
|
481
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Get the Markdown of a new instance of the given editor component, to insert it in the plain text
|
|
485
|
+
* mode. The component’s own `createMarkdown` function is used if any. Otherwise, a new node is
|
|
486
|
+
* created in a temporary editor and exported with the component’s transformer, just like the rich
|
|
487
|
+
* text editor does.
|
|
488
|
+
* @param {TextEditorComponent} component Editor component.
|
|
489
|
+
* @returns {string} Markdown. It can be empty, for example if the component’s output depends on a
|
|
490
|
+
* field value that is not set yet.
|
|
491
|
+
*/
|
|
492
|
+
export const getComponentMarkdown = ({ node, createNode, transformer, createMarkdown }) => {
|
|
493
|
+
if (createMarkdown) {
|
|
494
|
+
return createMarkdown().trim();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const editor = createEditor({
|
|
498
|
+
namespace: 'component',
|
|
499
|
+
nodes: node ? [/** @type {any} */ (node)] : [],
|
|
500
|
+
/**
|
|
501
|
+
* Throw an error, so the export below fails as a whole.
|
|
502
|
+
* @param {Error} error Error.
|
|
503
|
+
* @throws {Error} Always.
|
|
504
|
+
*/
|
|
505
|
+
onError: (error) => {
|
|
506
|
+
throw error;
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
let markdown = '';
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
editor.update(
|
|
514
|
+
() => {
|
|
515
|
+
const newNode = createNode();
|
|
516
|
+
|
|
517
|
+
// An inline node needs a paragraph to live in
|
|
518
|
+
getRoot().append(newNode.isInline() ? createParagraphNode().append(newNode) : newNode);
|
|
519
|
+
|
|
520
|
+
markdown = onEditorUpdate(editor, [transformer]);
|
|
521
|
+
},
|
|
522
|
+
{ discrete: true },
|
|
523
|
+
);
|
|
524
|
+
} catch {
|
|
525
|
+
// The component cannot be created or exported outside the rich text editor
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return markdown.trim();
|
|
529
|
+
};
|
|
530
|
+
|
|
482
531
|
/**
|
|
483
532
|
* Initialize the Lexical editor.
|
|
484
533
|
* @param {TextEditorConfig} config Editor configuration.
|
|
@@ -876,20 +925,28 @@ export const convertMarkdownToLexical = async (editor, value, enabledTransformer
|
|
|
876
925
|
// Pad blank blockquote lines so they are not imported as literal `>` text
|
|
877
926
|
value = padBlankBlockquoteLines(value);
|
|
878
927
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
928
|
+
/** @type {unknown} */
|
|
929
|
+
let error;
|
|
930
|
+
|
|
931
|
+
editor.update(
|
|
932
|
+
() => {
|
|
933
|
+
try {
|
|
934
|
+
convertFromMarkdownString(value, enabledTransformers);
|
|
935
|
+
} catch (ex) {
|
|
936
|
+
error = ex;
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
// Tell the import from a change made by the user, and commit it right away, so the node
|
|
940
|
+
// transforms, e.g. the one giving a code block its default language, have run by the time the
|
|
941
|
+
// content is exported below
|
|
942
|
+
{ tag: IMPORT_UPDATE_TAG, discrete: true },
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (error) {
|
|
946
|
+
throw new Error('Failed to convert Markdown', { cause: error });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return editor.read(() => exportMarkdown(enabledTransformers));
|
|
893
950
|
};
|
|
894
951
|
|
|
895
952
|
/**
|
|
@@ -92,12 +92,13 @@
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const { value:
|
|
95
|
+
const { value: exportedValue, selection } = /** @type {CustomEvent} */ (event).detail;
|
|
96
|
+
// Keep the value as given when the content is the one last imported, which the editor only
|
|
97
|
+
// writes in its own Markdown style, e.g. `_text_` for `*text*`: that’s not a change, whether
|
|
98
|
+
// the content is untouched or the user has undone their changes to it
|
|
99
|
+
const newValue = editorStore.getImportedValue(exportedValue) ?? exportedValue;
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
// `_text_` for `*text*`: that’s not a change, and the value would otherwise change as soon as
|
|
99
|
-
// it’s set
|
|
100
|
-
if (inputValue !== newValue && !editorStore.isRestyledImport(newValue)) {
|
|
101
|
+
if (inputValue !== newValue) {
|
|
101
102
|
// Temporarily disable rich text to prevent unnecessary Markdown conversion that resets
|
|
102
103
|
// Lexical nodes and selection, then restore the state
|
|
103
104
|
editorStore.useRichText = false;
|
|
@@ -223,10 +224,10 @@
|
|
|
223
224
|
.lexical-root[aria-invalid=true] {
|
|
224
225
|
border-color: var(--sui-error-border-color);
|
|
225
226
|
}
|
|
226
|
-
.lexical-root :global(
|
|
227
|
+
.lexical-root :global(:not(.sui):first-child) {
|
|
227
228
|
margin-top: 0;
|
|
228
229
|
}
|
|
229
|
-
.lexical-root :global(
|
|
230
|
+
.lexical-root :global(:not(.sui):last-child) {
|
|
230
231
|
margin-bottom: 0;
|
|
231
232
|
}
|
|
232
233
|
.lexical-root :global(strong.italic) {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @import { TextEditorBlockType, TextEditorFormatType } from '../../typedefs';
|
|
2
|
+
* @import {
|
|
3
|
+
* TextEditorBlockType,
|
|
4
|
+
* TextEditorComponent,
|
|
5
|
+
* TextEditorFormatType,
|
|
6
|
+
* } from '../../typedefs';
|
|
9
7
|
*/
|
|
10
8
|
/**
|
|
11
9
|
* Current value and selection of the `<textarea>`.
|
|
@@ -43,6 +41,7 @@ export function getRawTextState({ value, selectionStart, selectionEnd }: HTMLTex
|
|
|
43
41
|
export function focusTextArea(textArea: HTMLTextAreaElement): Promise<boolean>;
|
|
44
42
|
export function isRawTextEditable({ readOnly, disabled }: HTMLTextAreaElement): boolean;
|
|
45
43
|
export function applyRawTextEdit(textArea: HTMLTextAreaElement, { start, end, text, selectionStart, selectionEnd }: RawTextEdit): Promise<void>;
|
|
44
|
+
export function insertComponent(textArea: HTMLTextAreaElement, component: TextEditorComponent): Promise<void>;
|
|
46
45
|
/**
|
|
47
46
|
* Current value and selection of the `<textarea>`.
|
|
48
47
|
*/
|
|
@@ -88,3 +87,4 @@ export type RawTextEdit = {
|
|
|
88
87
|
};
|
|
89
88
|
import type { TextEditorFormatType } from '../../typedefs';
|
|
90
89
|
import type { TextEditorBlockType } from '../../typedefs';
|
|
90
|
+
import type { TextEditorComponent } from '../../typedefs';
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markdown editing helpers for the plain text mode of the text editor, where the toolbar buttons
|
|
3
3
|
* edit the Markdown source in the `<textarea>` instead of Lexical nodes. Apart from
|
|
4
|
-
* {@link getRawTextState} and {@link
|
|
5
|
-
* the current value and selection, and returns the edit to apply.
|
|
4
|
+
* {@link getRawTextState}, {@link applyRawTextEdit} and {@link insertComponent}, everything here is
|
|
5
|
+
* pure: a function takes the current value and selection, and returns the edit to apply.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { getComponentMarkdown } from './core.js';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
|
-
* @import {
|
|
11
|
+
* @import {
|
|
12
|
+
* TextEditorBlockType,
|
|
13
|
+
* TextEditorComponent,
|
|
14
|
+
* TextEditorFormatType,
|
|
15
|
+
* } from '../../typedefs';
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
/**
|
|
@@ -469,3 +475,26 @@ export const applyRawTextEdit = async (
|
|
|
469
475
|
|
|
470
476
|
textArea.setSelectionRange(selectionStart, selectionEnd);
|
|
471
477
|
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Insert the Markdown of a new instance of the given editor component into the `<textarea>`. A
|
|
481
|
+
* component matched by a single-line pattern is inserted inline at the caret; anything else is
|
|
482
|
+
* inserted as a separate block. If the component has no Markdown to insert, the focus just moves
|
|
483
|
+
* back to the `<textarea>`.
|
|
484
|
+
* @param {HTMLTextAreaElement} textArea `<textarea>` element.
|
|
485
|
+
* @param {TextEditorComponent} component Editor component.
|
|
486
|
+
* @returns {Promise<void>} Nothing.
|
|
487
|
+
*/
|
|
488
|
+
export const insertComponent = async (textArea, component) => {
|
|
489
|
+
const markdown = getComponentMarkdown(component);
|
|
490
|
+
|
|
491
|
+
if (!markdown) {
|
|
492
|
+
textArea.focus();
|
|
493
|
+
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const block = component.transformer?.type !== 'text-match';
|
|
498
|
+
|
|
499
|
+
await applyRawTextEdit(textArea, insertMarkdown(getRawTextState(textArea), markdown, { block }));
|
|
500
|
+
};
|
|
@@ -179,13 +179,12 @@ export const createEditorStore = () => {
|
|
|
179
179
|
editorId,
|
|
180
180
|
convertMarkdown,
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
183
|
-
* the editor’s own Markdown style.
|
|
182
|
+
* Get the value last imported if the given value, exported by the editor, is only that value
|
|
183
|
+
* written in the editor’s own Markdown style.
|
|
184
184
|
* @param {string} value Exported value.
|
|
185
|
-
* @returns {
|
|
185
|
+
* @returns {string | undefined} Imported value, or `undefined` if the content has changed.
|
|
186
186
|
*/
|
|
187
|
-
|
|
188
|
-
!!lastImport && lastImport.source === inputValue && lastImport.exported === value,
|
|
187
|
+
getImportedValue: (value) => (lastImport?.exported === value ? lastImport.source : undefined),
|
|
189
188
|
/* eslint-enable jsdoc/require-jsdoc */
|
|
190
189
|
};
|
|
191
190
|
};
|
|
@@ -236,6 +236,12 @@
|
|
|
236
236
|
pending = editorStore.pending;
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// An editor removed right after a change never converts it, so don’t leave a bound `pending` set
|
|
240
|
+
// for good, which would hold up anything waiting for the change
|
|
241
|
+
$effect(() => () => {
|
|
242
|
+
pending = false;
|
|
243
|
+
});
|
|
244
|
+
|
|
239
245
|
$effect(() => {
|
|
240
246
|
// The root initializes the editor before these effects first run, and stays initialized
|
|
241
247
|
/* v8 ignore next */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { getContext } from 'svelte';
|
|
7
7
|
import Button from '../../button/button.svelte';
|
|
8
8
|
import Icon from '../../icon/icon.svelte';
|
|
9
|
-
import
|
|
9
|
+
import { insertComponent } from '../raw-markdown.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @import { TextEditorComponent, TextEditorStore } from '../../../typedefs';
|
|
@@ -28,8 +28,6 @@
|
|
|
28
28
|
const editorStore = getContext('editorStore');
|
|
29
29
|
|
|
30
30
|
const { label, icon, createNode } = $derived(component);
|
|
31
|
-
|
|
32
|
-
let openDialog = $state(false);
|
|
33
31
|
</script>
|
|
34
32
|
|
|
35
33
|
<Button
|
|
@@ -39,9 +37,15 @@
|
|
|
39
37
|
aria-label={label}
|
|
40
38
|
aria-controls={editorStore.controlId}
|
|
41
39
|
onclick={() => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const { textArea, useRichText } = editorStore;
|
|
41
|
+
|
|
42
|
+
// Insert the component’s Markdown in the plain text mode
|
|
43
|
+
if (!useRichText) {
|
|
44
|
+
// The button is only enabled while the `<textarea>` is there
|
|
45
|
+
/* v8 ignore else */
|
|
46
|
+
if (textArea) {
|
|
47
|
+
insertComponent(textArea, component);
|
|
48
|
+
}
|
|
45
49
|
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
@@ -58,5 +62,3 @@
|
|
|
58
62
|
{/if}
|
|
59
63
|
{/snippet}
|
|
60
64
|
</Button>
|
|
61
|
-
|
|
62
|
-
<InsertComponentDialog {component} bind:open={openDialog} />
|
|
@@ -9,7 +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
|
|
12
|
+
import { insertComponent } from '../raw-markdown.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @import { TextEditorComponent, TextEditorStore } from '../../../typedefs';
|
|
@@ -29,13 +29,6 @@
|
|
|
29
29
|
|
|
30
30
|
/** @type {TextEditorStore} */
|
|
31
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);
|
|
39
32
|
</script>
|
|
40
33
|
|
|
41
34
|
<MenuButton label={_('_sui.insert')}>
|
|
@@ -49,10 +42,15 @@
|
|
|
49
42
|
<MenuItem
|
|
50
43
|
{label}
|
|
51
44
|
onclick={() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
const { textArea, useRichText } = editorStore;
|
|
46
|
+
|
|
47
|
+
// Insert the component’s Markdown in the plain text mode
|
|
48
|
+
if (!useRichText) {
|
|
49
|
+
// The item is only clickable while the `<textarea>` is there
|
|
50
|
+
/* v8 ignore else */
|
|
51
|
+
if (textArea) {
|
|
52
|
+
insertComponent(textArea, component);
|
|
53
|
+
}
|
|
56
54
|
|
|
57
55
|
return;
|
|
58
56
|
}
|
|
@@ -73,5 +71,3 @@
|
|
|
73
71
|
</Menu>
|
|
74
72
|
{/snippet}
|
|
75
73
|
</MenuButton>
|
|
76
|
-
|
|
77
|
-
<InsertComponentDialog component={dialogComponent} bind:open={openDialog} />
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
* @param {object} [options] Options.
|
|
89
89
|
* @param {boolean} [options.tabStopOnly] Whether to only focus an element in the tab order,
|
|
90
90
|
* without falling back to any other element.
|
|
91
|
-
* @returns {boolean} Whether an element has
|
|
91
|
+
* @returns {boolean} Whether an element has taken the focus.
|
|
92
92
|
*/
|
|
93
93
|
const focusContent = ({ tabStopOnly = false } = {}) => {
|
|
94
94
|
/* v8 ignore next 3 -- the callers check the content first */
|
|
@@ -96,19 +96,24 @@
|
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const
|
|
100
|
-
content.
|
|
101
|
-
|
|
102
|
-
);
|
|
99
|
+
const candidates = /** @type {HTMLElement[]} */ ([
|
|
100
|
+
...content.querySelectorAll('[tabindex="0"]:not([aria-disabled="true"])'),
|
|
101
|
+
...(tabStopOnly ? [] : content.querySelectorAll('[tabindex]:not([aria-disabled="true"])')),
|
|
102
|
+
]);
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Skip an element that can’t take the focus, e.g. a hidden one
|
|
105
|
+
const focused = candidates.some((element) => {
|
|
106
|
+
element.focus();
|
|
107
|
+
|
|
108
|
+
return document.activeElement === element;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!focused && !tabStopOnly) {
|
|
107
112
|
content.tabIndex = -1;
|
|
108
113
|
content.focus();
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
return
|
|
116
|
+
return focused;
|
|
112
117
|
};
|
|
113
118
|
|
|
114
119
|
// Keep the `open` prop and the instance in sync both ways. The instance opens the popup when
|
package/dist/typedefs.d.ts
CHANGED
|
@@ -759,6 +759,13 @@ export type TextEditorComponent = {
|
|
|
759
759
|
* Node transformer.
|
|
760
760
|
*/
|
|
761
761
|
transformer: Transformer;
|
|
762
|
+
/**
|
|
763
|
+
* Function to create the Markdown inserted when the
|
|
764
|
+
* component is chosen in the plain text mode. By default, a new node created with `createNode` is
|
|
765
|
+
* exported with `transformer`, which may result in an empty string, for example if the output
|
|
766
|
+
* depends on a field value that is not set yet.
|
|
767
|
+
*/
|
|
768
|
+
createMarkdown?: (() => string) | undefined;
|
|
762
769
|
};
|
|
763
770
|
export type TextEditorConfig = {
|
|
764
771
|
/**
|
|
@@ -1046,11 +1053,11 @@ export type TextEditorStore = {
|
|
|
1046
1053
|
*/
|
|
1047
1054
|
convertMarkdown: () => Promise<void>;
|
|
1048
1055
|
/**
|
|
1049
|
-
* Function to
|
|
1050
|
-
*
|
|
1051
|
-
* the editor’s own Markdown style, e.g. `_text_` for `*text
|
|
1056
|
+
* Function to get the
|
|
1057
|
+
* {@link TextEditorStore.inputValue} last imported if the given value, exported by the editor, is
|
|
1058
|
+
* only that value written in the editor’s own Markdown style, e.g. `_text_` for `*text*`.
|
|
1052
1059
|
*/
|
|
1053
|
-
|
|
1060
|
+
getImportedValue: (value: string) => string | undefined;
|
|
1054
1061
|
};
|
|
1055
1062
|
export type SelectedItemDetail = {
|
|
1056
1063
|
/**
|
package/dist/typedefs.js
CHANGED
|
@@ -318,6 +318,10 @@
|
|
|
318
318
|
* @property {(props?: Record<string, any>) => LexicalNode} createNode Function to create a new node
|
|
319
319
|
* instance.
|
|
320
320
|
* @property {Transformer} transformer Node transformer.
|
|
321
|
+
* @property {() => string} [createMarkdown] Function to create the Markdown inserted when the
|
|
322
|
+
* component is chosen in the plain text mode. By default, a new node created with `createNode` is
|
|
323
|
+
* exported with `transformer`, which may result in an empty string, for example if the output
|
|
324
|
+
* depends on a field value that is not set yet.
|
|
321
325
|
*/
|
|
322
326
|
|
|
323
327
|
/**
|
|
@@ -445,9 +449,9 @@
|
|
|
445
449
|
* @property {boolean} pending Whether the user has changed the rich text content, and the editor
|
|
446
450
|
* has yet to convert it to Markdown and update {@link TextEditorStore.inputValue}.
|
|
447
451
|
* @property {() => Promise<void>} convertMarkdown Function to trigger the Lexical converter.
|
|
448
|
-
* @property {(value: string) =>
|
|
449
|
-
*
|
|
450
|
-
* the editor’s own Markdown style, e.g. `_text_` for `*text
|
|
452
|
+
* @property {(value: string) => string | undefined} getImportedValue Function to get the
|
|
453
|
+
* {@link TextEditorStore.inputValue} last imported if the given value, exported by the editor, is
|
|
454
|
+
* only that value written in the editor’s own Markdown style, e.g. `_text_` for `*text*`.
|
|
451
455
|
*/
|
|
452
456
|
|
|
453
457
|
/**
|
package/package.json
CHANGED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
@component
|
|
3
|
-
A minimal rich text editor holding a single editor component, so it can be edited with its own
|
|
4
|
-
field UI outside the main editor, and exported to Markdown. Used to insert a component in the
|
|
5
|
-
plain text mode.
|
|
6
|
-
-->
|
|
7
|
-
<script>
|
|
8
|
-
import {
|
|
9
|
-
$createParagraphNode as createParagraphNode,
|
|
10
|
-
$getRoot as getRoot,
|
|
11
|
-
$insertNodes as insertNodes,
|
|
12
|
-
} from 'lexical';
|
|
13
|
-
import { getContext, setContext } from 'svelte';
|
|
14
|
-
import { onEditorUpdate } from '../core.js';
|
|
15
|
-
import LexicalRoot from '../lexical-root.svelte';
|
|
16
|
-
import { createEditorStore } from '../store.svelte.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @import { TextEditorComponent, TextEditorStore } from '../../../typedefs';
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {object} Props
|
|
24
|
-
* @property {TextEditorComponent} component Editor component to edit.
|
|
25
|
-
* @property {string} [markdown] Markdown output of the component.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/** @type {Props} */
|
|
29
|
-
let {
|
|
30
|
-
/* eslint-disable prefer-const */
|
|
31
|
-
component,
|
|
32
|
-
markdown = $bindable(''),
|
|
33
|
-
/* eslint-enable prefer-const */
|
|
34
|
-
} = $props();
|
|
35
|
-
|
|
36
|
-
/** @type {TextEditorStore} */
|
|
37
|
-
const parentStore = getContext('editorStore');
|
|
38
|
-
const editorStore = createEditorStore();
|
|
39
|
-
|
|
40
|
-
// svelte-ignore state_referenced_locally
|
|
41
|
-
editorStore.config = {
|
|
42
|
-
...parentStore.config,
|
|
43
|
-
modes: ['rich-text'],
|
|
44
|
-
components: [component],
|
|
45
|
-
useMarkdownShortcuts: false,
|
|
46
|
-
useEmojiAutocomplete: false,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
setContext('editorStore', editorStore);
|
|
50
|
-
|
|
51
|
-
let inserted = false;
|
|
52
|
-
|
|
53
|
-
$effect(() => {
|
|
54
|
-
const { editor, initialized } = editorStore;
|
|
55
|
-
|
|
56
|
-
if (!editor || !initialized || inserted) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
inserted = true;
|
|
61
|
-
|
|
62
|
-
editor.update(() => {
|
|
63
|
-
const paragraph = createParagraphNode();
|
|
64
|
-
|
|
65
|
-
getRoot().clear().append(paragraph);
|
|
66
|
-
paragraph.select();
|
|
67
|
-
insertNodes([component.createNode()]);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
$effect(() => {
|
|
72
|
-
markdown = editorStore.inputValue.trim();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get the component’s Markdown right now. The bound `markdown` is only updated after a short
|
|
77
|
-
* delay, so it may not reflect a field that has just been changed.
|
|
78
|
-
* @returns {string} Markdown.
|
|
79
|
-
*/
|
|
80
|
-
export const getMarkdown = () => {
|
|
81
|
-
const { editor, enabledTransformers } = editorStore;
|
|
82
|
-
|
|
83
|
-
// The dialog can only be submitted once the editor is there
|
|
84
|
-
/* v8 ignore next 3 */
|
|
85
|
-
if (!editor) {
|
|
86
|
-
return markdown;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return editor
|
|
90
|
-
.getEditorState()
|
|
91
|
-
.read(() => onEditorUpdate(editor, enabledTransformers))
|
|
92
|
-
.trim();
|
|
93
|
-
};
|
|
94
|
-
</script>
|
|
95
|
-
|
|
96
|
-
<LexicalRoot aria-label={component.label} />
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export default ComponentEditor;
|
|
2
|
-
type ComponentEditor = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<Props>): void;
|
|
5
|
-
} & {
|
|
6
|
-
getMarkdown: () => string;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* A minimal rich text editor holding a single editor component, so it can be edited with its own
|
|
10
|
-
* field UI outside the main editor, and exported to Markdown. Used to insert a component in the
|
|
11
|
-
* plain text mode.
|
|
12
|
-
*/
|
|
13
|
-
declare const ComponentEditor: import("svelte").Component<{
|
|
14
|
-
/**
|
|
15
|
-
* Editor component to edit.
|
|
16
|
-
*/
|
|
17
|
-
component: TextEditorComponent;
|
|
18
|
-
/**
|
|
19
|
-
* Markdown output of the component.
|
|
20
|
-
*/
|
|
21
|
-
markdown?: string | undefined;
|
|
22
|
-
}, {
|
|
23
|
-
getMarkdown: () => string;
|
|
24
|
-
}, "markdown">;
|
|
25
|
-
type Props = {
|
|
26
|
-
/**
|
|
27
|
-
* Editor component to edit.
|
|
28
|
-
*/
|
|
29
|
-
component: TextEditorComponent;
|
|
30
|
-
/**
|
|
31
|
-
* Markdown output of the component.
|
|
32
|
-
*/
|
|
33
|
-
markdown?: string | undefined;
|
|
34
|
-
};
|
|
35
|
-
import type { TextEditorComponent } from '../../../typedefs';
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
@component
|
|
3
|
-
A dialog to insert an editor component in the plain text mode. The component is edited with its
|
|
4
|
-
own field UI in a minimal rich text editor, then inserted into the `<textarea>` as Markdown.
|
|
5
|
-
-->
|
|
6
|
-
<script>
|
|
7
|
-
import { _ } from '@sveltia/i18n';
|
|
8
|
-
import { getContext } from 'svelte';
|
|
9
|
-
import Dialog from '../../dialog/dialog.svelte';
|
|
10
|
-
import { applyRawTextEdit, getRawTextState, insertMarkdown } from '../raw-markdown.js';
|
|
11
|
-
import ComponentEditor from './component-editor.svelte';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @import { TextEditorComponent, TextEditorStore } from '../../../typedefs';
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @typedef {object} Props
|
|
19
|
-
* @property {TextEditorComponent | undefined} component Editor component to insert.
|
|
20
|
-
* @property {boolean} [open] Whether to open the dialog.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** @type {Props} */
|
|
24
|
-
let {
|
|
25
|
-
/* eslint-disable prefer-const */
|
|
26
|
-
component,
|
|
27
|
-
open = $bindable(false),
|
|
28
|
-
/* eslint-enable prefer-const */
|
|
29
|
-
} = $props();
|
|
30
|
-
|
|
31
|
-
/** @type {TextEditorStore} */
|
|
32
|
-
const editorStore = getContext('editorStore');
|
|
33
|
-
|
|
34
|
-
let markdown = $state('');
|
|
35
|
-
/** @type {ComponentEditor | undefined} */
|
|
36
|
-
let componentEditor = $state();
|
|
37
|
-
/**
|
|
38
|
-
* Markdown taken from the editor as the dialog starts closing, while it’s still there.
|
|
39
|
-
* @type {string}
|
|
40
|
-
*/
|
|
41
|
-
let latestMarkdown = '';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Insert the component’s Markdown into the `<textarea>`, or just bring the focus back there.
|
|
45
|
-
* @param {CustomEvent} event `close` event.
|
|
46
|
-
*/
|
|
47
|
-
const onClose = (event) => {
|
|
48
|
-
const { textArea } = editorStore;
|
|
49
|
-
|
|
50
|
-
// The dialog can only be opened from a button, which needs the `<textarea>`
|
|
51
|
-
/* v8 ignore next 3 */
|
|
52
|
-
if (!textArea) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (event.detail.returnValue !== 'cancel' && latestMarkdown) {
|
|
57
|
-
// A component matched by a single-line pattern is inline; anything else is a block
|
|
58
|
-
const block = component?.transformer?.type !== 'text-match';
|
|
59
|
-
|
|
60
|
-
applyRawTextEdit(
|
|
61
|
-
textArea,
|
|
62
|
-
insertMarkdown(getRawTextState(textArea), latestMarkdown, { block }),
|
|
63
|
-
);
|
|
64
|
-
} else {
|
|
65
|
-
textArea.focus();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
markdown = '';
|
|
69
|
-
latestMarkdown = '';
|
|
70
|
-
};
|
|
71
|
-
</script>
|
|
72
|
-
|
|
73
|
-
<Dialog
|
|
74
|
-
title={component?.label ?? ''}
|
|
75
|
-
size="large"
|
|
76
|
-
bind:open
|
|
77
|
-
okLabel={_('_sui.insert')}
|
|
78
|
-
okDisabled={!markdown}
|
|
79
|
-
restoreFocus={false}
|
|
80
|
-
onClosing={() => {
|
|
81
|
-
// Get the latest Markdown, as the bound value is updated with a delay
|
|
82
|
-
latestMarkdown = componentEditor?.getMarkdown() ?? markdown;
|
|
83
|
-
}}
|
|
84
|
-
onClose={(event) => {
|
|
85
|
-
onClose(event);
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
<!-- The dialog unmounts its content once closed, so each opening gets a fresh editor -->
|
|
89
|
-
{#if component}
|
|
90
|
-
<ComponentEditor bind:this={componentEditor} {component} bind:markdown />
|
|
91
|
-
{/if}
|
|
92
|
-
</Dialog>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export default InsertComponentDialog;
|
|
2
|
-
type InsertComponentDialog = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<Props>): void;
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* A dialog to insert an editor component in the plain text mode. The component is edited with its
|
|
8
|
-
* own field UI in a minimal rich text editor, then inserted into the `<textarea>` as Markdown.
|
|
9
|
-
*/
|
|
10
|
-
declare const InsertComponentDialog: import("svelte").Component<{
|
|
11
|
-
/**
|
|
12
|
-
* Editor component to insert.
|
|
13
|
-
*/
|
|
14
|
-
component: TextEditorComponent | undefined;
|
|
15
|
-
/**
|
|
16
|
-
* Whether to open the dialog.
|
|
17
|
-
*/
|
|
18
|
-
open?: boolean | undefined;
|
|
19
|
-
}, {}, "open">;
|
|
20
|
-
type Props = {
|
|
21
|
-
/**
|
|
22
|
-
* Editor component to insert.
|
|
23
|
-
*/
|
|
24
|
-
component: TextEditorComponent | undefined;
|
|
25
|
-
/**
|
|
26
|
-
* Whether to open the dialog.
|
|
27
|
-
*/
|
|
28
|
-
open?: boolean | undefined;
|
|
29
|
-
};
|
|
30
|
-
import type { TextEditorComponent } from '../../../typedefs';
|