@riducms/plugin-richtext 0.1.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/LICENSE +21 -0
- package/README.md +26 -0
- package/package.json +52 -0
- package/src/field/rich-text-config.ts +47 -0
- package/src/field/rich-text-context.svelte.ts +8 -0
- package/src/field/rich-text-document.ts +20 -0
- package/src/field/rich-text-field.svelte +187 -0
- package/src/field/rich-text-footer.svelte +49 -0
- package/src/index.ts +33 -0
- package/src/menu/rich-text-block-toolbar.svelte +323 -0
- package/src/menu/rich-text-commands.ts +32 -0
- package/src/menu/rich-text-menu.svelte +107 -0
- package/src/menu/rich-text-options.ts +244 -0
- package/src/menu/rich-text-slash-menu.svelte +124 -0
- package/src/messages.ts +240 -0
- package/src/relationship/rich-text-relationship-node.svelte +141 -0
- package/src/relationship/rich-text-relationship-node.ts +105 -0
- package/src/relationship/rich-text-relationship-plugin.svelte +174 -0
- package/src/styles/integration.css +83 -0
- package/src/toolbar/rich-text-floating-toolbar.svelte +322 -0
- package/src/toolbar/toolbar-icon.svelte +47 -0
- package/src/upload/rich-text-upload-node.svelte +282 -0
- package/src/upload/rich-text-upload-node.ts +144 -0
- package/src/upload/rich-text-upload-plugin.svelte +183 -0
- package/src/vite-env.d.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haniel Ubogu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# `@riducms/plugin-richtext`
|
|
2
|
+
|
|
3
|
+
The Svelte/Lexical editor for Ridu's Go rich-text plugin. It exports the admin field registration and
|
|
4
|
+
authoring UI used by generated projects.
|
|
5
|
+
|
|
6
|
+
Add the Go and admin packages together. In an existing project:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm run ridu -- add richtext \
|
|
10
|
+
--go-package github.com/riducms/ridu/plugins/richtext \
|
|
11
|
+
--admin-package @riducms/plugin-richtext
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Add `richtext.New()` to `Config.Plugins` and author fields with `richtext.Field`. The generated admin
|
|
15
|
+
registry imports `richTextAdminPlugin`; do not register it again. Generation rejects incompatible Go
|
|
16
|
+
and npm pairing metadata before the editor loads.
|
|
17
|
+
|
|
18
|
+
Run `npm run dev`, verify the editor, then create and review the adapter migration before deployment:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm run ridu -- migrate create --name add-rich-text
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
See the [Rich text guide](../../website/src/content/docs/rich-text.md) for new- and
|
|
25
|
+
existing-project setup, feature selection, uploads/relationships, portable values, constraints,
|
|
26
|
+
safe Go rendering, migrations, and verification.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bugs": {
|
|
3
|
+
"url": "https://github.com/riducms/ridu/issues"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@hvniel/lexical-svelte": "^0.1.1",
|
|
7
|
+
"@lexical/code": "0.49.0",
|
|
8
|
+
"@lexical/history": "0.49.0",
|
|
9
|
+
"@lexical/link": "0.49.0",
|
|
10
|
+
"@lexical/list": "0.49.0",
|
|
11
|
+
"@lexical/rich-text": "0.49.0",
|
|
12
|
+
"@lexical/selection": "0.49.0",
|
|
13
|
+
"@lexical/utils": "0.49.0",
|
|
14
|
+
"@riducms/plugin": "0.1.0",
|
|
15
|
+
"@riducms/protocol": "0.1.0",
|
|
16
|
+
"@riducms/ui": "0.1.0",
|
|
17
|
+
"@types/prismjs": "^1.26.5",
|
|
18
|
+
"lexical": "0.49.0",
|
|
19
|
+
"prismjs": "^1.30.0",
|
|
20
|
+
"svelte": "^5.56.8"
|
|
21
|
+
},
|
|
22
|
+
"description": "Svelte and Lexical admin integration for Ridu's official rich-text plugin.",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@riducms/build": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://riducms.com",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"name": "@riducms/plugin-richtext",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"directory": "packages/plugin-richtext",
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/riducms/ridu.git"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "vite build",
|
|
47
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
48
|
+
"test": "bun test"
|
|
49
|
+
},
|
|
50
|
+
"type": "module",
|
|
51
|
+
"version": "0.1.0"
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type RichTextFeature =
|
|
2
|
+
"links" | "lists" | "code" | "horizontal-rule" | "uploads" | "relationships" | "blocks";
|
|
3
|
+
|
|
4
|
+
export interface RichTextConfig {
|
|
5
|
+
features: readonly RichTextFeature[];
|
|
6
|
+
uploadCollections: readonly string[];
|
|
7
|
+
relationshipCollections: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const features = new Set<RichTextFeature>([
|
|
11
|
+
"links",
|
|
12
|
+
"lists",
|
|
13
|
+
"code",
|
|
14
|
+
"horizontal-rule",
|
|
15
|
+
"uploads",
|
|
16
|
+
"relationships",
|
|
17
|
+
"blocks",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function parseRichTextConfig(value: unknown): RichTextConfig {
|
|
21
|
+
if (!isRecord(value)) return { features: [], uploadCollections: [], relationshipCollections: [] };
|
|
22
|
+
return {
|
|
23
|
+
features: Array.isArray(value.features) ? value.features.filter(isRichTextFeature) : [],
|
|
24
|
+
uploadCollections: Array.isArray(value.uploadCollections)
|
|
25
|
+
? value.uploadCollections.filter(
|
|
26
|
+
(collection): collection is string => typeof collection === "string"
|
|
27
|
+
)
|
|
28
|
+
: [],
|
|
29
|
+
relationshipCollections: Array.isArray(value.relationshipCollections)
|
|
30
|
+
? value.relationshipCollections.filter(
|
|
31
|
+
(collection): collection is string => typeof collection === "string"
|
|
32
|
+
)
|
|
33
|
+
: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hasRichTextFeature(config: RichTextConfig, feature: RichTextFeature): boolean {
|
|
38
|
+
return config.features.includes(feature);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isRichTextFeature(value: unknown): value is RichTextFeature {
|
|
42
|
+
return typeof value === "string" && features.has(value as RichTextFeature);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FieldAuthoringHost } from "@riducms/plugin";
|
|
2
|
+
import { createContext } from "svelte";
|
|
3
|
+
|
|
4
|
+
const [getRichTextAuthoringHost, setRichTextAuthoringHost] = createContext<
|
|
5
|
+
FieldAuthoringHost | undefined
|
|
6
|
+
>();
|
|
7
|
+
|
|
8
|
+
export { getRichTextAuthoringHost, setRichTextAuthoringHost };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function initialEditorState(value: unknown) {
|
|
2
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.root)) return null;
|
|
3
|
+
return JSON.stringify({ root: withElementDefaults(value.root) });
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function withElementDefaults(node: Record<string, unknown>): Record<string, unknown> {
|
|
7
|
+
if (!Array.isArray(node.children)) return node;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
...node,
|
|
11
|
+
children: node.children.map((child) => (isRecord(child) ? withElementDefaults(child) : child)),
|
|
12
|
+
direction: node.direction ?? null,
|
|
13
|
+
format: node.format ?? "",
|
|
14
|
+
indent: node.indent ?? 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldComponentProps } from "@riducms/plugin";
|
|
3
|
+
import { CodeNode } from "@lexical/code";
|
|
4
|
+
import { HistoryExtension } from "@lexical/history";
|
|
5
|
+
import { LinkNode } from "@lexical/link";
|
|
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";
|
|
19
|
+
|
|
20
|
+
import "@plugin-richtext/styles/integration.css";
|
|
21
|
+
import RichTextBlockToolbar from "@plugin-richtext/menu/rich-text-block-toolbar.svelte";
|
|
22
|
+
import RichTextFooter from "@plugin-richtext/field/rich-text-footer.svelte";
|
|
23
|
+
import RichTextFloatingToolbar from "@plugin-richtext/toolbar/rich-text-floating-toolbar.svelte";
|
|
24
|
+
import RichTextSlashMenu from "@plugin-richtext/menu/rich-text-slash-menu.svelte";
|
|
25
|
+
import { parseRichTextConfig, hasRichTextFeature } from "@plugin-richtext/field/rich-text-config";
|
|
26
|
+
import { setRichTextAuthoringHost } from "@plugin-richtext/field/rich-text-context.svelte";
|
|
27
|
+
import { initialEditorState } from "@plugin-richtext/field/rich-text-document";
|
|
28
|
+
import { UploadNode } from "@plugin-richtext/upload/rich-text-upload-node";
|
|
29
|
+
import RichTextUploadPlugin from "@plugin-richtext/upload/rich-text-upload-plugin.svelte";
|
|
30
|
+
import { RelationshipNode } from "@plugin-richtext/relationship/rich-text-relationship-node";
|
|
31
|
+
import RichTextRelationshipPlugin from "@plugin-richtext/relationship/rich-text-relationship-plugin.svelte";
|
|
32
|
+
|
|
33
|
+
let { field, form, authoring, i18n }: FieldComponentProps = $props();
|
|
34
|
+
const issues = $derived(form.issuesFor(field.path));
|
|
35
|
+
const messageID = $derived(`${field.id}-message`);
|
|
36
|
+
const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
|
|
37
|
+
const placeholderText = $derived(i18n.t("plugin.richtext:editor.placeholder"));
|
|
38
|
+
let canvasElement = $state<HTMLElement | null>(null);
|
|
39
|
+
// Field identity and plugin config are fixed for this mounted component.
|
|
40
|
+
// svelte-ignore state_referenced_locally
|
|
41
|
+
const config = parseRichTextConfig(field.plugin?.config);
|
|
42
|
+
// The host is stable for this mounted field and decorator descendants inherit it through portals.
|
|
43
|
+
// svelte-ignore state_referenced_locally
|
|
44
|
+
setRichTextAuthoringHost(authoring);
|
|
45
|
+
|
|
46
|
+
const theme = {
|
|
47
|
+
heading: {
|
|
48
|
+
h1: "ridu-richtext-heading ridu-richtext-h1 mt-[2.1rem] mb-[0.85rem] font-serif text-[2rem] leading-[1.12] tracking-[-0.015em] text-foreground font-normal text-balance max-[34rem]:text-[1.75rem]",
|
|
49
|
+
h2: "ridu-richtext-heading ridu-richtext-h2 mt-[2.1rem] mb-[0.85rem] font-serif text-[1.75rem] leading-[1.12] text-foreground font-normal text-balance",
|
|
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",
|
|
51
|
+
},
|
|
52
|
+
code: "ridu-richtext-code-block my-6 block overflow-x-auto rounded-[3px] border border-control-border bg-control px-[1.1rem] py-4 font-mono text-[12.5px] leading-[1.7] text-foreground-strong [tab-size:2]",
|
|
53
|
+
hr: "ridu-richtext-rule my-8 block cursor-pointer border-0 border-t border-border py-[0.65rem]",
|
|
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",
|
|
65
|
+
},
|
|
66
|
+
paragraph:
|
|
67
|
+
"ridu-richtext-paragraph relative mb-[0.9rem] text-[15.5px] leading-7 text-foreground-body text-pretty last:mb-0",
|
|
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]",
|
|
78
|
+
},
|
|
79
|
+
upload: "ridu-richtext-upload m-0",
|
|
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;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function changed(editorState: EditorState) {
|
|
113
|
+
form.set(field.path, { version: 1, root: editorState.toJSON().root });
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<div data-field-path={field.path}>
|
|
118
|
+
<FieldFrame
|
|
119
|
+
controlID={field.id}
|
|
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>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getAdminI18n } from "@riducms/plugin";
|
|
3
|
+
import { useLexicalComposerContext, useLexicalEditable } from "@hvniel/lexical-svelte";
|
|
4
|
+
import {
|
|
5
|
+
$createParagraphNode,
|
|
6
|
+
$getRoot,
|
|
7
|
+
$getSelection,
|
|
8
|
+
$isParagraphNode,
|
|
9
|
+
$isRangeSelection,
|
|
10
|
+
} from "lexical";
|
|
11
|
+
|
|
12
|
+
const editor = useLexicalComposerContext()[0];
|
|
13
|
+
const isEditable = useLexicalEditable();
|
|
14
|
+
const i18n = getAdminI18n();
|
|
15
|
+
|
|
16
|
+
function focusAtEnd() {
|
|
17
|
+
editor.focus(
|
|
18
|
+
() => {
|
|
19
|
+
editor.update(() => {
|
|
20
|
+
const root = $getRoot();
|
|
21
|
+
const last = root.getLastChild();
|
|
22
|
+
const paragraph =
|
|
23
|
+
$isParagraphNode(last) && last.getTextContentSize() === 0
|
|
24
|
+
? last
|
|
25
|
+
: $createParagraphNode();
|
|
26
|
+
if (paragraph.getParent() === null) root.append(paragraph);
|
|
27
|
+
paragraph.selectEnd();
|
|
28
|
+
const selection = $getSelection();
|
|
29
|
+
if ($isRangeSelection(selection)) selection.insertText("/");
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
{ defaultSelection: "rootEnd" }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
{#if isEditable()}
|
|
38
|
+
<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"
|
|
40
|
+
type="button"
|
|
41
|
+
onclick={focusAtEnd}
|
|
42
|
+
>
|
|
43
|
+
<span aria-hidden="true">+</span>
|
|
44
|
+
{i18n.t("plugin.richtext:editor.addBlock")}
|
|
45
|
+
<span class="text-foreground-sub max-[34rem]:hidden">
|
|
46
|
+
{i18n.t("plugin.richtext:editor.footerHint")}
|
|
47
|
+
</span>
|
|
48
|
+
</button>
|
|
49
|
+
{/if}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ADMIN_PLUGIN_API_VERSION, defineAdminPlugin, defineFieldPlugin } from "@riducms/plugin";
|
|
2
|
+
import Prism from "prismjs";
|
|
3
|
+
|
|
4
|
+
import { richTextMessages } from "@plugin-richtext/messages";
|
|
5
|
+
|
|
6
|
+
const browserGlobals = globalThis as typeof globalThis & { Prism?: typeof Prism };
|
|
7
|
+
browserGlobals.Prism ??= Prism;
|
|
8
|
+
|
|
9
|
+
const { default: RichTextField } = await import("@plugin-richtext/field/rich-text-field.svelte");
|
|
10
|
+
|
|
11
|
+
export const richTextFieldPlugin = defineFieldPlugin({
|
|
12
|
+
type: "plugin",
|
|
13
|
+
key: "richtext",
|
|
14
|
+
component: RichTextField,
|
|
15
|
+
canRender: (field) => field.plugin?.key === "richtext",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const richTextAdminPlugin = defineAdminPlugin({
|
|
19
|
+
apiVersion: ADMIN_PLUGIN_API_VERSION,
|
|
20
|
+
key: "richtext",
|
|
21
|
+
pairingVersion: 1,
|
|
22
|
+
fields: [richTextFieldPlugin],
|
|
23
|
+
messages: richTextMessages,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export interface RichTextDocument {
|
|
27
|
+
version: 1;
|
|
28
|
+
root: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { richTextMessages } from "@plugin-richtext/messages";
|
|
32
|
+
export type { RichTextConfig, RichTextFeature } from "@plugin-richtext/field/rich-text-config";
|
|
33
|
+
export type { SerializedUploadNode } from "@plugin-richtext/upload/rich-text-upload-node";
|