@riducms/plugin-richtext 0.2.3 → 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
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
TooltipRoot,
|
|
12
12
|
TooltipTrigger,
|
|
13
13
|
} from "@riducms/ui";
|
|
14
|
-
import {
|
|
14
|
+
import { Portal, useLexicalComposerContext } from "@hvniel/lexical-svelte";
|
|
15
15
|
import { mergeRegister } from "@lexical/utils";
|
|
16
16
|
import {
|
|
17
17
|
$createParagraphNode,
|
|
18
|
-
$createTextNode,
|
|
19
18
|
$getNearestNodeFromDOMNode,
|
|
20
19
|
$getNodeByKey,
|
|
21
20
|
$getRoot,
|
|
@@ -25,6 +24,7 @@
|
|
|
25
24
|
COMMAND_PRIORITY_LOW,
|
|
26
25
|
KEY_ARROW_DOWN_COMMAND,
|
|
27
26
|
KEY_ARROW_UP_COMMAND,
|
|
27
|
+
IS_FIREFOX,
|
|
28
28
|
type LexicalNode,
|
|
29
29
|
type NodeKey,
|
|
30
30
|
} from "lexical";
|
|
@@ -46,6 +46,13 @@
|
|
|
46
46
|
targetNodeKey: NodeKey;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
type DropTarget = {
|
|
50
|
+
element: HTMLElement;
|
|
51
|
+
before: boolean;
|
|
52
|
+
empty: boolean;
|
|
53
|
+
rect: DOMRect;
|
|
54
|
+
};
|
|
55
|
+
|
|
49
56
|
let {
|
|
50
57
|
anchorElement,
|
|
51
58
|
authoring,
|
|
@@ -60,8 +67,15 @@
|
|
|
60
67
|
let draggableElement = $state.raw<HTMLElement | null>(null);
|
|
61
68
|
let pickerState = $state.raw<PickerState | null>(null);
|
|
62
69
|
let pickerOpen = $state(false);
|
|
70
|
+
const pickerID = $props.id();
|
|
63
71
|
let query = $state("");
|
|
64
72
|
let menuElement = $state.raw<HTMLElement | null>(null);
|
|
73
|
+
let targetLineElement: HTMLElement | null = null;
|
|
74
|
+
let highlightElement: HTMLElement | null = null;
|
|
75
|
+
let highlightAnimation: Animation | undefined;
|
|
76
|
+
let highlightFrame: number | undefined;
|
|
77
|
+
let draggedNodeKey: NodeKey | null = null;
|
|
78
|
+
let focusFrame: number | undefined;
|
|
65
79
|
let movementAnnouncement = $state("");
|
|
66
80
|
let restorePickerFocus = true;
|
|
67
81
|
// The mounted field's authoring surface and feature set are stable.
|
|
@@ -74,11 +88,276 @@
|
|
|
74
88
|
richTextBlockTypes(getRichTextField().field)
|
|
75
89
|
);
|
|
76
90
|
const options = $derived(filterRichTextOptions(baseOptions, query, i18n.language));
|
|
91
|
+
const dragDataFormat = "application/x-ridu-richtext-block";
|
|
77
92
|
|
|
78
93
|
function isOnToolbar(element: HTMLElement) {
|
|
79
94
|
return element.closest(".ridu-richtext-block-toolbar") !== null;
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
function hideTargetLine() {
|
|
98
|
+
if (targetLineElement === null) return;
|
|
99
|
+
targetLineElement.style.opacity = "0";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleDragStart(event: DragEvent) {
|
|
103
|
+
if (draggableElement === null || event.dataTransfer === null) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
editor.read(() => {
|
|
109
|
+
draggedNodeKey = $getNearestNodeFromDOMNode(draggableElement!)?.getKey() ?? null;
|
|
110
|
+
});
|
|
111
|
+
if (draggedNodeKey === null) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
event.dataTransfer.effectAllowed = "move";
|
|
116
|
+
event.dataTransfer.setData(dragDataFormat, draggedNodeKey);
|
|
117
|
+
event.dataTransfer.setDragImage(draggableElement, 0, 0);
|
|
118
|
+
if (IS_FIREFOX) editor.getRootElement()?.focus({ preventScroll: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleDragEnd() {
|
|
122
|
+
if (draggedNodeKey === null) return;
|
|
123
|
+
draggedNodeKey = null;
|
|
124
|
+
draggableElement = null;
|
|
125
|
+
hideTargetLine();
|
|
126
|
+
const view = anchorElement.ownerDocument.defaultView;
|
|
127
|
+
if (view === null) return;
|
|
128
|
+
if (focusFrame !== undefined) view.cancelAnimationFrame(focusFrame);
|
|
129
|
+
// Native drag completion can move focus after the drop handler has run.
|
|
130
|
+
focusFrame = view.requestAnimationFrame(() => {
|
|
131
|
+
focusFrame = undefined;
|
|
132
|
+
editor.focus();
|
|
133
|
+
const root = editor.getRootElement();
|
|
134
|
+
if (root !== null && root.ownerDocument.activeElement !== root) {
|
|
135
|
+
root.focus({ preventScroll: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function withinEditorBuffer(
|
|
141
|
+
clientX: number,
|
|
142
|
+
clientY: number,
|
|
143
|
+
horizontalBuffer: number,
|
|
144
|
+
verticalBuffer: number
|
|
145
|
+
) {
|
|
146
|
+
const rect = anchorElement.getBoundingClientRect();
|
|
147
|
+
return (
|
|
148
|
+
clientX >= rect.left - horizontalBuffer &&
|
|
149
|
+
clientX <= rect.right + horizontalBuffer &&
|
|
150
|
+
clientY >= rect.top - verticalBuffer &&
|
|
151
|
+
clientY <= rect.bottom + verticalBuffer
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isFromAnotherEditor(target: EventTarget | null) {
|
|
156
|
+
const element =
|
|
157
|
+
target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
158
|
+
return element?.closest(".ridu-richtext-canvas") !== anchorElement;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isOwnedDropSurface(target: EventTarget | null) {
|
|
162
|
+
return target instanceof Node && anchorElement.contains(target) && !isFromAnotherEditor(target);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function findNearestBlock(clientY: number) {
|
|
166
|
+
const keys = editor.read(() => $getRoot().getChildrenKeys());
|
|
167
|
+
const blocks = keys
|
|
168
|
+
.map((key) => editor.getElementByKey(key))
|
|
169
|
+
.filter((element): element is HTMLElement => element !== null);
|
|
170
|
+
if (blocks.length === 0) return null;
|
|
171
|
+
|
|
172
|
+
let low = 0;
|
|
173
|
+
let high = blocks.length;
|
|
174
|
+
while (low < high) {
|
|
175
|
+
const middle = (low + high) >>> 1;
|
|
176
|
+
const rect = blocks[middle].getBoundingClientRect();
|
|
177
|
+
if (rect.bottom < clientY) low = middle + 1;
|
|
178
|
+
else high = middle;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const next = blocks[Math.min(low, blocks.length - 1)];
|
|
182
|
+
const previous = blocks[Math.max(0, low - 1)];
|
|
183
|
+
const nextRect = next.getBoundingClientRect();
|
|
184
|
+
const previousRect = previous.getBoundingClientRect();
|
|
185
|
+
const nextDistance = Math.max(nextRect.top - clientY, clientY - nextRect.bottom, 0);
|
|
186
|
+
const previousDistance = Math.max(previousRect.top - clientY, clientY - previousRect.bottom, 0);
|
|
187
|
+
return previousDistance < nextDistance ? previous : next;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findHoveredBlock(target: EventTarget | null, clientY: number) {
|
|
191
|
+
const root = editor.getRootElement();
|
|
192
|
+
if (root === null) return null;
|
|
193
|
+
if (target instanceof Element && root.contains(target)) {
|
|
194
|
+
let element: Element | null = target;
|
|
195
|
+
while (element !== null && element.parentElement !== root) element = element.parentElement;
|
|
196
|
+
if (element instanceof HTMLElement) {
|
|
197
|
+
const key = editor.read(() => {
|
|
198
|
+
const node = $getNearestNodeFromDOMNode(element!);
|
|
199
|
+
return node?.getParent()?.is($getRoot()) ? node.getKey() : null;
|
|
200
|
+
});
|
|
201
|
+
if (key !== null && editor.getElementByKey(key) === element) return element;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const block = findNearestBlock(clientY);
|
|
206
|
+
if (block === null) return null;
|
|
207
|
+
const rect = block.getBoundingClientRect();
|
|
208
|
+
const style = block.ownerDocument.defaultView?.getComputedStyle(block);
|
|
209
|
+
const marginTop = Number.parseFloat(style?.marginTop ?? "") || 0;
|
|
210
|
+
const marginBottom = Number.parseFloat(style?.marginBottom ?? "") || 0;
|
|
211
|
+
return clientY >= rect.top - marginTop && clientY <= rect.bottom + marginBottom ? block : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function findDropTarget(clientY: number) {
|
|
215
|
+
const element = findNearestBlock(clientY);
|
|
216
|
+
if (element === null) return null;
|
|
217
|
+
const rect = element.getBoundingClientRect();
|
|
218
|
+
const empty = editor.read(() => {
|
|
219
|
+
const node = $getNearestNodeFromDOMNode(element);
|
|
220
|
+
return $isParagraphNode(node) && node.isEmpty();
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
element,
|
|
224
|
+
before: clientY < rect.top + rect.height / 2,
|
|
225
|
+
empty,
|
|
226
|
+
rect,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// @lexical-scope
|
|
231
|
+
function $dropWouldMove(source: LexicalNode, target: LexicalNode, drop: DropTarget) {
|
|
232
|
+
if (source.is(target)) return false;
|
|
233
|
+
if (drop.empty) return true;
|
|
234
|
+
return drop.before
|
|
235
|
+
? !target.getPreviousSibling()?.is(source)
|
|
236
|
+
: !target.getNextSibling()?.is(source);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function positionTargetLine(target: DropTarget) {
|
|
240
|
+
if (targetLineElement === null) return;
|
|
241
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
242
|
+
const view = anchorElement.ownerDocument.defaultView;
|
|
243
|
+
if (view === null) return;
|
|
244
|
+
const margin = (element: Element | null, edge: "marginTop" | "marginBottom") =>
|
|
245
|
+
element === null ? 0 : Number.parseFloat(view.getComputedStyle(element)[edge]) || 0;
|
|
246
|
+
const gap = target.before
|
|
247
|
+
? Math.max(
|
|
248
|
+
margin(target.element, "marginTop"),
|
|
249
|
+
margin(target.element.previousElementSibling, "marginBottom")
|
|
250
|
+
)
|
|
251
|
+
: Math.max(
|
|
252
|
+
margin(target.element, "marginBottom"),
|
|
253
|
+
margin(target.element.nextElementSibling, "marginTop")
|
|
254
|
+
);
|
|
255
|
+
const top = target.empty
|
|
256
|
+
? target.rect.top + target.rect.height / 2
|
|
257
|
+
: target.before
|
|
258
|
+
? target.rect.top - gap / 2
|
|
259
|
+
: target.rect.bottom + gap / 2;
|
|
260
|
+
targetLineElement.style.width = `${target.rect.width}px`;
|
|
261
|
+
targetLineElement.style.transform = `translate(${target.rect.left - anchorRect.left}px, ${top - anchorRect.top + anchorElement.scrollTop - 2}px)`;
|
|
262
|
+
targetLineElement.style.opacity = "0.8";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function highlightMovedBlock(key: NodeKey) {
|
|
266
|
+
const view = anchorElement.ownerDocument.defaultView;
|
|
267
|
+
if (view === null) return;
|
|
268
|
+
if (highlightFrame !== undefined) view.cancelAnimationFrame(highlightFrame);
|
|
269
|
+
highlightAnimation?.cancel();
|
|
270
|
+
// Measure after Lexical reconciles the moved node into its new DOM position.
|
|
271
|
+
highlightFrame = view.requestAnimationFrame(() => {
|
|
272
|
+
highlightFrame = undefined;
|
|
273
|
+
const block = editor.getElementByKey(key);
|
|
274
|
+
if (block === null || highlightElement === null) return;
|
|
275
|
+
const rect = block.getBoundingClientRect();
|
|
276
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
277
|
+
highlightElement.style.width = `${rect.width + 8}px`;
|
|
278
|
+
highlightElement.style.height = `${rect.height + 8}px`;
|
|
279
|
+
highlightElement.style.transform = `translate(${rect.left - anchorRect.left - 4}px, ${rect.top - anchorRect.top + anchorElement.scrollTop - 4}px)`;
|
|
280
|
+
const reducedMotion = view.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
281
|
+
highlightAnimation = highlightElement.animate(
|
|
282
|
+
[{ opacity: 0.1 }, { opacity: 0.1, offset: reducedMotion ? 1 : 2 / 3 }, { opacity: 0 }],
|
|
283
|
+
{ duration: reducedMotion ? 1000 : 1500 }
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function positionMenu(element: HTMLElement) {
|
|
289
|
+
if (menuElement === null) return;
|
|
290
|
+
const blockRect = element.getBoundingClientRect();
|
|
291
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
292
|
+
const lineHeight = Number.parseFloat(
|
|
293
|
+
element.ownerDocument.defaultView?.getComputedStyle(element).lineHeight ?? ""
|
|
294
|
+
);
|
|
295
|
+
const height = Number.isFinite(lineHeight) ? lineHeight : blockRect.height;
|
|
296
|
+
const top = blockRect.top + (height - menuElement.getBoundingClientRect().height) / 2;
|
|
297
|
+
menuElement.style.transform = `translate(4px, ${top - anchorRect.top + anchorElement.scrollTop}px)`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function updateDropIndicator(event: DragEvent) {
|
|
301
|
+
if (draggedNodeKey === null) return false;
|
|
302
|
+
if (!isOwnedDropSurface(event.target)) {
|
|
303
|
+
hideTargetLine();
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const target = findDropTarget(event.clientY);
|
|
307
|
+
if (target === null) return false;
|
|
308
|
+
let canMove = false;
|
|
309
|
+
editor.read(() => {
|
|
310
|
+
const source = $getNodeByKey(draggedNodeKey!);
|
|
311
|
+
const destination = $getNearestNodeFromDOMNode(target.element);
|
|
312
|
+
canMove =
|
|
313
|
+
source !== null && destination !== null && $dropWouldMove(source, destination, target);
|
|
314
|
+
});
|
|
315
|
+
if (canMove) positionTargetLine(target);
|
|
316
|
+
else hideTargetLine();
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
event.stopPropagation();
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function handleDragLeave(event: DragEvent) {
|
|
323
|
+
if (draggedNodeKey === null) return;
|
|
324
|
+
const rect = anchorElement.getBoundingClientRect();
|
|
325
|
+
if (
|
|
326
|
+
event.clientX < rect.left ||
|
|
327
|
+
event.clientX > rect.right ||
|
|
328
|
+
event.clientY < rect.top ||
|
|
329
|
+
event.clientY > rect.bottom
|
|
330
|
+
)
|
|
331
|
+
hideTargetLine();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function handleDragEnter(event: DragEvent) {
|
|
335
|
+
if (draggedNodeKey !== null && !isOwnedDropSurface(event.target)) hideTargetLine();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function handleDocumentDrop(event: DragEvent) {
|
|
339
|
+
if (draggedNodeKey === null || !isOwnedDropSurface(event.target)) return;
|
|
340
|
+
const target = findDropTarget(event.clientY);
|
|
341
|
+
if (target === null) return;
|
|
342
|
+
const sourceKey = draggedNodeKey;
|
|
343
|
+
editor.update(() => {
|
|
344
|
+
const source = $getNodeByKey(sourceKey);
|
|
345
|
+
const destination = $getNearestNodeFromDOMNode(target.element);
|
|
346
|
+
if (source === null || destination === null || !$dropWouldMove(source, destination, target))
|
|
347
|
+
return;
|
|
348
|
+
if (target.empty) {
|
|
349
|
+
// replace() in Lexical 0.49 leaves the count stale when moving a sibling.
|
|
350
|
+
destination.insertBefore(source);
|
|
351
|
+
destination.remove();
|
|
352
|
+
} else if (target.before) destination.insertBefore(source);
|
|
353
|
+
else destination.insertAfter(source);
|
|
354
|
+
highlightMovedBlock(sourceKey);
|
|
355
|
+
});
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
event.stopPropagation();
|
|
358
|
+
handleDragEnd();
|
|
359
|
+
}
|
|
360
|
+
|
|
82
361
|
function closePicker() {
|
|
83
362
|
pickerOpen = false;
|
|
84
363
|
pickerState = null;
|
|
@@ -184,12 +463,13 @@
|
|
|
184
463
|
editor.update(() => {
|
|
185
464
|
const target = $getNodeByKey(state.targetNodeKey);
|
|
186
465
|
if (target === null) return;
|
|
187
|
-
const placeholder =
|
|
188
|
-
|
|
189
|
-
placeholder
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
466
|
+
const placeholder =
|
|
467
|
+
$isParagraphNode(target) && target.isEmpty() ? target : $createParagraphNode();
|
|
468
|
+
if (placeholder !== target) {
|
|
469
|
+
if (state.insertBefore) target.insertBefore(placeholder);
|
|
470
|
+
else target.insertAfter(placeholder);
|
|
471
|
+
}
|
|
472
|
+
placeholder.select();
|
|
193
473
|
option.select();
|
|
194
474
|
|
|
195
475
|
if (option.preservePlaceholder) return;
|
|
@@ -208,12 +488,39 @@
|
|
|
208
488
|
};
|
|
209
489
|
};
|
|
210
490
|
|
|
491
|
+
const attachTargetLine: Attachment<HTMLElement> = (element) => {
|
|
492
|
+
targetLineElement = element;
|
|
493
|
+
return () => {
|
|
494
|
+
if (targetLineElement === element) targetLineElement = null;
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const attachHighlight: Attachment<HTMLElement> = (element) => {
|
|
499
|
+
highlightElement = element;
|
|
500
|
+
return () => {
|
|
501
|
+
if (highlightElement === element) highlightElement = null;
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
|
|
211
505
|
const focusSearchInput: Attachment<HTMLInputElement> = (element) => {
|
|
212
506
|
element.focus();
|
|
213
507
|
};
|
|
214
508
|
|
|
215
|
-
$effect(() =>
|
|
216
|
-
|
|
509
|
+
$effect(() => {
|
|
510
|
+
const ownerDocument = anchorElement.ownerDocument;
|
|
511
|
+
function handleMouseMove(event: MouseEvent) {
|
|
512
|
+
if (draggedNodeKey !== null || pickerOpen) return;
|
|
513
|
+
if (event.target instanceof HTMLElement && isOnToolbar(event.target)) return;
|
|
514
|
+
draggableElement =
|
|
515
|
+
!isFromAnotherEditor(event.target) &&
|
|
516
|
+
withinEditorBuffer(event.clientX, event.clientY, 50, 25)
|
|
517
|
+
? findHoveredBlock(event.target, event.clientY)
|
|
518
|
+
: null;
|
|
519
|
+
}
|
|
520
|
+
function handleScroll() {
|
|
521
|
+
if (draggableElement !== null) positionMenu(draggableElement);
|
|
522
|
+
}
|
|
523
|
+
const unregisterCommands = mergeRegister(
|
|
217
524
|
editor.registerCommand(
|
|
218
525
|
KEY_ARROW_UP_COMMAND,
|
|
219
526
|
(event) => {
|
|
@@ -238,69 +545,90 @@
|
|
|
238
545
|
},
|
|
239
546
|
COMMAND_PRIORITY_LOW
|
|
240
547
|
)
|
|
241
|
-
)
|
|
242
|
-
|
|
548
|
+
);
|
|
549
|
+
ownerDocument.addEventListener("mousemove", handleMouseMove);
|
|
550
|
+
ownerDocument.addEventListener("dragover", updateDropIndicator, true);
|
|
551
|
+
ownerDocument.addEventListener("dragenter", handleDragEnter, true);
|
|
552
|
+
ownerDocument.addEventListener("dragleave", handleDragLeave, true);
|
|
553
|
+
ownerDocument.addEventListener("drop", handleDocumentDrop, true);
|
|
554
|
+
ownerDocument.addEventListener("scroll", handleScroll, true);
|
|
555
|
+
ownerDocument.defaultView?.addEventListener("resize", handleScroll);
|
|
556
|
+
return () => {
|
|
557
|
+
unregisterCommands();
|
|
558
|
+
ownerDocument.removeEventListener("mousemove", handleMouseMove);
|
|
559
|
+
ownerDocument.removeEventListener("dragover", updateDropIndicator, true);
|
|
560
|
+
ownerDocument.removeEventListener("dragenter", handleDragEnter, true);
|
|
561
|
+
ownerDocument.removeEventListener("dragleave", handleDragLeave, true);
|
|
562
|
+
ownerDocument.removeEventListener("drop", handleDocumentDrop, true);
|
|
563
|
+
ownerDocument.removeEventListener("scroll", handleScroll, true);
|
|
564
|
+
ownerDocument.defaultView?.removeEventListener("resize", handleScroll);
|
|
565
|
+
draggedNodeKey = null;
|
|
566
|
+
hideTargetLine();
|
|
567
|
+
if (focusFrame !== undefined) ownerDocument.defaultView?.cancelAnimationFrame(focusFrame);
|
|
568
|
+
if (highlightFrame !== undefined)
|
|
569
|
+
ownerDocument.defaultView?.cancelAnimationFrame(highlightFrame);
|
|
570
|
+
highlightAnimation?.cancel();
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
$effect(() => {
|
|
575
|
+
if (draggableElement !== null) positionMenu(draggableElement);
|
|
576
|
+
});
|
|
243
577
|
</script>
|
|
244
578
|
|
|
245
579
|
<PopoverRoot open={pickerOpen} onOpenChange={handlePickerOpenChange}>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
580
|
+
<Portal to={anchorElement}>
|
|
581
|
+
<ToolbarRoot
|
|
582
|
+
class={["ridu-richtext-block-toolbar", draggableElement !== null && "is-visible"]}
|
|
583
|
+
inert={draggableElement === null}
|
|
584
|
+
aria-hidden={draggableElement === null ? true : undefined}
|
|
585
|
+
aria-label={i18n.t("plugin.richtext:editor.blockActions")}
|
|
586
|
+
{@attach attachMenuElement}
|
|
587
|
+
>
|
|
588
|
+
<ToolbarButton
|
|
589
|
+
class="ridu-richtext-block-grip"
|
|
590
|
+
type="button"
|
|
591
|
+
draggable="true"
|
|
592
|
+
title={i18n.t("plugin.richtext:editor.dragToMoveBlock")}
|
|
593
|
+
aria-label={i18n.t("plugin.richtext:editor.dragToMoveBlock")}
|
|
594
|
+
ondragstart={handleDragStart}
|
|
595
|
+
ondragend={handleDragEnd}
|
|
257
596
|
>
|
|
258
|
-
<span
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
</TooltipRoot>
|
|
289
|
-
</ToolbarRoot>
|
|
290
|
-
{/snippet}
|
|
291
|
-
|
|
292
|
-
{#snippet targetLine(attach)}
|
|
293
|
-
<div
|
|
294
|
-
class="pointer-events-none absolute top-0 left-0 z-2 h-0.5 rounded-full bg-primary opacity-0 will-change-transform"
|
|
295
|
-
{@attach attach}
|
|
296
|
-
></div>
|
|
297
|
-
{/snippet}
|
|
298
|
-
</DraggableBlockPlugin>
|
|
597
|
+
<span aria-hidden="true"></span>
|
|
598
|
+
<span aria-hidden="true"></span>
|
|
599
|
+
<span aria-hidden="true"></span>
|
|
600
|
+
<span aria-hidden="true"></span>
|
|
601
|
+
<span aria-hidden="true"></span>
|
|
602
|
+
<span aria-hidden="true"></span>
|
|
603
|
+
</ToolbarButton>
|
|
604
|
+
<TooltipRoot>
|
|
605
|
+
<TooltipTrigger>
|
|
606
|
+
{#snippet child({ props })}
|
|
607
|
+
<ToolbarButton
|
|
608
|
+
{...props}
|
|
609
|
+
class="ridu-richtext-block-add"
|
|
610
|
+
type="button"
|
|
611
|
+
aria-label={i18n.t("plugin.richtext:editor.addBlock")}
|
|
612
|
+
aria-haspopup="dialog"
|
|
613
|
+
aria-expanded={pickerOpen}
|
|
614
|
+
aria-controls={pickerOpen ? pickerID : undefined}
|
|
615
|
+
onclick={openPicker}
|
|
616
|
+
>
|
|
617
|
+
<span aria-hidden="true">+</span>
|
|
618
|
+
</ToolbarButton>
|
|
619
|
+
{/snippet}
|
|
620
|
+
</TooltipTrigger>
|
|
621
|
+
<TooltipContent>{i18n.t("plugin.richtext:editor.addBlock")}</TooltipContent>
|
|
622
|
+
</TooltipRoot>
|
|
623
|
+
</ToolbarRoot>
|
|
624
|
+
<div class="ridu-richtext-drop-line" {@attach attachTargetLine}></div>
|
|
625
|
+
<div class="ridu-richtext-drop-highlight" aria-hidden="true" {@attach attachHighlight}></div>
|
|
626
|
+
</Portal>
|
|
299
627
|
|
|
300
628
|
{#if pickerOpen && menuElement !== null}
|
|
301
629
|
<PopoverContent
|
|
302
|
-
id=
|
|
303
|
-
class="
|
|
630
|
+
id={pickerID}
|
|
631
|
+
class="ridu-richtext-menu-popover"
|
|
304
632
|
role="dialog"
|
|
305
633
|
aria-label={i18n.t("plugin.richtext:editor.insertBlock")}
|
|
306
634
|
customAnchor={menuElement}
|
|
@@ -315,7 +643,7 @@
|
|
|
315
643
|
<CommandRoot label={i18n.t("plugin.richtext:editor.filterBlocks")} shouldFilter={false} loop>
|
|
316
644
|
<CommandInput
|
|
317
645
|
type="search"
|
|
318
|
-
class="
|
|
646
|
+
class="ridu-richtext-picker-search"
|
|
319
647
|
aria-label={i18n.t("plugin.richtext:editor.filterBlocks")}
|
|
320
648
|
placeholder={i18n.t("plugin.richtext:editor.filterBlocksPlaceholder")}
|
|
321
649
|
bind:value={query}
|
|
@@ -323,9 +651,9 @@
|
|
|
323
651
|
{@attach focusSearchInput}
|
|
324
652
|
/>
|
|
325
653
|
{#if options.length > 0}
|
|
326
|
-
<RichTextMenu {options} onSelect={selectOption}
|
|
654
|
+
<RichTextMenu {options} onSelect={selectOption} command />
|
|
327
655
|
{:else}
|
|
328
|
-
<p class="
|
|
656
|
+
<p class="ridu-richtext-menu-empty">
|
|
329
657
|
{i18n.t("plugin.richtext:editor.noMatchingBlocks")}
|
|
330
658
|
</p>
|
|
331
659
|
{/if}
|
|
@@ -334,6 +662,11 @@
|
|
|
334
662
|
{/if}
|
|
335
663
|
</PopoverRoot>
|
|
336
664
|
|
|
337
|
-
<p
|
|
665
|
+
<p
|
|
666
|
+
class="ridu-richtext-announcement"
|
|
667
|
+
aria-live="polite"
|
|
668
|
+
aria-atomic="true"
|
|
669
|
+
data-richtext-movement-status
|
|
670
|
+
>
|
|
338
671
|
{movementAnnouncement}
|
|
339
672
|
</p>
|
|
@@ -13,13 +13,22 @@ export interface OpenUploadBrowserPayload {
|
|
|
13
13
|
nodeKey?: NodeKey;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export
|
|
16
|
+
export interface OpenRelationshipBrowserPayload {
|
|
17
|
+
collectionSlug?: string;
|
|
18
|
+
documentID?: string;
|
|
19
|
+
mode?: "edit" | "insert" | "replace";
|
|
20
|
+
nodeKey?: NodeKey;
|
|
21
|
+
}
|
|
17
22
|
|
|
18
23
|
export interface UpdateUploadCaptionPayload {
|
|
19
24
|
caption: string;
|
|
20
25
|
nodeKey: NodeKey;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
export const OPEN_LINK_EDITOR_COMMAND: LexicalCommand<void> = createCommand(
|
|
29
|
+
"OPEN_LINK_EDITOR_COMMAND"
|
|
30
|
+
);
|
|
31
|
+
|
|
23
32
|
export const OPEN_UPLOAD_BROWSER_COMMAND: LexicalCommand<OpenUploadBrowserPayload> = createCommand(
|
|
24
33
|
"OPEN_UPLOAD_BROWSER_COMMAND"
|
|
25
34
|
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
@use "@riducms/ui/styles.scss" as ui;
|
|
2
|
+
|
|
3
|
+
@layer ridu.components {
|
|
4
|
+
.ridu-richtext-menu-popover.ridu-popover-content {
|
|
5
|
+
inline-size: min(200px, calc(100vw - 16px));
|
|
6
|
+
padding: 0;
|
|
7
|
+
border: 0;
|
|
8
|
+
border-radius: 3px;
|
|
9
|
+
background: var(--control-surface);
|
|
10
|
+
box-shadow: var(--richtext-popup-shadow);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.ridu-richtext-menu {
|
|
14
|
+
max-block-size: min(300px, var(--bits-popover-content-available-height, 300px));
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
padding: 0;
|
|
17
|
+
color: var(--foreground);
|
|
18
|
+
font-family: var(--font-sans);
|
|
19
|
+
font-size: 13px;
|
|
20
|
+
line-height: 20px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ridu-richtext-menu__group {
|
|
24
|
+
padding-block-end: 8px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ridu-richtext-menu .ridu-richtext-menu__heading {
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 0 10px;
|
|
30
|
+
color: var(--foreground-muted);
|
|
31
|
+
font-family: inherit;
|
|
32
|
+
font-size: 10px;
|
|
33
|
+
font-weight: 400;
|
|
34
|
+
letter-spacing: normal;
|
|
35
|
+
text-transform: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ridu-richtext-menu__items {
|
|
39
|
+
display: grid;
|
|
40
|
+
gap: 0;
|
|
41
|
+
margin: 0;
|
|
42
|
+
padding: 0;
|
|
43
|
+
list-style: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ridu-richtext-menu .ridu-richtext-menu__option {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 6px;
|
|
51
|
+
min-block-size: 30px;
|
|
52
|
+
block-size: 30px;
|
|
53
|
+
padding: 0 12px;
|
|
54
|
+
border: 0;
|
|
55
|
+
border-radius: 0;
|
|
56
|
+
color: inherit;
|
|
57
|
+
font: inherit;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
outline: none;
|
|
60
|
+
|
|
61
|
+
&:hover,
|
|
62
|
+
&.is-selected,
|
|
63
|
+
&[data-selected] {
|
|
64
|
+
background: var(--control-surface-hover);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
svg {
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
color: var(--foreground-muted);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ridu-richtext-menu__label {
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ridu-richtext-picker-search.ridu-command__input {
|
|
80
|
+
block-size: 36px;
|
|
81
|
+
padding: 0 10px;
|
|
82
|
+
border-block-end: 1px solid var(--control-border);
|
|
83
|
+
font-size: 13px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ridu-richtext-menu-empty {
|
|
87
|
+
padding: 12px;
|
|
88
|
+
margin: 0;
|
|
89
|
+
color: var(--foreground-muted);
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
}
|
|
92
|
+
}
|