@ohhwells/bridge 0.1.46 → 0.1.47

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/dist/index.cjs CHANGED
@@ -6661,6 +6661,8 @@ function applyNavOrderToContainer(container, order) {
6661
6661
  if (current.length === expected.length && current.every((key, i) => key === expected[i])) {
6662
6662
  return;
6663
6663
  }
6664
+ const lastCurrentLinkEl = current.length > 0 ? findCounterpartByHrefKey(current[current.length - 1], container) : null;
6665
+ const anchor = lastCurrentLinkEl?.parentElement === container ? lastCurrentLinkEl.nextElementSibling : null;
6664
6666
  const seen = /* @__PURE__ */ new Set();
6665
6667
  for (const hrefKey of desired) {
6666
6668
  if (seen.has(hrefKey)) continue;
@@ -6675,7 +6677,9 @@ function applyNavOrderToContainer(container, order) {
6675
6677
  orderedEls.push(el);
6676
6678
  }
6677
6679
  for (const el of orderedEls) {
6678
- if (container.lastElementChild !== el) {
6680
+ if (anchor) {
6681
+ if (el.nextSibling !== anchor) container.insertBefore(el, anchor);
6682
+ } else if (container.lastElementChild !== el) {
6679
6683
  container.appendChild(el);
6680
6684
  }
6681
6685
  }
@@ -7017,6 +7021,11 @@ function reconcileFooterOrderFromContent(content) {
7017
7021
  }
7018
7022
  applyFooterOrder(order);
7019
7023
  }
7024
+ function isRowLayoutColumn(links) {
7025
+ if (links.length < 2) return false;
7026
+ const firstTop = links[0].getBoundingClientRect().top;
7027
+ return links.every((link) => Math.abs(link.getBoundingClientRect().top - firstTop) < 4);
7028
+ }
7020
7029
  function buildLinkDropSlots(column, columnIndex) {
7021
7030
  const links = listFooterLinksInColumn(column);
7022
7031
  const colRect = column.getBoundingClientRect();
@@ -7034,6 +7043,32 @@ function buildLinkDropSlots(column, columnIndex) {
7034
7043
  });
7035
7044
  return slots;
7036
7045
  }
7046
+ if (isRowLayoutColumn(links)) {
7047
+ for (let i = 0; i <= links.length; i++) {
7048
+ let left;
7049
+ if (i === 0) {
7050
+ left = links[0].getBoundingClientRect().left - barThickness / 2;
7051
+ } else if (i === links.length) {
7052
+ const last = links[links.length - 1].getBoundingClientRect();
7053
+ left = last.right - barThickness / 2;
7054
+ } else {
7055
+ const prev = links[i - 1].getBoundingClientRect();
7056
+ const next = links[i].getBoundingClientRect();
7057
+ left = (prev.right + next.left) / 2 - barThickness / 2;
7058
+ }
7059
+ const heightRef = i === 0 ? links[0].getBoundingClientRect() : i === links.length ? links[links.length - 1].getBoundingClientRect() : links[i].getBoundingClientRect();
7060
+ slots.push({
7061
+ insertIndex: i,
7062
+ columnIndex,
7063
+ left,
7064
+ top: heightRef.top,
7065
+ width: barThickness,
7066
+ height: Math.max(heightRef.height, colRect.height * 0.8),
7067
+ direction: "vertical"
7068
+ });
7069
+ }
7070
+ return slots;
7071
+ }
7037
7072
  for (let i = 0; i <= links.length; i++) {
7038
7073
  let top;
7039
7074
  if (i === 0) {
@@ -7117,8 +7152,7 @@ function hitTestLinkDropSlot(clientX, clientY, draggedHrefKey) {
7117
7152
  return true;
7118
7153
  });
7119
7154
  for (const slot of slots) {
7120
- const cy = slot.top + slot.height / 2;
7121
- const dist = Math.abs(clientY - cy) + (inColX ? 0 : 1e3);
7155
+ const dist = slot.direction === "vertical" ? Math.abs(clientX - (slot.left + slot.width / 2)) : Math.abs(clientY - (slot.top + slot.height / 2));
7122
7156
  if (!best || dist < best.dist) best = { slot, dist };
7123
7157
  }
7124
7158
  }
@@ -7162,9 +7196,10 @@ function lockItemDuringDrag() {
7162
7196
  clearTextSelection();
7163
7197
  }
7164
7198
  function unlockItemDragInteraction() {
7199
+ const wasDragging = document.documentElement.hasAttribute("data-ohw-item-dragging");
7165
7200
  document.documentElement.removeAttribute("data-ohw-footer-press-drag");
7166
7201
  document.documentElement.removeAttribute("data-ohw-item-dragging");
7167
- clearTextSelection();
7202
+ if (wasDragging) clearTextSelection();
7168
7203
  }
7169
7204
  var armFooterPressDrag = armItemPressDrag;
7170
7205
  var lockFooterDuringDrag = lockItemDuringDrag;
@@ -7541,7 +7576,6 @@ function useNavItemDrag({
7541
7576
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
7542
7577
  if (!anchor || !isNavbarHrefKey(hrefKey)) return;
7543
7578
  if (isNavbarButton3(anchor) || isDragHandleDisabled2(anchor)) return;
7544
- armItemPressDrag();
7545
7579
  navPointerDragRef.current = {
7546
7580
  el: anchor,
7547
7581
  startX: e.clientX,
@@ -7568,6 +7602,7 @@ function useNavItemDrag({
7568
7602
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
7569
7603
  e.preventDefault();
7570
7604
  pending.started = true;
7605
+ armItemPressDrag();
7571
7606
  clearTextSelection();
7572
7607
  try {
7573
7608
  document.body.setPointerCapture(pending.pointerId);
@@ -7595,7 +7630,8 @@ function useNavItemDrag({
7595
7630
  }
7596
7631
  } catch {
7597
7632
  }
7598
- if (!pending?.started) {
7633
+ if (!pending) return;
7634
+ if (!pending.started) {
7599
7635
  unlockItemDragInteraction();
7600
7636
  return;
7601
7637
  }
@@ -7647,7 +7683,6 @@ function useNavItemDrag({
7647
7683
  const hrefKey = selected.getAttribute("data-ohw-href-key");
7648
7684
  if (!hrefKey || !isNavbarHrefKey(hrefKey)) return false;
7649
7685
  if (isNavbarButton3(selected) || isDragHandleDisabled2(selected)) return false;
7650
- armItemPressDrag();
7651
7686
  navPointerDragRef.current = {
7652
7687
  el: selected,
7653
7688
  startX: clientX,
@@ -8492,9 +8527,16 @@ function sanitizeHtml(html) {
8492
8527
  if (!SAFE_TAGS.has(el.tagName)) {
8493
8528
  parent.replaceChild(document.createTextNode(el.textContent ?? ""), el);
8494
8529
  } else {
8495
- const textAlign = el.style?.textAlign || el.getAttribute("align") || "";
8530
+ const htmlEl = el;
8531
+ const textAlign = htmlEl.style?.textAlign || el.getAttribute("align") || "";
8532
+ const fontWeight = htmlEl.style?.fontWeight || "";
8533
+ const fontStyle = htmlEl.style?.fontStyle || "";
8534
+ const textDecorationLine = htmlEl.style?.textDecorationLine || htmlEl.style?.textDecoration || "";
8496
8535
  for (const attr of Array.from(el.attributes)) el.removeAttribute(attr.name);
8497
- if (textAlign) el.style.textAlign = textAlign;
8536
+ if (textAlign) htmlEl.style.textAlign = textAlign;
8537
+ if (fontWeight) htmlEl.style.fontWeight = fontWeight;
8538
+ if (fontStyle) htmlEl.style.fontStyle = fontStyle;
8539
+ if (textDecorationLine) htmlEl.style.textDecorationLine = textDecorationLine;
8498
8540
  walk(el);
8499
8541
  }
8500
8542
  }
@@ -8682,6 +8724,79 @@ function resolveItemInteractionState(rect, parentScroll) {
8682
8724
  const { transform } = calcToolbarPos(rect, parentScroll, 120);
8683
8725
  return transform.includes("translateY(-100%)") ? "active-top" : "active-bottom";
8684
8726
  }
8727
+ function resolveSelectionStartElement(sel) {
8728
+ if (!sel || sel.rangeCount === 0) return null;
8729
+ const range = sel.getRangeAt(0);
8730
+ let startNode = range.startContainer;
8731
+ if (startNode.nodeType === Node.ELEMENT_NODE) {
8732
+ const el = startNode;
8733
+ startNode = el.childNodes[range.startOffset] ?? el.childNodes[range.startOffset - 1] ?? el;
8734
+ }
8735
+ return startNode.nodeType === Node.TEXT_NODE ? startNode.parentElement : startNode;
8736
+ }
8737
+ function detectActiveFormats(startEl, activeRoot) {
8738
+ const formats = /* @__PURE__ */ new Set();
8739
+ const cs = getComputedStyle(startEl);
8740
+ const weight = parseInt(cs.fontWeight, 10);
8741
+ let hasBold = cs.fontWeight === "bold" || !Number.isNaN(weight) && weight >= 600;
8742
+ let hasItalic = cs.fontStyle === "italic" || cs.fontStyle === "oblique";
8743
+ let hasUnderline = false;
8744
+ let hasStrike = false;
8745
+ let hasUl = false;
8746
+ let hasOl = false;
8747
+ for (let el = startEl; el; el = el.parentElement) {
8748
+ if (el.tagName === "B" || el.tagName === "STRONG") hasBold = true;
8749
+ if (el.tagName === "I" || el.tagName === "EM") hasItalic = true;
8750
+ const decoration = getComputedStyle(el).textDecorationLine;
8751
+ if (decoration.includes("underline") || el.tagName === "U" || el.tagName === "INS") hasUnderline = true;
8752
+ if (decoration.includes("line-through") || el.tagName === "S" || el.tagName === "STRIKE" || el.tagName === "DEL") hasStrike = true;
8753
+ if (el.tagName === "UL") hasUl = true;
8754
+ if (el.tagName === "OL") hasOl = true;
8755
+ if (el === activeRoot) break;
8756
+ }
8757
+ if (hasBold) formats.add("bold");
8758
+ if (hasItalic) formats.add("italic");
8759
+ if (hasUnderline) formats.add("underline");
8760
+ if (hasStrike) formats.add("strikeThrough");
8761
+ if (hasUl) formats.add("insertUnorderedList");
8762
+ if (hasOl) formats.add("insertOrderedList");
8763
+ const block = startEl.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? startEl;
8764
+ const align = getComputedStyle(block).textAlign;
8765
+ if (align === "center") formats.add("justifyCenter");
8766
+ else if (align === "right" || align === "end") formats.add("justifyRight");
8767
+ else formats.add("justifyLeft");
8768
+ return formats;
8769
+ }
8770
+ function setsEqual(a, b) {
8771
+ if (a.size !== b.size) return false;
8772
+ for (const item of a) {
8773
+ if (!b.has(item)) return false;
8774
+ }
8775
+ return true;
8776
+ }
8777
+ function textOffsetInRoot(root, node, offset) {
8778
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
8779
+ let total = 0;
8780
+ let current;
8781
+ while (current = walker.nextNode()) {
8782
+ if (current === node) return total + offset;
8783
+ total += (current.textContent ?? "").length;
8784
+ }
8785
+ return total;
8786
+ }
8787
+ function pointAtTextOffset(root, targetOffset) {
8788
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
8789
+ let total = 0;
8790
+ let current;
8791
+ let last = null;
8792
+ while (current = walker.nextNode()) {
8793
+ const len = (current.textContent ?? "").length;
8794
+ if (total + len >= targetOffset) return { node: current, offset: targetOffset - total };
8795
+ total += len;
8796
+ last = current;
8797
+ }
8798
+ return last ? { node: last, offset: (last.textContent ?? "").length } : null;
8799
+ }
8685
8800
  function FloatingToolbar({
8686
8801
  rect,
8687
8802
  parentScroll,
@@ -8874,6 +8989,7 @@ function OhhwellsBridge() {
8874
8989
  const [fetchState, setFetchState] = (0, import_react9.useState)("idle");
8875
8990
  const autoSaveTimers = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
8876
8991
  const activeElRef = (0, import_react9.useRef)(null);
8992
+ const pointerHeldRef = (0, import_react9.useRef)(false);
8877
8993
  const selectedElRef = (0, import_react9.useRef)(null);
8878
8994
  const selectedHrefKeyRef = (0, import_react9.useRef)(null);
8879
8995
  const selectedFooterColAttrRef = (0, import_react9.useRef)(null);
@@ -9560,7 +9676,6 @@ function OhhwellsBridge() {
9560
9676
  if (e.button !== 0) return;
9561
9677
  const selected = selectedElRef.current;
9562
9678
  if (!selected) return;
9563
- armFooterPressDrag();
9564
9679
  const hrefKey = selected.getAttribute("data-ohw-href-key");
9565
9680
  if (hrefKey && isFooterHrefKey(hrefKey)) {
9566
9681
  footerPointerDragRef.current = {
@@ -9672,6 +9787,8 @@ function OhhwellsBridge() {
9672
9787
  setSiblingHintRect(null);
9673
9788
  setSiblingHintRects([]);
9674
9789
  setIsItemDragging(false);
9790
+ const preActivationSelection = window.getSelection();
9791
+ const preservedRange = preActivationSelection && preActivationSelection.rangeCount > 0 && !preActivationSelection.isCollapsed && el.contains(preActivationSelection.getRangeAt(0).commonAncestorContainer) ? preActivationSelection.getRangeAt(0).cloneRange() : null;
9675
9792
  el.setAttribute("contenteditable", "true");
9676
9793
  el.setAttribute("data-ohw-editing", "");
9677
9794
  el.removeAttribute("data-ohw-hovered");
@@ -9679,7 +9796,11 @@ function OhhwellsBridge() {
9679
9796
  activeElRef.current = el;
9680
9797
  originalContentRef.current = el.innerHTML;
9681
9798
  el.focus({ preventScroll: true });
9682
- if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9799
+ if (preservedRange) {
9800
+ const selection = window.getSelection();
9801
+ selection?.removeAllRanges();
9802
+ selection?.addRange(preservedRange);
9803
+ } else if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9683
9804
  const { caretX, caretY } = options;
9684
9805
  placeCaretAtPoint(el, caretX, caretY);
9685
9806
  requestAnimationFrame(() => {
@@ -10135,7 +10256,9 @@ function OhhwellsBridge() {
10135
10256
  if (inActive || inActiveNav) {
10136
10257
  e.preventDefault();
10137
10258
  e.stopPropagation();
10138
- if (e.detail < 2) {
10259
+ const selection = window.getSelection();
10260
+ const hasRangeSelection = Boolean(selection && !selection.isCollapsed);
10261
+ if (e.detail < 2 && !hasRangeSelection) {
10139
10262
  placeCaretAtPoint(active, e.clientX, e.clientY);
10140
10263
  refreshActiveCommandsRef.current();
10141
10264
  }
@@ -11288,30 +11411,30 @@ function OhhwellsBridge() {
11288
11411
  const h = document.documentElement.scrollHeight;
11289
11412
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
11290
11413
  };
11414
+ let selectionChangeRaf = null;
11415
+ const runSelectionChange = () => {
11416
+ selectionChangeRaf = null;
11417
+ const activeRoot = activeElRef.current;
11418
+ if (!activeRoot) return;
11419
+ const startEl = resolveSelectionStartElement(window.getSelection());
11420
+ const next = startEl ? detectActiveFormats(startEl, activeRoot) : /* @__PURE__ */ new Set();
11421
+ setActiveCommands((prev) => setsEqual(prev, next) ? prev : next);
11422
+ };
11291
11423
  const handleSelectionChange = () => {
11292
- if (!activeElRef.current) return;
11293
- const next = /* @__PURE__ */ new Set();
11294
- for (const cmd of ["bold", "italic", "underline", "strikeThrough", "insertUnorderedList", "insertOrderedList"]) {
11295
- try {
11296
- if (document.queryCommandState(cmd)) next.add(cmd);
11297
- } catch {
11298
- }
11299
- }
11300
- const sel = window.getSelection();
11301
- const anchor = sel?.anchorNode;
11302
- if (anchor) {
11303
- const el = anchor.nodeType === Node.TEXT_NODE ? anchor.parentElement : anchor;
11304
- const block = el?.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? el;
11305
- if (block) {
11306
- const align = getComputedStyle(block).textAlign;
11307
- if (align === "center") next.add("justifyCenter");
11308
- else if (align === "right" || align === "end") next.add("justifyRight");
11309
- else next.add("justifyLeft");
11310
- }
11311
- }
11312
- setActiveCommands(next);
11424
+ if (pointerHeldRef.current) return;
11425
+ if (selectionChangeRaf !== null) return;
11426
+ selectionChangeRaf = requestAnimationFrame(runSelectionChange);
11427
+ };
11428
+ const markPointerHeld = (e) => {
11429
+ if (e.button !== 0) return;
11430
+ pointerHeldRef.current = true;
11313
11431
  };
11314
- refreshActiveCommandsRef.current = handleSelectionChange;
11432
+ const markPointerReleased = () => {
11433
+ if (!pointerHeldRef.current) return;
11434
+ pointerHeldRef.current = false;
11435
+ handleSelectionChange();
11436
+ };
11437
+ refreshActiveCommandsRef.current = runSelectionChange;
11315
11438
  const handleDocMouseLeave = () => {
11316
11439
  hoveredImageRef.current = null;
11317
11440
  setMediaHover(null);
@@ -11551,7 +11674,6 @@ function OhhwellsBridge() {
11551
11674
  const anchor = getNavigationItemAnchor(target);
11552
11675
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
11553
11676
  if (anchor && isFooterHrefKey(hrefKey)) {
11554
- armFooterPressDrag();
11555
11677
  footerPointerDragRef.current = {
11556
11678
  el: anchor,
11557
11679
  kind: "link",
@@ -11565,7 +11687,6 @@ function OhhwellsBridge() {
11565
11687
  }
11566
11688
  const col = target.closest("[data-ohw-footer-col]");
11567
11689
  if (col && !getNavigationItemAnchor(target)) {
11568
- armFooterPressDrag();
11569
11690
  footerPointerDragRef.current = {
11570
11691
  el: col,
11571
11692
  kind: "column",
@@ -11594,6 +11715,7 @@ function OhhwellsBridge() {
11594
11715
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
11595
11716
  e.preventDefault();
11596
11717
  pending.started = true;
11718
+ armFooterPressDrag();
11597
11719
  clearTextSelection();
11598
11720
  try {
11599
11721
  document.body.setPointerCapture(pending.pointerId);
@@ -11647,7 +11769,8 @@ function OhhwellsBridge() {
11647
11769
  }
11648
11770
  } catch {
11649
11771
  }
11650
- if (!pending?.started) {
11772
+ if (!pending) return;
11773
+ if (!pending.started) {
11651
11774
  unlockFooterDragInteraction();
11652
11775
  return;
11653
11776
  }
@@ -11740,9 +11863,39 @@ function OhhwellsBridge() {
11740
11863
  return () => window.removeEventListener("hashchange", onHashChange);
11741
11864
  }, [pathname]);
11742
11865
  const handleCommand = (0, import_react9.useCallback)((cmd) => {
11866
+ const el = activeElRef.current;
11867
+ const selBefore = window.getSelection();
11868
+ let savedOffsets = null;
11869
+ if (el && selBefore && selBefore.rangeCount > 0 && !selBefore.isCollapsed) {
11870
+ const r2 = selBefore.getRangeAt(0);
11871
+ if (el.contains(r2.commonAncestorContainer)) {
11872
+ savedOffsets = {
11873
+ start: textOffsetInRoot(el, r2.startContainer, r2.startOffset),
11874
+ end: textOffsetInRoot(el, r2.endContainer, r2.endOffset)
11875
+ };
11876
+ }
11877
+ }
11878
+ document.execCommand("styleWithCSS", false, true);
11743
11879
  document.execCommand(cmd, false);
11744
- activeElRef.current?.focus();
11745
- if (activeElRef.current) setToolbarRect(getEditMeasureEl(activeElRef.current).getBoundingClientRect());
11880
+ if (el && document.activeElement !== el) {
11881
+ el.focus();
11882
+ }
11883
+ const selAfter = window.getSelection();
11884
+ if (el && savedOffsets && selAfter?.isCollapsed) {
11885
+ const start = pointAtTextOffset(el, savedOffsets.start);
11886
+ const end = pointAtTextOffset(el, savedOffsets.end);
11887
+ if (start && end) {
11888
+ try {
11889
+ const range = document.createRange();
11890
+ range.setStart(start.node, start.offset);
11891
+ range.setEnd(end.node, end.offset);
11892
+ selAfter.removeAllRanges();
11893
+ selAfter.addRange(range);
11894
+ } catch {
11895
+ }
11896
+ }
11897
+ }
11898
+ if (el) setToolbarRect(getEditMeasureEl(el).getBoundingClientRect());
11746
11899
  refreshActiveCommandsRef.current();
11747
11900
  }, []);
11748
11901
  const handleStateChange = (0, import_react9.useCallback)((state) => {