@threadlabs/looma 0.28.1 → 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/editor/session.d.ts +39 -0
- package/package.json +1 -1
- package/vue/editor/compact/index.js +2 -2
- package/vue/editor/index.js +810 -207
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 { 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,241 @@ 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
|
|
1325
|
+
//#region src/editor/session.ts
|
|
1326
|
+
function selectionSnapshot(selection) {
|
|
1327
|
+
return {
|
|
1328
|
+
bookmark: selection.getBookmark(),
|
|
1329
|
+
from: selection.from,
|
|
1330
|
+
to: selection.to,
|
|
1331
|
+
text: selection instanceof TextSelection
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Creates the session's editor from `options`. A framework layer may pass its own `Editor`
|
|
1336
|
+
* subclass, such as `@tiptap/vue-3`'s reactive one, so the editor it exposes keeps that behavior.
|
|
1337
|
+
*/
|
|
1338
|
+
function createEditorSession(options, EditorClass = Editor) {
|
|
1339
|
+
const editor = new EditorClass(options);
|
|
1340
|
+
return {
|
|
1341
|
+
editor,
|
|
1342
|
+
attach(element) {
|
|
1343
|
+
const current = editor.options.element;
|
|
1344
|
+
if (editor.isDestroyed || current === element || !current.firstChild) return;
|
|
1345
|
+
element.append(...current.childNodes);
|
|
1346
|
+
editor.setOptions({ element });
|
|
1347
|
+
editor.createNodeViews();
|
|
1348
|
+
},
|
|
1349
|
+
setContent(content, selection) {
|
|
1350
|
+
if (editor.isDestroyed || sameDocument(editor.getJSON(), content)) return false;
|
|
1351
|
+
const previous = selection ?? selectionSnapshot(editor.view.state.selection);
|
|
1352
|
+
if (!editor.isFocused) {
|
|
1353
|
+
editor.commands.setContent(content, false);
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
editor.chain().setContent(content, false).command(({ tr }) => {
|
|
1357
|
+
try {
|
|
1358
|
+
tr.setSelection(previous.text ? TextSelection.create(tr.doc, previous.from, previous.to) : previous.bookmark.resolve(tr.doc));
|
|
1359
|
+
} catch {
|
|
1360
|
+
const max = tr.doc.content.size;
|
|
1361
|
+
const from = Math.max(1, Math.min(previous.from, max));
|
|
1362
|
+
const to = Math.max(from, Math.min(previous.to, max));
|
|
1363
|
+
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
|
1364
|
+
}
|
|
1365
|
+
return true;
|
|
1366
|
+
}).run();
|
|
1367
|
+
editor.view.focus();
|
|
1368
|
+
return true;
|
|
1369
|
+
},
|
|
1370
|
+
subscribe(listener) {
|
|
1371
|
+
editor.on("transaction", listener);
|
|
1372
|
+
return () => {
|
|
1373
|
+
editor.off("transaction", listener);
|
|
1374
|
+
};
|
|
1375
|
+
},
|
|
1376
|
+
destroy() {
|
|
1377
|
+
editor.destroy();
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
//#endregion
|
|
740
1382
|
//#region src/editor/shortcuts.ts
|
|
741
1383
|
/**
|
|
742
1384
|
* Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
|
|
@@ -2077,6 +2719,7 @@ var LoomaEditor = defineComponent({
|
|
|
2077
2719
|
let heldFormattingSelection = null;
|
|
2078
2720
|
let formattingPopup = null;
|
|
2079
2721
|
let linkPopup = null;
|
|
2722
|
+
let imagePopup = null;
|
|
2080
2723
|
let heldPressTimer;
|
|
2081
2724
|
let heldPress = null;
|
|
2082
2725
|
const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
|
|
@@ -2206,13 +2849,7 @@ var LoomaEditor = defineComponent({
|
|
|
2206
2849
|
let linkPressedSelection = null;
|
|
2207
2850
|
const rememberSelection = (instance) => {
|
|
2208
2851
|
if (!instance.isFocused) return;
|
|
2209
|
-
|
|
2210
|
-
lastFocusedSelection = {
|
|
2211
|
-
bookmark: selection.getBookmark(),
|
|
2212
|
-
from: selection.from,
|
|
2213
|
-
to: selection.to,
|
|
2214
|
-
text: selection instanceof TextSelection
|
|
2215
|
-
};
|
|
2852
|
+
lastFocusedSelection = selectionSnapshot(instance.view.state.selection);
|
|
2216
2853
|
};
|
|
2217
2854
|
const authorHighlight = !props.disableHighlight;
|
|
2218
2855
|
const codeHighlighting = createCodeHighlighting(props.codeLanguages, () => {
|
|
@@ -2228,83 +2865,96 @@ var LoomaEditor = defineComponent({
|
|
|
2228
2865
|
style: {}
|
|
2229
2866
|
});
|
|
2230
2867
|
const loadForCodeBlock = codeHighlighting.loadForCodeBlock;
|
|
2231
|
-
const editor =
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
if (view.isDestroyed) return;
|
|
2273
|
-
editorStateVersion.value += 1;
|
|
2274
|
-
const instance = editor.value;
|
|
2275
|
-
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
|
|
2276
|
-
linkPopup?.setProps({ getReferenceClientRect: () => link.getBoundingClientRect() });
|
|
2277
|
-
linkPopup?.show();
|
|
2868
|
+
const editor = shallowRef();
|
|
2869
|
+
let session;
|
|
2870
|
+
onMounted(() => {
|
|
2871
|
+
session = createEditorSession({
|
|
2872
|
+
extensions: [
|
|
2873
|
+
...getDefaultEditorExtensions({
|
|
2874
|
+
placeholder: props.placeholder,
|
|
2875
|
+
linkBaseUrl: props.linkBaseUrl,
|
|
2876
|
+
mention: mentionExtension ?? false,
|
|
2877
|
+
disableHighlight: props.disableHighlight,
|
|
2878
|
+
codeLanguages: props.codeLanguages ?? void 0,
|
|
2879
|
+
codeLowlight: codeHighlighting.lowlight,
|
|
2880
|
+
tableOfContentsNodeView,
|
|
2881
|
+
expandNodeView,
|
|
2882
|
+
dividerNodeView,
|
|
2883
|
+
calloutNodeView,
|
|
2884
|
+
imageNodeView
|
|
2885
|
+
}),
|
|
2886
|
+
imageDelivery.extension,
|
|
2887
|
+
slashExtension,
|
|
2888
|
+
...props.extensions
|
|
2889
|
+
],
|
|
2890
|
+
content: props.modelValue,
|
|
2891
|
+
editable: props.editable,
|
|
2892
|
+
editorProps: {
|
|
2893
|
+
attributes: {
|
|
2894
|
+
role: "textbox",
|
|
2895
|
+
"aria-multiline": "true",
|
|
2896
|
+
"aria-label": props.label
|
|
2897
|
+
},
|
|
2898
|
+
handleKeyDown: (_view, event) => onFormattingKeyDown(event),
|
|
2899
|
+
handleDOMEvents: { click: (view, event) => {
|
|
2900
|
+
if (!(event.target instanceof Element) || event.button !== 0) return false;
|
|
2901
|
+
const link = event.target.closest("a[href]");
|
|
2902
|
+
if (!link || !view.dom.contains(link)) return false;
|
|
2903
|
+
const href = validLinkHref(link.getAttribute("href") ?? "");
|
|
2904
|
+
if (!href) return false;
|
|
2905
|
+
if (isApplePlatform() ? event.metaKey : event.ctrlKey) {
|
|
2906
|
+
event.preventDefault();
|
|
2907
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
2908
|
+
return true;
|
|
2278
2909
|
}
|
|
2910
|
+
dismissedFormattingSelection = null;
|
|
2911
|
+
requestAnimationFrame(() => {
|
|
2912
|
+
if (view.isDestroyed) return;
|
|
2913
|
+
editorStateVersion.value += 1;
|
|
2914
|
+
const instance = editor.value;
|
|
2915
|
+
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
|
|
2916
|
+
linkPopup?.setAnchor(() => link.getBoundingClientRect());
|
|
2917
|
+
linkPopup?.show();
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
return false;
|
|
2921
|
+
} }
|
|
2922
|
+
},
|
|
2923
|
+
onCreate: ({ editor: instance }) => {
|
|
2924
|
+
emit("ready", instance);
|
|
2925
|
+
instance.state.doc.descendants((node) => {
|
|
2926
|
+
if (node.type.name === "codeBlock") loadForCodeBlock(instance, node.attrs.language, node.textContent);
|
|
2279
2927
|
});
|
|
2280
|
-
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
if (!formattingSelectionActive(instance)) formattingPopup?.hide();
|
|
2928
|
+
},
|
|
2929
|
+
onFocus: ({ editor: instance }) => rememberSelection(instance),
|
|
2930
|
+
onSelectionUpdate: ({ editor: instance }) => {
|
|
2931
|
+
rememberSelection(instance);
|
|
2932
|
+
if (floatingFormattingMode()) {
|
|
2933
|
+
const selection = instance.state.selection;
|
|
2934
|
+
if (!dismissedFormattingSelection?.eq(selection)) dismissedFormattingSelection = null;
|
|
2935
|
+
if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
|
|
2936
|
+
if (!formattingSelectionActive(instance)) formattingPopup?.hide();
|
|
2937
|
+
}
|
|
2938
|
+
if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
|
|
2939
|
+
},
|
|
2940
|
+
onUpdate: ({ editor: instance }) => {
|
|
2941
|
+
const value = instance.getJSON();
|
|
2942
|
+
emit("update:modelValue", value);
|
|
2943
|
+
emit("update", value);
|
|
2297
2944
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
onTransaction: () => {
|
|
2945
|
+
}, Editor$1);
|
|
2946
|
+
session.subscribe(() => {
|
|
2301
2947
|
editorStateVersion.value += 1;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2948
|
+
});
|
|
2949
|
+
editor.value = session.editor;
|
|
2950
|
+
});
|
|
2951
|
+
const attachDocument = (element) => {
|
|
2952
|
+
if (element instanceof HTMLElement) session?.attach(element);
|
|
2953
|
+
};
|
|
2954
|
+
onBeforeUnmount(() => {
|
|
2955
|
+
const element = session?.editor.options.element;
|
|
2956
|
+
element?.parentNode?.replaceChild(element.cloneNode(true), element);
|
|
2957
|
+
session?.destroy();
|
|
2308
2958
|
});
|
|
2309
2959
|
watch(() => {
|
|
2310
2960
|
editorStateVersion.value;
|
|
@@ -2404,7 +3054,7 @@ var LoomaEditor = defineComponent({
|
|
|
2404
3054
|
}).catch(() => {});
|
|
2405
3055
|
}
|
|
2406
3056
|
nextTick(() => {
|
|
2407
|
-
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));
|
|
2408
3058
|
(linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
|
|
2409
3059
|
});
|
|
2410
3060
|
};
|
|
@@ -2473,6 +3123,7 @@ var LoomaEditor = defineComponent({
|
|
|
2473
3123
|
const selection = linkSelection;
|
|
2474
3124
|
if (restoreFocus && selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
|
|
2475
3125
|
dismissedFormattingSelection = editor.value?.state.selection ?? null;
|
|
3126
|
+
heldFormattingSelection = null;
|
|
2476
3127
|
linkPopup?.hide();
|
|
2477
3128
|
formattingPopup?.hide();
|
|
2478
3129
|
};
|
|
@@ -2752,7 +3403,7 @@ var LoomaEditor = defineComponent({
|
|
|
2752
3403
|
tablePickerOpen,
|
|
2753
3404
|
formatOpen
|
|
2754
3405
|
], ([link, table, format]) => {
|
|
2755
|
-
if (link || table || format || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.
|
|
3406
|
+
if (link || table || format || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.element.contains(document.activeElement)) return;
|
|
2756
3407
|
heldFormattingSelection = null;
|
|
2757
3408
|
formattingPopup?.hide();
|
|
2758
3409
|
});
|
|
@@ -2773,31 +3424,9 @@ var LoomaEditor = defineComponent({
|
|
|
2773
3424
|
});
|
|
2774
3425
|
watch(() => props.modelValue, (value) => {
|
|
2775
3426
|
const instance = editor.value;
|
|
2776
|
-
if (!instance ||
|
|
3427
|
+
if (!instance || !session) return;
|
|
2777
3428
|
const wasFocused = instance.isFocused;
|
|
2778
|
-
|
|
2779
|
-
bookmark: instance.view.state.selection.getBookmark(),
|
|
2780
|
-
from: instance.view.state.selection.from,
|
|
2781
|
-
to: instance.view.state.selection.to,
|
|
2782
|
-
text: instance.view.state.selection instanceof TextSelection
|
|
2783
|
-
};
|
|
2784
|
-
if (!wasFocused) {
|
|
2785
|
-
instance.commands.setContent(value, false);
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
instance.chain().setContent(value, false).command(({ tr }) => {
|
|
2789
|
-
try {
|
|
2790
|
-
tr.setSelection(previousSelection.text ? TextSelection.create(tr.doc, previousSelection.from, previousSelection.to) : previousSelection.bookmark.resolve(tr.doc));
|
|
2791
|
-
} catch {
|
|
2792
|
-
const max = tr.doc.content.size;
|
|
2793
|
-
const from = Math.max(1, Math.min(previousSelection.from, max));
|
|
2794
|
-
const to = Math.max(from, Math.min(previousSelection.to, max));
|
|
2795
|
-
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
|
2796
|
-
}
|
|
2797
|
-
return true;
|
|
2798
|
-
}).run();
|
|
2799
|
-
instance.view.focus();
|
|
2800
|
-
rememberSelection(instance);
|
|
3429
|
+
if (session.setContent(value, lastFocusedSelection) && wasFocused) rememberSelection(instance);
|
|
2801
3430
|
}, { deep: true });
|
|
2802
3431
|
const closeTableUi = () => {
|
|
2803
3432
|
tableUi.toolbarOpen = false;
|
|
@@ -2956,7 +3585,7 @@ var LoomaEditor = defineComponent({
|
|
|
2956
3585
|
updateCodeUi();
|
|
2957
3586
|
setTimeout(() => {
|
|
2958
3587
|
if (!tableInteractionActive && !editor.value?.isFocused && !mobileToolbarShell.value?.contains(document.activeElement)) editorFocused.value = false;
|
|
2959
|
-
if (!editor.value?.isFocused && !formattingPickerOpen() && !formattingPopup?.
|
|
3588
|
+
if (!editor.value?.isFocused && !formattingPickerOpen() && !formattingPopup?.element.contains(document.activeElement)) heldFormattingSelection = null;
|
|
2960
3589
|
}, 0);
|
|
2961
3590
|
};
|
|
2962
3591
|
const bindEditorUi = (instance) => {
|
|
@@ -3011,44 +3640,6 @@ var LoomaEditor = defineComponent({
|
|
|
3011
3640
|
const tableToolbarRef = popupRef(tableToolbarShell);
|
|
3012
3641
|
const codeLanguageRef = popupRef(codeLanguageShell, true);
|
|
3013
3642
|
const mobileToolbarRef = popupRef(mobileToolbarShell, true);
|
|
3014
|
-
const tippyPresentations = /* @__PURE__ */ new WeakMap();
|
|
3015
|
-
const floatingTippyOptions = (placement, boundary = null) => ({
|
|
3016
|
-
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
3017
|
-
popperOptions: {
|
|
3018
|
-
strategy: "fixed",
|
|
3019
|
-
modifiers: [{
|
|
3020
|
-
name: "eventListeners",
|
|
3021
|
-
enabled: false
|
|
3022
|
-
}, ...boundary ? [{
|
|
3023
|
-
name: "flip",
|
|
3024
|
-
options: { boundary }
|
|
3025
|
-
}, {
|
|
3026
|
-
name: "preventOverflow",
|
|
3027
|
-
options: {
|
|
3028
|
-
boundary,
|
|
3029
|
-
altAxis: true
|
|
3030
|
-
}
|
|
3031
|
-
}] : []]
|
|
3032
|
-
},
|
|
3033
|
-
onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
|
|
3034
|
-
onMount: (instance) => {
|
|
3035
|
-
let presentation = tippyPresentations.get(instance.popper);
|
|
3036
|
-
if (!presentation) {
|
|
3037
|
-
presentation = createViewportSurface(instance.popper, {
|
|
3038
|
-
bare: true,
|
|
3039
|
-
position: () => {
|
|
3040
|
-
instance.popperInstance?.update();
|
|
3041
|
-
}
|
|
3042
|
-
});
|
|
3043
|
-
tippyPresentations.set(instance.popper, presentation);
|
|
3044
|
-
}
|
|
3045
|
-
presentation.show();
|
|
3046
|
-
},
|
|
3047
|
-
onHidden: (instance) => tippyPresentations.get(instance.popper)?.hide(),
|
|
3048
|
-
onDestroy: (instance) => tippyPresentations.get(instance.popper)?.destroy(),
|
|
3049
|
-
duration: 100,
|
|
3050
|
-
placement
|
|
3051
|
-
});
|
|
3052
3643
|
const onDocumentPointerDown = (event) => {
|
|
3053
3644
|
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
3054
3645
|
const inElement = (element) => Boolean(element && (path.includes(element) || event.target instanceof Node && element.contains(event.target)));
|
|
@@ -3076,7 +3667,7 @@ var LoomaEditor = defineComponent({
|
|
|
3076
3667
|
if (!heldPress || !floatingFormattingMode() || tableResizeActive || !props.editable || !instance.isFocused || slash.active || mention.active || !(selection instanceof TextSelection)) return;
|
|
3077
3668
|
dismissedFormattingSelection = null;
|
|
3078
3669
|
heldFormattingSelection = selection;
|
|
3079
|
-
formattingPopup?.
|
|
3670
|
+
formattingPopup?.setAnchor(() => posToDOMRect(instance.view, selection.from, selection.to));
|
|
3080
3671
|
formattingPopup?.show();
|
|
3081
3672
|
}, 500);
|
|
3082
3673
|
};
|
|
@@ -3088,7 +3679,11 @@ var LoomaEditor = defineComponent({
|
|
|
3088
3679
|
};
|
|
3089
3680
|
const onFormattingKeyDown = (event) => {
|
|
3090
3681
|
cancelHeldPress();
|
|
3091
|
-
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 = () => {
|
|
3092
3687
|
const selection = editor.value?.state.selection;
|
|
3093
3688
|
dismissedFormattingSelection = selection instanceof TextSelection || selection instanceof AllSelection ? selection : null;
|
|
3094
3689
|
heldFormattingSelection = null;
|
|
@@ -3096,8 +3691,57 @@ var LoomaEditor = defineComponent({
|
|
|
3096
3691
|
formattingPopup?.hide();
|
|
3097
3692
|
linkPopup?.hide();
|
|
3098
3693
|
hideTool();
|
|
3099
|
-
return true;
|
|
3100
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
|
+
}));
|
|
3101
3745
|
const onResizePointerDown = (event) => {
|
|
3102
3746
|
tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
|
|
3103
3747
|
};
|
|
@@ -3639,47 +4283,16 @@ var LoomaEditor = defineComponent({
|
|
|
3639
4283
|
placement: "top",
|
|
3640
4284
|
inverse: true
|
|
3641
4285
|
}, () => "Drag to resize column") : null,
|
|
3642
|
-
h(
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
maxWidth: "none",
|
|
3653
|
-
onCreate: (popup) => {
|
|
3654
|
-
formattingPopup = popup;
|
|
3655
|
-
},
|
|
3656
|
-
onDestroy: (popup) => {
|
|
3657
|
-
formattingPopup = null;
|
|
3658
|
-
tippyPresentations.get(popup.popper)?.destroy();
|
|
3659
|
-
},
|
|
3660
|
-
onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0
|
|
3661
|
-
}
|
|
3662
|
-
}, { 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]),
|
|
3663
|
-
h(Stack, {
|
|
3664
|
-
key: "looma-link-popup-host",
|
|
3665
|
-
gap: "none"
|
|
3666
|
-
}, () => [instance && props.editable ? h(BubbleMenu, {
|
|
3667
|
-
editor: instance,
|
|
3668
|
-
pluginKey: "looma-link-context-menu",
|
|
3669
|
-
updateDelay: 0,
|
|
3670
|
-
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)),
|
|
3671
|
-
tippyOptions: {
|
|
3672
|
-
...floatingTippyOptions("top"),
|
|
3673
|
-
maxWidth: Math.min(480, window.innerWidth - 24),
|
|
3674
|
-
onCreate: (popup) => {
|
|
3675
|
-
linkPopup = popup;
|
|
3676
|
-
},
|
|
3677
|
-
onDestroy: (popup) => {
|
|
3678
|
-
linkPopup = null;
|
|
3679
|
-
tippyPresentations.get(popup.popper)?.destroy();
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
}, { 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,
|
|
3683
4296
|
instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
|
|
3684
4297
|
class: "looma-editor__formatting-popover",
|
|
3685
4298
|
role: "region",
|
|
@@ -3695,22 +4308,12 @@ var LoomaEditor = defineComponent({
|
|
|
3695
4308
|
role: "toolbar",
|
|
3696
4309
|
"aria-label": "Editor tools"
|
|
3697
4310
|
}, [renderToolbar(instance, false)]) : null,
|
|
3698
|
-
instance && props.editable ? h(
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
placement: "top",
|
|
3705
|
-
duration: 100,
|
|
3706
|
-
maxWidth: window.innerWidth - 24,
|
|
3707
|
-
getReferenceClientRect: () => {
|
|
3708
|
-
const dom = instance.view.nodeDOM(instance.state.selection.from);
|
|
3709
|
-
return (dom instanceof Element ? dom.querySelector("img") ?? dom : instance.view.dom).getBoundingClientRect();
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3712
|
-
}, { default: () => h(ImageControls, { editor: instance }) }) : null,
|
|
3713
|
-
instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h(EditorContent, { 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,
|
|
4316
|
+
instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h("div", { ref: attachDocument })) : null,
|
|
3714
4317
|
codeUi.open ? h("div", {
|
|
3715
4318
|
ref: codeLanguageRef,
|
|
3716
4319
|
class: "looma-editor__code-language",
|