@threadlabs/looma 0.22.11 → 0.22.13
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.
|
@@ -9778,13 +9778,14 @@ var CALLOUT_COMMANDS = [
|
|
|
9778
9778
|
];
|
|
9779
9779
|
/**
|
|
9780
9780
|
* Builds Looma's default command policy as fresh objects for one editor.
|
|
9781
|
-
*
|
|
9782
|
-
* persistence, or
|
|
9781
|
+
* Image and link commands delegate their picker because Looma does not own
|
|
9782
|
+
* upload, persistence, or destination search. Link appears only when a picker
|
|
9783
|
+
* callback is supplied, so a headless default inventory has no dead action.
|
|
9783
9784
|
*
|
|
9784
9785
|
* @ownership Returned command objects belong to the caller and are recreated
|
|
9785
9786
|
* per call so one editor cannot mutate another editor's command inventory.
|
|
9786
9787
|
*/
|
|
9787
|
-
function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor) {
|
|
9788
|
+
function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor) {
|
|
9788
9789
|
return [
|
|
9789
9790
|
{
|
|
9790
9791
|
title: "Text",
|
|
@@ -9950,6 +9951,20 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor) {
|
|
|
9950
9951
|
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
9951
9952
|
}
|
|
9952
9953
|
},
|
|
9954
|
+
...onOpenLinkEditor ? [{
|
|
9955
|
+
title: "Link",
|
|
9956
|
+
description: "Link to a destination or URL",
|
|
9957
|
+
icon: "link",
|
|
9958
|
+
keywords: [
|
|
9959
|
+
"link",
|
|
9960
|
+
"url",
|
|
9961
|
+
"website"
|
|
9962
|
+
],
|
|
9963
|
+
command: ({ editor, range }) => {
|
|
9964
|
+
editor.chain().focus().deleteRange(range).run();
|
|
9965
|
+
onOpenLinkEditor();
|
|
9966
|
+
}
|
|
9967
|
+
}] : [],
|
|
9953
9968
|
{
|
|
9954
9969
|
title: "Image",
|
|
9955
9970
|
description: "Upload an image",
|
|
@@ -10081,9 +10096,10 @@ var LoomaSlashCommand = Extension.create({
|
|
|
10081
10096
|
function createLoomaSlashCommandExtension(options = {}) {
|
|
10082
10097
|
const onOpenImagePicker = options.onOpenImagePicker;
|
|
10083
10098
|
const onOpenChipEditor = options.onOpenChipEditor;
|
|
10099
|
+
const onOpenLinkEditor = options.onOpenLinkEditor;
|
|
10084
10100
|
return LoomaSlashCommand.configure({
|
|
10085
10101
|
...options,
|
|
10086
|
-
commands: options.commands ?? getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor)
|
|
10102
|
+
commands: options.commands ?? getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor)
|
|
10087
10103
|
});
|
|
10088
10104
|
}
|
|
10089
10105
|
//#endregion
|
|
@@ -49,16 +49,18 @@ export interface LoomaSlashCommandOptions {
|
|
|
49
49
|
onStateChange?: (state: LoomaSlashMenuSnapshot) => void;
|
|
50
50
|
onOpenImagePicker?: () => void;
|
|
51
51
|
onOpenChipEditor?: (editor: Editor, position: number) => void;
|
|
52
|
+
onOpenLinkEditor?: () => void;
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
55
|
* Builds Looma's default command policy as fresh objects for one editor.
|
|
55
|
-
*
|
|
56
|
-
* persistence, or
|
|
56
|
+
* Image and link commands delegate their picker because Looma does not own
|
|
57
|
+
* upload, persistence, or destination search. Link appears only when a picker
|
|
58
|
+
* callback is supplied, so a headless default inventory has no dead action.
|
|
57
59
|
*
|
|
58
60
|
* @ownership Returned command objects belong to the caller and are recreated
|
|
59
61
|
* per call so one editor cannot mutate another editor's command inventory.
|
|
60
62
|
*/
|
|
61
|
-
export declare function getDefaultSlashCommands(onOpenImagePicker?: () => void, onOpenChipEditor?: (editor: Editor, position: number) => void): LoomaSlashCommand[];
|
|
63
|
+
export declare function getDefaultSlashCommands(onOpenImagePicker?: () => void, onOpenChipEditor?: (editor: Editor, position: number) => void, onOpenLinkEditor?: () => void): LoomaSlashCommand[];
|
|
62
64
|
/**
|
|
63
65
|
* Framework-neutral slash-command extension used by the turnkey editor.
|
|
64
66
|
* Consumers embedding Looma into their own Tiptap instance can configure the
|
package/package.json
CHANGED
package/vue/components.css
CHANGED
|
@@ -9070,18 +9070,30 @@ html[data-ui-input-modality="touch"]
|
|
|
9070
9070
|
|
|
9071
9071
|
.looma-editor__link-context-actions {
|
|
9072
9072
|
display: flex;
|
|
9073
|
+
flex-wrap: wrap;
|
|
9073
9074
|
align-items: center;
|
|
9074
9075
|
gap: var(--ui-space-1);
|
|
9075
9076
|
}
|
|
9076
9077
|
|
|
9077
9078
|
.looma-editor__link-context-url {
|
|
9078
9079
|
min-width: 0;
|
|
9079
|
-
|
|
9080
|
-
overflow: hidden;
|
|
9080
|
+
flex: 1 1 100%;
|
|
9081
9081
|
padding-inline: var(--ui-space-2);
|
|
9082
9082
|
color: var(--ui-text-secondary);
|
|
9083
|
-
|
|
9084
|
-
|
|
9083
|
+
overflow-wrap: anywhere;
|
|
9084
|
+
font-size: var(--ui-font-size-sm);
|
|
9085
|
+
}
|
|
9086
|
+
|
|
9087
|
+
.looma-editor__link-context-url :is(strong, small) {
|
|
9088
|
+
display: block;
|
|
9089
|
+
}
|
|
9090
|
+
|
|
9091
|
+
.looma-editor__link-context-url strong {
|
|
9092
|
+
color: var(--ui-text-primary);
|
|
9093
|
+
}
|
|
9094
|
+
|
|
9095
|
+
.looma-editor__link-context-kind {
|
|
9096
|
+
font-size: var(--ui-font-size-xs);
|
|
9085
9097
|
}
|
|
9086
9098
|
|
|
9087
9099
|
.looma-editor__link-context-actions :is(button, a) {
|
|
@@ -9198,6 +9210,57 @@ html[data-ui-input-modality="touch"]
|
|
|
9198
9210
|
align-items: center;
|
|
9199
9211
|
}
|
|
9200
9212
|
|
|
9213
|
+
.looma-editor__link-modes {
|
|
9214
|
+
display: flex;
|
|
9215
|
+
gap: var(--ui-space-1);
|
|
9216
|
+
}
|
|
9217
|
+
|
|
9218
|
+
.looma-editor__link-modes button,
|
|
9219
|
+
.looma-editor__link-results button {
|
|
9220
|
+
min-height: var(--ui-control-min-block-size);
|
|
9221
|
+
padding: var(--ui-space-2);
|
|
9222
|
+
border: 1px solid var(--ui-control-border);
|
|
9223
|
+
border-radius: var(--ui-field-radius, var(--ui-radius-md));
|
|
9224
|
+
background: var(--ui-control-surface);
|
|
9225
|
+
color: var(--ui-text-primary);
|
|
9226
|
+
font: inherit;
|
|
9227
|
+
text-align: left;
|
|
9228
|
+
cursor: pointer;
|
|
9229
|
+
}
|
|
9230
|
+
|
|
9231
|
+
.looma-editor__link-modes button[aria-pressed="true"] {
|
|
9232
|
+
border-color: var(--ui-accent);
|
|
9233
|
+
color: var(--ui-accent);
|
|
9234
|
+
}
|
|
9235
|
+
|
|
9236
|
+
.looma-editor__link-results {
|
|
9237
|
+
display: flex;
|
|
9238
|
+
flex-direction: column;
|
|
9239
|
+
gap: var(--ui-space-1);
|
|
9240
|
+
max-height: 15rem;
|
|
9241
|
+
overflow-y: auto;
|
|
9242
|
+
}
|
|
9243
|
+
|
|
9244
|
+
.looma-editor__link-results p {
|
|
9245
|
+
margin: 0;
|
|
9246
|
+
color: var(--ui-text-secondary);
|
|
9247
|
+
font-size: var(--ui-font-size-sm);
|
|
9248
|
+
}
|
|
9249
|
+
|
|
9250
|
+
.looma-editor__link-results button {
|
|
9251
|
+
display: flex;
|
|
9252
|
+
flex-direction: column;
|
|
9253
|
+
width: 100%;
|
|
9254
|
+
}
|
|
9255
|
+
|
|
9256
|
+
.looma-editor__link-results button:is(:hover, :focus-visible) {
|
|
9257
|
+
background: var(--ui-surface-subtle);
|
|
9258
|
+
}
|
|
9259
|
+
|
|
9260
|
+
.looma-editor__link-results button small {
|
|
9261
|
+
color: var(--ui-text-secondary);
|
|
9262
|
+
}
|
|
9263
|
+
|
|
9201
9264
|
.looma-editor__link-error {
|
|
9202
9265
|
margin: 0;
|
|
9203
9266
|
color: var(--ui-field-danger, var(--ui-danger));
|
|
@@ -26,6 +26,25 @@ export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUplo
|
|
|
26
26
|
* bubble for selections); it does not alter document commands or stored content.
|
|
27
27
|
*/
|
|
28
28
|
export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover";
|
|
29
|
+
/** A destination supplied by the host application, such as a page or record. */
|
|
30
|
+
export interface LoomaLinkTarget {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
href: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Finds host records for a link without teaching the editor how they are stored.
|
|
38
|
+
* @ownership The host checks access and returns durable, openable href values.
|
|
39
|
+
* @failure A rejection leaves the existing document untouched and shows a retryable search error.
|
|
40
|
+
*/
|
|
41
|
+
export type LoomaLinkSearch = (query: string) => Promise<LoomaLinkTarget[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Describes a saved host link so a renamed record can show its current label.
|
|
44
|
+
* @ownership The host resolves identity and access; Looma keeps the saved href as is.
|
|
45
|
+
* @failure A rejection leaves the original href visible and editable.
|
|
46
|
+
*/
|
|
47
|
+
export type LoomaLinkResolve = (href: string) => Promise<LoomaLinkTarget | null>;
|
|
29
48
|
/**
|
|
30
49
|
* Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
|
|
31
50
|
* with declarative menus, image delivery, and table interaction chrome.
|
|
@@ -88,6 +107,19 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
88
107
|
type: NumberConstructor;
|
|
89
108
|
default: number;
|
|
90
109
|
};
|
|
110
|
+
/** Host-owned search and identity lookup for links to application records. */
|
|
111
|
+
linkSearch: {
|
|
112
|
+
type: PropType<LoomaLinkSearch | undefined>;
|
|
113
|
+
default: undefined;
|
|
114
|
+
};
|
|
115
|
+
linkResolve: {
|
|
116
|
+
type: PropType<LoomaLinkResolve | undefined>;
|
|
117
|
+
default: undefined;
|
|
118
|
+
};
|
|
119
|
+
linkTargetLabel: {
|
|
120
|
+
type: StringConstructor;
|
|
121
|
+
default: string;
|
|
122
|
+
};
|
|
91
123
|
uploadImage: {
|
|
92
124
|
type: PropType<LoomaImageUploader | undefined>;
|
|
93
125
|
default: undefined;
|
|
@@ -181,6 +213,19 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
181
213
|
type: NumberConstructor;
|
|
182
214
|
default: number;
|
|
183
215
|
};
|
|
216
|
+
/** Host-owned search and identity lookup for links to application records. */
|
|
217
|
+
linkSearch: {
|
|
218
|
+
type: PropType<LoomaLinkSearch | undefined>;
|
|
219
|
+
default: undefined;
|
|
220
|
+
};
|
|
221
|
+
linkResolve: {
|
|
222
|
+
type: PropType<LoomaLinkResolve | undefined>;
|
|
223
|
+
default: undefined;
|
|
224
|
+
};
|
|
225
|
+
linkTargetLabel: {
|
|
226
|
+
type: StringConstructor;
|
|
227
|
+
default: string;
|
|
228
|
+
};
|
|
184
229
|
uploadImage: {
|
|
185
230
|
type: PropType<LoomaImageUploader | undefined>;
|
|
186
231
|
default: undefined;
|
|
@@ -233,6 +278,9 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
233
278
|
mentionItems: LoomaMentionItem[];
|
|
234
279
|
mentionProvider: LoomaMentionProvider | undefined;
|
|
235
280
|
mentionLimit: number;
|
|
281
|
+
linkSearch: LoomaLinkSearch | undefined;
|
|
282
|
+
linkResolve: LoomaLinkResolve | undefined;
|
|
283
|
+
linkTargetLabel: string;
|
|
236
284
|
uploadImage: LoomaImageUploader | undefined;
|
|
237
285
|
resolveImageAttributes: LoomaImageAttributeResolver | undefined;
|
|
238
286
|
toolbarMode: LoomaEditorToolbarMode;
|
package/vue/editor/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from "@threadlabs/looma/editor";
|
|
2
2
|
export * from "./primitives.js";
|
|
3
|
-
export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
|
|
3
|
+
export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
|
|
4
4
|
export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "./image-delivery.js";
|
package/vue/editor/index.js
CHANGED
|
@@ -562,6 +562,19 @@ var LoomaEditor = defineComponent({
|
|
|
562
562
|
type: Number,
|
|
563
563
|
default: DEFAULT_MENTION_RESULT_LIMIT
|
|
564
564
|
},
|
|
565
|
+
/** Host-owned search and identity lookup for links to application records. */
|
|
566
|
+
linkSearch: {
|
|
567
|
+
type: Function,
|
|
568
|
+
default: void 0
|
|
569
|
+
},
|
|
570
|
+
linkResolve: {
|
|
571
|
+
type: Function,
|
|
572
|
+
default: void 0
|
|
573
|
+
},
|
|
574
|
+
linkTargetLabel: {
|
|
575
|
+
type: String,
|
|
576
|
+
default: "Item"
|
|
577
|
+
},
|
|
565
578
|
uploadImage: {
|
|
566
579
|
type: Function,
|
|
567
580
|
default: void 0
|
|
@@ -613,12 +626,22 @@ var LoomaEditor = defineComponent({
|
|
|
613
626
|
let blockActionTarget = null;
|
|
614
627
|
const linkAnchorId = `looma-editor-link-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
615
628
|
const linkUrlInput = ref(null);
|
|
629
|
+
const linkFormRoot = ref(null);
|
|
616
630
|
const linkOpen = ref(false);
|
|
617
631
|
const linkContextEditing = ref(false);
|
|
618
632
|
const linkHref = ref("");
|
|
619
633
|
const linkText = ref("");
|
|
620
634
|
const linkNewTab = ref(true);
|
|
621
635
|
const linkError = ref("");
|
|
636
|
+
const linkMode = ref("url");
|
|
637
|
+
const linkQuery = ref("");
|
|
638
|
+
const linkResults = ref([]);
|
|
639
|
+
const linkSearching = ref(false);
|
|
640
|
+
const linkSelectedTarget = ref(null);
|
|
641
|
+
const linkContextTarget = ref(null);
|
|
642
|
+
const linkTargetCache = /* @__PURE__ */ new Map();
|
|
643
|
+
let linkSearchSequence = 0;
|
|
644
|
+
let linkResolveSequence = 0;
|
|
622
645
|
const chipAnchorId = `looma-editor-chip-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
623
646
|
const chipInput = ref(null);
|
|
624
647
|
const chipOpen = ref(false);
|
|
@@ -746,6 +769,7 @@ var LoomaEditor = defineComponent({
|
|
|
746
769
|
const slashExtension = createLoomaSlashCommandExtension({
|
|
747
770
|
onOpenImagePicker: () => fileInput.value?.click(),
|
|
748
771
|
onOpenChipEditor: openChipEditor,
|
|
772
|
+
onOpenLinkEditor: () => openLinkEditor(),
|
|
749
773
|
onStateChange: (state) => {
|
|
750
774
|
Object.assign(slash, state);
|
|
751
775
|
}
|
|
@@ -862,6 +886,63 @@ var LoomaEditor = defineComponent({
|
|
|
862
886
|
emit("update", value);
|
|
863
887
|
}
|
|
864
888
|
});
|
|
889
|
+
watch(() => {
|
|
890
|
+
editorStateVersion.value;
|
|
891
|
+
return editor.value?.isActive("link") ? editor.value.getAttributes("link").href : null;
|
|
892
|
+
}, async (href) => {
|
|
893
|
+
const sequence = ++linkResolveSequence;
|
|
894
|
+
linkContextTarget.value = null;
|
|
895
|
+
if (!href || !props.linkResolve) return;
|
|
896
|
+
if (linkTargetCache.has(href)) {
|
|
897
|
+
linkContextTarget.value = linkTargetCache.get(href) ?? null;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const target = await props.linkResolve(href);
|
|
902
|
+
linkTargetCache.set(href, target);
|
|
903
|
+
if (sequence === linkResolveSequence) linkContextTarget.value = target;
|
|
904
|
+
} catch {}
|
|
905
|
+
});
|
|
906
|
+
watch([
|
|
907
|
+
linkQuery,
|
|
908
|
+
linkMode,
|
|
909
|
+
linkOpen,
|
|
910
|
+
linkContextEditing
|
|
911
|
+
], async ([query, mode, open, contextEditing]) => {
|
|
912
|
+
const sequence = ++linkSearchSequence;
|
|
913
|
+
linkResults.value = [];
|
|
914
|
+
linkSearching.value = false;
|
|
915
|
+
if (!props.linkSearch || mode !== "target" || !open && !contextEditing || query.trim().length < 2 || linkSelectedTarget.value && query === linkSelectedTarget.value.label) return;
|
|
916
|
+
linkSearching.value = true;
|
|
917
|
+
try {
|
|
918
|
+
const results = await props.linkSearch(query.trim());
|
|
919
|
+
if (sequence === linkSearchSequence) linkResults.value = results.filter((target) => validLinkHref(target.href));
|
|
920
|
+
} catch {
|
|
921
|
+
if (sequence === linkSearchSequence) linkError.value = "Search is unavailable. Try again.";
|
|
922
|
+
} finally {
|
|
923
|
+
if (sequence === linkSearchSequence) linkSearching.value = false;
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
const selectLinkTarget = (target) => {
|
|
927
|
+
linkSelectedTarget.value = target;
|
|
928
|
+
linkTargetCache.set(target.href, target);
|
|
929
|
+
linkHref.value = target.href;
|
|
930
|
+
linkQuery.value = target.label;
|
|
931
|
+
linkResults.value = [];
|
|
932
|
+
linkNewTab.value = false;
|
|
933
|
+
linkError.value = "";
|
|
934
|
+
};
|
|
935
|
+
const moveLinkResultFocus = (event, offset) => {
|
|
936
|
+
const buttons = [...linkFormRoot.value?.querySelectorAll("[data-link-result]") ?? []];
|
|
937
|
+
const next = buttons[buttons.indexOf(event.target) + offset];
|
|
938
|
+
if (next) {
|
|
939
|
+
event.preventDefault();
|
|
940
|
+
next.focus();
|
|
941
|
+
} else if (offset < 0) {
|
|
942
|
+
event.preventDefault();
|
|
943
|
+
linkFormRoot.value?.querySelector("[data-link-search]")?.focus();
|
|
944
|
+
}
|
|
945
|
+
};
|
|
865
946
|
const openLinkEditor = (context = false) => {
|
|
866
947
|
const instance = editor.value;
|
|
867
948
|
if (!instance || !props.editable) return;
|
|
@@ -883,17 +964,36 @@ var LoomaEditor = defineComponent({
|
|
|
883
964
|
linkNewTab.value = attrs.target !== "_self";
|
|
884
965
|
linkText.value = empty && !existing ? "" : instance.state.doc.textBetween(from, to);
|
|
885
966
|
linkError.value = "";
|
|
967
|
+
linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
|
|
968
|
+
linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
|
|
969
|
+
linkQuery.value = linkSelectedTarget.value?.label ?? "";
|
|
886
970
|
linkContextEditing.value = context;
|
|
887
971
|
linkOpen.value = !context;
|
|
888
|
-
|
|
972
|
+
if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
|
|
973
|
+
const href = linkHref.value;
|
|
974
|
+
props.linkResolve(href).then((target) => {
|
|
975
|
+
linkTargetCache.set(href, target);
|
|
976
|
+
if (linkHref.value === href && target) {
|
|
977
|
+
linkSelectedTarget.value = target;
|
|
978
|
+
linkMode.value = "target";
|
|
979
|
+
linkQuery.value = target.label;
|
|
980
|
+
}
|
|
981
|
+
}).catch(() => {});
|
|
982
|
+
}
|
|
983
|
+
nextTick(() => (linkMode.value === "url" ? linkUrlInput.value : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
|
|
889
984
|
};
|
|
890
985
|
const saveLink = () => {
|
|
891
986
|
const instance = editor.value;
|
|
892
987
|
const selection = linkSelection;
|
|
893
988
|
if (!instance || !selection) return;
|
|
894
|
-
const href = validLinkHref(linkHref.value);
|
|
989
|
+
const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
|
|
895
990
|
if (!href) {
|
|
896
|
-
linkError.value = "Enter an http, https, mailto, tel, or relative URL.";
|
|
991
|
+
linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
|
|
992
|
+
(linkMode.value === "url" ? linkUrlInput.value : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
|
|
996
|
+
linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
|
|
897
997
|
linkUrlInput.value?.focus();
|
|
898
998
|
return;
|
|
899
999
|
}
|
|
@@ -945,6 +1045,7 @@ var LoomaEditor = defineComponent({
|
|
|
945
1045
|
linkContextEditing.value = false;
|
|
946
1046
|
};
|
|
947
1047
|
const renderLinkForm = () => h("form", {
|
|
1048
|
+
ref: linkFormRoot,
|
|
948
1049
|
class: "looma-editor__link-form",
|
|
949
1050
|
"aria-label": "Edit link",
|
|
950
1051
|
onSubmit: (event) => {
|
|
@@ -959,7 +1060,47 @@ var LoomaEditor = defineComponent({
|
|
|
959
1060
|
linkError.value = "";
|
|
960
1061
|
}
|
|
961
1062
|
})]) : null,
|
|
962
|
-
|
|
1063
|
+
props.linkSearch ? h("div", {
|
|
1064
|
+
class: "looma-editor__link-modes",
|
|
1065
|
+
role: "group",
|
|
1066
|
+
"aria-label": "Link destination"
|
|
1067
|
+
}, [h("button", {
|
|
1068
|
+
type: "button",
|
|
1069
|
+
"aria-pressed": linkMode.value === "target",
|
|
1070
|
+
onClick: () => {
|
|
1071
|
+
linkMode.value = "target";
|
|
1072
|
+
linkHref.value = "";
|
|
1073
|
+
linkSelectedTarget.value = null;
|
|
1074
|
+
linkQuery.value = "";
|
|
1075
|
+
linkError.value = "";
|
|
1076
|
+
nextTick(() => linkFormRoot.value?.querySelector("[data-link-search]")?.focus());
|
|
1077
|
+
}
|
|
1078
|
+
}, props.linkTargetLabel), h("button", {
|
|
1079
|
+
type: "button",
|
|
1080
|
+
"aria-pressed": linkMode.value === "url",
|
|
1081
|
+
onClick: () => {
|
|
1082
|
+
linkMode.value = "url";
|
|
1083
|
+
linkHref.value = "";
|
|
1084
|
+
linkSelectedTarget.value = null;
|
|
1085
|
+
linkError.value = "";
|
|
1086
|
+
nextTick(() => linkUrlInput.value?.focus());
|
|
1087
|
+
}
|
|
1088
|
+
}, "URL")]) : null,
|
|
1089
|
+
linkMode.value === "target" && props.linkSearch ? h("label", [h("span", `Find a ${props.linkTargetLabel.toLowerCase()}`), h("input", {
|
|
1090
|
+
"data-link-search": "",
|
|
1091
|
+
type: "search",
|
|
1092
|
+
value: linkQuery.value,
|
|
1093
|
+
autocomplete: "off",
|
|
1094
|
+
"aria-invalid": linkError.value ? "true" : void 0,
|
|
1095
|
+
onInput: (event) => {
|
|
1096
|
+
linkQuery.value = event.target.value;
|
|
1097
|
+
linkSelectedTarget.value = null;
|
|
1098
|
+
linkError.value = "";
|
|
1099
|
+
},
|
|
1100
|
+
onKeydown: (event) => {
|
|
1101
|
+
if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
|
|
1102
|
+
}
|
|
1103
|
+
})]) : h("label", [h("span", "URL"), h("input", {
|
|
963
1104
|
ref: linkUrlInput,
|
|
964
1105
|
type: "text",
|
|
965
1106
|
inputmode: "url",
|
|
@@ -970,7 +1111,25 @@ var LoomaEditor = defineComponent({
|
|
|
970
1111
|
linkError.value = "";
|
|
971
1112
|
}
|
|
972
1113
|
})]),
|
|
973
|
-
|
|
1114
|
+
linkMode.value === "target" && props.linkSearch ? h("div", {
|
|
1115
|
+
class: "looma-editor__link-results",
|
|
1116
|
+
"aria-label": "Destination search results"
|
|
1117
|
+
}, [
|
|
1118
|
+
linkSearching.value ? h("p", { role: "status" }, "Searching…") : null,
|
|
1119
|
+
linkSelectedTarget.value ? h("p", { class: "looma-editor__link-selected" }, `Selected: ${linkSelectedTarget.value.label}${linkSelectedTarget.value.detail ? ` · ${linkSelectedTarget.value.detail}` : ""}`) : null,
|
|
1120
|
+
!linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length < 2 ? h("p", "Type at least two letters to search.") : null,
|
|
1121
|
+
!linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length >= 2 && !linkResults.value.length && !linkError.value ? h("p", "No results found.") : null,
|
|
1122
|
+
...linkResults.value.map((target) => h("button", {
|
|
1123
|
+
key: target.id,
|
|
1124
|
+
type: "button",
|
|
1125
|
+
"data-link-result": "",
|
|
1126
|
+
onClick: () => selectLinkTarget(target),
|
|
1127
|
+
onKeydown: (event) => {
|
|
1128
|
+
if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
|
|
1129
|
+
if (event.key === "ArrowUp") moveLinkResultFocus(event, -1);
|
|
1130
|
+
}
|
|
1131
|
+
}, [h("strong", target.label), target.detail ? h("small", target.detail) : null]))
|
|
1132
|
+
]) : h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
|
|
974
1133
|
type: "checkbox",
|
|
975
1134
|
checked: linkNewTab.value,
|
|
976
1135
|
onChange: (event) => {
|
|
@@ -981,7 +1140,7 @@ var LoomaEditor = defineComponent({
|
|
|
981
1140
|
class: "looma-editor__link-error",
|
|
982
1141
|
role: "alert"
|
|
983
1142
|
}, linkError.value) : null,
|
|
984
|
-
validLinkHref(linkHref.value) ? h("a", {
|
|
1143
|
+
linkMode.value === "url" && validLinkHref(linkHref.value) ? h("a", {
|
|
985
1144
|
class: "looma-editor__link-preview",
|
|
986
1145
|
href: validLinkHref(linkHref.value),
|
|
987
1146
|
target: "_blank",
|
|
@@ -1005,7 +1164,7 @@ var LoomaEditor = defineComponent({
|
|
|
1005
1164
|
h("span", {
|
|
1006
1165
|
class: "looma-editor__link-context-url",
|
|
1007
1166
|
title: href
|
|
1008
|
-
}, href),
|
|
1167
|
+
}, [h("small", { class: "looma-editor__link-context-kind" }, linkContextTarget.value ? props.linkTargetLabel : "External URL"), ...linkContextTarget.value ? [h("strong", linkContextTarget.value.label), linkContextTarget.value.detail ? h("small", linkContextTarget.value.detail) : null] : [href]]),
|
|
1009
1168
|
h("button", {
|
|
1010
1169
|
type: "button",
|
|
1011
1170
|
onPointerdown: (event) => event.preventDefault(),
|