@threadlabs/looma 0.26.21 → 0.26.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.26.21",
3
+ "version": "0.26.22",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -10097,6 +10097,15 @@ html[data-ui-input-modality="touch"]
10097
10097
  .looma-editor .ProseMirror li > :is(ul, ol) {
10098
10098
  margin-block: var(--ui-space-1) 0;
10099
10099
  }
10100
+
10101
+ /* A held navigation modifier makes links actionable without interrupting editing. */
10102
+ [data-component~="ui-editor-surface"] .ProseMirror[contenteditable="true"] a[href] {
10103
+ cursor: text;
10104
+ }
10105
+
10106
+ [data-component~="ui-editor-surface"] .ProseMirror[data-link-navigation] a[href] {
10107
+ cursor: pointer;
10108
+ }
10100
10109
  /**
10101
10110
  * Looma editor styles (tokens-based).
10102
10111
  * Tables, context menu, insert-table grid, slash menu. Use with ProseMirror/Tiptap content.
@@ -1822,6 +1822,7 @@ var LoomaEditor = defineComponent({
1822
1822
  let dismissedFormattingSelection = null;
1823
1823
  let heldFormattingSelection = null;
1824
1824
  let formattingPopup = null;
1825
+ let linkPopup = null;
1825
1826
  let heldPressTimer;
1826
1827
  let heldPress = null;
1827
1828
  const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
@@ -2032,7 +2033,30 @@ var LoomaEditor = defineComponent({
2032
2033
  "aria-multiline": "true",
2033
2034
  "aria-label": props.label
2034
2035
  },
2035
- handleKeyDown: (_view, event) => onFormattingKeyDown(event)
2036
+ handleKeyDown: (_view, event) => onFormattingKeyDown(event),
2037
+ handleDOMEvents: { click: (view, event) => {
2038
+ if (!(event.target instanceof Element) || event.button !== 0) return false;
2039
+ const link = event.target.closest("a[href]");
2040
+ if (!link || !view.dom.contains(link)) return false;
2041
+ const href = validLinkHref(link.getAttribute("href") ?? "");
2042
+ if (!href) return false;
2043
+ if (isApplePlatform() ? event.metaKey : event.ctrlKey) {
2044
+ event.preventDefault();
2045
+ window.open(href, "_blank", "noopener,noreferrer");
2046
+ return true;
2047
+ }
2048
+ dismissedFormattingSelection = null;
2049
+ requestAnimationFrame(() => {
2050
+ if (view.isDestroyed) return;
2051
+ editorStateVersion.value += 1;
2052
+ const instance = editor.value;
2053
+ if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
2054
+ linkPopup?.setProps({ getReferenceClientRect: () => link.getBoundingClientRect() });
2055
+ linkPopup?.show();
2056
+ }
2057
+ });
2058
+ return false;
2059
+ } }
2036
2060
  },
2037
2061
  onCreate: ({ editor: instance }) => {
2038
2062
  emit("ready", instance);
@@ -2360,31 +2384,38 @@ var LoomaEditor = defineComponent({
2360
2384
  h(Separator),
2361
2385
  h(Cluster, {
2362
2386
  gap: "xs",
2363
- justify: "between"
2364
- }, () => [h(Cluster, { gap: "xs" }, () => [linkSelection?.existing ? h(Button, {
2387
+ justify: "between",
2388
+ wrap: "nowrap"
2389
+ }, () => [linkSelection?.existing ? h(Button, {
2365
2390
  type: "button",
2366
2391
  variant: "ghost",
2367
2392
  tone: "danger",
2368
2393
  size: "sm",
2369
2394
  onClick: removeLink
2370
- }, () => "Remove link") : null, linkMode.value === "url" && validLinkHref(linkHref.value) ? h(Button, {
2395
+ }, () => "Remove link") : null, validLinkHref(linkHref.value) ? h(Button, {
2371
2396
  as: "a",
2372
- variant: "link",
2397
+ variant: "outline",
2398
+ tone: "accent",
2373
2399
  size: "sm",
2374
2400
  href: validLinkHref(linkHref.value),
2375
2401
  target: "_blank",
2376
2402
  rel: "noopener noreferrer"
2377
- }, () => "Preview link") : null]), h(Cluster, { gap: "xs" }, () => [h(Button, {
2403
+ }, () => "Open link") : null]),
2404
+ h(Cluster, {
2405
+ gap: "xs",
2406
+ justify: "end",
2407
+ wrap: "nowrap"
2408
+ }, () => [h(Button, {
2378
2409
  type: "button",
2379
- variant: "ghost",
2380
- tone: "neutral",
2410
+ variant: "outline",
2411
+ tone: "accent",
2381
2412
  size: "sm",
2382
2413
  onClick: cancelLink
2383
2414
  }, () => "Cancel"), h(Button, {
2384
2415
  type: "submit",
2385
2416
  variant: "solid",
2386
2417
  size: "sm"
2387
- }, () => "Save link")])])
2418
+ }, () => "Save link")])
2388
2419
  ])]);
2389
2420
  const renderLinkContext = (instance) => {
2390
2421
  if (linkContextEditing.value) return renderLinkForm();
@@ -2428,7 +2459,10 @@ var LoomaEditor = defineComponent({
2428
2459
  target: "_blank",
2429
2460
  rel: "noopener noreferrer",
2430
2461
  "aria-label": "Open link"
2431
- }, () => loomaIcon("external-link")) : null,
2462
+ }, () => h(Cluster, {
2463
+ gap: "xs",
2464
+ wrap: "nowrap"
2465
+ }, () => [loomaIcon("external-link"), "Open link"])) : null,
2432
2466
  safeHref ? h(Tooltip, { for: openId }, () => "Open link") : null,
2433
2467
  h(IconButton, {
2434
2468
  id: editId,
@@ -2787,12 +2821,13 @@ var LoomaEditor = defineComponent({
2787
2821
  };
2788
2822
  const onFormattingKeyDown = (event) => {
2789
2823
  cancelHeldPress();
2790
- if (event.key !== "Escape" || !floatingFormattingMode() || !(event.target instanceof Node) || !(root.value?.contains(event.target) || formattingPopup?.popper.contains(event.target)) || !(formatOpen.value || formattingPopup?.popper.style.visibility === "visible")) return false;
2824
+ if (event.key !== "Escape" || !(event.target instanceof Node) || !(root.value?.contains(event.target) || formattingPopup?.popper.contains(event.target) || linkPopup?.popper.contains(event.target)) || !(formatOpen.value || formattingPopup?.popper.style.visibility === "visible" || linkPopup?.popper.style.visibility === "visible")) return false;
2791
2825
  const selection = editor.value?.state.selection;
2792
2826
  dismissedFormattingSelection = selection instanceof TextSelection || selection instanceof AllSelection ? selection : null;
2793
2827
  heldFormattingSelection = null;
2794
2828
  formatOpen.value = false;
2795
2829
  formattingPopup?.hide();
2830
+ linkPopup?.hide();
2796
2831
  hideTool();
2797
2832
  return true;
2798
2833
  };
@@ -2877,7 +2912,17 @@ var LoomaEditor = defineComponent({
2877
2912
  trigger: "programmatic"
2878
2913
  });
2879
2914
  };
2915
+ const onLinkNavigationModifier = (event) => {
2916
+ const dom = editor.value?.view.dom;
2917
+ const active = isApplePlatform() ? event.metaKey : event.ctrlKey;
2918
+ if (dom && dom.hasAttribute("data-link-navigation") !== active) dom.toggleAttribute("data-link-navigation", active);
2919
+ };
2920
+ const clearLinkNavigationModifier = () => editor.value?.view.dom.removeAttribute("data-link-navigation");
2880
2921
  onMounted(() => {
2922
+ window.addEventListener("keydown", onLinkNavigationModifier);
2923
+ window.addEventListener("keyup", onLinkNavigationModifier);
2924
+ window.addEventListener("pointermove", onLinkNavigationModifier);
2925
+ window.addEventListener("blur", clearLinkNavigationModifier);
2881
2926
  updateMobileViewport();
2882
2927
  window.addEventListener("resize", onViewportChange);
2883
2928
  window.addEventListener("scroll", onViewportChange, true);
@@ -2894,6 +2939,10 @@ var LoomaEditor = defineComponent({
2894
2939
  root.value?.addEventListener("error", onImageError, true);
2895
2940
  });
2896
2941
  onBeforeUnmount(() => {
2942
+ window.removeEventListener("keydown", onLinkNavigationModifier);
2943
+ window.removeEventListener("keyup", onLinkNavigationModifier);
2944
+ window.removeEventListener("pointermove", onLinkNavigationModifier);
2945
+ window.removeEventListener("blur", clearLinkNavigationModifier);
2897
2946
  window.removeEventListener("resize", onViewportChange);
2898
2947
  window.removeEventListener("scroll", onViewportChange, true);
2899
2948
  window.visualViewport?.removeEventListener("resize", onViewportChange);
@@ -3358,19 +3407,26 @@ var LoomaEditor = defineComponent({
3358
3407
  }
3359
3408
  }] } } : {}
3360
3409
  }
3361
- }, { default: () => props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkContextEditing.value || instance.state.selection.empty && instance.isActive("link")) && !linkOpen.value ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover") }) : null,
3362
- instance && props.editable && !mobile.value && props.toolbarMode !== "contextual" ? h(BubbleMenu, {
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, {
3363
3412
  editor: instance,
3364
3413
  pluginKey: "looma-link-context-menu",
3365
- shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && !linkOpen.value,
3414
+ updateDelay: 0,
3415
+ shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && !linkOpen.value && !dismissedFormattingSelection?.eq(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkContextEditing.value || linkPopup?.popper.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(menuEditor)),
3366
3416
  tippyOptions: {
3417
+ onCreate: (popup) => {
3418
+ linkPopup = popup;
3419
+ },
3420
+ onDestroy: () => {
3421
+ linkPopup = null;
3422
+ },
3367
3423
  appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
3368
3424
  onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
3369
3425
  duration: 100,
3370
3426
  maxWidth: Math.min(480, window.innerWidth - 24),
3371
- placement: "bottom"
3427
+ placement: "top"
3372
3428
  }
3373
- }, { default: () => renderLinkContextCard(instance) }) : null,
3429
+ }, { default: () => props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? null : renderLinkContextCard(instance) }) : null,
3374
3430
  instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
3375
3431
  class: "looma-editor__formatting-popover",
3376
3432
  role: "region",