@riducms/plugin-richtext 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/block/rich-text-block-card.svelte +54 -4
- package/src/block/rich-text-block-name.ts +16 -0
- package/src/block/rich-text-block-node.ts +21 -0
- package/src/block/rich-text-blocks-plugin.svelte +15 -1
- package/src/field/rich-text-blocks.ts +3 -0
- package/src/field/rich-text-document.ts +5 -1
- package/src/menu/rich-text-commands.ts +14 -1
package/package.json
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"@lexical/rich-text": "0.49.0",
|
|
13
13
|
"@lexical/selection": "0.49.0",
|
|
14
14
|
"@lexical/utils": "0.49.0",
|
|
15
|
-
"@riducms/plugin": "0.2.
|
|
16
|
-
"@riducms/protocol": "0.2.
|
|
17
|
-
"@riducms/ui": "0.2.
|
|
15
|
+
"@riducms/plugin": "0.2.2",
|
|
16
|
+
"@riducms/protocol": "0.2.2",
|
|
17
|
+
"@riducms/ui": "0.2.2",
|
|
18
18
|
"@types/prismjs": "^1.26.5",
|
|
19
19
|
"lexical": "0.49.0",
|
|
20
20
|
"prismjs": "^1.30.0",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"description": "Svelte and Lexical admin integration for Ridu's official rich-text plugin.",
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@riducms/build": "0.2.
|
|
25
|
+
"@riducms/build": "0.2.2"
|
|
26
26
|
},
|
|
27
27
|
"exports": {
|
|
28
28
|
".": "./src/index.ts",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"test": "bun test"
|
|
60
60
|
},
|
|
61
61
|
"type": "module",
|
|
62
|
-
"version": "0.2.
|
|
62
|
+
"version": "0.2.2"
|
|
63
63
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
useLexicalEditable,
|
|
5
5
|
useLexicalNodeSelection,
|
|
6
6
|
} from "@hvniel/lexical-svelte";
|
|
7
|
-
import { $getNodeByKey, type NodeKey } from "lexical";
|
|
7
|
+
import { $getNodeByKey, REDO_COMMAND, UNDO_COMMAND, type NodeKey } from "lexical";
|
|
8
8
|
import { getAdminI18n } from "@riducms/plugin";
|
|
9
9
|
import { Button } from "@riducms/ui";
|
|
10
10
|
import {
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
getRichTextAuthoringHost,
|
|
13
13
|
} from "@plugin-richtext/field/rich-text-context.svelte";
|
|
14
14
|
import { blockSummary, richTextBlockTypes } from "@plugin-richtext/field/rich-text-blocks";
|
|
15
|
+
import { BlockNameHistorySession } from "@plugin-richtext/block/rich-text-block-name";
|
|
15
16
|
import {
|
|
16
17
|
OPEN_BLOCK_EDITOR_COMMAND,
|
|
17
18
|
DUPLICATE_BLOCK_COMMAND,
|
|
18
19
|
REMOVE_BLOCK_COMMAND,
|
|
19
20
|
MOVE_BLOCK_COMMAND,
|
|
21
|
+
UPDATE_BLOCK_NAME_COMMAND,
|
|
20
22
|
} from "@plugin-richtext/menu/rich-text-commands";
|
|
21
23
|
|
|
22
24
|
let {
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
const editable = useLexicalEditable();
|
|
30
32
|
const context = getRichTextField();
|
|
31
33
|
const authoring = getRichTextAuthoringHost();
|
|
34
|
+
const nameHistory = new BlockNameHistorySession();
|
|
32
35
|
// The decorator instance owns this fixed Lexical node key.
|
|
33
36
|
// svelte-ignore state_referenced_locally
|
|
34
37
|
const [selected, setSelected, clearSelected] = useLexicalNodeSelection(nodeKey);
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
const type = $derived(
|
|
42
45
|
richTextBlockTypes(context.field).find((candidate) => candidate.slug === blockType)
|
|
43
46
|
);
|
|
47
|
+
const nameField = $derived(type?.admin?.nameField);
|
|
44
48
|
const issues = $derived(authoring?.schemaIssues?.({ treeKey: "blocks", identity }) ?? []);
|
|
45
49
|
const recovery = $derived(type === undefined || !validEnvelope);
|
|
46
50
|
const summary = $derived(blockSummary(fields, type) || i18n.t("plugin.richtext:block.noSummary"));
|
|
@@ -49,6 +53,34 @@
|
|
|
49
53
|
if (!event.shiftKey) clearSelected();
|
|
50
54
|
setSelected(true);
|
|
51
55
|
}
|
|
56
|
+
function changeName(change: { field: string; value: string }) {
|
|
57
|
+
if (nameField === undefined || recovery || !editable()) return;
|
|
58
|
+
editor.dispatchCommand(UPDATE_BLOCK_NAME_COMMAND, {
|
|
59
|
+
nodeKey,
|
|
60
|
+
identity,
|
|
61
|
+
nameField,
|
|
62
|
+
change,
|
|
63
|
+
historyTag: nameHistory.nextTag(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function nameKeydown(event: KeyboardEvent) {
|
|
67
|
+
if (!(event.target instanceof HTMLInputElement)) return;
|
|
68
|
+
event.stopPropagation();
|
|
69
|
+
if (event.isComposing) return;
|
|
70
|
+
if (event.key === "Enter") {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (event.target.readOnly || event.target.disabled) return;
|
|
75
|
+
const key = event.key.toLowerCase();
|
|
76
|
+
const modifier = event.metaKey || event.ctrlKey;
|
|
77
|
+
const undo = modifier && key === "z" && !event.shiftKey;
|
|
78
|
+
const redo = modifier && ((key === "z" && event.shiftKey) || (key === "y" && !event.metaKey));
|
|
79
|
+
if (!undo && !redo) return;
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
nameHistory.reset();
|
|
82
|
+
editor.dispatchCommand(undo ? UNDO_COMMAND : REDO_COMMAND, undefined);
|
|
83
|
+
}
|
|
52
84
|
function edit() {
|
|
53
85
|
editor.dispatchCommand(OPEN_BLOCK_EDITOR_COMMAND, { nodeKey });
|
|
54
86
|
}
|
|
@@ -102,6 +134,7 @@
|
|
|
102
134
|
<article
|
|
103
135
|
class={[
|
|
104
136
|
"ridu-richtext-embedded-card my-5 rounded-md border border-control-border bg-control p-4 outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
137
|
+
nameField !== undefined && "whitespace-normal",
|
|
105
138
|
selected() && "border-primary",
|
|
106
139
|
issues.length > 0 && "border-destructive",
|
|
107
140
|
]}
|
|
@@ -111,11 +144,26 @@
|
|
|
111
144
|
data-invalid={issues.length > 0}
|
|
112
145
|
role="group"
|
|
113
146
|
>
|
|
114
|
-
|
|
147
|
+
{#if nameField !== undefined && authoring?.schemaHeader !== undefined}
|
|
148
|
+
<div
|
|
149
|
+
class="mb-2"
|
|
150
|
+
onfocuscapture={nameHistory.reset.bind(nameHistory)}
|
|
151
|
+
onblurcapture={nameHistory.reset.bind(nameHistory)}
|
|
152
|
+
onkeydowncapture={nameKeydown}
|
|
153
|
+
>
|
|
154
|
+
{@render authoring.schemaHeader({
|
|
155
|
+
treeKey: "blocks",
|
|
156
|
+
identity,
|
|
157
|
+
readOnly: !editable() || recovery,
|
|
158
|
+
onChange: changeName,
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
115
163
|
<button
|
|
116
164
|
type="button"
|
|
117
165
|
data-block-select
|
|
118
|
-
class="min-w-
|
|
166
|
+
class="min-w-24 flex-1 text-start rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
119
167
|
aria-label={i18n.t("plugin.richtext:block.select", {
|
|
120
168
|
label: type?.labels.singular ?? blockType,
|
|
121
169
|
})}
|
|
@@ -126,7 +174,9 @@
|
|
|
126
174
|
<span class="block text-xs font-medium text-foreground-muted">
|
|
127
175
|
{type?.labels.singular ?? blockType}
|
|
128
176
|
</span>
|
|
129
|
-
<span class="block mt-1 truncate text-sm text-foreground">
|
|
177
|
+
{#if nameField === undefined}<span class="block mt-1 truncate text-sm text-foreground">
|
|
178
|
+
{summary}
|
|
179
|
+
</span>{/if}
|
|
130
180
|
</button>
|
|
131
181
|
{#if editable()}
|
|
132
182
|
<div class="flex shrink-0 gap-1">
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HISTORY_MERGE_TAG, HISTORY_PUSH_TAG } from "lexical";
|
|
2
|
+
|
|
3
|
+
/** Groups one focused name-input session into one Lexical undo entry. */
|
|
4
|
+
export class BlockNameHistorySession {
|
|
5
|
+
#merge = false;
|
|
6
|
+
|
|
7
|
+
reset() {
|
|
8
|
+
this.#merge = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nextTag(): typeof HISTORY_PUSH_TAG | typeof HISTORY_MERGE_TAG {
|
|
12
|
+
const tag = this.#merge ? HISTORY_MERGE_TAG : HISTORY_PUSH_TAG;
|
|
13
|
+
this.#merge = true;
|
|
14
|
+
return tag;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { Decorator } from "@hvniel/lexical-svelte";
|
|
|
2
2
|
import {
|
|
3
3
|
$applyNodeReplacement,
|
|
4
4
|
$getDocument,
|
|
5
|
+
$getNodeByKey,
|
|
5
6
|
DecoratorNode,
|
|
6
7
|
type LexicalNode,
|
|
7
8
|
type NodeKey,
|
|
@@ -82,3 +83,23 @@ export function createBlockNode(fields: Record<string, unknown>): BlockNode {
|
|
|
82
83
|
export function isBlockNode(node: LexicalNode | null | undefined): node is BlockNode {
|
|
83
84
|
return node instanceof BlockNode;
|
|
84
85
|
}
|
|
86
|
+
|
|
87
|
+
export interface BlockNameUpdate {
|
|
88
|
+
nodeKey: NodeKey;
|
|
89
|
+
identity: string;
|
|
90
|
+
nameField: string;
|
|
91
|
+
change: { field: string; value: string };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// @lexical-scope
|
|
95
|
+
export function updateBlockName(update: BlockNameUpdate): boolean {
|
|
96
|
+
const node = $getNodeByKey(update.nodeKey);
|
|
97
|
+
if (
|
|
98
|
+
!isBlockNode(node) ||
|
|
99
|
+
node.getFields()._key !== update.identity ||
|
|
100
|
+
update.change.field !== update.nameField
|
|
101
|
+
)
|
|
102
|
+
return false;
|
|
103
|
+
node.setFields({ ...node.getFields(), [update.nameField]: update.change.value });
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
@@ -33,13 +33,18 @@
|
|
|
33
33
|
type BaseSelection,
|
|
34
34
|
type NodeKey,
|
|
35
35
|
} from "lexical";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
createBlockNode,
|
|
38
|
+
isBlockNode,
|
|
39
|
+
updateBlockName,
|
|
40
|
+
} from "@plugin-richtext/block/rich-text-block-node";
|
|
37
41
|
import { richTextBlockTypes } from "@plugin-richtext/field/rich-text-blocks";
|
|
38
42
|
import {
|
|
39
43
|
OPEN_BLOCK_EDITOR_COMMAND,
|
|
40
44
|
DUPLICATE_BLOCK_COMMAND,
|
|
41
45
|
REMOVE_BLOCK_COMMAND,
|
|
42
46
|
MOVE_BLOCK_COMMAND,
|
|
47
|
+
UPDATE_BLOCK_NAME_COMMAND,
|
|
43
48
|
} from "@plugin-richtext/menu/rich-text-commands";
|
|
44
49
|
|
|
45
50
|
let { authoring, field }: { authoring: FieldAuthoringHost | undefined; field: SchemaField } =
|
|
@@ -284,6 +289,15 @@
|
|
|
284
289
|
},
|
|
285
290
|
COMMAND_PRIORITY_EDITOR
|
|
286
291
|
),
|
|
292
|
+
editor.registerCommand(
|
|
293
|
+
UPDATE_BLOCK_NAME_COMMAND,
|
|
294
|
+
(update) => {
|
|
295
|
+
if (!editor.isEditable() || !updateBlockName(update)) return false;
|
|
296
|
+
$addUpdateTag(update.historyTag);
|
|
297
|
+
return true;
|
|
298
|
+
},
|
|
299
|
+
COMMAND_PRIORITY_EDITOR
|
|
300
|
+
),
|
|
287
301
|
...[KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND].map((command) =>
|
|
288
302
|
editor.registerCommand(
|
|
289
303
|
command,
|
|
@@ -17,6 +17,9 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function blockSummary(fields: Record<string, unknown>, type?: SchemaBlockType): string {
|
|
20
|
+
// Named cards render their guarded header instead. Never mirror an editorial name
|
|
21
|
+
// into plugin-owned text, because the configured field may be unreadable.
|
|
22
|
+
if (type?.admin?.nameField !== undefined) return "";
|
|
20
23
|
const summary = type?.admin?.rowLabel;
|
|
21
24
|
if (summary !== undefined) {
|
|
22
25
|
const value = summary
|
|
@@ -25,10 +25,14 @@ export function initialEditorState(value: unknown) {
|
|
|
25
25
|
|
|
26
26
|
function withElementDefaults(node: Record<string, unknown>): Record<string, unknown> {
|
|
27
27
|
if (!Array.isArray(node.children)) return node;
|
|
28
|
+
const children =
|
|
29
|
+
node.type === "root" && node.children.length === 0
|
|
30
|
+
? [{ type: "paragraph", children: [], direction: null, format: "", indent: 0 }]
|
|
31
|
+
: node.children.map((child) => (isRecord(child) ? withElementDefaults(child) : child));
|
|
28
32
|
|
|
29
33
|
return {
|
|
30
34
|
...node,
|
|
31
|
-
children
|
|
35
|
+
children,
|
|
32
36
|
direction: node.direction ?? null,
|
|
33
37
|
format: node.format ?? "",
|
|
34
38
|
indent: node.indent ?? 0,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createCommand,
|
|
3
|
+
HISTORY_MERGE_TAG,
|
|
4
|
+
HISTORY_PUSH_TAG,
|
|
5
|
+
type LexicalCommand,
|
|
6
|
+
type NodeKey,
|
|
7
|
+
} from "lexical";
|
|
2
8
|
|
|
3
9
|
export interface OpenUploadBrowserPayload {
|
|
4
10
|
collectionSlug: string;
|
|
@@ -41,3 +47,10 @@ export const DUPLICATE_BLOCK_COMMAND: LexicalCommand<NodeKey> =
|
|
|
41
47
|
export const REMOVE_BLOCK_COMMAND: LexicalCommand<NodeKey> = createCommand("REMOVE_BLOCK_COMMAND");
|
|
42
48
|
export const MOVE_BLOCK_COMMAND: LexicalCommand<{ nodeKey: NodeKey; direction: -1 | 1 }> =
|
|
43
49
|
createCommand("MOVE_BLOCK_COMMAND");
|
|
50
|
+
export const UPDATE_BLOCK_NAME_COMMAND: LexicalCommand<{
|
|
51
|
+
nodeKey: NodeKey;
|
|
52
|
+
identity: string;
|
|
53
|
+
nameField: string;
|
|
54
|
+
change: { field: string; value: string };
|
|
55
|
+
historyTag: typeof HISTORY_PUSH_TAG | typeof HISTORY_MERGE_TAG;
|
|
56
|
+
}> = createCommand("UPDATE_BLOCK_NAME_COMMAND");
|