@riducms/plugin-richtext 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/package.json +17 -6
- package/src/block/rich-text-block-card.svelte +157 -0
- package/src/block/rich-text-block-clipboard.ts +54 -0
- package/src/block/rich-text-block-node.ts +84 -0
- package/src/block/rich-text-blocks-plugin.svelte +316 -0
- package/src/document-validation.ts +104 -0
- package/src/document.ts +76 -0
- package/src/field/rich-text-blocks.ts +37 -0
- package/src/field/rich-text-config.ts +16 -13
- package/src/field/rich-text-context.svelte.ts +9 -0
- package/src/field/rich-text-document.ts +21 -1
- package/src/field/rich-text-editability-plugin.svelte +9 -0
- package/src/field/rich-text-editor.svelte +250 -0
- package/src/field/rich-text-field.svelte +57 -179
- package/src/field/rich-text-footer.svelte +5 -2
- package/src/index.ts +18 -14
- package/src/menu/rich-text-block-toolbar.svelte +17 -1
- package/src/menu/rich-text-commands.ts +11 -0
- package/src/menu/rich-text-options.ts +28 -1
- package/src/menu/rich-text-slash-menu.svelte +9 -1
- package/src/messages.ts +56 -0
- package/src/render/index.ts +6 -0
- package/src/render/rich-text.svelte +97 -0
- package/src/render.ts +164 -0
|
@@ -1,187 +1,65 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import { ListExtension } from "@lexical/list";
|
|
7
|
-
import { RichTextExtension } from "@lexical/rich-text";
|
|
8
|
-
import {
|
|
9
|
-
ContentEditable,
|
|
10
|
-
HorizontalRuleNode,
|
|
11
|
-
HorizontalRulePlugin,
|
|
12
|
-
LexicalExtensionComposer,
|
|
13
|
-
LinkPlugin,
|
|
14
|
-
OnChangePlugin,
|
|
15
|
-
} from "@hvniel/lexical-svelte";
|
|
16
|
-
import { defineExtension, type EditorState } from "lexical";
|
|
17
|
-
import { onDestroy } from "svelte";
|
|
18
|
-
import { FieldFrame } from "@riducms/ui";
|
|
2
|
+
import type { PluginFieldProps, PluginFieldBinding } from "@riducms/plugin";
|
|
3
|
+
import type { RichTextDocument } from "@plugin-richtext/document";
|
|
4
|
+
import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
5
|
+
import RichTextEditor from "@plugin-richtext/field/rich-text-editor.svelte";
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
h3: "ridu-richtext-heading ridu-richtext-h3 mt-[2.1rem] mb-[0.85rem] font-sans text-lg leading-[1.35] text-foreground font-semibold text-balance",
|
|
7
|
+
let {
|
|
8
|
+
field,
|
|
9
|
+
form,
|
|
10
|
+
config,
|
|
11
|
+
authoring,
|
|
12
|
+
i18n,
|
|
13
|
+
}: PluginFieldProps<RichTextDocument<unknown>, RichTextConfig> = $props();
|
|
14
|
+
let replacement = $state(0);
|
|
15
|
+
function fingerprint() {
|
|
16
|
+
return JSON.stringify([
|
|
17
|
+
field.schema.id,
|
|
18
|
+
config,
|
|
19
|
+
form.contentLocale,
|
|
20
|
+
form.resource,
|
|
21
|
+
field.rawValue,
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
// Local editor writes retain Lexical selection/history. Only an external parent
|
|
25
|
+
// replacement (save normalization, locale, schema, group paste) starts a new editor.
|
|
26
|
+
let accepted = fingerprint();
|
|
27
|
+
$effect(() => {
|
|
28
|
+
const incoming = fingerprint();
|
|
29
|
+
if (incoming !== accepted) {
|
|
30
|
+
accepted = incoming;
|
|
31
|
+
replacement++;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const editorField: PluginFieldBinding<RichTextDocument<unknown>, "plugin"> = {
|
|
35
|
+
get schema() {
|
|
36
|
+
return field.schema;
|
|
51
37
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
hrSelected: "is-selected",
|
|
55
|
-
link: "ridu-richtext-link cursor-pointer text-brand-primary-soft underline decoration-primary/40 decoration-1 underline-offset-[0.18em] hover:text-primary-hover hover:decoration-current",
|
|
56
|
-
list: {
|
|
57
|
-
checklist:
|
|
58
|
-
"ridu-richtext-checklist mt-1 mb-[1.35rem] list-none ps-0 text-[15.5px] leading-7 text-foreground-body",
|
|
59
|
-
listitemChecked: "ridu-richtext-list-item-checked line-through opacity-70",
|
|
60
|
-
listitemUnchecked: "ridu-richtext-list-item-unchecked",
|
|
61
|
-
listitem: "ridu-richtext-list-item ps-1",
|
|
62
|
-
nested: { listitem: "ridu-richtext-list-item-nested" },
|
|
63
|
-
ol: "ridu-richtext-list-ordered mt-1 mb-[1.35rem] list-decimal ps-[1.55rem] text-[15.5px] leading-7 text-foreground-body",
|
|
64
|
-
ul: "ridu-richtext-list-unordered mt-1 mb-[1.35rem] list-disc ps-[1.55rem] text-[15.5px] leading-7 text-foreground-body",
|
|
38
|
+
get value() {
|
|
39
|
+
return field.value;
|
|
65
40
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
quote:
|
|
69
|
-
"ridu-richtext-quote my-[1.85rem] border-s-2 border-primary/55 py-[0.1rem] ps-5 font-serif text-[19px] italic leading-6 text-foreground-strong text-pretty",
|
|
70
|
-
text: {
|
|
71
|
-
bold: "ridu-richtext-bold font-semibold",
|
|
72
|
-
code: "ridu-richtext-inline-code rounded-[3px] border border-control-border bg-control px-[0.32em] py-[0.12em] font-mono text-[0.86em] text-foreground-strong",
|
|
73
|
-
italic: "ridu-richtext-italic italic",
|
|
74
|
-
subscript: "ridu-richtext-subscript",
|
|
75
|
-
superscript: "ridu-richtext-superscript",
|
|
76
|
-
strikethrough: "ridu-richtext-strikethrough line-through",
|
|
77
|
-
underline: "ridu-richtext-underline underline underline-offset-[0.15em]",
|
|
41
|
+
get rawValue() {
|
|
42
|
+
return field.rawValue;
|
|
78
43
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Field identity is fixed for this mounted plugin component.
|
|
83
|
-
// svelte-ignore state_referenced_locally
|
|
84
|
-
const unregisterField = form.register(field.path);
|
|
85
|
-
onDestroy(unregisterField);
|
|
86
|
-
|
|
87
|
-
// The extension tree is identity-bearing and belongs to this field instance.
|
|
88
|
-
// svelte-ignore state_referenced_locally
|
|
89
|
-
const extension = defineExtension({
|
|
90
|
-
$initialEditorState: initialEditorState(form.get(field.path)),
|
|
91
|
-
dependencies: [
|
|
92
|
-
RichTextExtension,
|
|
93
|
-
HistoryExtension,
|
|
94
|
-
...(hasRichTextFeature(config, "lists") ? [ListExtension] : []),
|
|
95
|
-
],
|
|
96
|
-
name: "@riducms/plugin-richtext/editor",
|
|
97
|
-
namespace: `ridu-${field.id}`,
|
|
98
|
-
nodes: [
|
|
99
|
-
...(hasRichTextFeature(config, "links") ? [LinkNode] : []),
|
|
100
|
-
...(hasRichTextFeature(config, "code") ? [CodeNode] : []),
|
|
101
|
-
...(hasRichTextFeature(config, "horizontal-rule") ? [HorizontalRuleNode] : []),
|
|
102
|
-
...(hasRichTextFeature(config, "uploads") ? [UploadNode] : []),
|
|
103
|
-
...(hasRichTextFeature(config, "relationships") ? [RelationshipNode] : []),
|
|
104
|
-
],
|
|
105
|
-
editable: !field.admin.readOnly,
|
|
106
|
-
theme,
|
|
107
|
-
onError: (error) => {
|
|
108
|
-
throw error;
|
|
44
|
+
get issues() {
|
|
45
|
+
return field.issues;
|
|
109
46
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
47
|
+
get liveValidation() {
|
|
48
|
+
return field.liveValidation;
|
|
49
|
+
},
|
|
50
|
+
get readOnly() {
|
|
51
|
+
return field.readOnly;
|
|
52
|
+
},
|
|
53
|
+
get stale() {
|
|
54
|
+
return field.stale;
|
|
55
|
+
},
|
|
56
|
+
set: (value) => {
|
|
57
|
+
field.set(value);
|
|
58
|
+
accepted = fingerprint();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
115
61
|
</script>
|
|
116
62
|
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
label={field.admin.label}
|
|
121
|
-
required={field.required}
|
|
122
|
-
readOnly={field.admin.readOnly}
|
|
123
|
-
description={field.admin.description}
|
|
124
|
-
errors={issues.map((issue) => issue.message)}
|
|
125
|
-
class="min-w-0"
|
|
126
|
-
>
|
|
127
|
-
<div
|
|
128
|
-
class={[
|
|
129
|
-
"ridu-richtext-editor min-w-0 border-b border-transparent pb-[0.65rem] transition-colors duration-150",
|
|
130
|
-
issues.length > 0 && "border-b-destructive/22",
|
|
131
|
-
field.admin.readOnly && "pt-[1.6rem]",
|
|
132
|
-
issues.length > 0 && "has-error",
|
|
133
|
-
field.admin.readOnly && "is-read-only",
|
|
134
|
-
]}
|
|
135
|
-
aria-invalid={issues.length > 0}
|
|
136
|
-
>
|
|
137
|
-
<LexicalExtensionComposer {extension} contentEditable={null}>
|
|
138
|
-
<div class="relative" bind:this={canvasElement}>
|
|
139
|
-
<ContentEditable
|
|
140
|
-
id={field.id}
|
|
141
|
-
class={[
|
|
142
|
-
"ridu-richtext-content relative z-1 min-h-44 pt-[1.35rem] pe-0 pb-3 ps-9 text-foreground-body caret-primary outline-none focus-visible:outline-none max-[34rem]:min-h-36 max-[34rem]:pt-4 max-[34rem]:ps-8",
|
|
143
|
-
field.admin.readOnly
|
|
144
|
-
? "min-h-auto pt-0 ps-0 max-[34rem]:min-h-auto max-[34rem]:pt-0 max-[34rem]:ps-0"
|
|
145
|
-
: "border-s border-border transition-colors duration-150",
|
|
146
|
-
issues.length > 0 && !field.admin.readOnly && "border-s-destructive/72",
|
|
147
|
-
]}
|
|
148
|
-
ariaLabel={field.admin.label}
|
|
149
|
-
ariaDescribedBy={hasMessage ? messageID : undefined}
|
|
150
|
-
ariaInvalid={issues.length > 0}
|
|
151
|
-
ariaRequired={field.required}
|
|
152
|
-
aria-keyshortcuts="Alt+Shift+ArrowUp Alt+Shift+ArrowDown"
|
|
153
|
-
aria-placeholder={placeholderText}
|
|
154
|
-
>
|
|
155
|
-
{#snippet placeholder(editable)}
|
|
156
|
-
<span
|
|
157
|
-
class={[
|
|
158
|
-
"pointer-events-none absolute top-[1.35rem] start-9 z-0 text-[15.5px] leading-7 text-foreground-placeholder max-[34rem]:top-4 max-[34rem]:start-8",
|
|
159
|
-
!editable && "top-0 start-0 max-[34rem]:top-0 max-[34rem]:start-0",
|
|
160
|
-
]}
|
|
161
|
-
>
|
|
162
|
-
{editable ? placeholderText : i18n.t("plugin.richtext:editor.empty")}
|
|
163
|
-
</span>
|
|
164
|
-
{/snippet}
|
|
165
|
-
</ContentEditable>
|
|
166
|
-
</div>
|
|
167
|
-
{#if hasRichTextFeature(config, "links")}<LinkPlugin />{/if}
|
|
168
|
-
{#if hasRichTextFeature(config, "horizontal-rule")}<HorizontalRulePlugin />{/if}
|
|
169
|
-
{#if !field.admin.readOnly}
|
|
170
|
-
<RichTextSlashMenu {authoring} {config} />
|
|
171
|
-
<RichTextFloatingToolbar />
|
|
172
|
-
{#if canvasElement !== null}
|
|
173
|
-
<RichTextBlockToolbar anchorElement={canvasElement} {authoring} {config} />
|
|
174
|
-
{/if}
|
|
175
|
-
{#if hasRichTextFeature(config, "uploads")}
|
|
176
|
-
<RichTextUploadPlugin {authoring} {config} {field} />
|
|
177
|
-
{/if}
|
|
178
|
-
{#if hasRichTextFeature(config, "relationships")}
|
|
179
|
-
<RichTextRelationshipPlugin {authoring} {config} {field} />
|
|
180
|
-
{/if}
|
|
181
|
-
{/if}
|
|
182
|
-
<OnChangePlugin onChange={changed} ignoreSelectionChange />
|
|
183
|
-
<RichTextFooter />
|
|
184
|
-
</LexicalExtensionComposer>
|
|
185
|
-
</div>
|
|
186
|
-
</FieldFrame>
|
|
187
|
-
</div>
|
|
63
|
+
{#key replacement}
|
|
64
|
+
<RichTextEditor field={editorField} {form} {config} {authoring} {i18n} />
|
|
65
|
+
{/key}
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
const editor = useLexicalComposerContext()[0];
|
|
13
13
|
const isEditable = useLexicalEditable();
|
|
14
14
|
const i18n = getAdminI18n();
|
|
15
|
+
let { readOnly = false }: { readOnly?: boolean } = $props();
|
|
15
16
|
|
|
16
17
|
function focusAtEnd() {
|
|
18
|
+
if (!editor.isEditable()) return;
|
|
17
19
|
editor.focus(
|
|
18
20
|
() => {
|
|
19
21
|
editor.update(() => {
|
|
@@ -34,10 +36,11 @@
|
|
|
34
36
|
}
|
|
35
37
|
</script>
|
|
36
38
|
|
|
37
|
-
{#if
|
|
39
|
+
{#if !readOnly}
|
|
38
40
|
<button
|
|
39
|
-
class="mt-[0.55rem] inline-flex cursor-text items-center gap-[0.35rem] border-0 bg-transparent py-[0.35rem] font-mono text-[11px] leading-[1.2] text-foreground-sub transition-colors duration-150 hover:text-foreground-muted focus-visible:rounded focus-visible:text-foreground-muted focus-visible:outline-2 focus-visible:outline-ring/60 focus-visible:outline-offset-3"
|
|
41
|
+
class="mt-[0.55rem] inline-flex cursor-text items-center gap-[0.35rem] border-0 bg-transparent py-[0.35rem] font-mono text-[11px] leading-[1.2] text-foreground-sub transition-colors duration-150 hover:text-foreground-muted focus-visible:rounded focus-visible:text-foreground-muted focus-visible:outline-2 focus-visible:outline-ring/60 focus-visible:outline-offset-3 disabled:cursor-not-allowed disabled:opacity-45"
|
|
40
42
|
type="button"
|
|
43
|
+
disabled={!isEditable()}
|
|
41
44
|
onclick={focusAtEnd}
|
|
42
45
|
>
|
|
43
46
|
<span aria-hidden="true">+</span>
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAdminPlugin, definePluginField } from "@riducms/plugin/authoring/v1";
|
|
2
|
+
import { decodeRichTextDocument } from "@plugin-richtext/document-validation";
|
|
3
|
+
import { decodeRichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
2
4
|
import Prism from "prismjs";
|
|
3
5
|
|
|
4
6
|
import { richTextMessages } from "@plugin-richtext/messages";
|
|
@@ -8,25 +10,27 @@ browserGlobals.Prism ??= Prism;
|
|
|
8
10
|
|
|
9
11
|
const { default: RichTextField } = await import("@plugin-richtext/field/rich-text-field.svelte");
|
|
10
12
|
|
|
11
|
-
export const richTextFieldPlugin = defineFieldPlugin({
|
|
12
|
-
type: "plugin",
|
|
13
|
-
key: "richtext",
|
|
14
|
-
component: RichTextField,
|
|
15
|
-
canRender: (field) => field.plugin?.key === "richtext",
|
|
16
|
-
});
|
|
17
|
-
|
|
18
13
|
export const richTextAdminPlugin = defineAdminPlugin({
|
|
19
|
-
apiVersion: ADMIN_PLUGIN_API_VERSION,
|
|
20
14
|
key: "richtext",
|
|
21
15
|
pairingVersion: 1,
|
|
22
|
-
fields:
|
|
16
|
+
fields: {
|
|
17
|
+
richtext: definePluginField({
|
|
18
|
+
component: RichTextField,
|
|
19
|
+
decodeValue: decodeRichTextDocument,
|
|
20
|
+
decodeConfig: decodeRichTextConfig,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
23
|
messages: richTextMessages,
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
export type {
|
|
27
|
+
RichTextDocument,
|
|
28
|
+
RichTextDocumentInput,
|
|
29
|
+
RichTextNode,
|
|
30
|
+
RichTextRootNode,
|
|
31
|
+
RichTextBlockNode,
|
|
32
|
+
RichTextBlockRenderers,
|
|
33
|
+
} from "#richtext/document";
|
|
30
34
|
|
|
31
35
|
export { richTextMessages } from "@plugin-richtext/messages";
|
|
32
36
|
export type { RichTextConfig, RichTextFeature } from "@plugin-richtext/field/rich-text-config";
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
} from "lexical";
|
|
31
31
|
import type { Attachment } from "svelte/attachments";
|
|
32
32
|
|
|
33
|
+
import { getRichTextField } from "@plugin-richtext/field/rich-text-context.svelte";
|
|
34
|
+
import { richTextBlockTypes } from "@plugin-richtext/field/rich-text-blocks";
|
|
33
35
|
import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
36
|
+
import { OPEN_BLOCK_EDITOR_COMMAND } from "@plugin-richtext/menu/rich-text-commands";
|
|
34
37
|
import RichTextMenu from "@plugin-richtext/menu/rich-text-menu.svelte";
|
|
35
38
|
import {
|
|
36
39
|
buildRichTextOptions,
|
|
@@ -63,7 +66,13 @@
|
|
|
63
66
|
let restorePickerFocus = true;
|
|
64
67
|
// The mounted field's authoring surface and feature set are stable.
|
|
65
68
|
// svelte-ignore state_referenced_locally
|
|
66
|
-
const baseOptions = buildRichTextOptions(
|
|
69
|
+
const baseOptions = buildRichTextOptions(
|
|
70
|
+
editor,
|
|
71
|
+
config,
|
|
72
|
+
i18n,
|
|
73
|
+
authoring,
|
|
74
|
+
richTextBlockTypes(getRichTextField().field)
|
|
75
|
+
);
|
|
67
76
|
const options = $derived(filterRichTextOptions(baseOptions, query, i18n.language));
|
|
68
77
|
|
|
69
78
|
function isOnToolbar(element: HTMLElement) {
|
|
@@ -164,6 +173,13 @@
|
|
|
164
173
|
restorePickerFocus = option.restoreEditorFocus;
|
|
165
174
|
closePicker();
|
|
166
175
|
if (state === null) return;
|
|
176
|
+
if (option.blockType !== undefined) {
|
|
177
|
+
editor.dispatchCommand(OPEN_BLOCK_EDITOR_COMMAND, {
|
|
178
|
+
blockType: option.blockType,
|
|
179
|
+
position: state,
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
167
183
|
|
|
168
184
|
editor.update(() => {
|
|
169
185
|
const target = $getNodeByKey(state.targetNodeKey);
|
|
@@ -30,3 +30,14 @@ export const REMOVE_RELATIONSHIP_COMMAND: LexicalCommand<NodeKey> = createComman
|
|
|
30
30
|
|
|
31
31
|
export const UPDATE_UPLOAD_CAPTION_COMMAND: LexicalCommand<UpdateUploadCaptionPayload> =
|
|
32
32
|
createCommand("UPDATE_UPLOAD_CAPTION_COMMAND");
|
|
33
|
+
|
|
34
|
+
export const OPEN_BLOCK_EDITOR_COMMAND: LexicalCommand<{
|
|
35
|
+
blockType?: string;
|
|
36
|
+
nodeKey?: NodeKey;
|
|
37
|
+
position?: { targetNodeKey: NodeKey; insertBefore: boolean };
|
|
38
|
+
}> = createCommand("OPEN_BLOCK_EDITOR_COMMAND");
|
|
39
|
+
export const DUPLICATE_BLOCK_COMMAND: LexicalCommand<NodeKey> =
|
|
40
|
+
createCommand("DUPLICATE_BLOCK_COMMAND");
|
|
41
|
+
export const REMOVE_BLOCK_COMMAND: LexicalCommand<NodeKey> = createCommand("REMOVE_BLOCK_COMMAND");
|
|
42
|
+
export const MOVE_BLOCK_COMMAND: LexicalCommand<{ nodeKey: NodeKey; direction: -1 | 1 }> =
|
|
43
|
+
createCommand("MOVE_BLOCK_COMMAND");
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@lexical/list";
|
|
7
7
|
import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
|
|
8
8
|
import { $setBlocksType } from "@lexical/selection";
|
|
9
|
+
import type { SchemaBlockType } from "@riducms/protocol";
|
|
9
10
|
import type { AdminI18n, FieldAuthoringHost } from "@riducms/plugin";
|
|
10
11
|
import { INSERT_HORIZONTAL_RULE_COMMAND, MenuOption } from "@hvniel/lexical-svelte";
|
|
11
12
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
|
|
19
20
|
import {
|
|
20
21
|
OPEN_RELATIONSHIP_BROWSER_COMMAND,
|
|
22
|
+
OPEN_BLOCK_EDITOR_COMMAND,
|
|
21
23
|
OPEN_UPLOAD_BROWSER_COMMAND,
|
|
22
24
|
} from "@plugin-richtext/menu/rich-text-commands";
|
|
23
25
|
import { hasRichTextFeature, type RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
@@ -31,6 +33,7 @@ export class RichTextMenuOption extends MenuOption {
|
|
|
31
33
|
readonly keyHint: string;
|
|
32
34
|
readonly preservePlaceholder: boolean;
|
|
33
35
|
readonly restoreEditorFocus: boolean;
|
|
36
|
+
readonly blockType: string | undefined;
|
|
34
37
|
|
|
35
38
|
constructor(
|
|
36
39
|
key: string,
|
|
@@ -43,6 +46,7 @@ export class RichTextMenuOption extends MenuOption {
|
|
|
43
46
|
keyHint?: string;
|
|
44
47
|
preservePlaceholder?: boolean;
|
|
45
48
|
restoreEditorFocus?: boolean;
|
|
49
|
+
blockType?: string;
|
|
46
50
|
} = {}
|
|
47
51
|
) {
|
|
48
52
|
super(key);
|
|
@@ -54,6 +58,7 @@ export class RichTextMenuOption extends MenuOption {
|
|
|
54
58
|
this.keyHint = options.keyHint ?? "↵";
|
|
55
59
|
this.preservePlaceholder = options.preservePlaceholder ?? true;
|
|
56
60
|
this.restoreEditorFocus = options.restoreEditorFocus ?? true;
|
|
61
|
+
this.blockType = options.blockType;
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -61,7 +66,8 @@ export function buildRichTextOptions(
|
|
|
61
66
|
editor: LexicalEditor,
|
|
62
67
|
config: RichTextConfig,
|
|
63
68
|
i18n: AdminI18n,
|
|
64
|
-
authoring?: FieldAuthoringHost
|
|
69
|
+
authoring?: FieldAuthoringHost,
|
|
70
|
+
blockTypes: readonly SchemaBlockType[] = []
|
|
65
71
|
): RichTextMenuOption[] {
|
|
66
72
|
const options = [
|
|
67
73
|
new RichTextMenuOption(
|
|
@@ -215,6 +221,27 @@ export function buildRichTextOptions(
|
|
|
215
221
|
options.push(...relationshipOptions);
|
|
216
222
|
}
|
|
217
223
|
|
|
224
|
+
if (hasRichTextFeature(config, "blocks") && authoring?.beginSchemaDraft !== undefined) {
|
|
225
|
+
options.splice(
|
|
226
|
+
4,
|
|
227
|
+
0,
|
|
228
|
+
...blockTypes.map(
|
|
229
|
+
(type) =>
|
|
230
|
+
new RichTextMenuOption(
|
|
231
|
+
`block-${type.slug}`,
|
|
232
|
+
type.labels.singular,
|
|
233
|
+
i18n.t("plugin.richtext:block.description", { key: type.slug }),
|
|
234
|
+
"▣",
|
|
235
|
+
["block", type.slug, type.labels.singular],
|
|
236
|
+
() =>
|
|
237
|
+
queueMicrotask(() =>
|
|
238
|
+
editor.dispatchCommand(OPEN_BLOCK_EDITOR_COMMAND, { blockType: type.slug })
|
|
239
|
+
),
|
|
240
|
+
{ restoreEditorFocus: false, blockType: type.slug }
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
218
245
|
return options;
|
|
219
246
|
}
|
|
220
247
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
} from "@hvniel/lexical-svelte";
|
|
10
10
|
import { $isParagraphNode, type TextNode } from "lexical";
|
|
11
11
|
|
|
12
|
+
import { getRichTextField } from "@plugin-richtext/field/rich-text-context.svelte";
|
|
13
|
+
import { richTextBlockTypes } from "@plugin-richtext/field/rich-text-blocks";
|
|
12
14
|
import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
13
15
|
import RichTextMenu from "@plugin-richtext/menu/rich-text-menu.svelte";
|
|
14
16
|
import {
|
|
@@ -24,7 +26,13 @@
|
|
|
24
26
|
const trigger = createBasicTypeaheadTriggerMatch("/", { allowWhitespace: true, minLength: 0 });
|
|
25
27
|
// Menu options carry attachment identity and the mounted field's feature set is fixed.
|
|
26
28
|
// svelte-ignore state_referenced_locally
|
|
27
|
-
const baseOptions = buildRichTextOptions(
|
|
29
|
+
const baseOptions = buildRichTextOptions(
|
|
30
|
+
editor,
|
|
31
|
+
config,
|
|
32
|
+
i18n,
|
|
33
|
+
authoring,
|
|
34
|
+
richTextBlockTypes(getRichTextField().field)
|
|
35
|
+
);
|
|
28
36
|
let query = $state<string | null>(null);
|
|
29
37
|
let menuAnchor = $state.raw<{
|
|
30
38
|
contextElement: HTMLElement;
|
package/src/messages.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { defineAdminMessages } from "@riducms/plugin";
|
|
|
2
2
|
|
|
3
3
|
export const richTextMessages = defineAdminMessages({
|
|
4
4
|
fallback: {
|
|
5
|
+
"block.label": "{label} block",
|
|
6
|
+
"block.select": "Select {label} block",
|
|
7
|
+
"block.edit": "Edit",
|
|
8
|
+
"block.duplicate": "Duplicate",
|
|
9
|
+
"block.remove": "Remove",
|
|
10
|
+
"block.insertTitle": "Insert {label}",
|
|
11
|
+
"block.editTitle": "Edit {label}",
|
|
12
|
+
"block.description": "Structured block · {key}",
|
|
13
|
+
"block.noSummary": "No summary",
|
|
14
|
+
"block.unknown": "Unknown",
|
|
15
|
+
"block.recovery":
|
|
16
|
+
"This block type is no longer configured. Its content is preserved; export it before removing or migrating it.",
|
|
17
|
+
"block.export": "Export block JSON",
|
|
18
|
+
"block.issues": "{count} fields need attention. Edit this block to review.",
|
|
19
|
+
"block.pendingEdit": "Apply or cancel the open block before editing another.",
|
|
5
20
|
"asset.actions": "Asset actions",
|
|
6
21
|
"asset.addCaption": "Add a caption…",
|
|
7
22
|
"asset.captionFor": "Caption for {filename}",
|
|
@@ -24,6 +39,9 @@ export const richTextMessages = defineAdminMessages({
|
|
|
24
39
|
"editor.editLink": "Edit link",
|
|
25
40
|
"editor.editNamed": "Edit {name}",
|
|
26
41
|
"editor.empty": "No content",
|
|
42
|
+
"editor.recovery":
|
|
43
|
+
"Unsupported historical content at {path}. The original JSON is preserved. Export and migrate it before editing this field.",
|
|
44
|
+
"editor.exportDocument": "Export document JSON",
|
|
27
45
|
"editor.filterBlocks": "Filter blocks",
|
|
28
46
|
"editor.filterBlocksPlaceholder": "Filter blocks…",
|
|
29
47
|
"editor.footerHint": "— type / for commands · move with Alt+Shift+↑/↓",
|
|
@@ -81,6 +99,23 @@ export const richTextMessages = defineAdminMessages({
|
|
|
81
99
|
},
|
|
82
100
|
translations: {
|
|
83
101
|
fr: {
|
|
102
|
+
"block.label": "Bloc {label}",
|
|
103
|
+
"block.select": "Sélectionner le bloc {label}",
|
|
104
|
+
"block.edit": "Modifier",
|
|
105
|
+
"block.duplicate": "Dupliquer",
|
|
106
|
+
"block.remove": "Supprimer",
|
|
107
|
+
"block.insertTitle": "Insérer {label}",
|
|
108
|
+
"block.editTitle": "Modifier {label}",
|
|
109
|
+
"block.description": "Bloc structuré · {key}",
|
|
110
|
+
"block.noSummary": "Aucun résumé",
|
|
111
|
+
"block.unknown": "Inconnu",
|
|
112
|
+
"block.recovery":
|
|
113
|
+
"Ce type de bloc n’est plus configuré. Son contenu est conservé ; exportez-le avant de le supprimer ou de le migrer.",
|
|
114
|
+
"block.export": "Exporter le JSON du bloc",
|
|
115
|
+
"block.issues":
|
|
116
|
+
"{count} champs nécessitent votre attention. Modifiez ce bloc pour les vérifier.",
|
|
117
|
+
"block.pendingEdit":
|
|
118
|
+
"Appliquez ou annulez la modification en cours avant de modifier un autre bloc.",
|
|
84
119
|
"asset.actions": "Actions sur le fichier",
|
|
85
120
|
"asset.addCaption": "Ajouter une légende…",
|
|
86
121
|
"asset.captionFor": "Légende de {filename}",
|
|
@@ -103,6 +138,9 @@ export const richTextMessages = defineAdminMessages({
|
|
|
103
138
|
"editor.editLink": "Modifier le lien",
|
|
104
139
|
"editor.editNamed": "Modifier {name}",
|
|
105
140
|
"editor.empty": "Aucun contenu",
|
|
141
|
+
"editor.recovery":
|
|
142
|
+
"Contenu historique non pris en charge à {path}. Le JSON original est conservé. Exportez-le et migrez-le avant de modifier ce champ.",
|
|
143
|
+
"editor.exportDocument": "Exporter le document JSON",
|
|
106
144
|
"editor.filterBlocks": "Filtrer les blocs",
|
|
107
145
|
"editor.filterBlocksPlaceholder": "Filtrer les blocs…",
|
|
108
146
|
"editor.footerHint": "— saisir / pour les commandes · déplacer avec Alt+Maj+↑/↓",
|
|
@@ -159,6 +197,21 @@ export const richTextMessages = defineAdminMessages({
|
|
|
159
197
|
"relationship.unavailable": "Référence indisponible",
|
|
160
198
|
},
|
|
161
199
|
ar: {
|
|
200
|
+
"block.label": "كتلة {label}",
|
|
201
|
+
"block.select": "تحديد كتلة {label}",
|
|
202
|
+
"block.edit": "تعديل",
|
|
203
|
+
"block.duplicate": "تكرار",
|
|
204
|
+
"block.remove": "إزالة",
|
|
205
|
+
"block.insertTitle": "إدراج {label}",
|
|
206
|
+
"block.editTitle": "تعديل {label}",
|
|
207
|
+
"block.description": "كتلة منظمة · {key}",
|
|
208
|
+
"block.noSummary": "لا يوجد ملخص",
|
|
209
|
+
"block.unknown": "غير معروف",
|
|
210
|
+
"block.recovery":
|
|
211
|
+
"لم يعد نوع الكتلة هذا معرّفًا. تم الاحتفاظ بمحتواه؛ صدّره قبل إزالته أو ترحيله.",
|
|
212
|
+
"block.export": "تصدير JSON للكتلة",
|
|
213
|
+
"block.issues": "هناك {count} حقول تحتاج إلى مراجعة. عدّل هذه الكتلة لمراجعتها.",
|
|
214
|
+
"block.pendingEdit": "طبّق التعديل المفتوح أو ألغِه قبل تعديل كتلة أخرى.",
|
|
162
215
|
"asset.actions": "إجراءات الملف",
|
|
163
216
|
"asset.addCaption": "إضافة وصف…",
|
|
164
217
|
"asset.captionFor": "وصف {filename}",
|
|
@@ -181,6 +234,9 @@ export const richTextMessages = defineAdminMessages({
|
|
|
181
234
|
"editor.editLink": "تعديل الرابط",
|
|
182
235
|
"editor.editNamed": "تعديل {name}",
|
|
183
236
|
"editor.empty": "لا يوجد محتوى",
|
|
237
|
+
"editor.recovery":
|
|
238
|
+
"محتوى سابق غير مدعوم في {path}. تم الاحتفاظ ببيانات JSON الأصلية. صدّرها ورحّلها قبل تعديل هذا الحقل.",
|
|
239
|
+
"editor.exportDocument": "تصدير مستند JSON",
|
|
184
240
|
"editor.filterBlocks": "تصفية الكتل",
|
|
185
241
|
"editor.filterBlocksPlaceholder": "تصفية الكتل…",
|
|
186
242
|
"editor.footerHint": "— اكتب / لعرض الأوامر · انقل باستخدام Alt+Shift+↑/↓",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
|
|
3
|
+
export { default as RichText } from "#richtext/rich-text.svelte";
|
|
4
|
+
export type RichTextBlockComponents<Payload extends { blockType: string }> = {
|
|
5
|
+
[Slug in Payload["blockType"]]: Component<{ block: Extract<Payload, { blockType: Slug }> }>;
|
|
6
|
+
};
|