@threadlabs/looma 0.23.25 → 0.24.1
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.
|
@@ -219,8 +219,16 @@ export function createAnchoredSurface(surface, options = {}) {
|
|
|
219
219
|
fallbackPosition(surface, { left: point.x, right: point.x, top: point.y, bottom: point.y, width: 0, height: 0 }, "bottom-start", gap(), viewportGap);
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const rect = anchor?.getBoundingClientRect();
|
|
223
|
+
// A disappearing trigger has no usable position. Keep the last valid position, or wait before first opening.
|
|
224
|
+
if (!anchor || anchor.isConnected === false || !rect || rect.width === 0 && rect.height === 0
|
|
225
|
+
|| anchor instanceof owner.Element && owner.getComputedStyle(anchor).visibility !== "visible") {
|
|
226
|
+
if (lastAnchorRect) fallbackPosition(surface, lastAnchorRect, placement, gap(), viewportGap);
|
|
227
|
+
else hide(surface);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
lastAnchorRect = rect;
|
|
231
|
+
show(surface);
|
|
224
232
|
fallbackPosition(surface, lastAnchorRect, placement, gap(), viewportGap);
|
|
225
233
|
};
|
|
226
234
|
const schedule = () => {
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./looma-editor.css";
|
|
2
|
-
import type
|
|
2
|
+
import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
|
|
3
3
|
import { type PropType, type VNode } from "vue";
|
|
4
4
|
import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaMentionProvider, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
|
|
5
5
|
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageRenditionErrorDetail } from "./image-delivery.js";
|
|
@@ -23,8 +23,9 @@ export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUplo
|
|
|
23
23
|
/**
|
|
24
24
|
* Chooses whether formatting controls follow a selection, occupy persistent
|
|
25
25
|
* editor chrome, or open from an app-owned button (`popover`, with a text-only
|
|
26
|
-
* bubble for selections).
|
|
27
|
-
*
|
|
26
|
+
* bubble for selections). Floating tools open on text selection or a held press
|
|
27
|
+
* and dismiss on Escape/caret movement. `contextual` includes the full command
|
|
28
|
+
* set. Narrow viewports retain a dock; document commands/content are unchanged.
|
|
28
29
|
*/
|
|
29
30
|
export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover" | "contextual";
|
|
30
31
|
/**
|
package/vue/editor/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
|
|
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,
|
|
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, Card, Checkbox, Cluster, Combobox, Disclosure, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, 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";
|
|
4
5
|
import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
|
|
5
6
|
import { closeHistory } from "@tiptap/pm/history";
|
|
6
7
|
import { createLowlight } from "lowlight";
|
|
7
|
-
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
|
-
import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule } from "@tiptap/core";
|
|
8
|
+
import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
9
9
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
10
10
|
import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
|
|
11
11
|
export * from "@threadlabs/looma/editor";
|
|
@@ -1795,6 +1795,22 @@ var LoomaEditor = defineComponent({
|
|
|
1795
1795
|
const mobileToolbarStyle = ref({});
|
|
1796
1796
|
const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
|
|
1797
1797
|
const editorFocused = ref(false);
|
|
1798
|
+
let dismissedFormattingSelection = null;
|
|
1799
|
+
let heldFormattingSelection = null;
|
|
1800
|
+
let formattingPopup = null;
|
|
1801
|
+
let heldPressTimer;
|
|
1802
|
+
let heldPress = null;
|
|
1803
|
+
const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
|
|
1804
|
+
const formattingPickerOpen = () => linkOpen.value || tablePickerOpen.value;
|
|
1805
|
+
const cancelHeldPress = () => {
|
|
1806
|
+
clearTimeout(heldPressTimer);
|
|
1807
|
+
heldPressTimer = void 0;
|
|
1808
|
+
heldPress = null;
|
|
1809
|
+
};
|
|
1810
|
+
const formattingSelectionActive = (instance) => {
|
|
1811
|
+
const selection = instance.state.selection;
|
|
1812
|
+
return (selection instanceof TextSelection || selection instanceof AllSelection && Boolean(instance.state.doc.textContent)) && !dismissedFormattingSelection?.eq(selection) && (!selection.empty || Boolean(heldFormattingSelection?.eq(selection)));
|
|
1813
|
+
};
|
|
1798
1814
|
const editorStateVersion = ref(0);
|
|
1799
1815
|
const mobileToolbarMode = ref("formatting");
|
|
1800
1816
|
let dragLeaveTimer = null;
|
|
@@ -1995,6 +2011,12 @@ var LoomaEditor = defineComponent({
|
|
|
1995
2011
|
onFocus: ({ editor: instance }) => rememberSelection(instance),
|
|
1996
2012
|
onSelectionUpdate: ({ editor: instance }) => {
|
|
1997
2013
|
rememberSelection(instance);
|
|
2014
|
+
if (floatingFormattingMode()) {
|
|
2015
|
+
const selection = instance.state.selection;
|
|
2016
|
+
if (!dismissedFormattingSelection?.eq(selection)) dismissedFormattingSelection = null;
|
|
2017
|
+
if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
|
|
2018
|
+
if (!formattingSelectionActive(instance)) formattingPopup?.hide();
|
|
2019
|
+
}
|
|
1998
2020
|
if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
|
|
1999
2021
|
},
|
|
2000
2022
|
onTransaction: () => {
|
|
@@ -2442,13 +2464,21 @@ var LoomaEditor = defineComponent({
|
|
|
2442
2464
|
editor.value?.commands.dismissLoomaSlashMenu();
|
|
2443
2465
|
});
|
|
2444
2466
|
watch(() => props.editable, (editable) => {
|
|
2467
|
+
cancelHeldPress();
|
|
2468
|
+
heldFormattingSelection = null;
|
|
2445
2469
|
editor.value?.setEditable(editable);
|
|
2470
|
+
if (!editable) formattingPopup?.hide();
|
|
2446
2471
|
if (editor.value) imageDelivery.reset(editor.value);
|
|
2447
2472
|
root.value?.querySelectorAll("[data-looma-chip]").forEach((chip) => {
|
|
2448
2473
|
chip.setAttribute("aria-disabled", String(!editable));
|
|
2449
2474
|
});
|
|
2450
2475
|
if (!editable) chipOpen.value = false;
|
|
2451
2476
|
});
|
|
2477
|
+
watch([linkOpen, tablePickerOpen], ([link, table]) => {
|
|
2478
|
+
if (link || table || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.popper.contains(document.activeElement)) return;
|
|
2479
|
+
heldFormattingSelection = null;
|
|
2480
|
+
formattingPopup?.hide();
|
|
2481
|
+
});
|
|
2452
2482
|
watch(() => props.label, (label) => {
|
|
2453
2483
|
editor.value?.setOptions({ editorProps: { attributes: {
|
|
2454
2484
|
role: "textbox",
|
|
@@ -2638,10 +2668,12 @@ var LoomaEditor = defineComponent({
|
|
|
2638
2668
|
updateMobileViewport();
|
|
2639
2669
|
};
|
|
2640
2670
|
const onEditorBlur = () => {
|
|
2671
|
+
cancelHeldPress();
|
|
2641
2672
|
queueMicrotask(updateTableUi);
|
|
2642
2673
|
updateCodeUi();
|
|
2643
2674
|
setTimeout(() => {
|
|
2644
2675
|
if (!tableInteractionActive && !editor.value?.isFocused) editorFocused.value = false;
|
|
2676
|
+
if (!editor.value?.isFocused && !formattingPickerOpen() && !formattingPopup?.popper.contains(document.activeElement)) heldFormattingSelection = null;
|
|
2645
2677
|
}, 0);
|
|
2646
2678
|
};
|
|
2647
2679
|
const bindEditorUi = (instance) => {
|
|
@@ -2667,9 +2699,13 @@ var LoomaEditor = defineComponent({
|
|
|
2667
2699
|
}, { immediate: true });
|
|
2668
2700
|
const onViewportChange = (event) => {
|
|
2669
2701
|
if (event?.type === "scroll" && event.target instanceof Node && (tableToolbarShell.value?.contains(event.target) || tableMenuShell.value?.contains(event.target))) return;
|
|
2702
|
+
cancelHeldPress();
|
|
2703
|
+
const wasMobile = mobile.value;
|
|
2670
2704
|
updateMobileViewport();
|
|
2705
|
+
if (wasMobile !== mobile.value) heldFormattingSelection = null;
|
|
2671
2706
|
updateTableUi();
|
|
2672
2707
|
updateCodeUi();
|
|
2708
|
+
if (floatingFormattingMode() && editor.value && !formattingSelectionActive(editor.value)) formattingPopup?.hide();
|
|
2673
2709
|
tableUi.menuOpen = false;
|
|
2674
2710
|
};
|
|
2675
2711
|
const onDocumentPointerDown = (event) => {
|
|
@@ -2682,6 +2718,40 @@ var LoomaEditor = defineComponent({
|
|
|
2682
2718
|
}, 0);
|
|
2683
2719
|
if ((tableUi.toolbarOpen || tableUi.overlayOpen || tableUi.menuOpen) && !inElement(root.value) && !inTableUi) closeTableUi();
|
|
2684
2720
|
};
|
|
2721
|
+
const onFormattingPointerDown = (event) => {
|
|
2722
|
+
cancelHeldPress();
|
|
2723
|
+
const instance = editor.value;
|
|
2724
|
+
if (!floatingFormattingMode() || tableResizeActive || !props.editable || !instance || event.button !== 0 || !event.isPrimary || !(event.target instanceof Element) || !instance.view.dom.contains(event.target) || event.target.closest("button, input, select, textarea, [data-looma-image-node]")) return;
|
|
2725
|
+
heldPress = {
|
|
2726
|
+
id: event.pointerId,
|
|
2727
|
+
x: event.clientX,
|
|
2728
|
+
y: event.clientY
|
|
2729
|
+
};
|
|
2730
|
+
heldPressTimer = setTimeout(() => {
|
|
2731
|
+
heldPressTimer = void 0;
|
|
2732
|
+
const selection = instance.state.selection;
|
|
2733
|
+
if (!heldPress || !floatingFormattingMode() || tableResizeActive || !props.editable || !instance.isFocused || slash.active || mention.active || !(selection instanceof TextSelection)) return;
|
|
2734
|
+
dismissedFormattingSelection = null;
|
|
2735
|
+
heldFormattingSelection = selection;
|
|
2736
|
+
formattingPopup?.setProps({ getReferenceClientRect: () => posToDOMRect(instance.view, selection.from, selection.to) });
|
|
2737
|
+
formattingPopup?.show();
|
|
2738
|
+
}, 500);
|
|
2739
|
+
};
|
|
2740
|
+
const onFormattingPointerMove = (event) => {
|
|
2741
|
+
if (heldPress?.id === event.pointerId && Math.hypot(event.clientX - heldPress.x, event.clientY - heldPress.y) > 8) cancelHeldPress();
|
|
2742
|
+
};
|
|
2743
|
+
const onFormattingPointerEnd = (event) => {
|
|
2744
|
+
if (heldPress?.id === event.pointerId) cancelHeldPress();
|
|
2745
|
+
};
|
|
2746
|
+
const onFormattingKeyDown = (event) => {
|
|
2747
|
+
cancelHeldPress();
|
|
2748
|
+
if (event.key !== "Escape" || !floatingFormattingMode() || !(event.target instanceof Node) || !(root.value?.contains(event.target) || formattingPopup?.popper.contains(event.target))) return;
|
|
2749
|
+
const selection = editor.value?.state.selection;
|
|
2750
|
+
dismissedFormattingSelection = selection instanceof TextSelection || selection instanceof AllSelection ? selection : null;
|
|
2751
|
+
heldFormattingSelection = null;
|
|
2752
|
+
formattingPopup?.hide();
|
|
2753
|
+
hideTool();
|
|
2754
|
+
};
|
|
2685
2755
|
const onResizePointerDown = (event) => {
|
|
2686
2756
|
tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
|
|
2687
2757
|
};
|
|
@@ -2770,6 +2840,11 @@ var LoomaEditor = defineComponent({
|
|
|
2770
2840
|
window.visualViewport?.addEventListener("resize", onViewportChange);
|
|
2771
2841
|
window.visualViewport?.addEventListener("scroll", onViewportChange);
|
|
2772
2842
|
document.addEventListener("pointerdown", onDocumentPointerDown, true);
|
|
2843
|
+
document.addEventListener("pointerdown", onFormattingPointerDown);
|
|
2844
|
+
document.addEventListener("pointermove", onFormattingPointerMove);
|
|
2845
|
+
document.addEventListener("pointerup", onFormattingPointerEnd);
|
|
2846
|
+
document.addEventListener("pointercancel", onFormattingPointerEnd);
|
|
2847
|
+
document.addEventListener("keydown", onFormattingKeyDown);
|
|
2773
2848
|
document.addEventListener("pointerdown", onResizePointerDown, true);
|
|
2774
2849
|
document.addEventListener("pointerup", onResizePointerUp, true);
|
|
2775
2850
|
root.value?.addEventListener("error", onImageError, true);
|
|
@@ -2779,7 +2854,14 @@ var LoomaEditor = defineComponent({
|
|
|
2779
2854
|
window.removeEventListener("scroll", onViewportChange, true);
|
|
2780
2855
|
window.visualViewport?.removeEventListener("resize", onViewportChange);
|
|
2781
2856
|
window.visualViewport?.removeEventListener("scroll", onViewportChange);
|
|
2857
|
+
cancelHeldPress();
|
|
2858
|
+
formattingPopup = null;
|
|
2782
2859
|
document.removeEventListener("pointerdown", onDocumentPointerDown, true);
|
|
2860
|
+
document.removeEventListener("pointerdown", onFormattingPointerDown);
|
|
2861
|
+
document.removeEventListener("pointermove", onFormattingPointerMove);
|
|
2862
|
+
document.removeEventListener("pointerup", onFormattingPointerEnd);
|
|
2863
|
+
document.removeEventListener("pointercancel", onFormattingPointerEnd);
|
|
2864
|
+
document.removeEventListener("keydown", onFormattingKeyDown);
|
|
2783
2865
|
document.removeEventListener("pointerdown", onResizePointerDown, true);
|
|
2784
2866
|
document.removeEventListener("pointerup", onResizePointerUp, true);
|
|
2785
2867
|
root.value?.removeEventListener("error", onImageError, true);
|
|
@@ -3131,10 +3213,18 @@ var LoomaEditor = defineComponent({
|
|
|
3131
3213
|
instance && props.editable && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
|
|
3132
3214
|
editor: instance,
|
|
3133
3215
|
pluginKey: "looma-text-formatting-menu",
|
|
3134
|
-
|
|
3216
|
+
updateDelay: 0,
|
|
3217
|
+
shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (menuEditor.isFocused || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
|
|
3135
3218
|
tippyOptions: {
|
|
3219
|
+
onCreate: (popup) => {
|
|
3220
|
+
formattingPopup = popup;
|
|
3221
|
+
},
|
|
3222
|
+
onDestroy: () => {
|
|
3223
|
+
formattingPopup = null;
|
|
3224
|
+
},
|
|
3136
3225
|
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
3137
3226
|
onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
|
|
3227
|
+
onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0,
|
|
3138
3228
|
duration: 100,
|
|
3139
3229
|
maxWidth: "none",
|
|
3140
3230
|
placement: "top",
|