@threadlabs/looma 0.26.23 → 0.26.25

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/vue/editor/index.js +66 -73
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.26.23",
3
+ "version": "0.26.25",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -1760,6 +1760,7 @@ var LoomaEditor = defineComponent({
1760
1760
  const linkContextEditing = ref(false);
1761
1761
  const linkHref = ref("");
1762
1762
  const linkText = ref("");
1763
+ const linkTextEditable = ref(false);
1763
1764
  const linkNewTab = ref(true);
1764
1765
  const linkError = ref("");
1765
1766
  const linkMode = ref("url");
@@ -2129,6 +2130,8 @@ var LoomaEditor = defineComponent({
2129
2130
  linkResults.value = [];
2130
2131
  linkNewTab.value = false;
2131
2132
  linkError.value = "";
2133
+ if (linkTextEditable.value && !linkText.value.trim()) linkText.value = target.label;
2134
+ applyLink();
2132
2135
  };
2133
2136
  const moveLinkResultFocus = (event, offset) => {
2134
2137
  const buttons = [...linkFormRoot.value?.querySelectorAll("[data-link-result]") ?? []];
@@ -2157,6 +2160,7 @@ var LoomaEditor = defineComponent({
2157
2160
  to,
2158
2161
  existing
2159
2162
  };
2163
+ linkTextEditable.value = empty && !existing;
2160
2164
  const attrs = existing ? instance.getAttributes("link") : {};
2161
2165
  linkHref.value = typeof attrs.href === "string" ? attrs.href : "";
2162
2166
  linkNewTab.value = attrs.target !== "_self";
@@ -2178,38 +2182,45 @@ var LoomaEditor = defineComponent({
2178
2182
  }
2179
2183
  }).catch(() => {});
2180
2184
  }
2181
- nextTick(() => (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
2185
+ nextTick(() => {
2186
+ if (context) (props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? formattingPopup : linkPopup)?.setProps({ getReferenceClientRect: () => posToDOMRect(instance.view, instance.state.selection.from, instance.state.selection.to) });
2187
+ (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
2188
+ });
2182
2189
  };
2183
- const saveLink = () => {
2190
+ const applyLink = (reportError = false) => {
2184
2191
  const instance = editor.value;
2185
2192
  const selection = linkSelection;
2186
- if (!instance || !selection) return;
2193
+ if (!instance || !selection) return false;
2187
2194
  const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
2188
2195
  if (!href) {
2189
- linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
2190
- (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
2191
- return;
2196
+ if (reportError) linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
2197
+ return false;
2192
2198
  }
2193
2199
  if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
2194
- linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
2195
- linkDestinationInput()?.focus();
2196
- return;
2200
+ if (reportError) linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
2201
+ return false;
2197
2202
  }
2198
2203
  const attrs = {
2199
2204
  href: props.linkBaseUrl ? siteRelativeHref(href, props.linkBaseUrl) : href,
2200
2205
  target: linkNewTab.value ? "_blank" : "_self",
2201
2206
  rel: linkNewTab.value ? "noopener noreferrer" : null
2202
2207
  };
2203
- const chain = instance.chain().focus().setTextSelection({
2208
+ const current = instance.state.selection;
2209
+ const chain = instance.chain().setTextSelection({
2204
2210
  from: selection.from,
2205
2211
  to: selection.to
2206
2212
  });
2207
- if (selection.from === selection.to && !selection.existing) {
2213
+ if (linkTextEditable.value) {
2208
2214
  const text = linkText.value.trim();
2209
2215
  if (!text) {
2210
- linkError.value = "Enter link text.";
2211
- return;
2216
+ if (reportError) linkError.value = "Enter link text.";
2217
+ return false;
2212
2218
  }
2219
+ linkSelection = {
2220
+ from: selection.from,
2221
+ to: selection.from + text.length,
2222
+ existing: true
2223
+ };
2213
2224
  chain.insertContent({
2214
2225
  type: "text",
2215
2226
  text,
@@ -2217,24 +2228,17 @@ var LoomaEditor = defineComponent({
2217
2228
  type: "link",
2218
2229
  attrs
2219
2230
  }]
2220
- }).run();
2231
+ }).setTextSelection(selection.from).run();
2221
2232
  } else {
2222
2233
  if (selection.existing && selection.from === selection.to) chain.extendMarkRange("link");
2223
- chain.setLink(attrs).setTextSelection(selection.to).run();
2234
+ chain.setLink(attrs).setTextSelection({
2235
+ from: current.from,
2236
+ to: current.to
2237
+ }).run();
2238
+ selection.existing = true;
2224
2239
  }
2225
- linkOpen.value = false;
2226
- linkContextEditing.value = false;
2227
- };
2228
- const removeLink = () => {
2229
- const instance = editor.value;
2230
- const selection = linkSelection;
2231
- if (!instance || !selection?.existing) return;
2232
- instance.chain().focus().setTextSelection({
2233
- from: selection.from,
2234
- to: selection.to
2235
- }).extendMarkRange("link").unsetLink().setTextSelection(selection.to).run();
2236
- linkOpen.value = false;
2237
- linkContextEditing.value = false;
2240
+ linkError.value = "";
2241
+ return true;
2238
2242
  };
2239
2243
  const removeCurrentLink = () => {
2240
2244
  const instance = editor.value;
@@ -2242,11 +2246,14 @@ var LoomaEditor = defineComponent({
2242
2246
  instance.chain().focus().extendMarkRange("link").unsetLink().run();
2243
2247
  linkContextEditing.value = false;
2244
2248
  };
2245
- const cancelLink = () => {
2249
+ const dismissLink = (restoreFocus = true) => {
2246
2250
  linkOpen.value = false;
2247
2251
  linkContextEditing.value = false;
2248
2252
  const selection = linkSelection;
2249
- if (selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
2253
+ if (restoreFocus && selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
2254
+ dismissedFormattingSelection = editor.value?.state.selection ?? null;
2255
+ linkPopup?.hide();
2256
+ formattingPopup?.hide();
2250
2257
  };
2251
2258
  const renderLinkDestination = (mode) => [h(FormField, { invalid: Boolean(linkError.value) }, {
2252
2259
  label: () => h("label", props.linkSearch ? "Link destination" : "URL"),
@@ -2275,6 +2282,7 @@ var LoomaEditor = defineComponent({
2275
2282
  }
2276
2283
  linkHref.value = value;
2277
2284
  linkError.value = "";
2285
+ applyLink();
2278
2286
  },
2279
2287
  onKeydown: (event) => {
2280
2288
  if (mode === "target" && event.key === "ArrowDown") moveLinkResultFocus(event, 1);
@@ -2348,6 +2356,7 @@ var LoomaEditor = defineComponent({
2348
2356
  checked: linkNewTab.value,
2349
2357
  onChange: (event) => {
2350
2358
  linkNewTab.value = event.detail.checked;
2359
+ applyLink();
2351
2360
  }
2352
2361
  }, () => "Open in new tab")];
2353
2362
  const renderLinkForm = () => h("form", {
@@ -2356,7 +2365,18 @@ var LoomaEditor = defineComponent({
2356
2365
  "aria-label": "Edit link",
2357
2366
  onSubmit: (event) => {
2358
2367
  event.preventDefault();
2359
- saveLink();
2368
+ if (applyLink(true)) dismissLink();
2369
+ },
2370
+ onKeydown: (event) => {
2371
+ if (event.key === "Escape") {
2372
+ event.preventDefault();
2373
+ event.stopPropagation();
2374
+ dismissLink();
2375
+ } else if (event.key === "Enter" && event.target instanceof HTMLInputElement) {
2376
+ event.preventDefault();
2377
+ event.stopPropagation();
2378
+ if (applyLink(true)) dismissLink();
2379
+ }
2360
2380
  }
2361
2381
  }, [h(Stack, { gap: "s" }, () => [
2362
2382
  h(Text, {
@@ -2365,7 +2385,7 @@ var LoomaEditor = defineComponent({
2365
2385
  weight: "semibold"
2366
2386
  }, () => linkSelection?.existing ? "Edit link" : "Add link"),
2367
2387
  h(Stack, { gap: "s" }, () => renderLinkDestination(linkMode.value)),
2368
- linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h(FormField, {}, {
2388
+ linkTextEditable.value ? h(FormField, {}, {
2369
2389
  label: () => h("label", "Text"),
2370
2390
  default: () => h(Input, {
2371
2391
  size: "sm",
@@ -2373,6 +2393,7 @@ var LoomaEditor = defineComponent({
2373
2393
  onInput: (event) => {
2374
2394
  linkText.value = event.target.value;
2375
2395
  linkError.value = "";
2396
+ applyLink();
2376
2397
  }
2377
2398
  })
2378
2399
  }) : null,
@@ -2380,42 +2401,7 @@ var LoomaEditor = defineComponent({
2380
2401
  size: "sm",
2381
2402
  tone: "danger",
2382
2403
  role: "alert"
2383
- }, () => linkError.value) : null,
2384
- h(Separator),
2385
- h(Cluster, {
2386
- gap: "xs",
2387
- justify: "between",
2388
- wrap: "nowrap"
2389
- }, () => [linkSelection?.existing ? h(Button, {
2390
- type: "button",
2391
- variant: "ghost",
2392
- tone: "danger",
2393
- size: "sm",
2394
- onClick: removeLink
2395
- }, () => "Remove link") : null, validLinkHref(linkHref.value) ? h(Button, {
2396
- as: "a",
2397
- variant: "outline",
2398
- tone: "accent",
2399
- size: "sm",
2400
- href: validLinkHref(linkHref.value),
2401
- target: "_blank",
2402
- rel: "noopener noreferrer"
2403
- }, () => "Open link") : null]),
2404
- h(Cluster, {
2405
- gap: "xs",
2406
- justify: "end",
2407
- wrap: "nowrap"
2408
- }, () => [h(Button, {
2409
- type: "button",
2410
- variant: "outline",
2411
- tone: "accent",
2412
- size: "sm",
2413
- onClick: cancelLink
2414
- }, () => "Cancel"), h(Button, {
2415
- type: "submit",
2416
- variant: "solid",
2417
- size: "sm"
2418
- }, () => "Save link")])
2404
+ }, () => linkError.value) : null
2419
2405
  ])]);
2420
2406
  const renderLinkContext = (instance) => {
2421
2407
  if (linkContextEditing.value) return renderLinkForm();
@@ -2787,6 +2773,7 @@ var LoomaEditor = defineComponent({
2787
2773
  const onDocumentPointerDown = (event) => {
2788
2774
  const path = typeof event.composedPath === "function" ? event.composedPath() : [];
2789
2775
  const inElement = (element) => Boolean(element && (path.includes(element) || event.target instanceof Node && element.contains(event.target)));
2776
+ if ((linkOpen.value || linkContextEditing.value) && !inElement(linkFormRoot.value)) dismissLink(false);
2790
2777
  const inTableUi = inElement(tableToolbarShell.value) || inElement(tableOverlayShell.value) || inElement(tableMenuShell.value) || inElement(mobileToolbarShell.value);
2791
2778
  tableInteractionActive = inTableUi;
2792
2779
  if (inTableUi) setTimeout(() => {
@@ -3378,7 +3365,10 @@ var LoomaEditor = defineComponent({
3378
3365
  placement: "top",
3379
3366
  inverse: true
3380
3367
  }, () => "Drag to resize column") : null,
3381
- instance && (props.editable || props.selectionActions.length || props.rangeActions.length) && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3368
+ h(Stack, {
3369
+ key: "looma-formatting-popup-host",
3370
+ gap: "none"
3371
+ }, () => [instance && (props.editable || props.selectionActions.length || props.rangeActions.length) && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3382
3372
  editor: instance,
3383
3373
  pluginKey: "looma-text-formatting-menu",
3384
3374
  updateDelay: 0,
@@ -3407,8 +3397,11 @@ var LoomaEditor = defineComponent({
3407
3397
  }
3408
3398
  }] } } : {}
3409
3399
  }
3410
- }, { default: () => props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkContextEditing.value || instance.state.selection.empty && instance.isActive("link")) && !linkOpen.value && formattingSelectionActive(instance) ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover") }) : null,
3411
- instance && props.editable ? h(BubbleMenu, {
3400
+ }, { default: () => props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkContextEditing.value || instance.state.selection.empty && instance.isActive("link")) && !linkOpen.value && formattingSelectionActive(instance) ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover") }) : null]),
3401
+ h(Stack, {
3402
+ key: "looma-link-popup-host",
3403
+ gap: "none"
3404
+ }, () => [instance && props.editable ? h(BubbleMenu, {
3412
3405
  editor: instance,
3413
3406
  pluginKey: "looma-link-context-menu",
3414
3407
  updateDelay: 0,
@@ -3426,7 +3419,7 @@ var LoomaEditor = defineComponent({
3426
3419
  maxWidth: Math.min(480, window.innerWidth - 24),
3427
3420
  placement: "top"
3428
3421
  }
3429
- }, { default: () => props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? null : renderLinkContextCard(instance) }) : null,
3422
+ }, { default: () => props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? null : renderLinkContextCard(instance) }) : null]),
3430
3423
  instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
3431
3424
  class: "looma-editor__formatting-popover",
3432
3425
  role: "region",
@@ -3576,7 +3569,7 @@ var LoomaEditor = defineComponent({
3576
3569
  const current = editor.value?.state.selection;
3577
3570
  if (current && !current.empty && linkSelection && current.from === linkSelection.from && current.to === linkSelection.to) editor.value?.commands.setTextSelection(current.to);
3578
3571
  }
3579
- }, renderLinkForm),
3572
+ }, () => linkOpen.value ? renderLinkForm() : null),
3580
3573
  props.editable && props.compactToolbar && !mobile.value ? h(Popover, {
3581
3574
  class: "looma-editor__compact-formatting",
3582
3575
  for: formatAnchorId,