@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
|
@@ -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 {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Editor, type EditorOptions, type JSONContent } from "@tiptap/core";
|
|
2
|
+
import { type Selection, type SelectionBookmark } from "@tiptap/pm/state";
|
|
3
|
+
/** A selection remembered so a controlled document replacement can put it back. */
|
|
4
|
+
export interface LoomaSelectionSnapshot {
|
|
5
|
+
bookmark: SelectionBookmark;
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
text: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function selectionSnapshot(selection: Selection): LoomaSelectionSnapshot;
|
|
11
|
+
/**
|
|
12
|
+
* One editor's Tiptap instance, independent of any component framework.
|
|
13
|
+
*
|
|
14
|
+
* @ownership The session owns the `Editor` from creation until `destroy()`. A host renders an
|
|
15
|
+
* element for the document and hands it to `attach()`; it never creates or destroys the editor.
|
|
16
|
+
* @lifecycle The editor, its document, and its undo history survive `attach()` to a new element,
|
|
17
|
+
* so a host can remount its view without losing state.
|
|
18
|
+
*/
|
|
19
|
+
export interface LoomaEditorSession<E extends Editor = Editor> {
|
|
20
|
+
readonly editor: E;
|
|
21
|
+
/** Moves the editor's view into `element` (idempotent). */
|
|
22
|
+
attach(element: HTMLElement): void;
|
|
23
|
+
/**
|
|
24
|
+
* Applies a controlled document.
|
|
25
|
+
*
|
|
26
|
+
* @contract Returns false, and leaves the editor untouched, when the document already matches.
|
|
27
|
+
* A focused editor keeps `selection` (or its current selection) in the same transaction.
|
|
28
|
+
* @failure A selection the new document cannot hold becomes the closest valid text range.
|
|
29
|
+
*/
|
|
30
|
+
setContent(content: JSONContent, selection?: LoomaSelectionSnapshot | null): boolean;
|
|
31
|
+
/** Calls `listener` after every transaction; returns the unsubscribe function. */
|
|
32
|
+
subscribe(listener: () => void): () => void;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates the session's editor from `options`. A framework layer may pass its own `Editor`
|
|
37
|
+
* subclass, such as `@tiptap/vue-3`'s reactive one, so the editor it exposes keeps that behavior.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createEditorSession<E extends Editor = Editor>(options: Partial<EditorOptions>, EditorClass?: new (options: Partial<EditorOptions>) => E): LoomaEditorSession<E>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
|
|
2
2
|
import { Extension, Mark, Node, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
3
|
-
import { Editor, EditorContent } from "@tiptap/vue-3";
|
|
3
|
+
import { Editor as Editor$1, EditorContent } from "@tiptap/vue-3";
|
|
4
4
|
import { closeHistory, history, redo, undo } from "@tiptap/pm/history";
|
|
5
5
|
import { Plugin } from "@tiptap/pm/state";
|
|
6
6
|
//#region ../../node_modules/.pnpm/@tiptap+extension-document@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-document/dist/index.js
|
|
@@ -498,7 +498,7 @@ var LoomaCompactEditor = defineComponent({
|
|
|
498
498
|
setup(props, { attrs, emit, expose }) {
|
|
499
499
|
const editor = shallowRef();
|
|
500
500
|
onMounted(() => {
|
|
501
|
-
editor.value = new Editor({
|
|
501
|
+
editor.value = new Editor$1({
|
|
502
502
|
extensions: [...getCompactEditorExtensions(), ...props.extensions],
|
|
503
503
|
content: props.modelValue,
|
|
504
504
|
editable: props.editable,
|