@riducms/plugin-richtext 0.2.2 → 0.3.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/README.md +7 -1
- package/package.json +7 -5
- package/src/block/rich-text-block-card.scss +139 -0
- package/src/block/rich-text-block-card.svelte +42 -21
- package/src/block/rich-text-block-clipboard.ts +1 -1
- package/src/block/rich-text-block-node.ts +1 -1
- package/src/block/rich-text-blocks-plugin.svelte +5 -2
- package/src/document-validation.ts +7 -5
- package/src/field/reference-node-placement.ts +49 -0
- package/src/field/rich-text-blocks.ts +1 -5
- package/src/field/rich-text-config.ts +1 -4
- package/src/field/rich-text-document.ts +1 -4
- package/src/field/rich-text-editor.svelte +105 -57
- package/src/field/rich-text-footer.svelte +7 -12
- package/src/field/rich-text-markdown.ts +62 -0
- package/src/link/link-url.ts +30 -0
- package/src/link/rich-text-link-plugin.svelte +528 -0
- package/src/link/rich-text-link.scss +144 -0
- package/src/link/safe-link-transform.ts +20 -0
- package/src/link-url-policy.ts +14 -0
- package/src/menu/rich-text-block-toolbar.svelte +403 -70
- package/src/menu/rich-text-commands.ts +10 -1
- package/src/menu/rich-text-menu.scss +92 -0
- package/src/menu/rich-text-menu.svelte +62 -69
- package/src/menu/rich-text-option-filter.ts +30 -0
- package/src/menu/rich-text-options.ts +81 -84
- package/src/menu/rich-text-slash-menu.svelte +15 -11
- package/src/messages.ts +72 -27
- package/src/relationship/rich-text-relationship-node.scss +98 -0
- package/src/relationship/rich-text-relationship-node.svelte +38 -32
- package/src/relationship/rich-text-relationship-plugin.svelte +24 -40
- package/src/render/rich-text.svelte +10 -5
- package/src/render.ts +3 -8
- package/src/styles/integration.scss +384 -0
- package/src/toolbar/PAYLOAD-LICENSE.md +22 -0
- package/src/toolbar/rich-text-floating-toolbar.svelte +331 -169
- package/src/toolbar/rich-text-toolbar.scss +80 -0
- package/src/toolbar/toolbar-icon.svelte +200 -30
- package/src/upload/rich-text-upload-node.scss +308 -0
- package/src/upload/rich-text-upload-node.svelte +96 -125
- package/src/upload/rich-text-upload-node.ts +1 -0
- package/src/upload/rich-text-upload-plugin.svelte +10 -33
- package/src/styles/integration.css +0 -83
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
} from "@riducms/ui";
|
|
10
10
|
|
|
11
11
|
import type { RichTextMenuOption } from "@plugin-richtext/menu/rich-text-options";
|
|
12
|
+
import ToolbarIcon from "@plugin-richtext/toolbar/toolbar-icon.svelte";
|
|
13
|
+
import "@plugin-richtext/menu/rich-text-menu.scss";
|
|
12
14
|
|
|
13
15
|
let {
|
|
14
16
|
options,
|
|
15
17
|
selectedIndex = null,
|
|
16
18
|
onHighlight = () => undefined,
|
|
17
19
|
onSelect,
|
|
18
|
-
embedded = false,
|
|
19
20
|
command = false,
|
|
20
21
|
idPrefix = "richtext-menu-item",
|
|
21
22
|
}: {
|
|
@@ -23,85 +24,77 @@
|
|
|
23
24
|
selectedIndex?: number | null;
|
|
24
25
|
onHighlight?: (index: number) => void;
|
|
25
26
|
onSelect: (option: RichTextMenuOption) => void;
|
|
26
|
-
embedded?: boolean;
|
|
27
27
|
command?: boolean;
|
|
28
28
|
idPrefix?: string;
|
|
29
29
|
} = $props();
|
|
30
30
|
const i18n = getAdminI18n();
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
const groups = $derived(
|
|
32
|
+
[...new Set(options.map((option) => option.group))].map((name) => ({
|
|
33
|
+
name,
|
|
34
|
+
items: options
|
|
35
|
+
.map((option, index) => ({ option, index }))
|
|
36
|
+
.filter(({ option }) => option.group === name),
|
|
37
|
+
}))
|
|
38
|
+
);
|
|
37
39
|
</script>
|
|
38
40
|
|
|
39
41
|
{#snippet optionContent(option: RichTextMenuOption)}
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
aria-hidden="true"
|
|
43
|
-
>
|
|
44
|
-
{option.glyph}
|
|
45
|
-
</span>
|
|
46
|
-
<span class="grid min-w-0 gap-[0.1rem]">
|
|
47
|
-
<strong class="text-[13px] font-medium">{option.label}</strong>
|
|
48
|
-
<small class="text-[11px] text-foreground-faint">{option.description}</small>
|
|
49
|
-
</span>
|
|
50
|
-
<span class="font-mono text-[11px] text-foreground-faint">{option.keyHint}</span>
|
|
42
|
+
<ToolbarIcon name={option.icon} />
|
|
43
|
+
<span class="ridu-richtext-menu__label">{option.label}</span>
|
|
51
44
|
{/snippet}
|
|
52
45
|
|
|
53
46
|
{#if command}
|
|
54
|
-
<CommandList class=
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
class="
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</
|
|
74
|
-
|
|
47
|
+
<CommandList class="ridu-richtext-menu">
|
|
48
|
+
{#each groups as group (group.name)}
|
|
49
|
+
<CommandGroup class="ridu-richtext-menu__group">
|
|
50
|
+
<CommandGroupHeading class="ridu-richtext-menu__heading">
|
|
51
|
+
{i18n.t(`plugin.richtext:menu.${group.name}`)}
|
|
52
|
+
</CommandGroupHeading>
|
|
53
|
+
<CommandGroupItems class="ridu-richtext-menu__items">
|
|
54
|
+
{#each group.items as { option, index } (option.key)}
|
|
55
|
+
<CommandItem
|
|
56
|
+
class="ridu-richtext-menu__option"
|
|
57
|
+
id={`${idPrefix}-${index}`}
|
|
58
|
+
value={option.key}
|
|
59
|
+
keywords={[option.label, ...option.keywords]}
|
|
60
|
+
onSelect={() => onSelect(option)}
|
|
61
|
+
>
|
|
62
|
+
{@render optionContent(option)}
|
|
63
|
+
</CommandItem>
|
|
64
|
+
{/each}
|
|
65
|
+
</CommandGroupItems>
|
|
66
|
+
</CommandGroup>
|
|
67
|
+
{/each}
|
|
75
68
|
</CommandList>
|
|
76
69
|
{:else}
|
|
77
|
-
<div class=
|
|
78
|
-
|
|
79
|
-
class="
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{
|
|
103
|
-
</
|
|
104
|
-
|
|
105
|
-
|
|
70
|
+
<div class="ridu-richtext-menu" data-richtext-menu-surface>
|
|
71
|
+
{#each groups as group (group.name)}
|
|
72
|
+
<div class="ridu-richtext-menu__group" role="presentation">
|
|
73
|
+
<p class="ridu-richtext-menu__heading">{i18n.t(`plugin.richtext:menu.${group.name}`)}</p>
|
|
74
|
+
<ul class="ridu-richtext-menu__items" role="presentation">
|
|
75
|
+
{#each group.items as { option, index } (option.key)}
|
|
76
|
+
<li
|
|
77
|
+
id={`${idPrefix}-${index}`}
|
|
78
|
+
class={["ridu-richtext-menu__option", selectedIndex === index && "is-selected"]}
|
|
79
|
+
role="option"
|
|
80
|
+
aria-selected={selectedIndex === index}
|
|
81
|
+
tabindex="-1"
|
|
82
|
+
{@attach option.attach}
|
|
83
|
+
onmouseenter={() => onHighlight(index)}
|
|
84
|
+
onmousedown={(event) => event.preventDefault()}
|
|
85
|
+
onclick={() => onSelect(option)}
|
|
86
|
+
onkeydown={(event) => {
|
|
87
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
onSelect(option);
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{@render optionContent(option)}
|
|
94
|
+
</li>
|
|
95
|
+
{/each}
|
|
96
|
+
</ul>
|
|
97
|
+
</div>
|
|
98
|
+
{/each}
|
|
106
99
|
</div>
|
|
107
100
|
{/if}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface FilterableRichTextOption {
|
|
2
|
+
label: string;
|
|
3
|
+
description: string;
|
|
4
|
+
keywords: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function filterRichTextOptions<Option extends FilterableRichTextOption>(
|
|
8
|
+
options: readonly Option[],
|
|
9
|
+
query: string,
|
|
10
|
+
language?: string
|
|
11
|
+
): Option[] {
|
|
12
|
+
const normalized = query.trim().toLocaleLowerCase(language);
|
|
13
|
+
if (normalized === "") return [...options];
|
|
14
|
+
const compact = normalized.replace(/[\s\-_]/g, "");
|
|
15
|
+
const matches = (value: string) => {
|
|
16
|
+
const lower = value.toLocaleLowerCase(language);
|
|
17
|
+
return (
|
|
18
|
+
lower.includes(normalized) ||
|
|
19
|
+
(compact !== "" && lower.replace(/[\s\-_]/g, "").includes(compact))
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
const exactLabel = (option: Option) =>
|
|
23
|
+
option.label.toLocaleLowerCase(language).replace(/[\s\-_]/g, "") === compact;
|
|
24
|
+
return options
|
|
25
|
+
.filter(
|
|
26
|
+
(option) =>
|
|
27
|
+
matches(option.label) || matches(option.description) || option.keywords.some(matches)
|
|
28
|
+
)
|
|
29
|
+
.sort((a, b) => Number(exactLabel(b)) - Number(exactLabel(a)));
|
|
30
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { RichTextIconName } from "@plugin-richtext/toolbar/toolbar-icon.svelte";
|
|
1
2
|
import { $createCodeNode } from "@lexical/code";
|
|
2
3
|
import {
|
|
3
4
|
INSERT_CHECK_LIST_COMMAND,
|
|
4
5
|
INSERT_ORDERED_LIST_COMMAND,
|
|
5
6
|
INSERT_UNORDERED_LIST_COMMAND,
|
|
6
7
|
} from "@lexical/list";
|
|
7
|
-
import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
|
|
8
|
+
import { $createHeadingNode, $createQuoteNode, type HeadingTagType } from "@lexical/rich-text";
|
|
8
9
|
import { $setBlocksType } from "@lexical/selection";
|
|
9
10
|
import type { SchemaBlockType } from "@riducms/protocol";
|
|
10
11
|
import type { AdminI18n, FieldAuthoringHost } from "@riducms/plugin";
|
|
@@ -24,13 +25,15 @@ import {
|
|
|
24
25
|
} from "@plugin-richtext/menu/rich-text-commands";
|
|
25
26
|
import { hasRichTextFeature, type RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
26
27
|
|
|
28
|
+
export { filterRichTextOptions } from "@plugin-richtext/menu/rich-text-option-filter";
|
|
29
|
+
|
|
27
30
|
export class RichTextMenuOption extends MenuOption {
|
|
28
31
|
readonly label: string;
|
|
29
32
|
readonly description: string;
|
|
30
|
-
readonly
|
|
33
|
+
readonly icon: RichTextIconName;
|
|
34
|
+
readonly group: "lists" | "basic" | "blocks";
|
|
31
35
|
readonly keywords: readonly string[];
|
|
32
36
|
readonly select: () => void;
|
|
33
|
-
readonly keyHint: string;
|
|
34
37
|
readonly preservePlaceholder: boolean;
|
|
35
38
|
readonly restoreEditorFocus: boolean;
|
|
36
39
|
readonly blockType: string | undefined;
|
|
@@ -39,11 +42,11 @@ export class RichTextMenuOption extends MenuOption {
|
|
|
39
42
|
key: string,
|
|
40
43
|
label: string,
|
|
41
44
|
description: string,
|
|
42
|
-
|
|
45
|
+
icon: RichTextIconName,
|
|
43
46
|
keywords: readonly string[],
|
|
44
47
|
select: () => void,
|
|
45
48
|
options: {
|
|
46
|
-
|
|
49
|
+
group?: "lists" | "basic" | "blocks";
|
|
47
50
|
preservePlaceholder?: boolean;
|
|
48
51
|
restoreEditorFocus?: boolean;
|
|
49
52
|
blockType?: string;
|
|
@@ -52,10 +55,10 @@ export class RichTextMenuOption extends MenuOption {
|
|
|
52
55
|
super(key);
|
|
53
56
|
this.label = label;
|
|
54
57
|
this.description = description;
|
|
55
|
-
this.
|
|
58
|
+
this.icon = icon;
|
|
59
|
+
this.group = options.group ?? "basic";
|
|
56
60
|
this.keywords = keywords;
|
|
57
61
|
this.select = select;
|
|
58
|
-
this.keyHint = options.keyHint ?? "↵";
|
|
59
62
|
this.preservePlaceholder = options.preservePlaceholder ?? true;
|
|
60
63
|
this.restoreEditorFocus = options.restoreEditorFocus ?? true;
|
|
61
64
|
this.blockType = options.blockType;
|
|
@@ -74,31 +77,26 @@ export function buildRichTextOptions(
|
|
|
74
77
|
"paragraph",
|
|
75
78
|
i18n.t("plugin.richtext:option.paragraph.label"),
|
|
76
79
|
i18n.t("plugin.richtext:option.paragraph.description"),
|
|
77
|
-
"
|
|
80
|
+
"paragraph",
|
|
78
81
|
keywords(i18n.t("plugin.richtext:option.paragraph.keywords")),
|
|
79
82
|
() => $setBlock($createParagraphNode)
|
|
80
83
|
),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
i18n.t("plugin.richtext:option.heading3.label"),
|
|
92
|
-
i18n.t("plugin.richtext:option.heading3.description"),
|
|
93
|
-
"h3",
|
|
94
|
-
keywords(i18n.t("plugin.richtext:option.heading3.keywords")),
|
|
95
|
-
() => $setBlock(() => $createHeadingNode("h3"))
|
|
84
|
+
...[1, 2, 3, 4, 5, 6].map(
|
|
85
|
+
(level) =>
|
|
86
|
+
new RichTextMenuOption(
|
|
87
|
+
`heading-${level}`,
|
|
88
|
+
i18n.t("plugin.richtext:editor.heading", { level }),
|
|
89
|
+
"",
|
|
90
|
+
`h${level}` as RichTextIconName,
|
|
91
|
+
["heading", "title", `h${level}`],
|
|
92
|
+
() => $setBlock(() => $createHeadingNode(`h${level}` as HeadingTagType))
|
|
93
|
+
)
|
|
96
94
|
),
|
|
97
95
|
new RichTextMenuOption(
|
|
98
96
|
"quote",
|
|
99
97
|
i18n.t("plugin.richtext:option.quote.label"),
|
|
100
98
|
i18n.t("plugin.richtext:option.quote.description"),
|
|
101
|
-
|
|
99
|
+
"quote",
|
|
102
100
|
keywords(i18n.t("plugin.richtext:option.quote.keywords")),
|
|
103
101
|
() => $setBlock($createQuoteNode)
|
|
104
102
|
),
|
|
@@ -110,25 +108,28 @@ export function buildRichTextOptions(
|
|
|
110
108
|
"bulleted-list",
|
|
111
109
|
i18n.t("plugin.richtext:option.bulletedList.label"),
|
|
112
110
|
i18n.t("plugin.richtext:option.bulletedList.description"),
|
|
113
|
-
"
|
|
111
|
+
"unordered",
|
|
114
112
|
keywords(i18n.t("plugin.richtext:option.bulletedList.keywords")),
|
|
115
|
-
() => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
|
|
113
|
+
() => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
|
|
114
|
+
{ group: "lists" }
|
|
116
115
|
),
|
|
117
116
|
new RichTextMenuOption(
|
|
118
117
|
"numbered-list",
|
|
119
118
|
i18n.t("plugin.richtext:option.numberedList.label"),
|
|
120
119
|
i18n.t("plugin.richtext:option.numberedList.description"),
|
|
121
|
-
"
|
|
120
|
+
"ordered",
|
|
122
121
|
keywords(i18n.t("plugin.richtext:option.numberedList.keywords")),
|
|
123
|
-
() => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)
|
|
122
|
+
() => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
|
|
123
|
+
{ group: "lists" }
|
|
124
124
|
),
|
|
125
125
|
new RichTextMenuOption(
|
|
126
126
|
"checklist",
|
|
127
127
|
i18n.t("plugin.richtext:option.checklist.label"),
|
|
128
128
|
i18n.t("plugin.richtext:option.checklist.description"),
|
|
129
|
-
"
|
|
129
|
+
"check",
|
|
130
130
|
keywords(i18n.t("plugin.richtext:option.checklist.keywords")),
|
|
131
|
-
() => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)
|
|
131
|
+
() => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
|
|
132
|
+
{ group: "lists" }
|
|
132
133
|
)
|
|
133
134
|
);
|
|
134
135
|
}
|
|
@@ -139,7 +140,7 @@ export function buildRichTextOptions(
|
|
|
139
140
|
"code-block",
|
|
140
141
|
i18n.t("plugin.richtext:option.codeBlock.label"),
|
|
141
142
|
i18n.t("plugin.richtext:option.codeBlock.description"),
|
|
142
|
-
"
|
|
143
|
+
"code-block",
|
|
143
144
|
keywords(i18n.t("plugin.richtext:option.codeBlock.keywords")),
|
|
144
145
|
() => $setBlock($createCodeNode)
|
|
145
146
|
)
|
|
@@ -152,7 +153,7 @@ export function buildRichTextOptions(
|
|
|
152
153
|
"divider",
|
|
153
154
|
i18n.t("plugin.richtext:option.divider.label"),
|
|
154
155
|
i18n.t("plugin.richtext:option.divider.description"),
|
|
155
|
-
"
|
|
156
|
+
"divider",
|
|
156
157
|
keywords(i18n.t("plugin.richtext:option.divider.keywords")),
|
|
157
158
|
() => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
|
|
158
159
|
{ preservePlaceholder: false }
|
|
@@ -174,7 +175,7 @@ export function buildRichTextOptions(
|
|
|
174
175
|
i18n.t("plugin.richtext:option.upload.description", {
|
|
175
176
|
label: collection.labels.singular.toLocaleLowerCase(i18n.language),
|
|
176
177
|
}),
|
|
177
|
-
"
|
|
178
|
+
"upload",
|
|
178
179
|
[...keywords(i18n.t("plugin.richtext:option.upload.keywords")), collection.slug],
|
|
179
180
|
() =>
|
|
180
181
|
queueMicrotask(() =>
|
|
@@ -186,77 +187,73 @@ export function buildRichTextOptions(
|
|
|
186
187
|
)
|
|
187
188
|
);
|
|
188
189
|
}
|
|
189
|
-
options.
|
|
190
|
+
options.push(...uploadOptions);
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
collectionSlug: collection.slug,
|
|
212
|
-
})
|
|
213
|
-
),
|
|
214
|
-
{ restoreEditorFocus: false }
|
|
215
|
-
)
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
// Upload and relationship labels can intentionally overlap (for example,
|
|
219
|
-
// Asset and Related Asset). Preserve the established upload result first
|
|
220
|
-
// for keyboard selection and append the explicit relationship commands.
|
|
221
|
-
options.push(...relationshipOptions);
|
|
193
|
+
if (
|
|
194
|
+
hasRichTextFeature(config, "relationships") &&
|
|
195
|
+
authoring?.collections.some(
|
|
196
|
+
(collection) =>
|
|
197
|
+
config.relationshipCollections.length === 0 ||
|
|
198
|
+
config.relationshipCollections.includes(collection.slug)
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
options.push(
|
|
202
|
+
new RichTextMenuOption(
|
|
203
|
+
"relationship",
|
|
204
|
+
i18n.t("plugin.richtext:option.relationship.label"),
|
|
205
|
+
i18n.t("plugin.richtext:option.relationship.description"),
|
|
206
|
+
"relationship",
|
|
207
|
+
keywords(i18n.t("plugin.richtext:option.relationship.keywords")),
|
|
208
|
+
() => queueMicrotask(() => editor.dispatchCommand(OPEN_RELATIONSHIP_BROWSER_COMMAND, {})),
|
|
209
|
+
{ restoreEditorFocus: false }
|
|
210
|
+
)
|
|
211
|
+
);
|
|
222
212
|
}
|
|
223
213
|
|
|
224
214
|
if (hasRichTextFeature(config, "blocks") && authoring?.beginSchemaDraft !== undefined) {
|
|
225
|
-
options.
|
|
226
|
-
4,
|
|
227
|
-
0,
|
|
215
|
+
options.push(
|
|
228
216
|
...blockTypes.map(
|
|
229
217
|
(type) =>
|
|
230
218
|
new RichTextMenuOption(
|
|
231
219
|
`block-${type.slug}`,
|
|
232
220
|
type.labels.singular,
|
|
233
221
|
i18n.t("plugin.richtext:block.description", { key: type.slug }),
|
|
234
|
-
"
|
|
222
|
+
"block",
|
|
235
223
|
["block", type.slug, type.labels.singular],
|
|
236
224
|
() =>
|
|
237
225
|
queueMicrotask(() =>
|
|
238
226
|
editor.dispatchCommand(OPEN_BLOCK_EDITOR_COMMAND, { blockType: type.slug })
|
|
239
227
|
),
|
|
240
|
-
{ restoreEditorFocus: false, blockType: type.slug }
|
|
228
|
+
{ restoreEditorFocus: false, blockType: type.slug, group: "blocks" }
|
|
241
229
|
)
|
|
242
230
|
)
|
|
243
231
|
);
|
|
244
232
|
}
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
233
|
+
// Later basic features move their merged group after the list group in Payload's default menu.
|
|
234
|
+
const groupOrder = { lists: 0, basic: 1, blocks: 2 };
|
|
235
|
+
const iconOrder: RichTextIconName[] = [
|
|
236
|
+
"check",
|
|
237
|
+
"ordered",
|
|
238
|
+
"unordered",
|
|
239
|
+
"divider",
|
|
240
|
+
"upload",
|
|
241
|
+
"quote",
|
|
242
|
+
"relationship",
|
|
243
|
+
"h1",
|
|
244
|
+
"h2",
|
|
245
|
+
"h3",
|
|
246
|
+
"h4",
|
|
247
|
+
"h5",
|
|
248
|
+
"h6",
|
|
249
|
+
"paragraph",
|
|
250
|
+
"code-block",
|
|
251
|
+
"block",
|
|
252
|
+
];
|
|
253
|
+
return options.sort(
|
|
254
|
+
(a, b) =>
|
|
255
|
+
groupOrder[a.group] - groupOrder[b.group] ||
|
|
256
|
+
iconOrder.indexOf(a.icon) - iconOrder.indexOf(b.icon)
|
|
260
257
|
);
|
|
261
258
|
}
|
|
262
259
|
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
let { authoring, config }: { authoring: FieldAuthoringHost | undefined; config: RichTextConfig } =
|
|
23
23
|
$props();
|
|
24
24
|
const i18n = getAdminI18n();
|
|
25
|
+
const componentID = $props.id();
|
|
26
|
+
const menuLabelID = `${componentID}-label`;
|
|
25
27
|
const editor = useLexicalComposerContext()[0];
|
|
26
|
-
const trigger = createBasicTypeaheadTriggerMatch("/", {
|
|
28
|
+
const trigger = createBasicTypeaheadTriggerMatch("/", { minLength: 0 });
|
|
27
29
|
// Menu options carry attachment identity and the mounted field's feature set is fixed.
|
|
28
30
|
// svelte-ignore state_referenced_locally
|
|
29
31
|
const baseOptions = buildRichTextOptions(
|
|
@@ -38,16 +40,15 @@
|
|
|
38
40
|
contextElement: HTMLElement;
|
|
39
41
|
getBoundingClientRect: () => DOMRect;
|
|
40
42
|
} | null>(null);
|
|
41
|
-
let menuHost = $state<HTMLElement | null>(null);
|
|
43
|
+
let menuHost = $state.raw<HTMLElement | null>(null);
|
|
42
44
|
const options = $derived(filterRichTextOptions(baseOptions, query ?? "", i18n.language));
|
|
43
45
|
|
|
44
46
|
$effect(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
47
|
+
const anchor = menuHost?.parentElement;
|
|
48
|
+
if (anchor === undefined || anchor === null) return;
|
|
49
|
+
// The upstream anchor rewrites aria-label on every query resolution.
|
|
50
|
+
anchor.setAttribute("aria-labelledby", menuLabelID);
|
|
51
|
+
return () => anchor.removeAttribute("aria-labelledby");
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
function openMenu(resolution: MenuResolution) {
|
|
@@ -95,12 +96,16 @@
|
|
|
95
96
|
{trigger}
|
|
96
97
|
>
|
|
97
98
|
{#snippet menu(menuProps)}
|
|
98
|
-
<div bind:this={menuHost} role="presentation"
|
|
99
|
+
<div bind:this={menuHost} role="presentation">
|
|
100
|
+
<span id={menuLabelID} class="ridu-richtext-announcement">
|
|
101
|
+
{i18n.t("plugin.richtext:editor.insertBlock")}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
99
104
|
{#if menuProps.options.length > 0 && menuAnchor !== null && menuHost !== null}
|
|
100
105
|
<PopoverRoot open>
|
|
101
106
|
<PopoverContent
|
|
102
107
|
portalTo={menuHost}
|
|
103
|
-
class="
|
|
108
|
+
class="ridu-richtext-menu-popover"
|
|
104
109
|
role="presentation"
|
|
105
110
|
customAnchor={menuAnchor}
|
|
106
111
|
side="bottom"
|
|
@@ -122,7 +127,6 @@
|
|
|
122
127
|
?.setAttribute("aria-activedescendant", `typeahead-item-${index}`);
|
|
123
128
|
}}
|
|
124
129
|
onSelect={menuProps.selectOption}
|
|
125
|
-
embedded
|
|
126
130
|
idPrefix="typeahead-item"
|
|
127
131
|
/>
|
|
128
132
|
</PopoverContent>
|