@threadlabs/looma 0.22.12 → 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 +75 -6
- 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/extensions/slash-command.d.ts +5 -3
- 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 -297
- package/vue/editor/LoomaEditor.d.ts +11 -0
- package/vue/editor/index.js +262 -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);
|
|
@@ -769,6 +777,7 @@ var LoomaEditor = defineComponent({
|
|
|
769
777
|
const slashExtension = createLoomaSlashCommandExtension({
|
|
770
778
|
onOpenImagePicker: () => fileInput.value?.click(),
|
|
771
779
|
onOpenChipEditor: openChipEditor,
|
|
780
|
+
onOpenLinkEditor: () => openLinkEditor(),
|
|
772
781
|
onStateChange: (state) => {
|
|
773
782
|
Object.assign(slash, state);
|
|
774
783
|
}
|
|
@@ -849,6 +858,7 @@ var LoomaEditor = defineComponent({
|
|
|
849
858
|
extensions: [
|
|
850
859
|
...getDefaultEditorExtensions({
|
|
851
860
|
placeholder: props.placeholder,
|
|
861
|
+
linkBaseUrl: props.linkBaseUrl,
|
|
852
862
|
mention: mentionExtension ?? false,
|
|
853
863
|
disableHighlight: props.disableHighlight,
|
|
854
864
|
codeLanguages: props.codeLanguages ?? void 0,
|
|
@@ -965,7 +975,7 @@ var LoomaEditor = defineComponent({
|
|
|
965
975
|
linkError.value = "";
|
|
966
976
|
linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
|
|
967
977
|
linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
|
|
968
|
-
linkQuery.value = linkSelectedTarget.value?.label ??
|
|
978
|
+
linkQuery.value = linkSelectedTarget.value?.label ?? linkHref.value;
|
|
969
979
|
linkContextEditing.value = context;
|
|
970
980
|
linkOpen.value = !context;
|
|
971
981
|
if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
|
|
@@ -979,7 +989,7 @@ var LoomaEditor = defineComponent({
|
|
|
979
989
|
}
|
|
980
990
|
}).catch(() => {});
|
|
981
991
|
}
|
|
982
|
-
nextTick(() => (linkMode.value === "url" ?
|
|
992
|
+
nextTick(() => (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
|
|
983
993
|
};
|
|
984
994
|
const saveLink = () => {
|
|
985
995
|
const instance = editor.value;
|
|
@@ -988,16 +998,16 @@ var LoomaEditor = defineComponent({
|
|
|
988
998
|
const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
|
|
989
999
|
if (!href) {
|
|
990
1000
|
linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
|
|
991
|
-
(linkMode.value === "url" ?
|
|
1001
|
+
(linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
|
|
992
1002
|
return;
|
|
993
1003
|
}
|
|
994
1004
|
if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
|
|
995
1005
|
linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
|
|
996
|
-
|
|
1006
|
+
linkDestinationInput()?.focus();
|
|
997
1007
|
return;
|
|
998
1008
|
}
|
|
999
1009
|
const attrs = {
|
|
1000
|
-
href,
|
|
1010
|
+
href: props.linkBaseUrl ? siteRelativeHref(href, props.linkBaseUrl) : href,
|
|
1001
1011
|
target: linkNewTab.value ? "_blank" : "_self",
|
|
1002
1012
|
rel: linkNewTab.value ? "noopener noreferrer" : null
|
|
1003
1013
|
};
|
|
@@ -1021,7 +1031,7 @@ var LoomaEditor = defineComponent({
|
|
|
1021
1031
|
}).run();
|
|
1022
1032
|
} else {
|
|
1023
1033
|
if (selection.existing && selection.from === selection.to) chain.extendMarkRange("link");
|
|
1024
|
-
chain.setLink(attrs).run();
|
|
1034
|
+
chain.setLink(attrs).setTextSelection(selection.to).run();
|
|
1025
1035
|
}
|
|
1026
1036
|
linkOpen.value = false;
|
|
1027
1037
|
linkContextEditing.value = false;
|
|
@@ -1033,7 +1043,7 @@ var LoomaEditor = defineComponent({
|
|
|
1033
1043
|
instance.chain().focus().setTextSelection({
|
|
1034
1044
|
from: selection.from,
|
|
1035
1045
|
to: selection.to
|
|
1036
|
-
}).extendMarkRange("link").unsetLink().run();
|
|
1046
|
+
}).extendMarkRange("link").unsetLink().setTextSelection(selection.to).run();
|
|
1037
1047
|
linkOpen.value = false;
|
|
1038
1048
|
linkContextEditing.value = false;
|
|
1039
1049
|
};
|
|
@@ -1043,6 +1053,114 @@ var LoomaEditor = defineComponent({
|
|
|
1043
1053
|
instance.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
1044
1054
|
linkContextEditing.value = false;
|
|
1045
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")];
|
|
1046
1164
|
const renderLinkForm = () => h("form", {
|
|
1047
1165
|
ref: linkFormRoot,
|
|
1048
1166
|
class: "looma-editor__link-form",
|
|
@@ -1051,134 +1169,120 @@ var LoomaEditor = defineComponent({
|
|
|
1051
1169
|
event.preventDefault();
|
|
1052
1170
|
saveLink();
|
|
1053
1171
|
}
|
|
1054
|
-
}, [
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
onClick: () => {
|
|
1070
|
-
linkMode.value = "target";
|
|
1071
|
-
linkHref.value = "";
|
|
1072
|
-
linkSelectedTarget.value = null;
|
|
1073
|
-
linkQuery.value = "";
|
|
1074
|
-
linkError.value = "";
|
|
1075
|
-
nextTick(() => linkFormRoot.value?.querySelector("[data-link-search]")?.focus());
|
|
1076
|
-
}
|
|
1077
|
-
}, props.linkTargetLabel), h("button", {
|
|
1078
|
-
type: "button",
|
|
1079
|
-
"aria-pressed": linkMode.value === "url",
|
|
1080
|
-
onClick: () => {
|
|
1081
|
-
linkMode.value = "url";
|
|
1082
|
-
linkHref.value = "";
|
|
1083
|
-
linkSelectedTarget.value = null;
|
|
1084
|
-
linkError.value = "";
|
|
1085
|
-
nextTick(() => linkUrlInput.value?.focus());
|
|
1086
|
-
}
|
|
1087
|
-
}, "URL")]) : null,
|
|
1088
|
-
linkMode.value === "target" && props.linkSearch ? h("label", [h("span", `Find a ${props.linkTargetLabel.toLowerCase()}`), h("input", {
|
|
1089
|
-
"data-link-search": "",
|
|
1090
|
-
type: "search",
|
|
1091
|
-
value: linkQuery.value,
|
|
1092
|
-
autocomplete: "off",
|
|
1093
|
-
"aria-invalid": linkError.value ? "true" : void 0,
|
|
1094
|
-
onInput: (event) => {
|
|
1095
|
-
linkQuery.value = event.target.value;
|
|
1096
|
-
linkSelectedTarget.value = null;
|
|
1097
|
-
linkError.value = "";
|
|
1098
|
-
},
|
|
1099
|
-
onKeydown: (event) => {
|
|
1100
|
-
if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
|
|
1101
|
-
}
|
|
1102
|
-
})]) : h("label", [h("span", "URL"), h("input", {
|
|
1103
|
-
ref: linkUrlInput,
|
|
1104
|
-
type: "text",
|
|
1105
|
-
inputmode: "url",
|
|
1106
|
-
value: linkHref.value,
|
|
1107
|
-
"aria-invalid": linkError.value ? "true" : void 0,
|
|
1108
|
-
onInput: (event) => {
|
|
1109
|
-
linkHref.value = event.target.value;
|
|
1110
|
-
linkError.value = "";
|
|
1111
|
-
}
|
|
1112
|
-
})]),
|
|
1113
|
-
linkMode.value === "target" && props.linkSearch ? h("div", {
|
|
1114
|
-
class: "looma-editor__link-results",
|
|
1115
|
-
"aria-label": "Destination search results"
|
|
1116
|
-
}, [
|
|
1117
|
-
linkSearching.value ? h("p", { role: "status" }, "Searching…") : null,
|
|
1118
|
-
linkSelectedTarget.value ? h("p", { class: "looma-editor__link-selected" }, `Selected: ${linkSelectedTarget.value.label}${linkSelectedTarget.value.detail ? ` · ${linkSelectedTarget.value.detail}` : ""}`) : null,
|
|
1119
|
-
!linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length < 2 ? h("p", "Type at least two letters to search.") : null,
|
|
1120
|
-
!linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length >= 2 && !linkResults.value.length && !linkError.value ? h("p", "No results found.") : null,
|
|
1121
|
-
...linkResults.value.map((target) => h("button", {
|
|
1122
|
-
key: target.id,
|
|
1123
|
-
type: "button",
|
|
1124
|
-
"data-link-result": "",
|
|
1125
|
-
onClick: () => selectLinkTarget(target),
|
|
1126
|
-
onKeydown: (event) => {
|
|
1127
|
-
if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
|
|
1128
|
-
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 = "";
|
|
1129
1187
|
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
linkNewTab.value = event.target.checked;
|
|
1136
|
-
}
|
|
1137
|
-
}), h("span", "Open in new tab")]),
|
|
1138
|
-
linkError.value ? h("p", {
|
|
1139
|
-
class: "looma-editor__link-error",
|
|
1188
|
+
})
|
|
1189
|
+
}) : null,
|
|
1190
|
+
linkError.value ? h(Text, {
|
|
1191
|
+
size: "sm",
|
|
1192
|
+
tone: "danger",
|
|
1140
1193
|
role: "alert"
|
|
1141
|
-
}, linkError.value) : null,
|
|
1142
|
-
|
|
1143
|
-
|
|
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",
|
|
1144
1209
|
href: validLinkHref(linkHref.value),
|
|
1145
1210
|
target: "_blank",
|
|
1146
1211
|
rel: "noopener noreferrer"
|
|
1147
|
-
}, "Preview link") : null,
|
|
1148
|
-
h("div", { class: "looma-editor__link-actions" }, [linkSelection?.existing ? h("button", {
|
|
1212
|
+
}, () => "Preview link") : null]), h(Cluster, { gap: "xs" }, () => [h(Button, {
|
|
1149
1213
|
type: "button",
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
+
])]);
|
|
1153
1224
|
const renderLinkContext = (instance) => {
|
|
1154
1225
|
if (linkContextEditing.value) return renderLinkForm();
|
|
1155
1226
|
const rawHref = instance.getAttributes("link").href;
|
|
1156
1227
|
const href = typeof rawHref === "string" ? rawHref : "";
|
|
1157
1228
|
const safeHref = validLinkHref(href);
|
|
1158
|
-
|
|
1159
|
-
|
|
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",
|
|
1160
1236
|
role: "group",
|
|
1161
1237
|
"aria-label": "Link actions"
|
|
1162
|
-
}, [
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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",
|
|
1169
1273
|
onPointerdown: (event) => event.preventDefault(),
|
|
1170
1274
|
onClick: () => openLinkEditor(true)
|
|
1171
|
-
},
|
|
1172
|
-
h("
|
|
1173
|
-
|
|
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",
|
|
1174
1282
|
onPointerdown: (event) => event.preventDefault(),
|
|
1175
1283
|
onClick: removeCurrentLink
|
|
1176
|
-
},
|
|
1177
|
-
|
|
1178
|
-
href: safeHref,
|
|
1179
|
-
target: "_blank",
|
|
1180
|
-
rel: "noopener noreferrer"
|
|
1181
|
-
}, "Open link") : null
|
|
1284
|
+
}, () => loomaIcon("unlink")),
|
|
1285
|
+
h(Tooltip, { for: removeId }, () => "Remove link")
|
|
1182
1286
|
]);
|
|
1183
1287
|
};
|
|
1184
1288
|
const captureBlockAction = () => {
|
|
@@ -1723,29 +1827,34 @@ var LoomaEditor = defineComponent({
|
|
|
1723
1827
|
...authorHighlight ? [commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run(), "Mod-Shift-h")] : [],
|
|
1724
1828
|
commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run(), "Mod-e")
|
|
1725
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")));
|
|
1726
1856
|
const buttons = textOnly ? textButtons : [
|
|
1727
1857
|
...textButtons,
|
|
1728
|
-
h(IconButton, {
|
|
1729
|
-
id: linkAnchorId,
|
|
1730
|
-
class: "looma-editor__toolbar-button",
|
|
1731
|
-
label: "Link",
|
|
1732
|
-
size: "sm",
|
|
1733
|
-
variant: "ghost",
|
|
1734
|
-
"aria-expanded": linkOpen.value ? "true" : "false",
|
|
1735
|
-
"aria-pressed": instance.isActive("link") ? "true" : "false",
|
|
1736
|
-
"data-active": instance.isActive("link") ? "true" : "false",
|
|
1737
|
-
onPointerenter: () => showTool(linkAnchorId, "Link", false),
|
|
1738
|
-
onPointerleave: hideTool,
|
|
1739
|
-
onPointerdown: () => {
|
|
1740
|
-
const selection = instance.state.selection;
|
|
1741
|
-
linkPressedSelection = selection.empty ? null : {
|
|
1742
|
-
from: selection.from,
|
|
1743
|
-
to: selection.to
|
|
1744
|
-
};
|
|
1745
|
-
},
|
|
1746
|
-
onFocusin: () => showTool(linkAnchorId, "Link", true),
|
|
1747
|
-
onFocusout: hideTool
|
|
1748
|
-
}, () => loomaIcon("link")),
|
|
1749
1858
|
h("span", {
|
|
1750
1859
|
class: "divider",
|
|
1751
1860
|
"aria-hidden": "true"
|
|
@@ -1900,10 +2009,15 @@ var LoomaEditor = defineComponent({
|
|
|
1900
2009
|
appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
|
|
1901
2010
|
onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
|
|
1902
2011
|
duration: 100,
|
|
1903
|
-
maxWidth:
|
|
2012
|
+
maxWidth: Math.min(480, window.innerWidth - 24),
|
|
1904
2013
|
placement: "bottom"
|
|
1905
2014
|
}
|
|
1906
|
-
}, { 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,
|
|
1907
2021
|
instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
|
|
1908
2022
|
class: "looma-editor__formatting-popover",
|
|
1909
2023
|
role: "region",
|
|
@@ -2027,13 +2141,16 @@ var LoomaEditor = defineComponent({
|
|
|
2027
2141
|
}, chipColor.value === color ? "✓" : "")))])),
|
|
2028
2142
|
h(Popover, {
|
|
2029
2143
|
class: "looma-editor__link-popover",
|
|
2144
|
+
size: "sm",
|
|
2030
2145
|
open: linkOpen.value,
|
|
2031
|
-
for:
|
|
2146
|
+
for: linkPopoverAnchorId.value,
|
|
2032
2147
|
placement: mobile.value ? "top-start" : "bottom-start",
|
|
2033
2148
|
onOpen: () => openLinkEditor(),
|
|
2034
2149
|
onClose: () => {
|
|
2035
2150
|
linkOpen.value = false;
|
|
2036
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);
|
|
2037
2154
|
}
|
|
2038
2155
|
}, renderLinkForm),
|
|
2039
2156
|
h(Menu, {
|