@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
|
@@ -1,69 +1,101 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { $createHeadingNode, $createQuoteNode, type HeadingTagType } from "@lexical/rich-text";
|
|
3
|
+
import { $setBlocksType } from "@lexical/selection";
|
|
4
|
+
import {
|
|
5
|
+
INSERT_ORDERED_LIST_COMMAND,
|
|
6
|
+
INSERT_UNORDERED_LIST_COMMAND,
|
|
7
|
+
INSERT_CHECK_LIST_COMMAND,
|
|
8
|
+
} from "@lexical/list";
|
|
9
|
+
import { hasRichTextFeature, type RichTextConfig } from "@plugin-richtext/field/rich-text-config";
|
|
10
|
+
import ChevronDownIcon from "~icons/lucide/chevron-down";
|
|
2
11
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
3
|
-
import { mergeRegister } from "@lexical/utils";
|
|
12
|
+
import { $findMatchingParent, mergeRegister } from "@lexical/utils";
|
|
4
13
|
import { getAdminI18n } from "@riducms/plugin";
|
|
5
14
|
import {
|
|
6
|
-
Button,
|
|
7
|
-
Input,
|
|
8
|
-
PopoverContent,
|
|
9
|
-
PopoverRoot,
|
|
10
|
-
PopoverTrigger,
|
|
11
15
|
ToolbarButton,
|
|
12
16
|
ToolbarRoot,
|
|
17
|
+
MenuRoot,
|
|
18
|
+
MenuTrigger,
|
|
19
|
+
MenuContent,
|
|
20
|
+
MenuItem,
|
|
13
21
|
} from "@riducms/ui";
|
|
14
22
|
import { Portal, useLexicalComposerContext, useLexicalEditable } from "@hvniel/lexical-svelte";
|
|
23
|
+
import type { Attachment } from "svelte/attachments";
|
|
15
24
|
import {
|
|
16
25
|
$getSelection,
|
|
26
|
+
$createParagraphNode,
|
|
27
|
+
$isElementNode,
|
|
28
|
+
$isTextNode,
|
|
29
|
+
FORMAT_ELEMENT_COMMAND,
|
|
30
|
+
INDENT_CONTENT_COMMAND,
|
|
31
|
+
OUTDENT_CONTENT_COMMAND,
|
|
32
|
+
type ElementFormatType,
|
|
33
|
+
type ElementNode,
|
|
17
34
|
$isRangeSelection,
|
|
18
35
|
COMMAND_PRIORITY_LOW,
|
|
36
|
+
KEY_TAB_COMMAND,
|
|
19
37
|
SELECTION_CHANGE_COMMAND,
|
|
20
38
|
type TextFormatType,
|
|
21
39
|
} from "lexical";
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
import
|
|
40
|
+
import "@plugin-richtext/toolbar/rich-text-toolbar.scss";
|
|
41
|
+
import { OPEN_LINK_EDITOR_COMMAND } from "@plugin-richtext/menu/rich-text-commands";
|
|
42
|
+
import { $isHeadingNode, $isQuoteNode } from "@lexical/rich-text";
|
|
43
|
+
import { $isListNode } from "@lexical/list";
|
|
44
|
+
import ToolbarIcon, { type RichTextIconName } from "@plugin-richtext/toolbar/toolbar-icon.svelte";
|
|
25
45
|
|
|
46
|
+
let { config }: { config: RichTextConfig } = $props();
|
|
26
47
|
const editor = useLexicalComposerContext()[0];
|
|
27
48
|
const isEditable = useLexicalEditable();
|
|
28
49
|
const i18n = getAdminI18n();
|
|
29
|
-
const formatButtons
|
|
30
|
-
format:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}[] = [
|
|
34
|
-
{ format: "bold", icon: "bold", label: i18n.t("plugin.richtext:editor.bold") },
|
|
35
|
-
{ format: "italic", icon: "italic", label: i18n.t("plugin.richtext:editor.italic") },
|
|
36
|
-
{ format: "underline", icon: "underline", label: i18n.t("plugin.richtext:editor.underline") },
|
|
50
|
+
const formatButtons = [
|
|
51
|
+
{ format: "bold", icon: "bold", labelKey: "plugin.richtext:editor.bold" },
|
|
52
|
+
{ format: "italic", icon: "italic", labelKey: "plugin.richtext:editor.italic" },
|
|
53
|
+
{ format: "underline", icon: "underline", labelKey: "plugin.richtext:editor.underline" },
|
|
37
54
|
{
|
|
38
55
|
format: "strikethrough",
|
|
39
56
|
icon: "strike",
|
|
40
|
-
|
|
57
|
+
labelKey: "plugin.richtext:editor.strikethrough",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
format: "subscript",
|
|
61
|
+
icon: "subscript",
|
|
62
|
+
labelKey: "plugin.richtext:editor.subscript",
|
|
41
63
|
},
|
|
42
|
-
{ format: "subscript", icon: "subscript", label: i18n.t("plugin.richtext:editor.subscript") },
|
|
43
64
|
{
|
|
44
65
|
format: "superscript",
|
|
45
66
|
icon: "superscript",
|
|
46
|
-
|
|
67
|
+
labelKey: "plugin.richtext:editor.superscript",
|
|
47
68
|
},
|
|
48
|
-
{ format: "code", icon: "code",
|
|
49
|
-
];
|
|
50
|
-
const toolbarButtonClass =
|
|
51
|
-
|
|
69
|
+
{ format: "code", icon: "code", labelKey: "plugin.richtext:editor.inlineCode" },
|
|
70
|
+
] as const;
|
|
71
|
+
const toolbarButtonClass = "ridu-richtext-toolbar-button";
|
|
72
|
+
let textMenuOpen = $state(false);
|
|
73
|
+
let alignMenuOpen = $state(false);
|
|
74
|
+
let canOutdent = $state(false);
|
|
52
75
|
let visible = $state(false);
|
|
53
76
|
let formats = $state.raw<Set<TextFormatType>>(new Set());
|
|
54
77
|
let link = $state(false);
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
let linkDraft = $state("");
|
|
78
|
+
let blockType = $state<RichTextIconName>("paragraph");
|
|
79
|
+
let alignment = $state<ElementFormatType>("left");
|
|
58
80
|
let left = $state(0);
|
|
59
81
|
let top = $state(0);
|
|
60
|
-
let toolbar
|
|
61
|
-
let
|
|
62
|
-
let
|
|
82
|
+
let toolbar: HTMLElement | null = null;
|
|
83
|
+
let restoreMenuFocus = true;
|
|
84
|
+
let selectingPointer: number | undefined;
|
|
85
|
+
function isOwnEditableNode(node: EventTarget | null, root: HTMLElement) {
|
|
86
|
+
const element =
|
|
87
|
+
node instanceof Element ? node : node instanceof Node ? node.parentElement : null;
|
|
88
|
+
return element?.closest("[contenteditable]") === root;
|
|
89
|
+
}
|
|
63
90
|
|
|
64
91
|
// @lexical-scope
|
|
65
92
|
function $updateToolbar() {
|
|
66
|
-
|
|
93
|
+
// Keep the portal out of the pointer path until the native selection gesture ends.
|
|
94
|
+
if (selectingPointer !== undefined) {
|
|
95
|
+
visible = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (textMenuOpen || alignMenuOpen) return;
|
|
67
99
|
const selection = $getSelection();
|
|
68
100
|
const domSelection = editor._window?.getSelection() ?? window.getSelection();
|
|
69
101
|
const root = editor.getRootElement();
|
|
@@ -76,21 +108,51 @@
|
|
|
76
108
|
domSelection === null ||
|
|
77
109
|
domSelection.rangeCount === 0 ||
|
|
78
110
|
root === null ||
|
|
79
|
-
!
|
|
111
|
+
!isOwnEditableNode(domSelection.anchorNode, root) ||
|
|
112
|
+
(!root.contains(document.activeElement) && !toolbar?.contains(document.activeElement))
|
|
80
113
|
) {
|
|
81
114
|
visible = false;
|
|
82
115
|
return;
|
|
83
116
|
}
|
|
84
117
|
|
|
118
|
+
const block = $findMatchingParent(
|
|
119
|
+
selection.anchor.getNode(),
|
|
120
|
+
(node): node is ElementNode => $isElementNode(node) && !node.isInline()
|
|
121
|
+
);
|
|
122
|
+
canOutdent = (block?.getIndent() ?? 0) > 0;
|
|
123
|
+
alignment = block?.getFormatType() || "left";
|
|
124
|
+
const list = $findMatchingParent(selection.anchor.getNode(), $isListNode);
|
|
125
|
+
blockType = $isHeadingNode(block)
|
|
126
|
+
? block.getTag()
|
|
127
|
+
: $isQuoteNode(block)
|
|
128
|
+
? "quote"
|
|
129
|
+
: $isListNode(list)
|
|
130
|
+
? list.getListType() === "number"
|
|
131
|
+
? "ordered"
|
|
132
|
+
: list.getListType() === "check"
|
|
133
|
+
? "check"
|
|
134
|
+
: "unordered"
|
|
135
|
+
: "paragraph";
|
|
85
136
|
formats = new Set(
|
|
86
137
|
formatButtons.map(({ format }) => format).filter((format) => selection.hasFormat(format))
|
|
87
138
|
);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
139
|
+
const [start, end] = selection.isBackward()
|
|
140
|
+
? [selection.focus, selection.anchor]
|
|
141
|
+
: [selection.anchor, selection.focus];
|
|
142
|
+
// Boundary nodes can be included without contributing any selected characters.
|
|
143
|
+
const selectedText = selection
|
|
144
|
+
.getNodes()
|
|
145
|
+
.filter($isTextNode)
|
|
146
|
+
.filter((node) => {
|
|
147
|
+
const size = node.getTextContentSize();
|
|
148
|
+
if (size === 0) return false;
|
|
149
|
+
if (start.type === "text" && start.key === node.getKey() && start.offset === size)
|
|
150
|
+
return false;
|
|
151
|
+
return !(end.type === "text" && end.key === node.getKey() && end.offset === 0);
|
|
152
|
+
});
|
|
153
|
+
link =
|
|
154
|
+
selectedText.length > 0 &&
|
|
155
|
+
selectedText.every((node) => $findMatchingParent(node, $isLinkNode) !== null);
|
|
94
156
|
visible = true;
|
|
95
157
|
positionToolbar(domSelection.getRangeAt(0).getBoundingClientRect());
|
|
96
158
|
}
|
|
@@ -105,15 +167,52 @@
|
|
|
105
167
|
selectionRect.left + selectionRect.width / 2 - width / 2
|
|
106
168
|
)
|
|
107
169
|
);
|
|
108
|
-
const preferredTop = selectionRect.
|
|
170
|
+
const preferredTop = selectionRect.bottom + 6;
|
|
109
171
|
const maximumTop = window.innerHeight - height - 12;
|
|
110
|
-
top = Math.max(12,
|
|
172
|
+
top = Math.max(12, preferredTop <= maximumTop ? preferredTop : selectionRect.top - height - 6);
|
|
111
173
|
}
|
|
112
174
|
|
|
113
175
|
function updateFromEditor() {
|
|
114
176
|
editor.getEditorState().read($updateToolbar, { editor });
|
|
115
177
|
}
|
|
116
178
|
|
|
179
|
+
const attachToolbar: Attachment<HTMLElement> = (element) => {
|
|
180
|
+
toolbar = element;
|
|
181
|
+
updateFromEditor();
|
|
182
|
+
return () => {
|
|
183
|
+
if (toolbar === element) toolbar = null;
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function focusToolbar(event: KeyboardEvent) {
|
|
188
|
+
if (event.shiftKey || !visible || toolbar === null) return false;
|
|
189
|
+
const firstControl = toolbar.querySelector<HTMLElement>(
|
|
190
|
+
"button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex='-1'])"
|
|
191
|
+
);
|
|
192
|
+
if (firstControl === null) return false;
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
firstControl.focus();
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function beginPointerSelection(event: PointerEvent) {
|
|
199
|
+
const root = editor.getRootElement();
|
|
200
|
+
if (event.button !== 0 || root === null || !isOwnEditableNode(event.target, root)) return;
|
|
201
|
+
selectingPointer = event.pointerId;
|
|
202
|
+
visible = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function endPointerSelection(event: PointerEvent) {
|
|
206
|
+
if (event.pointerId !== selectingPointer) return;
|
|
207
|
+
selectingPointer = undefined;
|
|
208
|
+
updateFromEditor();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resetPointerSelection() {
|
|
212
|
+
selectingPointer = undefined;
|
|
213
|
+
updateFromEditor();
|
|
214
|
+
}
|
|
215
|
+
|
|
117
216
|
function preserveSelection(event: MouseEvent) {
|
|
118
217
|
if (event.target instanceof HTMLInputElement) return;
|
|
119
218
|
event.preventDefault();
|
|
@@ -128,67 +227,15 @@
|
|
|
128
227
|
queueMicrotask(updateFromEditor);
|
|
129
228
|
}
|
|
130
229
|
|
|
131
|
-
async function openLinkEditor() {
|
|
132
|
-
linkDraft = linkURL || "https://";
|
|
133
|
-
restoreLinkFocus = true;
|
|
134
|
-
linkOpen = true;
|
|
135
|
-
await tick();
|
|
136
|
-
window.setTimeout(() => {
|
|
137
|
-
linkInput?.focus();
|
|
138
|
-
linkInput?.select();
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function handleLinkOpenChange(open: boolean) {
|
|
143
|
-
if (open) await openLinkEditor();
|
|
144
|
-
else closeLinkEditor();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function sanitizeURL(value: string) {
|
|
148
|
-
const trimmed = value.trim();
|
|
149
|
-
if (/^(?:https?:|mailto:|tel:)/i.test(trimmed)) return trimmed;
|
|
150
|
-
return `https://${trimmed}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function applyLink() {
|
|
154
|
-
if (linkDraft.trim() === "" || linkDraft === "https://") return;
|
|
155
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeURL(linkDraft));
|
|
156
|
-
closeLinkEditor();
|
|
157
|
-
queueMicrotask(() => editor.focus());
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function removeLink() {
|
|
161
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
162
|
-
closeLinkEditor();
|
|
163
|
-
queueMicrotask(() => editor.focus());
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function closeLinkEditor() {
|
|
167
|
-
linkOpen = false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function handleLinkKeydown(event: KeyboardEvent) {
|
|
171
|
-
if (event.key === "Enter") {
|
|
172
|
-
event.preventDefault();
|
|
173
|
-
applyLink();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function focusLinkInput(event: Event) {
|
|
178
|
-
event.preventDefault();
|
|
179
|
-
linkInput?.focus();
|
|
180
|
-
linkInput?.select();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
230
|
function restoreEditorFocus(event: Event) {
|
|
184
231
|
event.preventDefault();
|
|
185
|
-
const shouldRestore =
|
|
186
|
-
|
|
232
|
+
const shouldRestore = restoreMenuFocus;
|
|
233
|
+
restoreMenuFocus = true;
|
|
187
234
|
if (shouldRestore) queueMicrotask(() => editor.focus());
|
|
188
235
|
}
|
|
189
236
|
|
|
190
237
|
function preserveOutsideFocus(event: Event) {
|
|
191
|
-
|
|
238
|
+
restoreMenuFocus = false;
|
|
192
239
|
const target =
|
|
193
240
|
event.target instanceof Element
|
|
194
241
|
? event.target.closest<HTMLElement>(
|
|
@@ -198,17 +245,53 @@
|
|
|
198
245
|
if (target !== null) window.setTimeout(() => target.focus());
|
|
199
246
|
}
|
|
200
247
|
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
248
|
+
function changeBlock(type: string) {
|
|
249
|
+
if (!editor.isEditable()) return;
|
|
250
|
+
editor.update(() => {
|
|
251
|
+
const selection = $getSelection();
|
|
252
|
+
if (!$isRangeSelection(selection)) return;
|
|
253
|
+
if (type === "ordered") editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
254
|
+
else if (type === "unordered")
|
|
255
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
256
|
+
else if (type === "check") editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);
|
|
257
|
+
else
|
|
258
|
+
$setBlocksType(selection, () =>
|
|
259
|
+
type === "paragraph"
|
|
260
|
+
? $createParagraphNode()
|
|
261
|
+
: type === "quote"
|
|
262
|
+
? $createQuoteNode()
|
|
263
|
+
: $createHeadingNode(type as HeadingTagType)
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
textMenuOpen = false;
|
|
267
|
+
editor.focus();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function alignText(format: ElementFormatType) {
|
|
271
|
+
if (!editor.isEditable()) return;
|
|
272
|
+
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, format);
|
|
273
|
+
alignMenuOpen = false;
|
|
274
|
+
editor.focus();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function toggleLink() {
|
|
278
|
+
if (!editor.isEditable()) return;
|
|
279
|
+
if (link) editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
280
|
+
else editor.dispatchCommand(OPEN_LINK_EDITOR_COMMAND, undefined);
|
|
281
|
+
}
|
|
205
282
|
|
|
206
283
|
$effect(() => {
|
|
284
|
+
document.addEventListener("pointerdown", beginPointerSelection, true);
|
|
285
|
+
document.addEventListener("pointerup", endPointerSelection, true);
|
|
286
|
+
document.addEventListener("pointercancel", endPointerSelection, true);
|
|
207
287
|
document.addEventListener("selectionchange", updateFromEditor);
|
|
288
|
+
document.addEventListener("focusin", updateFromEditor);
|
|
208
289
|
document.addEventListener("scroll", updateFromEditor, true);
|
|
290
|
+
window.addEventListener("blur", resetPointerSelection);
|
|
209
291
|
window.addEventListener("resize", updateFromEditor);
|
|
210
292
|
const unregister = mergeRegister(
|
|
211
293
|
editor.registerUpdateListener(({ editorState }) => editorState.read($updateToolbar)),
|
|
294
|
+
editor.registerCommand(KEY_TAB_COMMAND, focusToolbar, COMMAND_PRIORITY_LOW),
|
|
212
295
|
editor.registerCommand(
|
|
213
296
|
SELECTION_CHANGE_COMMAND,
|
|
214
297
|
() => {
|
|
@@ -220,8 +303,13 @@
|
|
|
220
303
|
);
|
|
221
304
|
return () => {
|
|
222
305
|
unregister();
|
|
306
|
+
document.removeEventListener("pointerdown", beginPointerSelection, true);
|
|
307
|
+
document.removeEventListener("pointerup", endPointerSelection, true);
|
|
308
|
+
document.removeEventListener("pointercancel", endPointerSelection, true);
|
|
223
309
|
document.removeEventListener("selectionchange", updateFromEditor);
|
|
310
|
+
document.removeEventListener("focusin", updateFromEditor);
|
|
224
311
|
document.removeEventListener("scroll", updateFromEditor, true);
|
|
312
|
+
window.removeEventListener("blur", resetPointerSelection);
|
|
225
313
|
window.removeEventListener("resize", updateFromEditor);
|
|
226
314
|
};
|
|
227
315
|
});
|
|
@@ -230,93 +318,167 @@
|
|
|
230
318
|
{#if visible}
|
|
231
319
|
<Portal>
|
|
232
320
|
<ToolbarRoot
|
|
233
|
-
|
|
234
|
-
class="
|
|
321
|
+
{@attach attachToolbar}
|
|
322
|
+
class="ridu-richtext-floating-toolbar"
|
|
235
323
|
aria-label={i18n.t("plugin.richtext:editor.formatText")}
|
|
236
324
|
tabindex={-1}
|
|
237
325
|
style={`left: ${left}px; top: ${top}px;`}
|
|
238
326
|
onmousedown={preserveSelection}
|
|
239
327
|
>
|
|
240
|
-
{
|
|
241
|
-
<
|
|
242
|
-
class={toolbarButtonClass}
|
|
243
|
-
type="button"
|
|
244
|
-
active={formats.has(button.format)}
|
|
245
|
-
aria-label={button.label}
|
|
246
|
-
aria-pressed={formats.has(button.format)}
|
|
247
|
-
onclick={() => formatText(button.format)}
|
|
248
|
-
>
|
|
249
|
-
<ToolbarIcon name={button.icon} />
|
|
250
|
-
</ToolbarButton>
|
|
251
|
-
{/each}
|
|
252
|
-
<span class="mx-[0.15rem] h-[1.15rem] w-px bg-border" aria-hidden="true"></span>
|
|
253
|
-
<PopoverRoot open={linkOpen} onOpenChange={handleLinkOpenChange}>
|
|
254
|
-
<PopoverTrigger>
|
|
328
|
+
<MenuRoot bind:open={textMenuOpen}>
|
|
329
|
+
<MenuTrigger>
|
|
255
330
|
{#snippet child({ props })}
|
|
256
331
|
<ToolbarButton
|
|
257
332
|
{...props}
|
|
258
|
-
class=
|
|
259
|
-
|
|
260
|
-
active={link}
|
|
261
|
-
aria-label={i18n.t(
|
|
262
|
-
link ? "plugin.richtext:editor.editLink" : "plugin.richtext:editor.addLink"
|
|
263
|
-
)}
|
|
264
|
-
aria-pressed={link}
|
|
333
|
+
class="ridu-richtext-toolbar-button ridu-richtext-toolbar-menu"
|
|
334
|
+
aria-label={i18n.t("plugin.richtext:editor.textStyle")}
|
|
265
335
|
>
|
|
266
|
-
<ToolbarIcon name="
|
|
336
|
+
<ToolbarIcon name={blockType} /><ChevronDownIcon width="10" />
|
|
267
337
|
</ToolbarButton>
|
|
268
338
|
{/snippet}
|
|
269
|
-
</
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
role="dialog"
|
|
274
|
-
aria-label={i18n.t("plugin.richtext:editor.editLink")}
|
|
275
|
-
trapFocus
|
|
276
|
-
side="bottom"
|
|
339
|
+
</MenuTrigger>
|
|
340
|
+
<MenuContent
|
|
341
|
+
class="ridu-richtext-format-menu"
|
|
342
|
+
sideOffset={0}
|
|
277
343
|
align="start"
|
|
278
|
-
onmousedown={preserveSelection}
|
|
279
|
-
onOpenAutoFocus={focusLinkInput}
|
|
280
344
|
onCloseAutoFocus={restoreEditorFocus}
|
|
281
345
|
onInteractOutside={preserveOutsideFocus}
|
|
282
346
|
>
|
|
283
|
-
<
|
|
284
|
-
class="
|
|
285
|
-
|
|
347
|
+
<MenuItem
|
|
348
|
+
class="ridu-richtext-format-option"
|
|
349
|
+
data-active={blockType === "paragraph" || undefined}
|
|
350
|
+
onSelect={() => changeBlock("paragraph")}
|
|
286
351
|
>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
onkeydown={handleLinkKeydown}
|
|
296
|
-
/>
|
|
297
|
-
<div class="flex justify-end gap-[0.35rem]">
|
|
298
|
-
{#if link}<Button
|
|
299
|
-
variant="ghost"
|
|
300
|
-
size="sm"
|
|
301
|
-
class="me-auto h-7 px-[0.55rem] text-[11px] text-destructive hover:text-foreground"
|
|
302
|
-
onclick={removeLink}
|
|
303
|
-
>
|
|
304
|
-
{i18n.t("plugin.richtext:editor.remove")}
|
|
305
|
-
</Button>{/if}
|
|
306
|
-
<Button
|
|
307
|
-
variant="ghost"
|
|
308
|
-
size="sm"
|
|
309
|
-
class="h-7 px-[0.55rem] text-[11px]"
|
|
310
|
-
onclick={closeLinkEditor}
|
|
352
|
+
<ToolbarIcon name="paragraph" />
|
|
353
|
+
{i18n.t("plugin.richtext:option.paragraph.label")}
|
|
354
|
+
</MenuItem>
|
|
355
|
+
{#each [1, 2, 3, 4, 5, 6] as level (level)}
|
|
356
|
+
<MenuItem
|
|
357
|
+
class="ridu-richtext-format-option"
|
|
358
|
+
data-active={blockType === `h${level}` || undefined}
|
|
359
|
+
onSelect={() => changeBlock(`h${level}`)}
|
|
311
360
|
>
|
|
312
|
-
{
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
361
|
+
<ToolbarIcon name={`h${level}` as RichTextIconName} />
|
|
362
|
+
{i18n.t("plugin.richtext:editor.heading", { level })}
|
|
363
|
+
</MenuItem>
|
|
364
|
+
{/each}
|
|
365
|
+
{#if hasRichTextFeature(config, "lists")}
|
|
366
|
+
<MenuItem
|
|
367
|
+
class="ridu-richtext-format-option"
|
|
368
|
+
data-active={blockType === "ordered" || undefined}
|
|
369
|
+
onSelect={() => changeBlock("ordered")}
|
|
370
|
+
>
|
|
371
|
+
<ToolbarIcon name="ordered" />
|
|
372
|
+
{i18n.t("plugin.richtext:option.numberedList.label")}
|
|
373
|
+
</MenuItem>
|
|
374
|
+
<MenuItem
|
|
375
|
+
class="ridu-richtext-format-option"
|
|
376
|
+
data-active={blockType === "unordered" || undefined}
|
|
377
|
+
onSelect={() => changeBlock("unordered")}
|
|
378
|
+
>
|
|
379
|
+
<ToolbarIcon name="unordered" />
|
|
380
|
+
{i18n.t("plugin.richtext:option.bulletedList.label")}
|
|
381
|
+
</MenuItem>
|
|
382
|
+
<MenuItem
|
|
383
|
+
class="ridu-richtext-format-option"
|
|
384
|
+
data-active={blockType === "check" || undefined}
|
|
385
|
+
onSelect={() => changeBlock("check")}
|
|
386
|
+
>
|
|
387
|
+
<ToolbarIcon name="check" />
|
|
388
|
+
{i18n.t("plugin.richtext:option.checklist.label")}
|
|
389
|
+
</MenuItem>
|
|
390
|
+
{/if}
|
|
391
|
+
<MenuItem
|
|
392
|
+
class="ridu-richtext-format-option"
|
|
393
|
+
data-active={blockType === "quote" || undefined}
|
|
394
|
+
onSelect={() => changeBlock("quote")}
|
|
395
|
+
>
|
|
396
|
+
<ToolbarIcon name="quote" />
|
|
397
|
+
{i18n.t("plugin.richtext:option.quote.label")}
|
|
398
|
+
</MenuItem>
|
|
399
|
+
</MenuContent>
|
|
400
|
+
</MenuRoot>
|
|
401
|
+
<span class="ridu-richtext-toolbar-separator" aria-hidden="true"></span>
|
|
402
|
+
<MenuRoot bind:open={alignMenuOpen}>
|
|
403
|
+
<MenuTrigger>
|
|
404
|
+
{#snippet child({ props })}
|
|
405
|
+
<ToolbarButton
|
|
406
|
+
{...props}
|
|
407
|
+
class="ridu-richtext-toolbar-button ridu-richtext-toolbar-menu"
|
|
408
|
+
aria-label={i18n.t("plugin.richtext:editor.alignment")}
|
|
409
|
+
>
|
|
410
|
+
<ToolbarIcon
|
|
411
|
+
name={alignment === "center" || alignment === "right" || alignment === "justify"
|
|
412
|
+
? alignment
|
|
413
|
+
: "left"}
|
|
414
|
+
/><ChevronDownIcon width="10" />
|
|
415
|
+
</ToolbarButton>
|
|
416
|
+
{/snippet}
|
|
417
|
+
</MenuTrigger>
|
|
418
|
+
<MenuContent
|
|
419
|
+
class="ridu-richtext-format-menu"
|
|
420
|
+
sideOffset={0}
|
|
421
|
+
align="start"
|
|
422
|
+
onCloseAutoFocus={restoreEditorFocus}
|
|
423
|
+
onInteractOutside={preserveOutsideFocus}
|
|
424
|
+
>
|
|
425
|
+
{#each ["left", "center", "right", "justify"] as format (format)}
|
|
426
|
+
<MenuItem
|
|
427
|
+
class="ridu-richtext-format-option"
|
|
428
|
+
data-active={alignment === format || undefined}
|
|
429
|
+
onSelect={() => alignText(format as ElementFormatType)}
|
|
430
|
+
>
|
|
431
|
+
<ToolbarIcon name={format as RichTextIconName} />
|
|
432
|
+
{i18n.t(`plugin.richtext:editor.align.${format}`)}
|
|
433
|
+
</MenuItem>
|
|
434
|
+
{/each}
|
|
435
|
+
</MenuContent>
|
|
436
|
+
</MenuRoot>
|
|
437
|
+
<ToolbarButton
|
|
438
|
+
class={toolbarButtonClass}
|
|
439
|
+
disabled={!canOutdent}
|
|
440
|
+
aria-label={i18n.t("plugin.richtext:editor.outdent")}
|
|
441
|
+
onclick={() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)}
|
|
442
|
+
>
|
|
443
|
+
<ToolbarIcon name="outdent" />
|
|
444
|
+
</ToolbarButton>
|
|
445
|
+
<ToolbarButton
|
|
446
|
+
class={toolbarButtonClass}
|
|
447
|
+
aria-label={i18n.t("plugin.richtext:editor.indent")}
|
|
448
|
+
onclick={() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)}
|
|
449
|
+
>
|
|
450
|
+
<ToolbarIcon name="indent" />
|
|
451
|
+
</ToolbarButton>
|
|
452
|
+
<span class="ridu-richtext-toolbar-separator" aria-hidden="true"></span>
|
|
453
|
+
|
|
454
|
+
{#each formatButtons.filter((button) => button.format !== "code" || hasRichTextFeature(config, "code")) as button (button.format)}
|
|
455
|
+
<ToolbarButton
|
|
456
|
+
class={toolbarButtonClass}
|
|
457
|
+
type="button"
|
|
458
|
+
active={formats.has(button.format)}
|
|
459
|
+
aria-label={i18n.t(button.labelKey)}
|
|
460
|
+
aria-pressed={formats.has(button.format)}
|
|
461
|
+
title={i18n.t(button.labelKey)}
|
|
462
|
+
onclick={() => formatText(button.format)}
|
|
463
|
+
>
|
|
464
|
+
<ToolbarIcon name={button.icon} />
|
|
465
|
+
</ToolbarButton>
|
|
466
|
+
{/each}
|
|
467
|
+
<span class="ridu-richtext-toolbar-separator" aria-hidden="true"></span>
|
|
468
|
+
{#if hasRichTextFeature(config, "links")}
|
|
469
|
+
<ToolbarButton
|
|
470
|
+
class={toolbarButtonClass}
|
|
471
|
+
active={link}
|
|
472
|
+
aria-label={i18n.t(
|
|
473
|
+
link ? "plugin.richtext:editor.removeLink" : "plugin.richtext:editor.addLink"
|
|
474
|
+
)}
|
|
475
|
+
aria-pressed={link}
|
|
476
|
+
aria-haspopup={link ? undefined : "dialog"}
|
|
477
|
+
onclick={toggleLink}
|
|
478
|
+
>
|
|
479
|
+
<ToolbarIcon name="link" />
|
|
480
|
+
</ToolbarButton>
|
|
481
|
+
{/if}
|
|
320
482
|
</ToolbarRoot>
|
|
321
483
|
</Portal>
|
|
322
484
|
{/if}
|