@threadlabs/looma 0.28.2 → 0.28.3
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/components/shared/overlay.js +30 -7
- package/editor/selection-surface.d.ts +66 -0
- package/package.json +1 -1
- package/vue/editor/index.js +664 -103
|
@@ -235,8 +235,22 @@ export function writtenPlace(surface, ownedStyles = []) {
|
|
|
235
235
|
return { rect, floating };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
// The visible part of a boundary element, or the visual viewport when there is none or it is off screen.
|
|
239
|
+
function containment(surface, boundary) {
|
|
240
|
+
const visible = viewport(surface.ownerDocument.defaultView);
|
|
241
|
+
const element = typeof boundary === "function" ? boundary() : boundary;
|
|
242
|
+
if (!element) return visible;
|
|
243
|
+
const box = element.getBoundingClientRect();
|
|
244
|
+
const left = Math.max(visible.left, box.left);
|
|
245
|
+
const top = Math.max(visible.top, box.top);
|
|
246
|
+
const right = Math.min(visible.right, box.right);
|
|
247
|
+
const bottom = Math.min(visible.bottom, box.bottom);
|
|
248
|
+
if (right <= left || bottom <= top) return visible;
|
|
249
|
+
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function fallbackPosition(surface, anchor, placement, gap, viewportGap, surfaceRect = layoutRect(surface), boundary = null) {
|
|
253
|
+
const bounds = containment(surface, boundary);
|
|
240
254
|
const [preferred, align = "center"] = placement.split("-");
|
|
241
255
|
const side = ["top", "bottom", "left", "right"].includes(preferred) ? preferred : "bottom";
|
|
242
256
|
const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" }[side];
|
|
@@ -271,9 +285,12 @@ function fallbackPosition(surface, anchor, placement, gap, viewportGap, surfaceR
|
|
|
271
285
|
surface.style.bottom = "auto";
|
|
272
286
|
}
|
|
273
287
|
|
|
274
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Applies the shared flip/shift policy to a live element or consumer-supplied virtual rectangle.
|
|
290
|
+
* `boundary` (an element, or a function returning one) narrows the room to its visible part.
|
|
291
|
+
*/
|
|
275
292
|
export function positionAnchoredSurface(surface, rect, options = {}) {
|
|
276
|
-
fallbackPosition(surface, rect, options.placement ?? "bottom-start", options.gap ?? 4, options.viewportGap ?? 8);
|
|
293
|
+
fallbackPosition(surface, rect, options.placement ?? "bottom-start", options.gap ?? 4, options.viewportGap ?? 8, undefined, options.boundary);
|
|
277
294
|
}
|
|
278
295
|
|
|
279
296
|
/**
|
|
@@ -379,6 +396,7 @@ export function createAnchoredSurface(surface, options = {}) {
|
|
|
379
396
|
let placement = options.placement ?? "bottom-start";
|
|
380
397
|
const gap = () => Math.max(0, Number(typeof options.gap === "function" ? options.gap() : options.gap ?? 4) || 0);
|
|
381
398
|
const viewportGap = Math.max(0, options.viewportGap ?? 8);
|
|
399
|
+
const boundary = options.boundary ?? null;
|
|
382
400
|
let anchor = options.anchor ?? null;
|
|
383
401
|
let point = null;
|
|
384
402
|
let open = false;
|
|
@@ -402,17 +420,22 @@ export function createAnchoredSurface(surface, options = {}) {
|
|
|
402
420
|
// A disappearing trigger has no usable position. Keep the last valid position, or wait before first opening.
|
|
403
421
|
if (!point && typeof anchor !== "function" && (anchor.isConnected === false || rect.width === 0 && rect.height === 0
|
|
404
422
|
|| anchor instanceof owner.Element && owner.getComputedStyle(anchor).visibility !== "visible")) {
|
|
405
|
-
if (lastAnchorRect) positionAnchoredSurface(surface, lastAnchorRect, { placement, gap: gap(), viewportGap });
|
|
423
|
+
if (lastAnchorRect) positionAnchoredSurface(surface, lastAnchorRect, { placement, gap: gap(), viewportGap, boundary });
|
|
406
424
|
else hide(surface);
|
|
407
425
|
return;
|
|
408
426
|
}
|
|
409
427
|
lastAnchorRect = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height };
|
|
410
428
|
show(surface);
|
|
411
|
-
|
|
429
|
+
if (options.maxWidth !== undefined) {
|
|
430
|
+
const maxWidth = typeof options.maxWidth === "function" ? options.maxWidth() : options.maxWidth;
|
|
431
|
+
surface.style.maxWidth = maxWidth === null ? "" : `${maxWidth}px`;
|
|
432
|
+
}
|
|
433
|
+
positionAnchoredSurface(surface, rect, { placement: point ? "bottom-start" : placement, gap: gap(), viewportGap, boundary });
|
|
412
434
|
};
|
|
413
435
|
surface.style.position = "fixed";
|
|
414
436
|
surface.style.margin = "0";
|
|
415
|
-
|
|
437
|
+
// `bare` resets the browser's popover chrome on a surface that has none of its own.
|
|
438
|
+
const presentation = createViewportSurface(surface, { position, bare: options.bare });
|
|
416
439
|
// Resize and scroll events do not report an ancestor's transition or layout shift.
|
|
417
440
|
// While visible, sample the live anchor and only re-position when its bounds change.
|
|
418
441
|
const trackAnchor = () => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Editor } from "@tiptap/core";
|
|
2
|
+
import { type EditorState } from "@tiptap/pm/state";
|
|
3
|
+
import type { EditorView } from "@tiptap/pm/view";
|
|
4
|
+
type Rect = {
|
|
5
|
+
left: number;
|
|
6
|
+
top: number;
|
|
7
|
+
right: number;
|
|
8
|
+
bottom: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
/** What `shouldShow` receives on each selection or document change. */
|
|
13
|
+
export interface SelectionSurfaceContext {
|
|
14
|
+
editor: Editor;
|
|
15
|
+
view: EditorView;
|
|
16
|
+
state: EditorState;
|
|
17
|
+
oldState?: EditorState;
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SelectionSurfaceOptions {
|
|
22
|
+
editor: Editor;
|
|
23
|
+
/**
|
|
24
|
+
* The surface element, the only child of a host element that stays in place. It leaves the
|
|
25
|
+
* document while closed, so its content is absent rather than hidden, and returns to the host,
|
|
26
|
+
* in the top layer, while open.
|
|
27
|
+
*/
|
|
28
|
+
element: HTMLElement;
|
|
29
|
+
pluginKey: string;
|
|
30
|
+
shouldShow(context: SelectionSurfaceContext): boolean;
|
|
31
|
+
/** Waits this long after the last change before deciding, for non-empty selections. */
|
|
32
|
+
updateDelay?: number;
|
|
33
|
+
placement?: "top" | "bottom";
|
|
34
|
+
/** The rectangle to anchor to; the selection's by default. */
|
|
35
|
+
anchor?: () => Rect;
|
|
36
|
+
/** Keeps the surface inside this element as well as the viewport. */
|
|
37
|
+
boundary?: () => HTMLElement | null;
|
|
38
|
+
maxWidth?: () => number | null;
|
|
39
|
+
/** Elements whose pointer presses do not dismiss the surface, such as the editor's own root. */
|
|
40
|
+
related?: () => HTMLElement[];
|
|
41
|
+
/** Return false to keep the surface open, for example while a picker in it has focus. */
|
|
42
|
+
onHide?: () => boolean | void;
|
|
43
|
+
/** Escape or a press outside reached this surface first on the shared overlay stack. */
|
|
44
|
+
onDismiss?: (reason: "escape" | "light-dismiss") => void;
|
|
45
|
+
}
|
|
46
|
+
/** A selection-anchored surface: the editor's floating toolbar, link card, and image actions. */
|
|
47
|
+
export interface SelectionSurface {
|
|
48
|
+
readonly element: HTMLElement;
|
|
49
|
+
isOpen(): boolean;
|
|
50
|
+
show(): void;
|
|
51
|
+
hide(): void;
|
|
52
|
+
/** Anchors to `anchor` until the next change the surface decides on. */
|
|
53
|
+
setAnchor(anchor: () => Rect): void;
|
|
54
|
+
destroy(): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Shows `element` beside the selection whenever `shouldShow` allows, as Tiptap's bubble menu does,
|
|
58
|
+
* and presents it through Looma's shared overlay layer: in the top layer, placed by the shared
|
|
59
|
+
* flip/shift policy, travelling with its anchor, and registered on the overlay stack.
|
|
60
|
+
*
|
|
61
|
+
* @lifecycle Registers a ProseMirror plugin now; `destroy()` unregisters it and closes the surface.
|
|
62
|
+
* The editor destroying its view closes the surface too.
|
|
63
|
+
* @contract A press inside the surface keeps it open when the editor blurs; dragging hides it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createSelectionSurface(options: SelectionSurfaceOptions): SelectionSurface;
|
|
66
|
+
export {};
|
package/package.json
CHANGED
package/vue/editor/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
|
|
2
2
|
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig, resolveTableCellAt, siteRelativeHref } from "@threadlabs/looma/editor";
|
|
3
3
|
import { Button, Callout, Card, Checkbox, Cluster, Combobox, Disclosure, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, EditorToolbar as EditorToolbar$1, FormField, Icon, IconButton, Image, Input, InputGroup, List, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
4
|
-
import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
|
|
5
|
-
import {
|
|
4
|
+
import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
|
|
5
|
+
import { Editor as Editor$1 } from "@tiptap/vue-3";
|
|
6
6
|
import { closeHistory } from "@tiptap/pm/history";
|
|
7
7
|
import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
8
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
@@ -25,9 +25,98 @@ var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
|
|
|
25
25
|
};
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/components/shared/overlay.js
|
|
28
|
+
function stateFor(document) {
|
|
29
|
+
const key = Symbol.for("@threadlabs/looma/overlay-stack/v1");
|
|
30
|
+
let state = document[key];
|
|
31
|
+
if (state) return state;
|
|
32
|
+
state = {
|
|
33
|
+
records: [],
|
|
34
|
+
modalCount: 0,
|
|
35
|
+
listening: false
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(document, key, { value: state });
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
function syncScrollLock(document, state) {
|
|
41
|
+
document.documentElement.toggleAttribute("data-ui-scroll-lock", state.modalCount > 0);
|
|
42
|
+
if (state.modalCount > 0) document.documentElement.style.overflow = "hidden";
|
|
43
|
+
else document.documentElement.style.removeProperty("overflow");
|
|
44
|
+
}
|
|
45
|
+
function syncModalBackdrops(state) {
|
|
46
|
+
const topModal = state.records.findLast((record) => record.modal);
|
|
47
|
+
for (const record of state.records) (record.modalElement ?? record.element).toggleAttribute("data-ui-backdrop-hidden", Boolean(record.modal && record !== topModal));
|
|
48
|
+
}
|
|
49
|
+
function requestClose(document, reason, trigger) {
|
|
50
|
+
const record = stateFor(document).records.at(-1);
|
|
51
|
+
if (!record || record.dismissible === false && (reason === "escape" || reason === "light-dismiss") || record.canClose?.(reason) === false) return false;
|
|
52
|
+
record.requestClose(reason, trigger);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function ensureListeners(document, state) {
|
|
56
|
+
if (state.listening) return;
|
|
57
|
+
state.onKeydown = (event) => {
|
|
58
|
+
if (event.key === "Escape" && !event.defaultPrevented && requestClose(document, "escape", "keyboard")) event.preventDefault();
|
|
59
|
+
};
|
|
60
|
+
state.onPointerdown = (event) => {
|
|
61
|
+
const top = state.records.at(-1);
|
|
62
|
+
if (!top || top.dismissible === false) return;
|
|
63
|
+
const boundary = [top.element, ...top.relatedElements ?? []];
|
|
64
|
+
if (event.clientX === 0 && event.clientY === 0) return;
|
|
65
|
+
const rect = top.element.getBoundingClientRect();
|
|
66
|
+
if (!(event.target === top.element && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) && event.composedPath().some((target) => target instanceof Node && boundary.some((element) => element.contains(target)))) return;
|
|
67
|
+
requestClose(document, "light-dismiss", "pointer");
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("keydown", state.onKeydown);
|
|
70
|
+
document.addEventListener("pointerdown", state.onPointerdown, true);
|
|
71
|
+
state.listening = true;
|
|
72
|
+
}
|
|
73
|
+
function removeListeners(document, state) {
|
|
74
|
+
if (!state.listening || state.records.length) return;
|
|
75
|
+
document.removeEventListener("keydown", state.onKeydown);
|
|
76
|
+
document.removeEventListener("pointerdown", state.onPointerdown, true);
|
|
77
|
+
state.listening = false;
|
|
78
|
+
}
|
|
79
|
+
function openOverlay(record) {
|
|
80
|
+
const document = record.element.ownerDocument;
|
|
81
|
+
const state = stateFor(document);
|
|
82
|
+
const index = state.records.findIndex((entry) => entry.id === record.id);
|
|
83
|
+
if (index >= 0) {
|
|
84
|
+
const previous = state.records[index];
|
|
85
|
+
state.records[index] = record;
|
|
86
|
+
if (previous.modal !== record.modal) {
|
|
87
|
+
state.modalCount += record.modal ? 1 : -1;
|
|
88
|
+
syncScrollLock(document, state);
|
|
89
|
+
}
|
|
90
|
+
if (previous.modalElement && previous.modalElement !== record.modalElement) previous.modalElement.removeAttribute("data-ui-backdrop-hidden");
|
|
91
|
+
syncModalBackdrops(state);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
state.records.push(record);
|
|
95
|
+
if (record.modal) {
|
|
96
|
+
state.modalCount += 1;
|
|
97
|
+
syncScrollLock(document, state);
|
|
98
|
+
}
|
|
99
|
+
syncModalBackdrops(state);
|
|
100
|
+
ensureListeners(document, state);
|
|
101
|
+
announceOverlayOpen(document, record.id);
|
|
102
|
+
}
|
|
28
103
|
function announceOverlayOpen(document, id) {
|
|
29
104
|
document.dispatchEvent(new CustomEvent("ui-overlay-open", { detail: { id } }));
|
|
30
105
|
}
|
|
106
|
+
function closeOverlay(document, id) {
|
|
107
|
+
const state = stateFor(document);
|
|
108
|
+
const index = state.records.findIndex((record) => record.id === id);
|
|
109
|
+
if (index >= 0) {
|
|
110
|
+
const [record] = state.records.splice(index, 1);
|
|
111
|
+
(record.modalElement ?? record.element).removeAttribute("data-ui-backdrop-hidden");
|
|
112
|
+
if (record?.modal) {
|
|
113
|
+
state.modalCount = Math.max(0, state.modalCount - 1);
|
|
114
|
+
syncScrollLock(document, state);
|
|
115
|
+
}
|
|
116
|
+
syncModalBackdrops(state);
|
|
117
|
+
}
|
|
118
|
+
removeListeners(document, state);
|
|
119
|
+
}
|
|
31
120
|
function topLayerOpen(surface) {
|
|
32
121
|
try {
|
|
33
122
|
return surface.matches(":popover-open");
|
|
@@ -48,6 +137,178 @@ function hide(surface) {
|
|
|
48
137
|
} catch {}
|
|
49
138
|
surface.hidden = true;
|
|
50
139
|
}
|
|
140
|
+
function viewport(owner) {
|
|
141
|
+
const visual = owner.visualViewport;
|
|
142
|
+
const left = visual?.offsetLeft ?? 0;
|
|
143
|
+
const top = visual?.offsetTop ?? 0;
|
|
144
|
+
const width = visual?.width ?? owner.innerWidth;
|
|
145
|
+
const height = visual?.height ?? owner.innerHeight;
|
|
146
|
+
return {
|
|
147
|
+
left,
|
|
148
|
+
top,
|
|
149
|
+
right: left + width,
|
|
150
|
+
bottom: top + height,
|
|
151
|
+
width,
|
|
152
|
+
height
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function clamp(rect, bounds, gutter) {
|
|
156
|
+
const availableWidth = Math.max(0, bounds.width - gutter * 2);
|
|
157
|
+
const availableHeight = Math.max(0, bounds.height - gutter * 2);
|
|
158
|
+
const left = rect.width > availableWidth ? bounds.left + gutter : Math.min(Math.max(rect.left, bounds.left + gutter), bounds.right - gutter - rect.width);
|
|
159
|
+
const top = rect.height > availableHeight ? bounds.top + gutter : Math.min(Math.max(rect.top, bounds.top + gutter), bounds.bottom - gutter - rect.height);
|
|
160
|
+
return {
|
|
161
|
+
x: left - rect.left,
|
|
162
|
+
y: top - rect.top
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function layoutRect(surface) {
|
|
166
|
+
const box = surface.getBoundingClientRect();
|
|
167
|
+
const width = surface.offsetWidth || box.width;
|
|
168
|
+
const height = surface.offsetHeight || box.height;
|
|
169
|
+
const left = box.left + (box.width - width) / 2;
|
|
170
|
+
const top = box.top + (box.height - height) / 2;
|
|
171
|
+
return {
|
|
172
|
+
left,
|
|
173
|
+
top,
|
|
174
|
+
right: left + width,
|
|
175
|
+
bottom: top + height,
|
|
176
|
+
width,
|
|
177
|
+
height
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Where an unanchored surface is written, for presenting it there in the top layer. The top layer
|
|
182
|
+
* takes a surface out of page flow, and engines disagree on its static position there (Chromium
|
|
183
|
+
* uses the viewport origin), so measure the surface in flow before it is shown, with the inline
|
|
184
|
+
* styles its caller owns cleared. `rect()` then carries that place with the box that holds it, so
|
|
185
|
+
* the surface travels with scrolling and with layout around it. A fixed surface keeps its measured
|
|
186
|
+
* place; one an integration places with inline insets the caller does not own keeps its live box.
|
|
187
|
+
* `floating` reports a surface placed that way or written in a fixed or absolute box, which answers to
|
|
188
|
+
* the viewport. Call it only while the surface is outside the top layer.
|
|
189
|
+
*/
|
|
190
|
+
function writtenPlace(surface, ownedStyles = []) {
|
|
191
|
+
const owner = surface.ownerDocument.defaultView;
|
|
192
|
+
const up = (node) => node.assignedSlot ?? node.parentElement ?? node.getRootNode().host ?? null;
|
|
193
|
+
let parent = up(surface);
|
|
194
|
+
while (parent && owner.getComputedStyle(parent).display === "contents") parent = up(parent);
|
|
195
|
+
const popover = surface.getAttribute("popover");
|
|
196
|
+
const hidden = surface.hidden;
|
|
197
|
+
const saved = ownedStyles.map((name) => [name, surface.style.getPropertyValue(name)]);
|
|
198
|
+
if (popover !== null) surface.removeAttribute("popover");
|
|
199
|
+
if (hidden) surface.hidden = false;
|
|
200
|
+
for (const name of ownedStyles) surface.style.removeProperty(name);
|
|
201
|
+
const spot = layoutRect(surface);
|
|
202
|
+
const floats = (node) => node instanceof owner.Element && /^(fixed|absolute)$/.test(owner.getComputedStyle(node).position);
|
|
203
|
+
const placed = ["left", "top"].some((name) => !ownedStyles.includes(name) && surface.style.getPropertyValue(name));
|
|
204
|
+
const fixed = owner.getComputedStyle(surface).position === "fixed";
|
|
205
|
+
const floating = placed || floats(surface) || floats(surface.offsetParent);
|
|
206
|
+
for (const [name, value] of saved) if (value) surface.style.setProperty(name, value);
|
|
207
|
+
if (hidden) surface.hidden = true;
|
|
208
|
+
if (popover !== null) surface.setAttribute("popover", popover);
|
|
209
|
+
surface.offsetWidth;
|
|
210
|
+
const scroll = () => !parent || parent === surface.ownerDocument.documentElement || parent === surface.ownerDocument.scrollingElement ? [0, 0] : [parent.scrollLeft, parent.scrollTop];
|
|
211
|
+
let offset = null;
|
|
212
|
+
const rect = () => {
|
|
213
|
+
let { left, top } = placed ? layoutRect(surface) : spot;
|
|
214
|
+
if (!placed && !fixed && parent) {
|
|
215
|
+
const base = parent.getBoundingClientRect();
|
|
216
|
+
const [x, y] = scroll();
|
|
217
|
+
offset ??= {
|
|
218
|
+
left: spot.left - base.left + x,
|
|
219
|
+
top: spot.top - base.top + y
|
|
220
|
+
};
|
|
221
|
+
left = base.left + offset.left - x;
|
|
222
|
+
top = base.top + offset.top - y;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
left,
|
|
226
|
+
top,
|
|
227
|
+
right: left,
|
|
228
|
+
bottom: top,
|
|
229
|
+
width: 0,
|
|
230
|
+
height: 0
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
rect,
|
|
235
|
+
floating
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function containment(surface, boundary) {
|
|
239
|
+
const visible = viewport(surface.ownerDocument.defaultView);
|
|
240
|
+
const element = typeof boundary === "function" ? boundary() : boundary;
|
|
241
|
+
if (!element) return visible;
|
|
242
|
+
const box = element.getBoundingClientRect();
|
|
243
|
+
const left = Math.max(visible.left, box.left);
|
|
244
|
+
const top = Math.max(visible.top, box.top);
|
|
245
|
+
const right = Math.min(visible.right, box.right);
|
|
246
|
+
const bottom = Math.min(visible.bottom, box.bottom);
|
|
247
|
+
if (right <= left || bottom <= top) return visible;
|
|
248
|
+
return {
|
|
249
|
+
left,
|
|
250
|
+
top,
|
|
251
|
+
right,
|
|
252
|
+
bottom,
|
|
253
|
+
width: right - left,
|
|
254
|
+
height: bottom - top
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function fallbackPosition(surface, anchor, placement, gap, viewportGap, surfaceRect = layoutRect(surface), boundary = null) {
|
|
258
|
+
const bounds = containment(surface, boundary);
|
|
259
|
+
const [preferred, align = "center"] = placement.split("-");
|
|
260
|
+
const side = [
|
|
261
|
+
"top",
|
|
262
|
+
"bottom",
|
|
263
|
+
"left",
|
|
264
|
+
"right"
|
|
265
|
+
].includes(preferred) ? preferred : "bottom";
|
|
266
|
+
const opposite = {
|
|
267
|
+
top: "bottom",
|
|
268
|
+
bottom: "top",
|
|
269
|
+
left: "right",
|
|
270
|
+
right: "left"
|
|
271
|
+
}[side];
|
|
272
|
+
const available = {
|
|
273
|
+
top: anchor.top - bounds.top - viewportGap,
|
|
274
|
+
bottom: bounds.bottom - anchor.bottom - viewportGap,
|
|
275
|
+
left: anchor.left - bounds.left - viewportGap,
|
|
276
|
+
right: bounds.right - anchor.right - viewportGap
|
|
277
|
+
};
|
|
278
|
+
const needed = (side === "top" || side === "bottom" ? surfaceRect.height : surfaceRect.width) + gap;
|
|
279
|
+
const actual = available[side] < needed && available[opposite] > available[side] ? opposite : side;
|
|
280
|
+
surface.dataset.uiActualPlacement = actual;
|
|
281
|
+
surface.dataset.uiActualAlign = align;
|
|
282
|
+
const rtl = getComputedStyle(surface).direction === "rtl";
|
|
283
|
+
let left;
|
|
284
|
+
let top;
|
|
285
|
+
if (actual === "top" || actual === "bottom") {
|
|
286
|
+
top = actual === "top" ? anchor.top - surfaceRect.height - gap : anchor.bottom + gap;
|
|
287
|
+
left = align === "start" ? rtl ? anchor.right - surfaceRect.width : anchor.left : align === "end" ? rtl ? anchor.left : anchor.right - surfaceRect.width : (anchor.left + anchor.right - surfaceRect.width) / 2;
|
|
288
|
+
} else {
|
|
289
|
+
left = actual === "left" ? anchor.left - surfaceRect.width - gap : anchor.right + gap;
|
|
290
|
+
top = align === "start" ? anchor.top : align === "end" ? anchor.bottom - surfaceRect.height : (anchor.top + anchor.bottom - surfaceRect.height) / 2;
|
|
291
|
+
}
|
|
292
|
+
const shift = clamp({
|
|
293
|
+
left,
|
|
294
|
+
top,
|
|
295
|
+
right: left + surfaceRect.width,
|
|
296
|
+
bottom: top + surfaceRect.height,
|
|
297
|
+
width: surfaceRect.width,
|
|
298
|
+
height: surfaceRect.height
|
|
299
|
+
}, bounds, viewportGap);
|
|
300
|
+
surface.style.left = `${Math.round(left + shift.x)}px`;
|
|
301
|
+
surface.style.top = `${Math.round(top + shift.y)}px`;
|
|
302
|
+
surface.style.right = "auto";
|
|
303
|
+
surface.style.bottom = "auto";
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Applies the shared flip/shift policy to a live element or consumer-supplied virtual rectangle.
|
|
307
|
+
* `boundary` (an element, or a function returning one) narrows the room to its visible part.
|
|
308
|
+
*/
|
|
309
|
+
function positionAnchoredSurface(surface, rect, options = {}) {
|
|
310
|
+
fallbackPosition(surface, rect, options.placement ?? "bottom-start", options.gap ?? 4, options.viewportGap ?? 8, void 0, options.boundary);
|
|
311
|
+
}
|
|
51
312
|
/**
|
|
52
313
|
* One document-scoped scroll/resize listener and one frame for all active floating surfaces.
|
|
53
314
|
* The registry lives on the document so HTML and Vue bundles share the same coordinator.
|
|
@@ -174,6 +435,152 @@ function createViewportSurface(surface, options = {}) {
|
|
|
174
435
|
}
|
|
175
436
|
};
|
|
176
437
|
}
|
|
438
|
+
function createAnchoredSurface(surface, options = {}) {
|
|
439
|
+
const owner = surface.ownerDocument.defaultView;
|
|
440
|
+
let placement = options.placement ?? "bottom-start";
|
|
441
|
+
const gap = () => Math.max(0, Number(typeof options.gap === "function" ? options.gap() : options.gap ?? 4) || 0);
|
|
442
|
+
const viewportGap = Math.max(0, options.viewportGap ?? 8);
|
|
443
|
+
const boundary = options.boundary ?? null;
|
|
444
|
+
let anchor = options.anchor ?? null;
|
|
445
|
+
let point = null;
|
|
446
|
+
let open = false;
|
|
447
|
+
let destroyed = false;
|
|
448
|
+
let anchorFrame = null;
|
|
449
|
+
let lastAnchorRect = null;
|
|
450
|
+
let written = null;
|
|
451
|
+
const anchorRect = () => typeof anchor === "function" ? anchor() : anchor ? anchor.getBoundingClientRect() : written?.rect();
|
|
452
|
+
const position = () => {
|
|
453
|
+
if (!anchor && !point) {
|
|
454
|
+
if (!written) return;
|
|
455
|
+
show(surface);
|
|
456
|
+
const rect = lastAnchorRect = written.rect();
|
|
457
|
+
Object.assign(surface.style, {
|
|
458
|
+
left: `${Math.round(rect.left)}px`,
|
|
459
|
+
top: `${Math.round(rect.top)}px`,
|
|
460
|
+
right: "auto",
|
|
461
|
+
bottom: "auto"
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const rect = point ? {
|
|
466
|
+
left: point.x,
|
|
467
|
+
right: point.x,
|
|
468
|
+
top: point.y,
|
|
469
|
+
bottom: point.y,
|
|
470
|
+
width: 0,
|
|
471
|
+
height: 0
|
|
472
|
+
} : anchorRect();
|
|
473
|
+
if (!rect) return;
|
|
474
|
+
if (!point && typeof anchor !== "function" && (anchor.isConnected === false || rect.width === 0 && rect.height === 0 || anchor instanceof owner.Element && owner.getComputedStyle(anchor).visibility !== "visible")) {
|
|
475
|
+
if (lastAnchorRect) positionAnchoredSurface(surface, lastAnchorRect, {
|
|
476
|
+
placement,
|
|
477
|
+
gap: gap(),
|
|
478
|
+
viewportGap,
|
|
479
|
+
boundary
|
|
480
|
+
});
|
|
481
|
+
else hide(surface);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
lastAnchorRect = {
|
|
485
|
+
left: rect.left,
|
|
486
|
+
top: rect.top,
|
|
487
|
+
right: rect.right,
|
|
488
|
+
bottom: rect.bottom,
|
|
489
|
+
width: rect.width,
|
|
490
|
+
height: rect.height
|
|
491
|
+
};
|
|
492
|
+
show(surface);
|
|
493
|
+
if (options.maxWidth !== void 0) {
|
|
494
|
+
const maxWidth = typeof options.maxWidth === "function" ? options.maxWidth() : options.maxWidth;
|
|
495
|
+
surface.style.maxWidth = maxWidth === null ? "" : `${maxWidth}px`;
|
|
496
|
+
}
|
|
497
|
+
positionAnchoredSurface(surface, rect, {
|
|
498
|
+
placement: point ? "bottom-start" : placement,
|
|
499
|
+
gap: gap(),
|
|
500
|
+
viewportGap,
|
|
501
|
+
boundary
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
surface.style.position = "fixed";
|
|
505
|
+
surface.style.margin = "0";
|
|
506
|
+
const presentation = createViewportSurface(surface, {
|
|
507
|
+
position,
|
|
508
|
+
bare: options.bare
|
|
509
|
+
});
|
|
510
|
+
const trackAnchor = () => {
|
|
511
|
+
anchorFrame = null;
|
|
512
|
+
if (!open || !(anchor || written) || point) return;
|
|
513
|
+
const rect = anchorRect();
|
|
514
|
+
if (rect && (!lastAnchorRect || rect.left !== lastAnchorRect.left || rect.top !== lastAnchorRect.top || rect.width !== lastAnchorRect.width || rect.height !== lastAnchorRect.height)) presentation.refresh();
|
|
515
|
+
anchorFrame = owner.requestAnimationFrame(trackAnchor);
|
|
516
|
+
};
|
|
517
|
+
const stopTracking = () => {
|
|
518
|
+
if (anchorFrame !== null) owner.cancelAnimationFrame(anchorFrame);
|
|
519
|
+
anchorFrame = null;
|
|
520
|
+
lastAnchorRect = null;
|
|
521
|
+
};
|
|
522
|
+
const syncTracking = () => {
|
|
523
|
+
if (open && (anchor || written) && !point) {
|
|
524
|
+
if (anchorFrame === null) anchorFrame = owner.requestAnimationFrame(trackAnchor);
|
|
525
|
+
} else stopTracking();
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
setAnchor(next) {
|
|
529
|
+
point = null;
|
|
530
|
+
anchor = next;
|
|
531
|
+
syncTracking();
|
|
532
|
+
presentation.refresh();
|
|
533
|
+
},
|
|
534
|
+
setPlacement(next) {
|
|
535
|
+
placement = next || "bottom-start";
|
|
536
|
+
presentation.refresh();
|
|
537
|
+
},
|
|
538
|
+
show() {
|
|
539
|
+
if (destroyed) return;
|
|
540
|
+
point = null;
|
|
541
|
+
open = true;
|
|
542
|
+
if (anchor || topLayerOpen(surface)) {
|
|
543
|
+
presentation.show();
|
|
544
|
+
syncTracking();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
queueMicrotask(() => {
|
|
548
|
+
if (destroyed || !open || point || anchor || topLayerOpen(surface)) return;
|
|
549
|
+
written = writtenPlace(surface, [
|
|
550
|
+
"position",
|
|
551
|
+
"margin",
|
|
552
|
+
"left",
|
|
553
|
+
"top",
|
|
554
|
+
"right",
|
|
555
|
+
"bottom"
|
|
556
|
+
]);
|
|
557
|
+
presentation.show();
|
|
558
|
+
syncTracking();
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
showAtPoint(next) {
|
|
562
|
+
if (destroyed) return;
|
|
563
|
+
point = next;
|
|
564
|
+
open = true;
|
|
565
|
+
syncTracking();
|
|
566
|
+
presentation.show();
|
|
567
|
+
},
|
|
568
|
+
hide() {
|
|
569
|
+
open = false;
|
|
570
|
+
point = null;
|
|
571
|
+
syncTracking();
|
|
572
|
+
presentation.hide();
|
|
573
|
+
},
|
|
574
|
+
refresh: presentation.refresh,
|
|
575
|
+
destroy() {
|
|
576
|
+
destroyed = true;
|
|
577
|
+
open = false;
|
|
578
|
+
stopTracking();
|
|
579
|
+
presentation.destroy();
|
|
580
|
+
anchor = null;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
177
584
|
//#endregion
|
|
178
585
|
//#region ../../node_modules/.pnpm/@tiptap+extension-image@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-image/dist/index.js
|
|
179
586
|
/**
|
|
@@ -737,6 +1144,184 @@ function selectedTableCellElement(editor) {
|
|
|
737
1144
|
return selectionElement(editor)?.closest("td, th");
|
|
738
1145
|
}
|
|
739
1146
|
//#endregion
|
|
1147
|
+
//#region src/editor/selection-surface.ts
|
|
1148
|
+
var surfaceSequence = 0;
|
|
1149
|
+
/**
|
|
1150
|
+
* Shows `element` beside the selection whenever `shouldShow` allows, as Tiptap's bubble menu does,
|
|
1151
|
+
* and presents it through Looma's shared overlay layer: in the top layer, placed by the shared
|
|
1152
|
+
* flip/shift policy, travelling with its anchor, and registered on the overlay stack.
|
|
1153
|
+
*
|
|
1154
|
+
* @lifecycle Registers a ProseMirror plugin now; `destroy()` unregisters it and closes the surface.
|
|
1155
|
+
* The editor destroying its view closes the surface too.
|
|
1156
|
+
* @contract A press inside the surface keeps it open when the editor blurs; dragging hides it.
|
|
1157
|
+
*/
|
|
1158
|
+
function createSelectionSurface(options) {
|
|
1159
|
+
const { editor, element } = options;
|
|
1160
|
+
const document = element.ownerDocument;
|
|
1161
|
+
const id = `looma-selection-surface-${++surfaceSequence}`;
|
|
1162
|
+
const selectionRect = () => {
|
|
1163
|
+
const { view, state } = editor;
|
|
1164
|
+
const { ranges } = state.selection;
|
|
1165
|
+
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
|
1166
|
+
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
|
1167
|
+
if (isNodeSelection(state.selection)) {
|
|
1168
|
+
let node = view.nodeDOM(from);
|
|
1169
|
+
const wrapper = node?.dataset?.nodeViewWrapper ? node : node?.querySelector?.("[data-node-view-wrapper]");
|
|
1170
|
+
if (wrapper) node = wrapper.firstChild;
|
|
1171
|
+
if (node instanceof Element) return node.getBoundingClientRect();
|
|
1172
|
+
}
|
|
1173
|
+
return posToDOMRect(view, from, to);
|
|
1174
|
+
};
|
|
1175
|
+
let anchor = options.anchor ?? selectionRect;
|
|
1176
|
+
let lastRect = null;
|
|
1177
|
+
let open = false;
|
|
1178
|
+
let destroyed = false;
|
|
1179
|
+
let preventHide = false;
|
|
1180
|
+
let timer;
|
|
1181
|
+
const presentation = createAnchoredSurface(element, {
|
|
1182
|
+
anchor: () => {
|
|
1183
|
+
if (editor.isDestroyed) return lastRect;
|
|
1184
|
+
try {
|
|
1185
|
+
lastRect = anchor();
|
|
1186
|
+
} catch {}
|
|
1187
|
+
return lastRect;
|
|
1188
|
+
},
|
|
1189
|
+
placement: options.placement ?? "top",
|
|
1190
|
+
gap: 10,
|
|
1191
|
+
viewportGap: 5,
|
|
1192
|
+
boundary: options.boundary,
|
|
1193
|
+
maxWidth: options.maxWidth,
|
|
1194
|
+
bare: true
|
|
1195
|
+
});
|
|
1196
|
+
const host = element.parentElement;
|
|
1197
|
+
element.hidden = true;
|
|
1198
|
+
element.tabIndex = 0;
|
|
1199
|
+
element.remove();
|
|
1200
|
+
const show = () => {
|
|
1201
|
+
if (destroyed) return;
|
|
1202
|
+
if (!open) {
|
|
1203
|
+
open = true;
|
|
1204
|
+
openOverlay({
|
|
1205
|
+
id,
|
|
1206
|
+
modal: false,
|
|
1207
|
+
element,
|
|
1208
|
+
relatedElements: options.related?.() ?? [],
|
|
1209
|
+
dismissible: true,
|
|
1210
|
+
requestClose: (reason) => {
|
|
1211
|
+
if (reason === "escape" || reason === "light-dismiss") options.onDismiss?.(reason);
|
|
1212
|
+
else hide();
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (!element.isConnected) host?.append(element);
|
|
1217
|
+
presentation.show();
|
|
1218
|
+
};
|
|
1219
|
+
const close = () => {
|
|
1220
|
+
open = false;
|
|
1221
|
+
presentation.hide();
|
|
1222
|
+
element.remove();
|
|
1223
|
+
closeOverlay(document, id);
|
|
1224
|
+
};
|
|
1225
|
+
const hide = () => {
|
|
1226
|
+
if (!open || options.onHide?.() === false) return;
|
|
1227
|
+
close();
|
|
1228
|
+
};
|
|
1229
|
+
const decide = (view, oldState, selectionChanged, docChanged) => {
|
|
1230
|
+
const { state, composing } = view;
|
|
1231
|
+
if (composing || !selectionChanged && !docChanged) return;
|
|
1232
|
+
const { ranges } = state.selection;
|
|
1233
|
+
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
|
1234
|
+
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
|
1235
|
+
if (!options.shouldShow({
|
|
1236
|
+
editor,
|
|
1237
|
+
view,
|
|
1238
|
+
state,
|
|
1239
|
+
oldState,
|
|
1240
|
+
from,
|
|
1241
|
+
to
|
|
1242
|
+
})) {
|
|
1243
|
+
hide();
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
anchor = options.anchor ?? selectionRect;
|
|
1247
|
+
show();
|
|
1248
|
+
presentation.refresh();
|
|
1249
|
+
};
|
|
1250
|
+
const update = (view, oldState) => {
|
|
1251
|
+
const selectionChanged = !oldState?.selection.eq(view.state.selection);
|
|
1252
|
+
const docChanged = !oldState?.doc.eq(view.state.doc);
|
|
1253
|
+
const delay = options.updateDelay ?? 0;
|
|
1254
|
+
if (delay > 0 && view.state.selection.from !== view.state.selection.to) {
|
|
1255
|
+
if (!selectionChanged && !docChanged) return;
|
|
1256
|
+
clearTimeout(timer);
|
|
1257
|
+
timer = setTimeout(() => decide(view, oldState, selectionChanged, docChanged), delay);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
decide(view, oldState, selectionChanged, docChanged);
|
|
1261
|
+
};
|
|
1262
|
+
const onPointerDown = () => {
|
|
1263
|
+
preventHide = true;
|
|
1264
|
+
};
|
|
1265
|
+
const onDragStart = () => hide();
|
|
1266
|
+
const onFocus = () => {
|
|
1267
|
+
setTimeout(() => {
|
|
1268
|
+
if (!destroyed && !editor.isDestroyed) update(editor.view);
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1271
|
+
const onBlur = ({ event }) => {
|
|
1272
|
+
if (preventHide) {
|
|
1273
|
+
preventHide = false;
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const next = event?.relatedTarget;
|
|
1277
|
+
if (next instanceof Node && (element.contains(next) || next === editor.view.dom)) return;
|
|
1278
|
+
hide();
|
|
1279
|
+
};
|
|
1280
|
+
const onElementBlur = (event) => onBlur({ event });
|
|
1281
|
+
const key = new PluginKey(options.pluginKey);
|
|
1282
|
+
const plugin = new Plugin({
|
|
1283
|
+
key,
|
|
1284
|
+
view: (view) => {
|
|
1285
|
+
element.addEventListener("mousedown", onPointerDown, { capture: true });
|
|
1286
|
+
element.addEventListener("blur", onElementBlur);
|
|
1287
|
+
view.dom.addEventListener("dragstart", onDragStart);
|
|
1288
|
+
editor.on("focus", onFocus);
|
|
1289
|
+
editor.on("blur", onBlur);
|
|
1290
|
+
return {
|
|
1291
|
+
update,
|
|
1292
|
+
destroy() {
|
|
1293
|
+
clearTimeout(timer);
|
|
1294
|
+
element.removeEventListener("mousedown", onPointerDown, { capture: true });
|
|
1295
|
+
element.removeEventListener("blur", onElementBlur);
|
|
1296
|
+
view.dom.removeEventListener("dragstart", onDragStart);
|
|
1297
|
+
editor.off("focus", onFocus);
|
|
1298
|
+
editor.off("blur", onBlur);
|
|
1299
|
+
close();
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
editor.registerPlugin(plugin);
|
|
1305
|
+
return {
|
|
1306
|
+
element,
|
|
1307
|
+
isOpen: () => open,
|
|
1308
|
+
show,
|
|
1309
|
+
hide,
|
|
1310
|
+
setAnchor(next) {
|
|
1311
|
+
anchor = next;
|
|
1312
|
+
presentation.refresh();
|
|
1313
|
+
},
|
|
1314
|
+
destroy() {
|
|
1315
|
+
if (destroyed) return;
|
|
1316
|
+
destroyed = true;
|
|
1317
|
+
if (!editor.isDestroyed) editor.unregisterPlugin(key);
|
|
1318
|
+
clearTimeout(timer);
|
|
1319
|
+
close();
|
|
1320
|
+
presentation.destroy();
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
//#endregion
|
|
740
1325
|
//#region src/editor/session.ts
|
|
741
1326
|
function selectionSnapshot(selection) {
|
|
742
1327
|
return {
|
|
@@ -2134,6 +2719,7 @@ var LoomaEditor = defineComponent({
|
|
|
2134
2719
|
let heldFormattingSelection = null;
|
|
2135
2720
|
let formattingPopup = null;
|
|
2136
2721
|
let linkPopup = null;
|
|
2722
|
+
let imagePopup = null;
|
|
2137
2723
|
let heldPressTimer;
|
|
2138
2724
|
let heldPress = null;
|
|
2139
2725
|
const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
|
|
@@ -2327,7 +2913,7 @@ var LoomaEditor = defineComponent({
|
|
|
2327
2913
|
editorStateVersion.value += 1;
|
|
2328
2914
|
const instance = editor.value;
|
|
2329
2915
|
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
|
|
2330
|
-
linkPopup?.
|
|
2916
|
+
linkPopup?.setAnchor(() => link.getBoundingClientRect());
|
|
2331
2917
|
linkPopup?.show();
|
|
2332
2918
|
}
|
|
2333
2919
|
});
|
|
@@ -2468,7 +3054,7 @@ var LoomaEditor = defineComponent({
|
|
|
2468
3054
|
}).catch(() => {});
|
|
2469
3055
|
}
|
|
2470
3056
|
nextTick(() => {
|
|
2471
|
-
if (context) (props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? formattingPopup : linkPopup)?.
|
|
3057
|
+
if (context) (props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? formattingPopup : linkPopup)?.setAnchor(() => posToDOMRect(instance.view, instance.state.selection.from, instance.state.selection.to));
|
|
2472
3058
|
(linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
|
|
2473
3059
|
});
|
|
2474
3060
|
};
|
|
@@ -2537,6 +3123,7 @@ var LoomaEditor = defineComponent({
|
|
|
2537
3123
|
const selection = linkSelection;
|
|
2538
3124
|
if (restoreFocus && selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
|
|
2539
3125
|
dismissedFormattingSelection = editor.value?.state.selection ?? null;
|
|
3126
|
+
heldFormattingSelection = null;
|
|
2540
3127
|
linkPopup?.hide();
|
|
2541
3128
|
formattingPopup?.hide();
|
|
2542
3129
|
};
|
|
@@ -2816,7 +3403,7 @@ var LoomaEditor = defineComponent({
|
|
|
2816
3403
|
tablePickerOpen,
|
|
2817
3404
|
formatOpen
|
|
2818
3405
|
], ([link, table, format]) => {
|
|
2819
|
-
if (link || table || format || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.
|
|
3406
|
+
if (link || table || format || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.element.contains(document.activeElement)) return;
|
|
2820
3407
|
heldFormattingSelection = null;
|
|
2821
3408
|
formattingPopup?.hide();
|
|
2822
3409
|
});
|
|
@@ -2998,7 +3585,7 @@ var LoomaEditor = defineComponent({
|
|
|
2998
3585
|
updateCodeUi();
|
|
2999
3586
|
setTimeout(() => {
|
|
3000
3587
|
if (!tableInteractionActive && !editor.value?.isFocused && !mobileToolbarShell.value?.contains(document.activeElement)) editorFocused.value = false;
|
|
3001
|
-
if (!editor.value?.isFocused && !formattingPickerOpen() && !formattingPopup?.
|
|
3588
|
+
if (!editor.value?.isFocused && !formattingPickerOpen() && !formattingPopup?.element.contains(document.activeElement)) heldFormattingSelection = null;
|
|
3002
3589
|
}, 0);
|
|
3003
3590
|
};
|
|
3004
3591
|
const bindEditorUi = (instance) => {
|
|
@@ -3053,44 +3640,6 @@ var LoomaEditor = defineComponent({
|
|
|
3053
3640
|
const tableToolbarRef = popupRef(tableToolbarShell);
|
|
3054
3641
|
const codeLanguageRef = popupRef(codeLanguageShell, true);
|
|
3055
3642
|
const mobileToolbarRef = popupRef(mobileToolbarShell, true);
|
|
3056
|
-
const tippyPresentations = /* @__PURE__ */ new WeakMap();
|
|
3057
|
-
const floatingTippyOptions = (placement, boundary = null) => ({
|
|
3058
|
-
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
3059
|
-
popperOptions: {
|
|
3060
|
-
strategy: "fixed",
|
|
3061
|
-
modifiers: [{
|
|
3062
|
-
name: "eventListeners",
|
|
3063
|
-
enabled: false
|
|
3064
|
-
}, ...boundary ? [{
|
|
3065
|
-
name: "flip",
|
|
3066
|
-
options: { boundary }
|
|
3067
|
-
}, {
|
|
3068
|
-
name: "preventOverflow",
|
|
3069
|
-
options: {
|
|
3070
|
-
boundary,
|
|
3071
|
-
altAxis: true
|
|
3072
|
-
}
|
|
3073
|
-
}] : []]
|
|
3074
|
-
},
|
|
3075
|
-
onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
|
|
3076
|
-
onMount: (instance) => {
|
|
3077
|
-
let presentation = tippyPresentations.get(instance.popper);
|
|
3078
|
-
if (!presentation) {
|
|
3079
|
-
presentation = createViewportSurface(instance.popper, {
|
|
3080
|
-
bare: true,
|
|
3081
|
-
position: () => {
|
|
3082
|
-
instance.popperInstance?.update();
|
|
3083
|
-
}
|
|
3084
|
-
});
|
|
3085
|
-
tippyPresentations.set(instance.popper, presentation);
|
|
3086
|
-
}
|
|
3087
|
-
presentation.show();
|
|
3088
|
-
},
|
|
3089
|
-
onHidden: (instance) => tippyPresentations.get(instance.popper)?.hide(),
|
|
3090
|
-
onDestroy: (instance) => tippyPresentations.get(instance.popper)?.destroy(),
|
|
3091
|
-
duration: 100,
|
|
3092
|
-
placement
|
|
3093
|
-
});
|
|
3094
3643
|
const onDocumentPointerDown = (event) => {
|
|
3095
3644
|
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
3096
3645
|
const inElement = (element) => Boolean(element && (path.includes(element) || event.target instanceof Node && element.contains(event.target)));
|
|
@@ -3118,7 +3667,7 @@ var LoomaEditor = defineComponent({
|
|
|
3118
3667
|
if (!heldPress || !floatingFormattingMode() || tableResizeActive || !props.editable || !instance.isFocused || slash.active || mention.active || !(selection instanceof TextSelection)) return;
|
|
3119
3668
|
dismissedFormattingSelection = null;
|
|
3120
3669
|
heldFormattingSelection = selection;
|
|
3121
|
-
formattingPopup?.
|
|
3670
|
+
formattingPopup?.setAnchor(() => posToDOMRect(instance.view, selection.from, selection.to));
|
|
3122
3671
|
formattingPopup?.show();
|
|
3123
3672
|
}, 500);
|
|
3124
3673
|
};
|
|
@@ -3130,7 +3679,11 @@ var LoomaEditor = defineComponent({
|
|
|
3130
3679
|
};
|
|
3131
3680
|
const onFormattingKeyDown = (event) => {
|
|
3132
3681
|
cancelHeldPress();
|
|
3133
|
-
if (event.key !== "Escape" || !(event.target instanceof Node) || !(root.value?.contains(event.target) || formattingPopup?.
|
|
3682
|
+
if (event.key !== "Escape" || !(event.target instanceof Node) || !(root.value?.contains(event.target) || formattingPopup?.element.contains(event.target) || linkPopup?.element.contains(event.target)) || !(formatOpen.value || formattingPopup?.isOpen() || linkPopup?.isOpen())) return false;
|
|
3683
|
+
dismissFormatting();
|
|
3684
|
+
return true;
|
|
3685
|
+
};
|
|
3686
|
+
const dismissFormatting = () => {
|
|
3134
3687
|
const selection = editor.value?.state.selection;
|
|
3135
3688
|
dismissedFormattingSelection = selection instanceof TextSelection || selection instanceof AllSelection ? selection : null;
|
|
3136
3689
|
heldFormattingSelection = null;
|
|
@@ -3138,8 +3691,57 @@ var LoomaEditor = defineComponent({
|
|
|
3138
3691
|
formattingPopup?.hide();
|
|
3139
3692
|
linkPopup?.hide();
|
|
3140
3693
|
hideTool();
|
|
3141
|
-
return true;
|
|
3142
3694
|
};
|
|
3695
|
+
const surfaceRef = (assign, options) => {
|
|
3696
|
+
let current = null;
|
|
3697
|
+
return (node) => {
|
|
3698
|
+
const element = node instanceof HTMLElement ? node : null;
|
|
3699
|
+
if (current?.element === element) return;
|
|
3700
|
+
current?.destroy();
|
|
3701
|
+
current = element && editor.value ? createSelectionSurface(options(element, editor.value)) : null;
|
|
3702
|
+
assign(current);
|
|
3703
|
+
};
|
|
3704
|
+
};
|
|
3705
|
+
const editorRoot = () => root.value ? [root.value] : [];
|
|
3706
|
+
const formattingSurfaceRef = surfaceRef((surface) => {
|
|
3707
|
+
formattingPopup = surface;
|
|
3708
|
+
}, (element, instance) => ({
|
|
3709
|
+
editor: instance,
|
|
3710
|
+
element,
|
|
3711
|
+
pluginKey: "looma-text-formatting-menu",
|
|
3712
|
+
shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (!props.editable && (props.selectionActions.length > 0 || props.rangeActions.length > 0) || menuEditor.isFocused || formattingPopup?.element.contains(document.activeElement) || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
|
|
3713
|
+
boundary: () => props.toolbarMode === "contextual" ? root.value : null,
|
|
3714
|
+
related: editorRoot,
|
|
3715
|
+
onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0,
|
|
3716
|
+
onDismiss: (reason) => reason === "escape" ? dismissFormatting() : formattingPopup?.hide()
|
|
3717
|
+
}));
|
|
3718
|
+
const linkSurfaceRef = surfaceRef((surface) => {
|
|
3719
|
+
linkPopup = surface;
|
|
3720
|
+
}, (element, instance) => ({
|
|
3721
|
+
editor: instance,
|
|
3722
|
+
element,
|
|
3723
|
+
pluginKey: "looma-link-context-menu",
|
|
3724
|
+
shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && !linkOpen.value && !dismissedFormattingSelection?.eq(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkContextEditing.value || linkPopup?.element.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(menuEditor)),
|
|
3725
|
+
maxWidth: () => Math.min(480, window.innerWidth - 24),
|
|
3726
|
+
related: editorRoot,
|
|
3727
|
+
onDismiss: (reason) => reason === "escape" ? dismissFormatting() : linkPopup?.hide()
|
|
3728
|
+
}));
|
|
3729
|
+
const imageSurfaceRef = surfaceRef((surface) => {
|
|
3730
|
+
imagePopup = surface;
|
|
3731
|
+
}, (element, instance) => ({
|
|
3732
|
+
editor: instance,
|
|
3733
|
+
element,
|
|
3734
|
+
pluginKey: "looma-image-actions",
|
|
3735
|
+
updateDelay: 250,
|
|
3736
|
+
shouldShow: ({ editor: menuEditor }) => menuEditor.isEditable && menuEditor.state.selection instanceof NodeSelection && menuEditor.state.selection.node.type.name === "image",
|
|
3737
|
+
anchor: () => {
|
|
3738
|
+
const dom = instance.view.nodeDOM(instance.state.selection.from);
|
|
3739
|
+
return (dom instanceof Element ? dom.querySelector("img") ?? dom : instance.view.dom).getBoundingClientRect();
|
|
3740
|
+
},
|
|
3741
|
+
maxWidth: () => window.innerWidth - 24,
|
|
3742
|
+
related: editorRoot,
|
|
3743
|
+
onDismiss: () => imagePopup?.hide()
|
|
3744
|
+
}));
|
|
3143
3745
|
const onResizePointerDown = (event) => {
|
|
3144
3746
|
tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
|
|
3145
3747
|
};
|
|
@@ -3681,47 +4283,16 @@ var LoomaEditor = defineComponent({
|
|
|
3681
4283
|
placement: "top",
|
|
3682
4284
|
inverse: true
|
|
3683
4285
|
}, () => "Drag to resize column") : null,
|
|
3684
|
-
h(
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
maxWidth: "none",
|
|
3695
|
-
onCreate: (popup) => {
|
|
3696
|
-
formattingPopup = popup;
|
|
3697
|
-
},
|
|
3698
|
-
onDestroy: (popup) => {
|
|
3699
|
-
formattingPopup = null;
|
|
3700
|
-
tippyPresentations.get(popup.popper)?.destroy();
|
|
3701
|
-
},
|
|
3702
|
-
onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0
|
|
3703
|
-
}
|
|
3704
|
-
}, { default: () => props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkContextEditing.value || instance.state.selection.empty && instance.isActive("link")) && !linkOpen.value && formattingSelectionActive(instance) ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover") }) : null]),
|
|
3705
|
-
h(Stack, {
|
|
3706
|
-
key: "looma-link-popup-host",
|
|
3707
|
-
gap: "none"
|
|
3708
|
-
}, () => [instance && props.editable ? h(BubbleMenu, {
|
|
3709
|
-
editor: instance,
|
|
3710
|
-
pluginKey: "looma-link-context-menu",
|
|
3711
|
-
updateDelay: 0,
|
|
3712
|
-
shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && !linkOpen.value && !dismissedFormattingSelection?.eq(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkContextEditing.value || linkPopup?.popper.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(menuEditor)),
|
|
3713
|
-
tippyOptions: {
|
|
3714
|
-
...floatingTippyOptions("top"),
|
|
3715
|
-
maxWidth: Math.min(480, window.innerWidth - 24),
|
|
3716
|
-
onCreate: (popup) => {
|
|
3717
|
-
linkPopup = popup;
|
|
3718
|
-
},
|
|
3719
|
-
onDestroy: (popup) => {
|
|
3720
|
-
linkPopup = null;
|
|
3721
|
-
tippyPresentations.get(popup.popper)?.destroy();
|
|
3722
|
-
}
|
|
3723
|
-
}
|
|
3724
|
-
}, { default: () => props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? null : renderLinkContextCard(instance) }) : null]),
|
|
4286
|
+
instance && (props.editable || props.selectionActions.length || props.rangeActions.length) && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h("div", { key: "looma-formatting-host" }, [h("div", {
|
|
4287
|
+
ref: formattingSurfaceRef,
|
|
4288
|
+
class: "looma-editor__floating-shell looma-editor__formatting-shell",
|
|
4289
|
+
hidden: true
|
|
4290
|
+
}, [props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkContextEditing.value || instance.state.selection.empty && instance.isActive("link")) && !linkOpen.value && formattingSelectionActive(instance) ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover")])]) : null,
|
|
4291
|
+
instance && props.editable ? h("div", { key: "looma-link-host" }, [h("div", {
|
|
4292
|
+
ref: linkSurfaceRef,
|
|
4293
|
+
class: "looma-editor__floating-shell looma-editor__link-shell",
|
|
4294
|
+
hidden: true
|
|
4295
|
+
}, [props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? null : renderLinkContextCard(instance)])]) : null,
|
|
3725
4296
|
instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
|
|
3726
4297
|
class: "looma-editor__formatting-popover",
|
|
3727
4298
|
role: "region",
|
|
@@ -3737,21 +4308,11 @@ var LoomaEditor = defineComponent({
|
|
|
3737
4308
|
role: "toolbar",
|
|
3738
4309
|
"aria-label": "Editor tools"
|
|
3739
4310
|
}, [renderToolbar(instance, false)]) : null,
|
|
3740
|
-
instance && props.editable ? h(
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
3746
|
-
placement: "top",
|
|
3747
|
-
duration: 100,
|
|
3748
|
-
maxWidth: window.innerWidth - 24,
|
|
3749
|
-
getReferenceClientRect: () => {
|
|
3750
|
-
const dom = instance.view.nodeDOM(instance.state.selection.from);
|
|
3751
|
-
return (dom instanceof Element ? dom.querySelector("img") ?? dom : instance.view.dom).getBoundingClientRect();
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
}, { default: () => h(ImageControls, { editor: instance }) }) : null,
|
|
4311
|
+
instance && props.editable ? h("div", { key: "looma-image-host" }, [h("div", {
|
|
4312
|
+
ref: imageSurfaceRef,
|
|
4313
|
+
class: "looma-editor__floating-shell looma-editor__image-shell",
|
|
4314
|
+
hidden: true
|
|
4315
|
+
}, [h(ImageControls, { editor: instance })])]) : null,
|
|
3755
4316
|
instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h("div", { ref: attachDocument })) : null,
|
|
3756
4317
|
codeUi.open ? h("div", {
|
|
3757
4318
|
ref: codeLanguageRef,
|