@threadlabs/looma 0.22.13 → 0.22.14
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/icons.js +4 -0
- package/components/ui-card/ui-card.html +21 -2
- package/components/ui-icon/ui-icon.html +4 -0
- package/components/ui-input/ui-input.html +1 -0
- package/components/ui-input-group/ui-input-group.html +11 -0
- package/components/ui-popover/ui-popover.html +15 -0
- package/components/ui-scroll-area/ui-scroll-area.html +12 -0
- package/components/ui-search-result-row/ui-search-result-row.html +11 -0
- package/components/ui-stack/ui-stack.html +6 -3
- package/components/ui-text/ui-text.html +12 -0
- package/dist/index.js +1 -1
- package/editor/chunks/extensions.js +55 -2
- package/editor/extensions/index.d.ts +1 -0
- package/editor/extensions/index.js +2 -2
- package/editor/extensions/preset.d.ts +2 -0
- package/editor/extensions/relative-links.d.ts +8 -0
- package/editor/icons.d.ts +4 -0
- package/editor/index.js +7 -3
- package/package.json +1 -1
- package/styles/ui-card.css +17 -0
- package/styles/ui-input-group.css +9 -0
- package/styles/ui-input.css +1 -0
- package/styles/ui-popover.css +13 -0
- package/styles/ui-scroll-area.css +10 -0
- package/styles/ui-search-result-row.css +9 -0
- package/styles/ui-stack.css +3 -0
- package/styles/ui-text.css +8 -0
- package/vanilla/UiCard.d.ts +2 -1
- package/vanilla/UiCard.js +1 -1
- package/vanilla/UiIcon.js +1 -1
- package/vanilla/UiInputGroup.d.ts +1 -0
- package/vanilla/UiInputGroup.js +1 -1
- package/vanilla/UiPopover.d.ts +1 -0
- package/vanilla/UiPopover.js +1 -1
- package/vanilla/UiScrollArea.d.ts +1 -0
- package/vanilla/UiScrollArea.js +1 -1
- package/vanilla/UiSearchResultRow.d.ts +1 -0
- package/vanilla/UiSearchResultRow.js +1 -1
- package/vanilla/UiStack.d.ts +1 -1
- package/vanilla/UiStack.js +1 -1
- package/vanilla/UiText.d.ts +2 -0
- package/vanilla/UiText.js +1 -1
- package/vue/UiCard.d.ts +12 -3
- package/vue/UiCard.vue +49 -5
- package/vue/UiIcon.vue +23 -0
- package/vue/UiInput.vue +1 -0
- package/vue/UiInputGroup.d.ts +9 -0
- package/vue/UiInputGroup.vue +33 -2
- package/vue/UiPopover.d.ts +9 -0
- package/vue/UiPopover.vue +43 -2
- package/vue/UiScrollArea.d.ts +9 -0
- package/vue/UiScrollArea.vue +35 -2
- package/vue/UiSearchResultRow.d.ts +9 -0
- package/vue/UiSearchResultRow.vue +38 -1
- package/vue/UiStack.d.ts +3 -3
- package/vue/UiStack.vue +10 -4
- package/vue/UiText.d.ts +18 -0
- package/vue/UiText.vue +70 -0
- package/vue/chunks/UiCard.js +39 -3
- package/vue/chunks/UiIcon.js +69 -1
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiInputGroup.js +59 -22
- package/vue/chunks/UiPopover.js +37 -5
- package/vue/chunks/UiScrollArea.js +66 -23
- package/vue/chunks/UiSearchResultRow.js +28 -2
- package/vue/chunks/UiStack.js +3 -2
- package/vue/chunks/UiText.js +92 -4
- package/vue/components.css +174 -301
- package/vue/editor/LoomaEditor.d.ts +11 -0
- package/vue/editor/index.js +261 -145
|
@@ -107,6 +107,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
107
107
|
type: NumberConstructor;
|
|
108
108
|
default: number;
|
|
109
109
|
};
|
|
110
|
+
/** Site origin for relative links. Applied to newly inserted links, including paste over selected text. */
|
|
111
|
+
linkBaseUrl: {
|
|
112
|
+
type: StringConstructor;
|
|
113
|
+
default: string;
|
|
114
|
+
};
|
|
110
115
|
/** Host-owned search and identity lookup for links to application records. */
|
|
111
116
|
linkSearch: {
|
|
112
117
|
type: PropType<LoomaLinkSearch | undefined>;
|
|
@@ -213,6 +218,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
213
218
|
type: NumberConstructor;
|
|
214
219
|
default: number;
|
|
215
220
|
};
|
|
221
|
+
/** Site origin for relative links. Applied to newly inserted links, including paste over selected text. */
|
|
222
|
+
linkBaseUrl: {
|
|
223
|
+
type: StringConstructor;
|
|
224
|
+
default: string;
|
|
225
|
+
};
|
|
216
226
|
/** Host-owned search and identity lookup for links to application records. */
|
|
217
227
|
linkSearch: {
|
|
218
228
|
type: PropType<LoomaLinkSearch | undefined>;
|
|
@@ -270,6 +280,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
270
280
|
}>, {
|
|
271
281
|
label: string;
|
|
272
282
|
placeholder: string;
|
|
283
|
+
linkBaseUrl: string;
|
|
273
284
|
disableHighlight: boolean;
|
|
274
285
|
codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
|
|
275
286
|
modelValue: JSONContent;
|
package/vue/editor/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
|
|
2
|
-
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
|
|
3
|
-
import { Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Menu, MenuItem, Popover, Tooltip } from "@threadlabs/looma/vue";
|
|
2
|
+
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar, siteRelativeHref } from "@threadlabs/looma/editor";
|
|
3
|
+
import { Button, Card, Checkbox, Cluster, Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, IconButton, Input, InputGroup, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
4
4
|
import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
|
|
5
5
|
import { closeHistory } from "@tiptap/pm/history";
|
|
6
6
|
import { createLowlight } from "lowlight";
|
|
@@ -501,6 +501,8 @@ function loomaIcon(name) {
|
|
|
501
501
|
"aria-hidden": "true",
|
|
502
502
|
focusable: "false",
|
|
503
503
|
viewBox: "0 0 24 24",
|
|
504
|
+
width: 16,
|
|
505
|
+
height: 16,
|
|
504
506
|
fill: "none",
|
|
505
507
|
stroke: "currentColor",
|
|
506
508
|
"stroke-width": 2,
|
|
@@ -562,6 +564,11 @@ var LoomaEditor = defineComponent({
|
|
|
562
564
|
type: Number,
|
|
563
565
|
default: DEFAULT_MENTION_RESULT_LIMIT
|
|
564
566
|
},
|
|
567
|
+
/** Site origin for relative links. Applied to newly inserted links, including paste over selected text. */
|
|
568
|
+
linkBaseUrl: {
|
|
569
|
+
type: String,
|
|
570
|
+
default: ""
|
|
571
|
+
},
|
|
565
572
|
/** Host-owned search and identity lookup for links to application records. */
|
|
566
573
|
linkSearch: {
|
|
567
574
|
type: Function,
|
|
@@ -625,7 +632,8 @@ var LoomaEditor = defineComponent({
|
|
|
625
632
|
const blockActionAnchorId = `ui-block-tools-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
626
633
|
let blockActionTarget = null;
|
|
627
634
|
const linkAnchorId = `looma-editor-link-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
628
|
-
const
|
|
635
|
+
const linkPopoverAnchorId = ref(linkAnchorId);
|
|
636
|
+
const linkDestinationInput = () => linkFormRoot.value?.querySelector("[data-link-destination]");
|
|
629
637
|
const linkFormRoot = ref(null);
|
|
630
638
|
const linkOpen = ref(false);
|
|
631
639
|
const linkContextEditing = ref(false);
|
|
@@ -850,6 +858,7 @@ var LoomaEditor = defineComponent({
|
|
|
850
858
|
extensions: [
|
|
851
859
|
...getDefaultEditorExtensions({
|
|
852
860
|
placeholder: props.placeholder,
|
|
861
|
+
linkBaseUrl: props.linkBaseUrl,
|
|
853
862
|
mention: mentionExtension ?? false,
|
|
854
863
|
disableHighlight: props.disableHighlight,
|
|
855
864
|
codeLanguages: props.codeLanguages ?? void 0,
|
|
@@ -966,7 +975,7 @@ var LoomaEditor = defineComponent({
|
|
|
966
975
|
linkError.value = "";
|
|
967
976
|
linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
|
|
968
977
|
linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
|
|
969
|
-
linkQuery.value = linkSelectedTarget.value?.label ??
|
|
978
|
+
linkQuery.value = linkSelectedTarget.value?.label ?? linkHref.value;
|
|
970
979
|
linkContextEditing.value = context;
|
|
971
980
|
linkOpen.value = !context;
|
|
972
981
|
if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
|
|
@@ -980,7 +989,7 @@ var LoomaEditor = defineComponent({
|
|
|
980
989
|
}
|
|
981
990
|
}).catch(() => {});
|
|
982
991
|
}
|
|
983
|
-
nextTick(() => (linkMode.value === "url" ?
|
|
992
|
+
nextTick(() => (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
|
|
984
993
|
};
|
|
985
994
|
const saveLink = () => {
|
|
986
995
|
const instance = editor.value;
|
|
@@ -989,16 +998,16 @@ var LoomaEditor = defineComponent({
|
|
|
989
998
|
const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
|
|
990
999
|
if (!href) {
|
|
991
1000
|
linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
|
|
992
|
-
(linkMode.value === "url" ?
|
|
1001
|
+
(linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
|
|
993
1002
|
return;
|
|
994
1003
|
}
|
|
995
1004
|
if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
|
|
996
1005
|
linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
|
|
997
|
-
|
|
1006
|
+
linkDestinationInput()?.focus();
|
|
998
1007
|
return;
|
|
999
1008
|
}
|
|
1000
1009
|
const attrs = {
|
|
1001
|
-
href,
|
|
1010
|
+
href: props.linkBaseUrl ? siteRelativeHref(href, props.linkBaseUrl) : href,
|
|
1002
1011
|
target: linkNewTab.value ? "_blank" : "_self",
|
|
1003
1012
|
rel: linkNewTab.value ? "noopener noreferrer" : null
|
|
1004
1013
|
};
|
|
@@ -1022,7 +1031,7 @@ var LoomaEditor = defineComponent({
|
|
|
1022
1031
|
}).run();
|
|
1023
1032
|
} else {
|
|
1024
1033
|
if (selection.existing && selection.from === selection.to) chain.extendMarkRange("link");
|
|
1025
|
-
chain.setLink(attrs).run();
|
|
1034
|
+
chain.setLink(attrs).setTextSelection(selection.to).run();
|
|
1026
1035
|
}
|
|
1027
1036
|
linkOpen.value = false;
|
|
1028
1037
|
linkContextEditing.value = false;
|
|
@@ -1034,7 +1043,7 @@ var LoomaEditor = defineComponent({
|
|
|
1034
1043
|
instance.chain().focus().setTextSelection({
|
|
1035
1044
|
from: selection.from,
|
|
1036
1045
|
to: selection.to
|
|
1037
|
-
}).extendMarkRange("link").unsetLink().run();
|
|
1046
|
+
}).extendMarkRange("link").unsetLink().setTextSelection(selection.to).run();
|
|
1038
1047
|
linkOpen.value = false;
|
|
1039
1048
|
linkContextEditing.value = false;
|
|
1040
1049
|
};
|
|
@@ -1044,6 +1053,114 @@ var LoomaEditor = defineComponent({
|
|
|
1044
1053
|
instance.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
1045
1054
|
linkContextEditing.value = false;
|
|
1046
1055
|
};
|
|
1056
|
+
const cancelLink = () => {
|
|
1057
|
+
linkOpen.value = false;
|
|
1058
|
+
linkContextEditing.value = false;
|
|
1059
|
+
const selection = linkSelection;
|
|
1060
|
+
if (selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
|
|
1061
|
+
};
|
|
1062
|
+
const renderLinkDestination = (mode) => [h(FormField, { invalid: Boolean(linkError.value) }, {
|
|
1063
|
+
label: () => h("label", props.linkSearch ? "Link destination" : "URL"),
|
|
1064
|
+
default: () => {
|
|
1065
|
+
const input = h(Input, {
|
|
1066
|
+
"data-link-destination": "",
|
|
1067
|
+
...props.linkSearch ? {
|
|
1068
|
+
"data-link-search": "",
|
|
1069
|
+
type: "text",
|
|
1070
|
+
role: "searchbox",
|
|
1071
|
+
autocomplete: "off",
|
|
1072
|
+
placeholder: `Search ${props.linkTargetLabel.toLowerCase()}s or paste a URL…`
|
|
1073
|
+
} : {
|
|
1074
|
+
type: "text",
|
|
1075
|
+
inputmode: "url",
|
|
1076
|
+
placeholder: "https://example.com"
|
|
1077
|
+
},
|
|
1078
|
+
size: props.linkSearch ? "md" : "sm",
|
|
1079
|
+
value: props.linkSearch ? linkQuery.value : linkHref.value,
|
|
1080
|
+
onInput: (event) => {
|
|
1081
|
+
const value = event.target.value;
|
|
1082
|
+
if (props.linkSearch) {
|
|
1083
|
+
linkQuery.value = value;
|
|
1084
|
+
linkMode.value = /^(https?:\/\/|mailto:|tel:)/i.test(value.trim()) || linkSelection?.existing && /^(\/|\.\.?\/|#|\?)/.test(value.trim()) ? "url" : "target";
|
|
1085
|
+
linkSelectedTarget.value = null;
|
|
1086
|
+
}
|
|
1087
|
+
linkHref.value = value;
|
|
1088
|
+
linkError.value = "";
|
|
1089
|
+
},
|
|
1090
|
+
onKeydown: (event) => {
|
|
1091
|
+
if (mode === "target" && event.key === "ArrowDown") moveLinkResultFocus(event, 1);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
return props.linkSearch ? h(InputGroup, { prefixKind: "icon" }, {
|
|
1095
|
+
prefix: () => loomaIcon(mode === "target" ? "search" : "link"),
|
|
1096
|
+
default: () => input,
|
|
1097
|
+
action: () => linkQuery.value ? h(IconButton, {
|
|
1098
|
+
label: "Clear search",
|
|
1099
|
+
variant: "ghost",
|
|
1100
|
+
size: "sm",
|
|
1101
|
+
onClick: () => {
|
|
1102
|
+
linkQuery.value = "";
|
|
1103
|
+
linkHref.value = "";
|
|
1104
|
+
linkMode.value = "target";
|
|
1105
|
+
linkSelectedTarget.value = null;
|
|
1106
|
+
linkError.value = "";
|
|
1107
|
+
nextTick(() => linkFormRoot.value?.querySelector("[data-link-search]")?.focus());
|
|
1108
|
+
}
|
|
1109
|
+
}, () => loomaIcon("x")) : null
|
|
1110
|
+
}) : input;
|
|
1111
|
+
}
|
|
1112
|
+
}), mode === "target" ? h(ScrollArea, {
|
|
1113
|
+
size: "sm",
|
|
1114
|
+
"aria-label": "Destination search results"
|
|
1115
|
+
}, () => [
|
|
1116
|
+
linkSearching.value ? h(Text, {
|
|
1117
|
+
size: "sm",
|
|
1118
|
+
tone: "secondary",
|
|
1119
|
+
role: "status"
|
|
1120
|
+
}, () => "Searching…") : null,
|
|
1121
|
+
!linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length < 2 ? h(Text, {
|
|
1122
|
+
size: "sm",
|
|
1123
|
+
tone: "secondary"
|
|
1124
|
+
}, () => "Type at least two letters to search.") : null,
|
|
1125
|
+
!linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length >= 2 && !linkResults.value.length && !linkError.value ? h(Text, {
|
|
1126
|
+
size: "sm",
|
|
1127
|
+
tone: "secondary"
|
|
1128
|
+
}, () => "No results found.") : null,
|
|
1129
|
+
...(linkSelectedTarget.value ? [linkSelectedTarget.value] : linkResults.value).map((target) => h(SearchResultRow, {
|
|
1130
|
+
key: target.id,
|
|
1131
|
+
density: "compact",
|
|
1132
|
+
selected: target.id === linkSelectedTarget.value?.id,
|
|
1133
|
+
"data-link-result": "",
|
|
1134
|
+
onClick: () => selectLinkTarget(target),
|
|
1135
|
+
onKeydown: (event) => {
|
|
1136
|
+
if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
|
|
1137
|
+
if (event.key === "ArrowUp") moveLinkResultFocus(event, -1);
|
|
1138
|
+
}
|
|
1139
|
+
}, {
|
|
1140
|
+
leading: () => loomaIcon("file-text"),
|
|
1141
|
+
title: () => h(Text, {
|
|
1142
|
+
as: "strong",
|
|
1143
|
+
size: "sm",
|
|
1144
|
+
weight: "medium",
|
|
1145
|
+
truncate: true,
|
|
1146
|
+
title: target.label
|
|
1147
|
+
}, () => target.label),
|
|
1148
|
+
meta: () => target.detail ? h(Text, {
|
|
1149
|
+
as: "span",
|
|
1150
|
+
size: "xs",
|
|
1151
|
+
tone: "secondary",
|
|
1152
|
+
truncate: true,
|
|
1153
|
+
title: target.detail
|
|
1154
|
+
}, () => target.detail) : null,
|
|
1155
|
+
trailing: () => target.id === linkSelectedTarget.value?.id ? loomaIcon("check") : null
|
|
1156
|
+
}))
|
|
1157
|
+
]) : h(Checkbox, {
|
|
1158
|
+
size: "sm",
|
|
1159
|
+
checked: linkNewTab.value,
|
|
1160
|
+
onChange: (event) => {
|
|
1161
|
+
linkNewTab.value = event.detail.checked;
|
|
1162
|
+
}
|
|
1163
|
+
}, () => "Open in new tab")];
|
|
1047
1164
|
const renderLinkForm = () => h("form", {
|
|
1048
1165
|
ref: linkFormRoot,
|
|
1049
1166
|
class: "looma-editor__link-form",
|
|
@@ -1052,134 +1169,120 @@ var LoomaEditor = defineComponent({
|
|
|
1052
1169
|
event.preventDefault();
|
|
1053
1170
|
saveLink();
|
|
1054
1171
|
}
|
|
1055
|
-
}, [
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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", {
|
|
1104
|
-
ref: linkUrlInput,
|
|
1105
|
-
type: "text",
|
|
1106
|
-
inputmode: "url",
|
|
1107
|
-
value: linkHref.value,
|
|
1108
|
-
"aria-invalid": linkError.value ? "true" : void 0,
|
|
1109
|
-
onInput: (event) => {
|
|
1110
|
-
linkHref.value = event.target.value;
|
|
1111
|
-
linkError.value = "";
|
|
1112
|
-
}
|
|
1113
|
-
})]),
|
|
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);
|
|
1172
|
+
}, [h(Stack, { gap: "s" }, () => [
|
|
1173
|
+
h(Text, {
|
|
1174
|
+
as: "h3",
|
|
1175
|
+
size: "sm",
|
|
1176
|
+
weight: "semibold"
|
|
1177
|
+
}, () => linkSelection?.existing ? "Edit link" : "Add link"),
|
|
1178
|
+
h(Stack, { gap: "s" }, () => renderLinkDestination(linkMode.value)),
|
|
1179
|
+
linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h(FormField, {}, {
|
|
1180
|
+
label: () => h("label", "Text"),
|
|
1181
|
+
default: () => h(Input, {
|
|
1182
|
+
size: "sm",
|
|
1183
|
+
value: linkText.value,
|
|
1184
|
+
onInput: (event) => {
|
|
1185
|
+
linkText.value = event.target.value;
|
|
1186
|
+
linkError.value = "";
|
|
1130
1187
|
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
linkNewTab.value = event.target.checked;
|
|
1137
|
-
}
|
|
1138
|
-
}), h("span", "Open in new tab")]),
|
|
1139
|
-
linkError.value ? h("p", {
|
|
1140
|
-
class: "looma-editor__link-error",
|
|
1188
|
+
})
|
|
1189
|
+
}) : null,
|
|
1190
|
+
linkError.value ? h(Text, {
|
|
1191
|
+
size: "sm",
|
|
1192
|
+
tone: "danger",
|
|
1141
1193
|
role: "alert"
|
|
1142
|
-
}, linkError.value) : null,
|
|
1143
|
-
|
|
1144
|
-
|
|
1194
|
+
}, () => linkError.value) : null,
|
|
1195
|
+
h(Separator),
|
|
1196
|
+
h(Cluster, {
|
|
1197
|
+
gap: "xs",
|
|
1198
|
+
justify: "between"
|
|
1199
|
+
}, () => [h(Cluster, { gap: "xs" }, () => [linkSelection?.existing ? h(Button, {
|
|
1200
|
+
type: "button",
|
|
1201
|
+
variant: "ghost",
|
|
1202
|
+
tone: "danger",
|
|
1203
|
+
size: "sm",
|
|
1204
|
+
onClick: removeLink
|
|
1205
|
+
}, () => "Remove link") : null, linkMode.value === "url" && validLinkHref(linkHref.value) ? h(Button, {
|
|
1206
|
+
as: "a",
|
|
1207
|
+
variant: "link",
|
|
1208
|
+
size: "sm",
|
|
1145
1209
|
href: validLinkHref(linkHref.value),
|
|
1146
1210
|
target: "_blank",
|
|
1147
1211
|
rel: "noopener noreferrer"
|
|
1148
|
-
}, "Preview link") : null,
|
|
1149
|
-
h("div", { class: "looma-editor__link-actions" }, [linkSelection?.existing ? h("button", {
|
|
1212
|
+
}, () => "Preview link") : null]), h(Cluster, { gap: "xs" }, () => [h(Button, {
|
|
1150
1213
|
type: "button",
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1214
|
+
variant: "ghost",
|
|
1215
|
+
tone: "neutral",
|
|
1216
|
+
size: "sm",
|
|
1217
|
+
onClick: cancelLink
|
|
1218
|
+
}, () => "Cancel"), h(Button, {
|
|
1219
|
+
type: "submit",
|
|
1220
|
+
variant: "solid",
|
|
1221
|
+
size: "sm"
|
|
1222
|
+
}, () => "Save link")])])
|
|
1223
|
+
])]);
|
|
1154
1224
|
const renderLinkContext = (instance) => {
|
|
1155
1225
|
if (linkContextEditing.value) return renderLinkForm();
|
|
1156
1226
|
const rawHref = instance.getAttributes("link").href;
|
|
1157
1227
|
const href = typeof rawHref === "string" ? rawHref : "";
|
|
1158
1228
|
const safeHref = validLinkHref(href);
|
|
1159
|
-
|
|
1160
|
-
|
|
1229
|
+
const editId = `${toolbarScope}-link-edit`;
|
|
1230
|
+
const removeId = `${toolbarScope}-link-remove`;
|
|
1231
|
+
const openId = `${toolbarScope}-link-open`;
|
|
1232
|
+
const target = linkContextTarget.value;
|
|
1233
|
+
return h(Cluster, {
|
|
1234
|
+
gap: "xs",
|
|
1235
|
+
wrap: "nowrap",
|
|
1161
1236
|
role: "group",
|
|
1162
1237
|
"aria-label": "Link actions"
|
|
1163
|
-
}, [
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1238
|
+
}, () => [
|
|
1239
|
+
loomaIcon(target ? "file-text" : "link"),
|
|
1240
|
+
h(Stack, {
|
|
1241
|
+
gap: "none",
|
|
1242
|
+
title: href,
|
|
1243
|
+
"aria-label": `${target ? props.linkTargetLabel : "URL"}: ${target?.label ?? href}`
|
|
1244
|
+
}, () => [h(Text, {
|
|
1245
|
+
as: "strong",
|
|
1246
|
+
size: "sm",
|
|
1247
|
+
weight: "medium",
|
|
1248
|
+
font: "sans",
|
|
1249
|
+
wrap: "anywhere"
|
|
1250
|
+
}, () => target?.label ?? href), target?.detail ? h(Text, {
|
|
1251
|
+
as: "small",
|
|
1252
|
+
size: "xs",
|
|
1253
|
+
tone: "secondary",
|
|
1254
|
+
font: "sans"
|
|
1255
|
+
}, () => target.detail) : null]),
|
|
1256
|
+
safeHref ? h(Button, {
|
|
1257
|
+
id: openId,
|
|
1258
|
+
as: "a",
|
|
1259
|
+
variant: "ghost",
|
|
1260
|
+
tone: "neutral",
|
|
1261
|
+
size: "sm",
|
|
1262
|
+
href: safeHref,
|
|
1263
|
+
target: "_blank",
|
|
1264
|
+
rel: "noopener noreferrer",
|
|
1265
|
+
"aria-label": "Open link"
|
|
1266
|
+
}, () => loomaIcon("external-link")) : null,
|
|
1267
|
+
safeHref ? h(Tooltip, { for: openId }, () => "Open link") : null,
|
|
1268
|
+
h(IconButton, {
|
|
1269
|
+
id: editId,
|
|
1270
|
+
label: "Edit link",
|
|
1271
|
+
variant: "ghost",
|
|
1272
|
+
size: "sm",
|
|
1170
1273
|
onPointerdown: (event) => event.preventDefault(),
|
|
1171
1274
|
onClick: () => openLinkEditor(true)
|
|
1172
|
-
},
|
|
1173
|
-
h("
|
|
1174
|
-
|
|
1275
|
+
}, () => loomaIcon("pencil")),
|
|
1276
|
+
h(Tooltip, { for: editId }, () => "Edit link"),
|
|
1277
|
+
h(IconButton, {
|
|
1278
|
+
id: removeId,
|
|
1279
|
+
label: "Remove link",
|
|
1280
|
+
variant: "ghost",
|
|
1281
|
+
size: "sm",
|
|
1175
1282
|
onPointerdown: (event) => event.preventDefault(),
|
|
1176
1283
|
onClick: removeCurrentLink
|
|
1177
|
-
},
|
|
1178
|
-
|
|
1179
|
-
href: safeHref,
|
|
1180
|
-
target: "_blank",
|
|
1181
|
-
rel: "noopener noreferrer"
|
|
1182
|
-
}, "Open link") : null
|
|
1284
|
+
}, () => loomaIcon("unlink")),
|
|
1285
|
+
h(Tooltip, { for: removeId }, () => "Remove link")
|
|
1183
1286
|
]);
|
|
1184
1287
|
};
|
|
1185
1288
|
const captureBlockAction = () => {
|
|
@@ -1724,29 +1827,34 @@ var LoomaEditor = defineComponent({
|
|
|
1724
1827
|
...authorHighlight ? [commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run(), "Mod-Shift-h")] : [],
|
|
1725
1828
|
commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run(), "Mod-e")
|
|
1726
1829
|
];
|
|
1830
|
+
const linkTriggerId = textOnly ? `${linkAnchorId}-selection` : linkAnchorId;
|
|
1831
|
+
textButtons.push(h(IconButton, {
|
|
1832
|
+
id: linkTriggerId,
|
|
1833
|
+
class: "looma-editor__toolbar-button",
|
|
1834
|
+
label: "Link",
|
|
1835
|
+
size: "sm",
|
|
1836
|
+
variant: "ghost",
|
|
1837
|
+
"aria-expanded": linkOpen.value ? "true" : "false",
|
|
1838
|
+
"aria-pressed": instance.isActive("link") ? "true" : "false",
|
|
1839
|
+
"data-active": instance.isActive("link") ? "true" : "false",
|
|
1840
|
+
onPointerenter: () => showTool(linkTriggerId, "Link", false),
|
|
1841
|
+
onPointerleave: hideTool,
|
|
1842
|
+
onPointerdown: () => {
|
|
1843
|
+
linkPopoverAnchorId.value = linkTriggerId;
|
|
1844
|
+
const selection = instance.state.selection;
|
|
1845
|
+
linkPressedSelection = selection.empty ? null : {
|
|
1846
|
+
from: selection.from,
|
|
1847
|
+
to: selection.to
|
|
1848
|
+
};
|
|
1849
|
+
},
|
|
1850
|
+
onFocusin: () => {
|
|
1851
|
+
linkPopoverAnchorId.value = linkTriggerId;
|
|
1852
|
+
showTool(linkTriggerId, "Link", true);
|
|
1853
|
+
},
|
|
1854
|
+
onFocusout: hideTool
|
|
1855
|
+
}, () => loomaIcon("link")));
|
|
1727
1856
|
const buttons = textOnly ? textButtons : [
|
|
1728
1857
|
...textButtons,
|
|
1729
|
-
h(IconButton, {
|
|
1730
|
-
id: linkAnchorId,
|
|
1731
|
-
class: "looma-editor__toolbar-button",
|
|
1732
|
-
label: "Link",
|
|
1733
|
-
size: "sm",
|
|
1734
|
-
variant: "ghost",
|
|
1735
|
-
"aria-expanded": linkOpen.value ? "true" : "false",
|
|
1736
|
-
"aria-pressed": instance.isActive("link") ? "true" : "false",
|
|
1737
|
-
"data-active": instance.isActive("link") ? "true" : "false",
|
|
1738
|
-
onPointerenter: () => showTool(linkAnchorId, "Link", false),
|
|
1739
|
-
onPointerleave: hideTool,
|
|
1740
|
-
onPointerdown: () => {
|
|
1741
|
-
const selection = instance.state.selection;
|
|
1742
|
-
linkPressedSelection = selection.empty ? null : {
|
|
1743
|
-
from: selection.from,
|
|
1744
|
-
to: selection.to
|
|
1745
|
-
};
|
|
1746
|
-
},
|
|
1747
|
-
onFocusin: () => showTool(linkAnchorId, "Link", true),
|
|
1748
|
-
onFocusout: hideTool
|
|
1749
|
-
}, () => loomaIcon("link")),
|
|
1750
1858
|
h("span", {
|
|
1751
1859
|
class: "divider",
|
|
1752
1860
|
"aria-hidden": "true"
|
|
@@ -1901,10 +2009,15 @@ var LoomaEditor = defineComponent({
|
|
|
1901
2009
|
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
1902
2010
|
onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
|
|
1903
2011
|
duration: 100,
|
|
1904
|
-
maxWidth:
|
|
2012
|
+
maxWidth: Math.min(480, window.innerWidth - 24),
|
|
1905
2013
|
placement: "bottom"
|
|
1906
2014
|
}
|
|
1907
|
-
}, { default: () => h(
|
|
2015
|
+
}, { default: () => h(Card, {
|
|
2016
|
+
variant: "elevated",
|
|
2017
|
+
padding: linkContextEditing.value ? "sm" : "xs",
|
|
2018
|
+
size: linkContextEditing.value ? "sm" : "content",
|
|
2019
|
+
class: ["looma-editor__link-context", { "looma-editor__link-context--editing": linkContextEditing.value }]
|
|
2020
|
+
}, () => [renderLinkContext(instance)]) }) : null,
|
|
1908
2021
|
instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
|
|
1909
2022
|
class: "looma-editor__formatting-popover",
|
|
1910
2023
|
role: "region",
|
|
@@ -2028,13 +2141,16 @@ var LoomaEditor = defineComponent({
|
|
|
2028
2141
|
}, chipColor.value === color ? "✓" : "")))])),
|
|
2029
2142
|
h(Popover, {
|
|
2030
2143
|
class: "looma-editor__link-popover",
|
|
2144
|
+
size: "sm",
|
|
2031
2145
|
open: linkOpen.value,
|
|
2032
|
-
for:
|
|
2146
|
+
for: linkPopoverAnchorId.value,
|
|
2033
2147
|
placement: mobile.value ? "top-start" : "bottom-start",
|
|
2034
2148
|
onOpen: () => openLinkEditor(),
|
|
2035
2149
|
onClose: () => {
|
|
2036
2150
|
linkOpen.value = false;
|
|
2037
2151
|
linkPressedSelection = null;
|
|
2152
|
+
const current = editor.value?.state.selection;
|
|
2153
|
+
if (current && !current.empty && linkSelection && current.from === linkSelection.from && current.to === linkSelection.to) editor.value?.commands.setTextSelection(current.to);
|
|
2038
2154
|
}
|
|
2039
2155
|
}, renderLinkForm),
|
|
2040
2156
|
h(Menu, {
|