@threadlabs/looma 0.28.9 → 0.28.10
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/editor/link-editing.d.ts +85 -0
- package/package.json +1 -1
- package/vue/editor/LoomaEditor.d.ts +2 -19
- package/vue/editor/index.js +323 -223
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
/** A destination supplied by the host application, such as a page or record. */
|
|
3
|
+
export interface LoomaLinkTarget {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
href: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Finds host records for a link without teaching the editor how they are stored.
|
|
11
|
+
* @ownership The host checks access and returns durable, openable href values.
|
|
12
|
+
* @failure A rejection leaves the existing document untouched and shows a retryable search error.
|
|
13
|
+
*/
|
|
14
|
+
export type LoomaLinkSearch = (query: string) => Promise<LoomaLinkTarget[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Describes a saved host link so a renamed record can show its current label.
|
|
17
|
+
* @ownership The host resolves identity and access; Looma keeps the saved href as is.
|
|
18
|
+
* @failure A rejection leaves the original href visible and editable.
|
|
19
|
+
*/
|
|
20
|
+
export type LoomaLinkResolve = (href: string) => Promise<LoomaLinkTarget | null>;
|
|
21
|
+
/** The link form's state and the resolved target of the link at the caret. */
|
|
22
|
+
export interface LoomaLinkEditingState {
|
|
23
|
+
/** Where the form shows: nowhere, in the toolbar's popover, or in place of the link card. */
|
|
24
|
+
readonly surface: "none" | "popover" | "context";
|
|
25
|
+
/** The range the form links, and whether it was already a link when the form opened. */
|
|
26
|
+
readonly range: {
|
|
27
|
+
readonly from: number;
|
|
28
|
+
readonly to: number;
|
|
29
|
+
readonly existing: boolean;
|
|
30
|
+
} | null;
|
|
31
|
+
readonly mode: "target" | "url";
|
|
32
|
+
readonly query: string;
|
|
33
|
+
readonly href: string;
|
|
34
|
+
readonly text: string;
|
|
35
|
+
readonly textEditable: boolean;
|
|
36
|
+
readonly newTab: boolean;
|
|
37
|
+
readonly error: string;
|
|
38
|
+
readonly results: readonly LoomaLinkTarget[];
|
|
39
|
+
readonly searching: boolean;
|
|
40
|
+
readonly selected: LoomaLinkTarget | null;
|
|
41
|
+
/** What the link at the caret resolves to, for the link card. */
|
|
42
|
+
readonly current: LoomaLinkTarget | null;
|
|
43
|
+
}
|
|
44
|
+
export interface LoomaLinkEditingOptions {
|
|
45
|
+
editor: Editor;
|
|
46
|
+
/** Read on each use, so a host can change them while the editor runs. */
|
|
47
|
+
search: () => LoomaLinkSearch | null | undefined;
|
|
48
|
+
resolve: () => LoomaLinkResolve | null | undefined;
|
|
49
|
+
baseUrl: () => string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The editor's link form as a state machine: what the form holds, how it applies to the
|
|
53
|
+
* document as the reader types, and the host's search and resolve calls.
|
|
54
|
+
*
|
|
55
|
+
* @ownership The host owns presentation: where the form shows, focus, and which selection it
|
|
56
|
+
* opens on. This owns the form's state and the link mark it writes.
|
|
57
|
+
* @lifecycle Listens to the editor's transactions until `destroy()`; results of a search or lookup
|
|
58
|
+
* that finish after a newer one, or after `destroy()`, are dropped.
|
|
59
|
+
*/
|
|
60
|
+
export interface LoomaLinkEditing {
|
|
61
|
+
readonly state: LoomaLinkEditingState;
|
|
62
|
+
/** Calls `listener` after every state change; returns the unsubscribe function. */
|
|
63
|
+
subscribe(listener: () => void): () => void;
|
|
64
|
+
/** Opens the form on `from`–`to`, filled from the link there if there is one. */
|
|
65
|
+
open(from: number, to: number, surface: "popover" | "context"): void;
|
|
66
|
+
setDestination(value: string): void;
|
|
67
|
+
clear(): void;
|
|
68
|
+
choose(target: LoomaLinkTarget): void;
|
|
69
|
+
setNewTab(newTab: boolean): void;
|
|
70
|
+
setText(text: string): void;
|
|
71
|
+
/**
|
|
72
|
+
* Writes the form's link to its range without moving focus.
|
|
73
|
+
* @failure Returns false and leaves the document unchanged when the destination or text is
|
|
74
|
+
* missing or invalid; `reportError` also sets the form's error.
|
|
75
|
+
*/
|
|
76
|
+
apply(reportError?: boolean): boolean;
|
|
77
|
+
close(): void;
|
|
78
|
+
/** Removes the link at the caret. */
|
|
79
|
+
remove(): void;
|
|
80
|
+
destroy(): void;
|
|
81
|
+
}
|
|
82
|
+
/** The status line above search results: progress, a hint, or an empty result. */
|
|
83
|
+
export declare function linkFormStatus(state: LoomaLinkEditingState): string;
|
|
84
|
+
export declare const LOOMA_LINK_EDITING_IDLE: LoomaLinkEditingState;
|
|
85
|
+
export declare function createLinkEditing(options: LoomaLinkEditingOptions): LoomaLinkEditing;
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
|
|
|
4
4
|
import { type PropType, type VNode } from "vue";
|
|
5
5
|
import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaMentionProvider, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
|
|
6
6
|
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
|
|
7
|
+
import { type LoomaLinkResolve, type LoomaLinkSearch } from "../../editor/link-editing.js";
|
|
7
8
|
/**
|
|
8
9
|
* Host upload result normalized into the editor's durable image descriptor.
|
|
9
10
|
* `url` becomes the stored `src`; optional dimensions preserve layout before
|
|
@@ -62,25 +63,7 @@ export interface LoomaEditorSelectionActionDetail {
|
|
|
62
63
|
* fresh defaults and invokes this factory when the inventory prop changes.
|
|
63
64
|
*/
|
|
64
65
|
export type LoomaEditorSlashCommands = LoomaSlashCommand[] | ((defaults: LoomaSlashCommand[]) => LoomaSlashCommand[]);
|
|
65
|
-
|
|
66
|
-
export interface LoomaLinkTarget {
|
|
67
|
-
id: string;
|
|
68
|
-
label: string;
|
|
69
|
-
detail?: string;
|
|
70
|
-
href: string;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Finds host records for a link without teaching the editor how they are stored.
|
|
74
|
-
* @ownership The host checks access and returns durable, openable href values.
|
|
75
|
-
* @failure A rejection leaves the existing document untouched and shows a retryable search error.
|
|
76
|
-
*/
|
|
77
|
-
export type LoomaLinkSearch = (query: string) => Promise<LoomaLinkTarget[]>;
|
|
78
|
-
/**
|
|
79
|
-
* Describes a saved host link so a renamed record can show its current label.
|
|
80
|
-
* @ownership The host resolves identity and access; Looma keeps the saved href as is.
|
|
81
|
-
* @failure A rejection leaves the original href visible and editable.
|
|
82
|
-
*/
|
|
83
|
-
export type LoomaLinkResolve = (href: string) => Promise<LoomaLinkTarget | null>;
|
|
66
|
+
export type { LoomaLinkResolve, LoomaLinkSearch, LoomaLinkTarget } from "../../editor/link-editing.js";
|
|
84
67
|
/**
|
|
85
68
|
* Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
|
|
86
69
|
* with declarative menus, image delivery, and table interaction chrome.
|
package/vue/editor/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 } from "@threadlabs/looma/editor";
|
|
3
3
|
import { Button, Callout, Card, Checkbox, Cluster, Disclosure, EditorBlockActions, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, Icon, IconButton, Image, Input, List, Menu, MenuItem, Popover, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
4
4
|
import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
|
|
5
5
|
import { Editor as Editor$1 } from "@tiptap/vue-3";
|
|
@@ -7,6 +7,7 @@ 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";
|
|
9
9
|
import { createLowlight } from "lowlight";
|
|
10
|
+
import "@tiptap/pm/transform";
|
|
10
11
|
import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
|
|
11
12
|
export * from "@threadlabs/looma/editor";
|
|
12
13
|
//#region src/vue/editor/primitives.ts
|
|
@@ -1121,6 +1122,273 @@ function validLinkHref(value) {
|
|
|
1121
1122
|
}
|
|
1122
1123
|
}
|
|
1123
1124
|
//#endregion
|
|
1125
|
+
//#region src/editor/extensions/relative-links.ts
|
|
1126
|
+
/** Keep other origins and non-web schemes intact; same-origin links retain path, query, and fragment. */
|
|
1127
|
+
function siteRelativeHref(href, baseUrl) {
|
|
1128
|
+
if (!/^(https?:\/\/|\/\/)/i.test(href.trim())) return href;
|
|
1129
|
+
try {
|
|
1130
|
+
const base = new URL(baseUrl);
|
|
1131
|
+
const url = new URL(href, base);
|
|
1132
|
+
if (!/^https?:$/.test(base.protocol) || url.origin !== base.origin || url.username || url.password) return href;
|
|
1133
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
1134
|
+
} catch {
|
|
1135
|
+
return href;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/editor/link-editing.ts
|
|
1140
|
+
/** The status line above search results: progress, a hint, or an empty result. */
|
|
1141
|
+
function linkFormStatus(state) {
|
|
1142
|
+
if (state.searching) return "Searching…";
|
|
1143
|
+
if (state.selected) return "";
|
|
1144
|
+
if (state.query.trim().length < 2) return "Type at least two letters to search.";
|
|
1145
|
+
return state.results.length || state.error ? "" : "No results found.";
|
|
1146
|
+
}
|
|
1147
|
+
var LOOMA_LINK_EDITING_IDLE = Object.freeze({
|
|
1148
|
+
surface: "none",
|
|
1149
|
+
range: null,
|
|
1150
|
+
mode: "url",
|
|
1151
|
+
query: "",
|
|
1152
|
+
href: "",
|
|
1153
|
+
text: "",
|
|
1154
|
+
textEditable: false,
|
|
1155
|
+
newTab: true,
|
|
1156
|
+
error: "",
|
|
1157
|
+
results: [],
|
|
1158
|
+
searching: false,
|
|
1159
|
+
selected: null,
|
|
1160
|
+
current: null
|
|
1161
|
+
});
|
|
1162
|
+
function createLinkEditing(options) {
|
|
1163
|
+
const { editor } = options;
|
|
1164
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1165
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1166
|
+
let state = LOOMA_LINK_EDITING_IDLE;
|
|
1167
|
+
let searchSequence = 0;
|
|
1168
|
+
let resolveSequence = 0;
|
|
1169
|
+
let currentHref = null;
|
|
1170
|
+
let destroyed = false;
|
|
1171
|
+
const set = (patch) => {
|
|
1172
|
+
const previous = state;
|
|
1173
|
+
state = {
|
|
1174
|
+
...state,
|
|
1175
|
+
...patch
|
|
1176
|
+
};
|
|
1177
|
+
if (state.query !== previous.query || state.mode !== previous.mode || state.surface !== previous.surface) search();
|
|
1178
|
+
listeners.forEach((listener) => listener());
|
|
1179
|
+
};
|
|
1180
|
+
const search = async () => {
|
|
1181
|
+
const sequence = ++searchSequence;
|
|
1182
|
+
const { query, mode, surface, selected } = state;
|
|
1183
|
+
state = {
|
|
1184
|
+
...state,
|
|
1185
|
+
results: [],
|
|
1186
|
+
searching: false
|
|
1187
|
+
};
|
|
1188
|
+
const find = options.search();
|
|
1189
|
+
if (!find || mode !== "target" || surface === "none" || query.trim().length < 2 || selected && query === selected.label) return;
|
|
1190
|
+
state = {
|
|
1191
|
+
...state,
|
|
1192
|
+
searching: true
|
|
1193
|
+
};
|
|
1194
|
+
try {
|
|
1195
|
+
const results = await find(query.trim());
|
|
1196
|
+
if (sequence === searchSequence && !destroyed) set({ results: results.filter((target) => validLinkHref(target.href)) });
|
|
1197
|
+
} catch {
|
|
1198
|
+
if (sequence === searchSequence && !destroyed) set({ error: "Search is unavailable. Try again." });
|
|
1199
|
+
} finally {
|
|
1200
|
+
if (sequence === searchSequence && !destroyed) set({ searching: false });
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
const resolveCurrent = async (href) => {
|
|
1204
|
+
const sequence = ++resolveSequence;
|
|
1205
|
+
if (state.current) set({ current: null });
|
|
1206
|
+
const resolve = options.resolve();
|
|
1207
|
+
if (!href || !resolve) return;
|
|
1208
|
+
if (cache.has(href)) {
|
|
1209
|
+
set({ current: cache.get(href) ?? null });
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
const target = await resolve(href);
|
|
1214
|
+
cache.set(href, target);
|
|
1215
|
+
if (sequence === resolveSequence && !destroyed) set({ current: target });
|
|
1216
|
+
} catch {}
|
|
1217
|
+
};
|
|
1218
|
+
const onTransaction = () => {
|
|
1219
|
+
const href = editor.isActive("link") ? String(editor.getAttributes("link").href ?? "") : null;
|
|
1220
|
+
if (href !== currentHref) {
|
|
1221
|
+
currentHref = href;
|
|
1222
|
+
resolveCurrent(href);
|
|
1223
|
+
}
|
|
1224
|
+
if (state.surface === "context" && editor.state.selection.from !== state.range?.from) set({ surface: "none" });
|
|
1225
|
+
};
|
|
1226
|
+
editor.on("transaction", onTransaction);
|
|
1227
|
+
const apply = (reportError = false) => {
|
|
1228
|
+
const range = state.range;
|
|
1229
|
+
if (editor.isDestroyed || !range) return false;
|
|
1230
|
+
const fail = (error) => {
|
|
1231
|
+
if (reportError) set({ error });
|
|
1232
|
+
return false;
|
|
1233
|
+
};
|
|
1234
|
+
const href = state.mode === "target" ? validLinkHref(state.selected?.href ?? "") : validLinkHref(state.href);
|
|
1235
|
+
if (!href) return fail(state.mode === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.");
|
|
1236
|
+
if (state.mode === "url" && options.search() && !range.existing && !/^(https?:|mailto:|tel:)/i.test(href)) return fail("Enter a full website, email, or phone URL, or choose a destination.");
|
|
1237
|
+
const baseUrl = options.baseUrl();
|
|
1238
|
+
const attrs = {
|
|
1239
|
+
href: baseUrl ? siteRelativeHref(href, baseUrl) : href,
|
|
1240
|
+
target: state.newTab ? "_blank" : "_self",
|
|
1241
|
+
rel: state.newTab ? "noopener noreferrer" : null
|
|
1242
|
+
};
|
|
1243
|
+
const current = editor.state.selection;
|
|
1244
|
+
const chain = editor.chain().setTextSelection({
|
|
1245
|
+
from: range.from,
|
|
1246
|
+
to: range.to
|
|
1247
|
+
});
|
|
1248
|
+
if (state.textEditable) {
|
|
1249
|
+
const text = state.text.trim();
|
|
1250
|
+
if (!text) return fail("Enter link text.");
|
|
1251
|
+
chain.insertContent({
|
|
1252
|
+
type: "text",
|
|
1253
|
+
text,
|
|
1254
|
+
marks: [{
|
|
1255
|
+
type: "link",
|
|
1256
|
+
attrs
|
|
1257
|
+
}]
|
|
1258
|
+
}).setTextSelection(range.from).run();
|
|
1259
|
+
set({
|
|
1260
|
+
range: {
|
|
1261
|
+
from: range.from,
|
|
1262
|
+
to: range.from + text.length,
|
|
1263
|
+
existing: true
|
|
1264
|
+
},
|
|
1265
|
+
error: ""
|
|
1266
|
+
});
|
|
1267
|
+
} else {
|
|
1268
|
+
if (range.existing && range.from === range.to) chain.extendMarkRange("link");
|
|
1269
|
+
chain.setLink(attrs).setTextSelection({
|
|
1270
|
+
from: current.from,
|
|
1271
|
+
to: current.to
|
|
1272
|
+
}).run();
|
|
1273
|
+
set({
|
|
1274
|
+
range: {
|
|
1275
|
+
...range,
|
|
1276
|
+
existing: true
|
|
1277
|
+
},
|
|
1278
|
+
error: ""
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return true;
|
|
1282
|
+
};
|
|
1283
|
+
return {
|
|
1284
|
+
get state() {
|
|
1285
|
+
return state;
|
|
1286
|
+
},
|
|
1287
|
+
subscribe(listener) {
|
|
1288
|
+
listeners.add(listener);
|
|
1289
|
+
return () => {
|
|
1290
|
+
listeners.delete(listener);
|
|
1291
|
+
};
|
|
1292
|
+
},
|
|
1293
|
+
open(from, to, surface) {
|
|
1294
|
+
const empty = from === to;
|
|
1295
|
+
const existing = editor.isActive("link");
|
|
1296
|
+
const attrs = existing ? editor.getAttributes("link") : {};
|
|
1297
|
+
const href = typeof attrs.href === "string" ? attrs.href : "";
|
|
1298
|
+
const selected = cache.get(href) ?? null;
|
|
1299
|
+
set({
|
|
1300
|
+
surface,
|
|
1301
|
+
range: {
|
|
1302
|
+
from,
|
|
1303
|
+
to,
|
|
1304
|
+
existing
|
|
1305
|
+
},
|
|
1306
|
+
textEditable: empty && !existing,
|
|
1307
|
+
href,
|
|
1308
|
+
newTab: attrs.target !== "_self",
|
|
1309
|
+
text: empty && !existing ? "" : editor.state.doc.textBetween(from, to),
|
|
1310
|
+
error: "",
|
|
1311
|
+
selected,
|
|
1312
|
+
mode: options.search() && (!existing || selected) ? "target" : "url",
|
|
1313
|
+
query: selected?.label ?? href
|
|
1314
|
+
});
|
|
1315
|
+
const resolve = options.resolve();
|
|
1316
|
+
if (existing && resolve && !cache.has(href)) resolve(href).then((target) => {
|
|
1317
|
+
cache.set(href, target);
|
|
1318
|
+
if (!destroyed && state.href === href && target) set({
|
|
1319
|
+
selected: target,
|
|
1320
|
+
mode: "target",
|
|
1321
|
+
query: target.label
|
|
1322
|
+
});
|
|
1323
|
+
}).catch(() => {});
|
|
1324
|
+
},
|
|
1325
|
+
setDestination(value) {
|
|
1326
|
+
if (options.search()) {
|
|
1327
|
+
const trimmed = value.trim();
|
|
1328
|
+
const url = /^(https?:\/\/|mailto:|tel:)/i.test(trimmed) || Boolean(state.range?.existing && /^(\/|\.\.?\/|#|\?)/.test(trimmed));
|
|
1329
|
+
set({
|
|
1330
|
+
query: value,
|
|
1331
|
+
mode: url ? "url" : "target",
|
|
1332
|
+
selected: null,
|
|
1333
|
+
href: value,
|
|
1334
|
+
error: ""
|
|
1335
|
+
});
|
|
1336
|
+
} else set({
|
|
1337
|
+
href: value,
|
|
1338
|
+
error: ""
|
|
1339
|
+
});
|
|
1340
|
+
apply();
|
|
1341
|
+
},
|
|
1342
|
+
clear() {
|
|
1343
|
+
set({
|
|
1344
|
+
query: "",
|
|
1345
|
+
href: "",
|
|
1346
|
+
mode: "target",
|
|
1347
|
+
selected: null,
|
|
1348
|
+
error: ""
|
|
1349
|
+
});
|
|
1350
|
+
},
|
|
1351
|
+
choose(target) {
|
|
1352
|
+
cache.set(target.href, target);
|
|
1353
|
+
set({
|
|
1354
|
+
selected: target,
|
|
1355
|
+
href: target.href,
|
|
1356
|
+
query: target.label,
|
|
1357
|
+
results: [],
|
|
1358
|
+
newTab: false,
|
|
1359
|
+
error: "",
|
|
1360
|
+
...state.textEditable && !state.text.trim() ? { text: target.label } : {}
|
|
1361
|
+
});
|
|
1362
|
+
apply();
|
|
1363
|
+
},
|
|
1364
|
+
setNewTab(newTab) {
|
|
1365
|
+
set({ newTab });
|
|
1366
|
+
apply();
|
|
1367
|
+
},
|
|
1368
|
+
setText(text) {
|
|
1369
|
+
set({
|
|
1370
|
+
text,
|
|
1371
|
+
error: ""
|
|
1372
|
+
});
|
|
1373
|
+
apply();
|
|
1374
|
+
},
|
|
1375
|
+
apply,
|
|
1376
|
+
close() {
|
|
1377
|
+
if (state.surface !== "none") set({ surface: "none" });
|
|
1378
|
+
},
|
|
1379
|
+
remove() {
|
|
1380
|
+
if (!editor.isActive("link")) return;
|
|
1381
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
1382
|
+
if (state.surface === "context") set({ surface: "none" });
|
|
1383
|
+
},
|
|
1384
|
+
destroy() {
|
|
1385
|
+
destroyed = true;
|
|
1386
|
+
listeners.clear();
|
|
1387
|
+
editor.off("transaction", onTransaction);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
//#endregion
|
|
1124
1392
|
//#region src/editor/selection-geometry.ts
|
|
1125
1393
|
/** A plain copy of a client rectangle, the anchor shape the slash and mention menus accept as a prop. */
|
|
1126
1394
|
function menuAnchorRect(rect) {
|
|
@@ -2493,28 +2761,13 @@ var LoomaEditor = defineComponent({
|
|
|
2493
2761
|
const element = instance?.$el;
|
|
2494
2762
|
linkFormRoot.value = element instanceof HTMLElement ? element : null;
|
|
2495
2763
|
};
|
|
2496
|
-
|
|
2497
|
-
const
|
|
2498
|
-
const linkHref = ref("");
|
|
2499
|
-
const linkText = ref("");
|
|
2500
|
-
const linkTextEditable = ref(false);
|
|
2501
|
-
const linkNewTab = ref(true);
|
|
2502
|
-
const linkError = ref("");
|
|
2503
|
-
const linkMode = ref("url");
|
|
2504
|
-
const linkQuery = ref("");
|
|
2505
|
-
const linkResults = ref([]);
|
|
2506
|
-
const linkSearching = ref(false);
|
|
2507
|
-
const linkSelectedTarget = ref(null);
|
|
2508
|
-
const linkContextTarget = ref(null);
|
|
2509
|
-
const linkTargetCache = /* @__PURE__ */ new Map();
|
|
2510
|
-
let linkSearchSequence = 0;
|
|
2511
|
-
let linkResolveSequence = 0;
|
|
2764
|
+
let linkEditing;
|
|
2765
|
+
const linkState = shallowRef(LOOMA_LINK_EDITING_IDLE);
|
|
2512
2766
|
const chipAnchorId = `looma-editor-chip-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
2513
2767
|
const chipInput = ref(null);
|
|
2514
2768
|
const chipOpen = ref(false);
|
|
2515
2769
|
const chipLabel = ref("");
|
|
2516
2770
|
const chipColor = ref("neutral");
|
|
2517
|
-
let linkSelection = null;
|
|
2518
2771
|
const toolbarScope = `looma-editor-tool-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
2519
2772
|
const tooltipFor = ref("");
|
|
2520
2773
|
const tooltipLabel = ref("");
|
|
@@ -2565,7 +2818,7 @@ var LoomaEditor = defineComponent({
|
|
|
2565
2818
|
let heldPressTimer;
|
|
2566
2819
|
let heldPress = null;
|
|
2567
2820
|
const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
|
|
2568
|
-
const formattingPickerOpen = () =>
|
|
2821
|
+
const formattingPickerOpen = () => linkState.value.surface === "popover" || tablePickerOpen.value || formatOpen.value;
|
|
2569
2822
|
const cancelHeldPress = () => {
|
|
2570
2823
|
clearTimeout(heldPressTimer);
|
|
2571
2824
|
heldPressTimer = void 0;
|
|
@@ -2754,7 +3007,7 @@ var LoomaEditor = defineComponent({
|
|
|
2754
3007
|
if (view.isDestroyed) return;
|
|
2755
3008
|
editorStateVersion.value += 1;
|
|
2756
3009
|
const instance = editor.value;
|
|
2757
|
-
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") &&
|
|
3010
|
+
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && linkState.value.surface !== "popover") {
|
|
2758
3011
|
linkPopup?.setAnchor(() => link.getBoundingClientRect());
|
|
2759
3012
|
linkPopup?.show();
|
|
2760
3013
|
}
|
|
@@ -2777,7 +3030,6 @@ var LoomaEditor = defineComponent({
|
|
|
2777
3030
|
if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
|
|
2778
3031
|
if (!formattingSelectionActive(instance)) formattingPopup?.hide();
|
|
2779
3032
|
}
|
|
2780
|
-
if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
|
|
2781
3033
|
},
|
|
2782
3034
|
onUpdate: ({ editor: instance }) => {
|
|
2783
3035
|
const value = instance.getJSON();
|
|
@@ -2789,6 +3041,16 @@ var LoomaEditor = defineComponent({
|
|
|
2789
3041
|
editorStateVersion.value += 1;
|
|
2790
3042
|
});
|
|
2791
3043
|
editor.value = session.editor;
|
|
3044
|
+
const engine = createLinkEditing({
|
|
3045
|
+
editor: session.editor,
|
|
3046
|
+
search: () => props.linkSearch,
|
|
3047
|
+
resolve: () => props.linkResolve,
|
|
3048
|
+
baseUrl: () => props.linkBaseUrl
|
|
3049
|
+
});
|
|
3050
|
+
engine.subscribe(() => {
|
|
3051
|
+
linkState.value = engine.state;
|
|
3052
|
+
});
|
|
3053
|
+
linkEditing = engine;
|
|
2792
3054
|
});
|
|
2793
3055
|
const attachDocument = (element) => {
|
|
2794
3056
|
if (element instanceof HTMLElement) session?.attach(element);
|
|
@@ -2796,56 +3058,9 @@ var LoomaEditor = defineComponent({
|
|
|
2796
3058
|
onBeforeUnmount(() => {
|
|
2797
3059
|
const element = session?.editor.options.element;
|
|
2798
3060
|
element?.parentNode?.replaceChild(element.cloneNode(true), element);
|
|
3061
|
+
linkEditing?.destroy();
|
|
2799
3062
|
session?.destroy();
|
|
2800
3063
|
});
|
|
2801
|
-
watch(() => {
|
|
2802
|
-
editorStateVersion.value;
|
|
2803
|
-
return editor.value?.isActive("link") ? editor.value.getAttributes("link").href : null;
|
|
2804
|
-
}, async (href) => {
|
|
2805
|
-
const sequence = ++linkResolveSequence;
|
|
2806
|
-
linkContextTarget.value = null;
|
|
2807
|
-
if (!href || !props.linkResolve) return;
|
|
2808
|
-
if (linkTargetCache.has(href)) {
|
|
2809
|
-
linkContextTarget.value = linkTargetCache.get(href) ?? null;
|
|
2810
|
-
return;
|
|
2811
|
-
}
|
|
2812
|
-
try {
|
|
2813
|
-
const target = await props.linkResolve(href);
|
|
2814
|
-
linkTargetCache.set(href, target);
|
|
2815
|
-
if (sequence === linkResolveSequence) linkContextTarget.value = target;
|
|
2816
|
-
} catch {}
|
|
2817
|
-
});
|
|
2818
|
-
watch([
|
|
2819
|
-
linkQuery,
|
|
2820
|
-
linkMode,
|
|
2821
|
-
linkOpen,
|
|
2822
|
-
linkContextEditing
|
|
2823
|
-
], async ([query, mode, open, contextEditing]) => {
|
|
2824
|
-
const sequence = ++linkSearchSequence;
|
|
2825
|
-
linkResults.value = [];
|
|
2826
|
-
linkSearching.value = false;
|
|
2827
|
-
if (!props.linkSearch || mode !== "target" || !open && !contextEditing || query.trim().length < 2 || linkSelectedTarget.value && query === linkSelectedTarget.value.label) return;
|
|
2828
|
-
linkSearching.value = true;
|
|
2829
|
-
try {
|
|
2830
|
-
const results = await props.linkSearch(query.trim());
|
|
2831
|
-
if (sequence === linkSearchSequence) linkResults.value = results.filter((target) => validLinkHref(target.href));
|
|
2832
|
-
} catch {
|
|
2833
|
-
if (sequence === linkSearchSequence) linkError.value = "Search is unavailable. Try again.";
|
|
2834
|
-
} finally {
|
|
2835
|
-
if (sequence === linkSearchSequence) linkSearching.value = false;
|
|
2836
|
-
}
|
|
2837
|
-
});
|
|
2838
|
-
const selectLinkTarget = (target) => {
|
|
2839
|
-
linkSelectedTarget.value = target;
|
|
2840
|
-
linkTargetCache.set(target.href, target);
|
|
2841
|
-
linkHref.value = target.href;
|
|
2842
|
-
linkQuery.value = target.label;
|
|
2843
|
-
linkResults.value = [];
|
|
2844
|
-
linkNewTab.value = false;
|
|
2845
|
-
linkError.value = "";
|
|
2846
|
-
if (linkTextEditable.value && !linkText.value.trim()) linkText.value = target.label;
|
|
2847
|
-
applyLink();
|
|
2848
|
-
};
|
|
2849
3064
|
const openLinkEditor = (context = false) => {
|
|
2850
3065
|
const instance = editor.value;
|
|
2851
3066
|
if (!instance || !props.editable) return;
|
|
@@ -2855,130 +3070,37 @@ var LoomaEditor = defineComponent({
|
|
|
2855
3070
|
linkPressedSelection = null;
|
|
2856
3071
|
const from = selection?.from ?? current.from;
|
|
2857
3072
|
const to = selection?.to ?? current.to;
|
|
2858
|
-
|
|
2859
|
-
const existing = instance.isActive("link");
|
|
2860
|
-
linkSelection = {
|
|
2861
|
-
from,
|
|
2862
|
-
to,
|
|
2863
|
-
existing
|
|
2864
|
-
};
|
|
2865
|
-
linkTextEditable.value = empty && !existing;
|
|
2866
|
-
const attrs = existing ? instance.getAttributes("link") : {};
|
|
2867
|
-
linkHref.value = typeof attrs.href === "string" ? attrs.href : "";
|
|
2868
|
-
linkNewTab.value = attrs.target !== "_self";
|
|
2869
|
-
linkText.value = empty && !existing ? "" : instance.state.doc.textBetween(from, to);
|
|
2870
|
-
linkError.value = "";
|
|
2871
|
-
linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
|
|
2872
|
-
linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
|
|
2873
|
-
linkQuery.value = linkSelectedTarget.value?.label ?? linkHref.value;
|
|
2874
|
-
linkContextEditing.value = context;
|
|
2875
|
-
linkOpen.value = !context;
|
|
2876
|
-
if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
|
|
2877
|
-
const href = linkHref.value;
|
|
2878
|
-
props.linkResolve(href).then((target) => {
|
|
2879
|
-
linkTargetCache.set(href, target);
|
|
2880
|
-
if (linkHref.value === href && target) {
|
|
2881
|
-
linkSelectedTarget.value = target;
|
|
2882
|
-
linkMode.value = "target";
|
|
2883
|
-
linkQuery.value = target.label;
|
|
2884
|
-
}
|
|
2885
|
-
}).catch(() => {});
|
|
2886
|
-
}
|
|
3073
|
+
linkEditing?.open(from, to, context ? "context" : "popover");
|
|
2887
3074
|
nextTick(() => {
|
|
2888
3075
|
if (context) (props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? formattingPopup : linkPopup)?.setAnchor(() => posToDOMRect(instance.view, instance.state.selection.from, instance.state.selection.to));
|
|
2889
|
-
(
|
|
3076
|
+
(linkState.value.mode === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
|
|
2890
3077
|
});
|
|
2891
3078
|
};
|
|
2892
|
-
const applyLink = (reportError = false) => {
|
|
2893
|
-
const instance = editor.value;
|
|
2894
|
-
const selection = linkSelection;
|
|
2895
|
-
if (!instance || !selection) return false;
|
|
2896
|
-
const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
|
|
2897
|
-
if (!href) {
|
|
2898
|
-
if (reportError) linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
|
|
2899
|
-
return false;
|
|
2900
|
-
}
|
|
2901
|
-
if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
|
|
2902
|
-
if (reportError) linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
|
|
2903
|
-
return false;
|
|
2904
|
-
}
|
|
2905
|
-
const attrs = {
|
|
2906
|
-
href: props.linkBaseUrl ? siteRelativeHref(href, props.linkBaseUrl) : href,
|
|
2907
|
-
target: linkNewTab.value ? "_blank" : "_self",
|
|
2908
|
-
rel: linkNewTab.value ? "noopener noreferrer" : null
|
|
2909
|
-
};
|
|
2910
|
-
const current = instance.state.selection;
|
|
2911
|
-
const chain = instance.chain().setTextSelection({
|
|
2912
|
-
from: selection.from,
|
|
2913
|
-
to: selection.to
|
|
2914
|
-
});
|
|
2915
|
-
if (linkTextEditable.value) {
|
|
2916
|
-
const text = linkText.value.trim();
|
|
2917
|
-
if (!text) {
|
|
2918
|
-
if (reportError) linkError.value = "Enter link text.";
|
|
2919
|
-
return false;
|
|
2920
|
-
}
|
|
2921
|
-
linkSelection = {
|
|
2922
|
-
from: selection.from,
|
|
2923
|
-
to: selection.from + text.length,
|
|
2924
|
-
existing: true
|
|
2925
|
-
};
|
|
2926
|
-
chain.insertContent({
|
|
2927
|
-
type: "text",
|
|
2928
|
-
text,
|
|
2929
|
-
marks: [{
|
|
2930
|
-
type: "link",
|
|
2931
|
-
attrs
|
|
2932
|
-
}]
|
|
2933
|
-
}).setTextSelection(selection.from).run();
|
|
2934
|
-
} else {
|
|
2935
|
-
if (selection.existing && selection.from === selection.to) chain.extendMarkRange("link");
|
|
2936
|
-
chain.setLink(attrs).setTextSelection({
|
|
2937
|
-
from: current.from,
|
|
2938
|
-
to: current.to
|
|
2939
|
-
}).run();
|
|
2940
|
-
selection.existing = true;
|
|
2941
|
-
}
|
|
2942
|
-
linkError.value = "";
|
|
2943
|
-
return true;
|
|
2944
|
-
};
|
|
2945
|
-
const removeCurrentLink = () => {
|
|
2946
|
-
const instance = editor.value;
|
|
2947
|
-
if (!instance?.isActive("link")) return;
|
|
2948
|
-
instance.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
2949
|
-
linkContextEditing.value = false;
|
|
2950
|
-
};
|
|
2951
3079
|
const dismissLink = (restoreFocus = true) => {
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
if (restoreFocus && selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
|
|
3080
|
+
linkEditing?.close();
|
|
3081
|
+
const range = linkState.value.range;
|
|
3082
|
+
if (restoreFocus && range) editor.value?.chain().focus().setTextSelection(range.to).run();
|
|
2956
3083
|
dismissedFormattingSelection = editor.value?.state.selection ?? null;
|
|
2957
3084
|
heldFormattingSelection = null;
|
|
2958
3085
|
linkPopup?.hide();
|
|
2959
3086
|
formattingPopup?.hide();
|
|
2960
3087
|
};
|
|
2961
|
-
const linkStatus = () => {
|
|
2962
|
-
if (linkSearching.value) return "Searching…";
|
|
2963
|
-
if (linkSelectedTarget.value) return "";
|
|
2964
|
-
if (linkQuery.value.trim().length < 2) return "Type at least two letters to search.";
|
|
2965
|
-
return linkResults.value.length || linkError.value ? "" : "No results found.";
|
|
2966
|
-
};
|
|
2967
3088
|
const renderLinkForm = () => {
|
|
2968
|
-
const
|
|
3089
|
+
const state = linkState.value;
|
|
3090
|
+
const shown = state.selected ? [state.selected] : state.results;
|
|
2969
3091
|
return h(EditorLinkForm, {
|
|
2970
3092
|
ref: setLinkFormRoot,
|
|
2971
3093
|
class: "looma-editor__link-form",
|
|
2972
|
-
existing: Boolean(
|
|
3094
|
+
existing: Boolean(state.range?.existing),
|
|
2973
3095
|
searchable: Boolean(props.linkSearch),
|
|
2974
|
-
mode:
|
|
2975
|
-
value: props.linkSearch ?
|
|
3096
|
+
mode: state.mode,
|
|
3097
|
+
value: props.linkSearch ? state.query : state.href,
|
|
2976
3098
|
placeholder: props.linkSearch ? `Search ${props.linkTargetLabel.toLowerCase()}s or paste a URL…` : "https://example.com",
|
|
2977
|
-
newTab:
|
|
2978
|
-
textEditable:
|
|
2979
|
-
text:
|
|
2980
|
-
error:
|
|
2981
|
-
status:
|
|
3099
|
+
newTab: state.newTab,
|
|
3100
|
+
textEditable: state.textEditable,
|
|
3101
|
+
text: state.text,
|
|
3102
|
+
error: state.error,
|
|
3103
|
+
status: linkFormStatus(state),
|
|
2982
3104
|
results: shown.map(({ id, label, detail }) => detail ? {
|
|
2983
3105
|
id,
|
|
2984
3106
|
label,
|
|
@@ -2987,46 +3109,23 @@ var LoomaEditor = defineComponent({
|
|
|
2987
3109
|
id,
|
|
2988
3110
|
label
|
|
2989
3111
|
}),
|
|
2990
|
-
selected:
|
|
2991
|
-
onDestination: (event) =>
|
|
2992
|
-
|
|
2993
|
-
if (props.linkSearch) {
|
|
2994
|
-
linkQuery.value = value;
|
|
2995
|
-
linkMode.value = /^(https?:\/\/|mailto:|tel:)/i.test(value.trim()) || linkSelection?.existing && /^(\/|\.\.?\/|#|\?)/.test(value.trim()) ? "url" : "target";
|
|
2996
|
-
linkSelectedTarget.value = null;
|
|
2997
|
-
}
|
|
2998
|
-
linkHref.value = value;
|
|
2999
|
-
linkError.value = "";
|
|
3000
|
-
applyLink();
|
|
3001
|
-
},
|
|
3002
|
-
onClear: () => {
|
|
3003
|
-
linkQuery.value = "";
|
|
3004
|
-
linkHref.value = "";
|
|
3005
|
-
linkMode.value = "target";
|
|
3006
|
-
linkSelectedTarget.value = null;
|
|
3007
|
-
linkError.value = "";
|
|
3008
|
-
},
|
|
3112
|
+
selected: state.selected?.id ?? "",
|
|
3113
|
+
onDestination: (event) => linkEditing?.setDestination(event.detail.value),
|
|
3114
|
+
onClear: () => linkEditing?.clear(),
|
|
3009
3115
|
onChoose: (event) => {
|
|
3010
3116
|
const target = shown.find(({ id }) => id === event.detail.id);
|
|
3011
|
-
if (target)
|
|
3012
|
-
},
|
|
3013
|
-
onNewTab: (event) => {
|
|
3014
|
-
linkNewTab.value = event.detail.newTab;
|
|
3015
|
-
applyLink();
|
|
3016
|
-
},
|
|
3017
|
-
onText: (event) => {
|
|
3018
|
-
linkText.value = event.detail.text;
|
|
3019
|
-
linkError.value = "";
|
|
3020
|
-
applyLink();
|
|
3117
|
+
if (target) linkEditing?.choose(target);
|
|
3021
3118
|
},
|
|
3119
|
+
onNewTab: (event) => linkEditing?.setNewTab(event.detail.newTab),
|
|
3120
|
+
onText: (event) => linkEditing?.setText(event.detail.text),
|
|
3022
3121
|
onSave: () => {
|
|
3023
|
-
if (
|
|
3122
|
+
if (linkEditing?.apply(true)) dismissLink();
|
|
3024
3123
|
},
|
|
3025
3124
|
onDismiss: () => dismissLink()
|
|
3026
3125
|
});
|
|
3027
3126
|
};
|
|
3028
3127
|
const renderLinkContextCard = (instance) => {
|
|
3029
|
-
if (
|
|
3128
|
+
if (linkState.value.surface === "context") return h(Card, {
|
|
3030
3129
|
variant: "elevated",
|
|
3031
3130
|
padding: "sm",
|
|
3032
3131
|
size: "sm",
|
|
@@ -3034,7 +3133,7 @@ var LoomaEditor = defineComponent({
|
|
|
3034
3133
|
}, () => [renderLinkForm()]);
|
|
3035
3134
|
const rawHref = instance.getAttributes("link").href;
|
|
3036
3135
|
const href = typeof rawHref === "string" ? rawHref : "";
|
|
3037
|
-
const target =
|
|
3136
|
+
const target = linkState.value.current;
|
|
3038
3137
|
return h(EditorLinkCard, {
|
|
3039
3138
|
class: "looma-editor__link-context",
|
|
3040
3139
|
href,
|
|
@@ -3043,7 +3142,7 @@ var LoomaEditor = defineComponent({
|
|
|
3043
3142
|
detail: target?.detail ?? "",
|
|
3044
3143
|
targetLabel: props.linkTargetLabel,
|
|
3045
3144
|
onEdit: () => openLinkEditor(true),
|
|
3046
|
-
onRemove:
|
|
3145
|
+
onRemove: () => linkEditing?.remove()
|
|
3047
3146
|
});
|
|
3048
3147
|
};
|
|
3049
3148
|
const runBlockAction = (value) => {
|
|
@@ -3088,11 +3187,11 @@ var LoomaEditor = defineComponent({
|
|
|
3088
3187
|
if (!editable) chipOpen.value = false;
|
|
3089
3188
|
});
|
|
3090
3189
|
watch([
|
|
3091
|
-
|
|
3190
|
+
() => linkState.value.surface === "popover",
|
|
3092
3191
|
tablePickerOpen,
|
|
3093
3192
|
formatOpen
|
|
3094
|
-
], ([
|
|
3095
|
-
if (
|
|
3193
|
+
], ([linking, table, format]) => {
|
|
3194
|
+
if (linking || table || format || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.element.contains(document.activeElement)) return;
|
|
3096
3195
|
heldFormattingSelection = null;
|
|
3097
3196
|
formattingPopup?.hide();
|
|
3098
3197
|
});
|
|
@@ -3337,7 +3436,7 @@ var LoomaEditor = defineComponent({
|
|
|
3337
3436
|
const onDocumentPointerDown = (event) => {
|
|
3338
3437
|
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
3339
3438
|
const inElement = (element) => Boolean(element && (path.includes(element) || event.target instanceof Node && element.contains(event.target)));
|
|
3340
|
-
if (
|
|
3439
|
+
if (linkState.value.surface !== "none" && !inElement(linkFormRoot.value)) dismissLink(false);
|
|
3341
3440
|
const inTableUi = inElement(tableToolbarShell.value) || inElement(tableOverlayShell.value) || inElement(tableMenuShell.value) || inElement(mobileToolbarShell.value);
|
|
3342
3441
|
tableInteractionActive = inTableUi;
|
|
3343
3442
|
if (inTableUi) setTimeout(() => {
|
|
@@ -3403,7 +3502,7 @@ var LoomaEditor = defineComponent({
|
|
|
3403
3502
|
editor: instance,
|
|
3404
3503
|
element,
|
|
3405
3504
|
pluginKey: "looma-text-formatting-menu",
|
|
3406
|
-
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) ||
|
|
3505
|
+
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) || linkState.value.surface === "context" || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
|
|
3407
3506
|
boundary: () => props.toolbarMode === "contextual" ? root.value : null,
|
|
3408
3507
|
related: editorRoot,
|
|
3409
3508
|
onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0,
|
|
@@ -3415,7 +3514,7 @@ var LoomaEditor = defineComponent({
|
|
|
3415
3514
|
editor: instance,
|
|
3416
3515
|
element,
|
|
3417
3516
|
pluginKey: "looma-link-context-menu",
|
|
3418
|
-
shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") &&
|
|
3517
|
+
shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && linkState.value.surface !== "popover" && !dismissedFormattingSelection?.eq(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkState.value.surface === "context" || linkPopup?.element.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(menuEditor)),
|
|
3419
3518
|
maxWidth: () => Math.min(480, window.innerWidth - 24),
|
|
3420
3519
|
related: editorRoot,
|
|
3421
3520
|
onDismiss: (reason) => reason === "escape" ? dismissFormatting() : linkPopup?.hide()
|
|
@@ -3833,7 +3932,7 @@ var LoomaEditor = defineComponent({
|
|
|
3833
3932
|
label: "Link",
|
|
3834
3933
|
size: "sm",
|
|
3835
3934
|
variant: "ghost",
|
|
3836
|
-
"aria-expanded":
|
|
3935
|
+
"aria-expanded": linkState.value.surface === "popover" ? "true" : "false",
|
|
3837
3936
|
"aria-pressed": instance.isActive("link") ? "true" : "false",
|
|
3838
3937
|
"data-active": instance.isActive("link") ? "true" : "false",
|
|
3839
3938
|
onPointerenter: () => showTool(linkTriggerId, "Link", false),
|
|
@@ -4017,7 +4116,7 @@ var LoomaEditor = defineComponent({
|
|
|
4017
4116
|
ref: formattingSurfaceRef,
|
|
4018
4117
|
class: "looma-editor__floating-shell looma-editor__formatting-shell",
|
|
4019
4118
|
hidden: true
|
|
4020
|
-
}, [props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (
|
|
4119
|
+
}, [props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkState.value.surface === "context" || instance.state.selection.empty && instance.isActive("link")) && linkState.value.surface !== "popover" && formattingSelectionActive(instance) ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover")])]) : null,
|
|
4021
4120
|
instance && props.editable ? h("div", { key: "looma-link-host" }, [h("div", {
|
|
4022
4121
|
ref: linkSurfaceRef,
|
|
4023
4122
|
class: "looma-editor__floating-shell looma-editor__link-shell",
|
|
@@ -4055,7 +4154,7 @@ var LoomaEditor = defineComponent({
|
|
|
4055
4154
|
languages: codeLanguageOptions.value,
|
|
4056
4155
|
onLanguage: (event) => chooseCodeLanguage(event.detail.value)
|
|
4057
4156
|
})]) : null,
|
|
4058
|
-
instance && (props.editable || props.selectionActions.length && !instance.state.selection.empty || activeRangeActions(instance).length > 0) && mobile.value && (editorFocused.value ||
|
|
4157
|
+
instance && (props.editable || props.selectionActions.length && !instance.state.selection.empty || activeRangeActions(instance).length > 0) && mobile.value && (editorFocused.value || linkState.value.surface === "popover" || !props.editable) ? h("div", {
|
|
4059
4158
|
key: "looma-mobile-toolbar-shell",
|
|
4060
4159
|
ref: mobileToolbarRef,
|
|
4061
4160
|
class: "looma-editor__mobile-toolbar-shell",
|
|
@@ -4150,17 +4249,18 @@ var LoomaEditor = defineComponent({
|
|
|
4150
4249
|
h(Popover, {
|
|
4151
4250
|
class: "looma-editor__link-popover",
|
|
4152
4251
|
size: "sm",
|
|
4153
|
-
open:
|
|
4252
|
+
open: linkState.value.surface === "popover",
|
|
4154
4253
|
for: linkPopoverAnchorId.value,
|
|
4155
4254
|
placement: mobile.value ? "top-start" : "bottom-start",
|
|
4156
4255
|
onOpen: () => openLinkEditor(),
|
|
4157
4256
|
onClose: () => {
|
|
4158
|
-
|
|
4257
|
+
if (linkState.value.surface === "popover") linkEditing?.close();
|
|
4159
4258
|
linkPressedSelection = null;
|
|
4160
4259
|
const current = editor.value?.state.selection;
|
|
4161
|
-
|
|
4260
|
+
const range = linkState.value.range;
|
|
4261
|
+
if (current && !current.empty && range && current.from === range.from && current.to === range.to) editor.value?.commands.setTextSelection(current.to);
|
|
4162
4262
|
}
|
|
4163
|
-
}, () =>
|
|
4263
|
+
}, () => linkState.value.surface === "popover" ? renderLinkForm() : null),
|
|
4164
4264
|
props.editable && props.compactToolbar && !mobile.value ? h(Popover, {
|
|
4165
4265
|
class: "looma-editor__compact-formatting",
|
|
4166
4266
|
for: formatAnchorId,
|