@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.js CHANGED
@@ -6600,6 +6600,8 @@ function applyNavOrderToContainer(container, order) {
6600
6600
  if (current.length === expected.length && current.every((key, i) => key === expected[i])) {
6601
6601
  return;
6602
6602
  }
6603
+ const lastCurrentLinkEl = current.length > 0 ? findCounterpartByHrefKey(current[current.length - 1], container) : null;
6604
+ const anchor = lastCurrentLinkEl?.parentElement === container ? lastCurrentLinkEl.nextElementSibling : null;
6603
6605
  const seen = /* @__PURE__ */ new Set();
6604
6606
  for (const hrefKey of desired) {
6605
6607
  if (seen.has(hrefKey)) continue;
@@ -6614,7 +6616,9 @@ function applyNavOrderToContainer(container, order) {
6614
6616
  orderedEls.push(el);
6615
6617
  }
6616
6618
  for (const el of orderedEls) {
6617
- if (container.lastElementChild !== el) {
6619
+ if (anchor) {
6620
+ if (el.nextSibling !== anchor) container.insertBefore(el, anchor);
6621
+ } else if (container.lastElementChild !== el) {
6618
6622
  container.appendChild(el);
6619
6623
  }
6620
6624
  }
@@ -6956,6 +6960,11 @@ function reconcileFooterOrderFromContent(content) {
6956
6960
  }
6957
6961
  applyFooterOrder(order);
6958
6962
  }
6963
+ function isRowLayoutColumn(links) {
6964
+ if (links.length < 2) return false;
6965
+ const firstTop = links[0].getBoundingClientRect().top;
6966
+ return links.every((link) => Math.abs(link.getBoundingClientRect().top - firstTop) < 4);
6967
+ }
6959
6968
  function buildLinkDropSlots(column, columnIndex) {
6960
6969
  const links = listFooterLinksInColumn(column);
6961
6970
  const colRect = column.getBoundingClientRect();
@@ -6973,6 +6982,32 @@ function buildLinkDropSlots(column, columnIndex) {
6973
6982
  });
6974
6983
  return slots;
6975
6984
  }
6985
+ if (isRowLayoutColumn(links)) {
6986
+ for (let i = 0; i <= links.length; i++) {
6987
+ let left;
6988
+ if (i === 0) {
6989
+ left = links[0].getBoundingClientRect().left - barThickness / 2;
6990
+ } else if (i === links.length) {
6991
+ const last = links[links.length - 1].getBoundingClientRect();
6992
+ left = last.right - barThickness / 2;
6993
+ } else {
6994
+ const prev = links[i - 1].getBoundingClientRect();
6995
+ const next = links[i].getBoundingClientRect();
6996
+ left = (prev.right + next.left) / 2 - barThickness / 2;
6997
+ }
6998
+ const heightRef = i === 0 ? links[0].getBoundingClientRect() : i === links.length ? links[links.length - 1].getBoundingClientRect() : links[i].getBoundingClientRect();
6999
+ slots.push({
7000
+ insertIndex: i,
7001
+ columnIndex,
7002
+ left,
7003
+ top: heightRef.top,
7004
+ width: barThickness,
7005
+ height: Math.max(heightRef.height, colRect.height * 0.8),
7006
+ direction: "vertical"
7007
+ });
7008
+ }
7009
+ return slots;
7010
+ }
6976
7011
  for (let i = 0; i <= links.length; i++) {
6977
7012
  let top;
6978
7013
  if (i === 0) {
@@ -7056,8 +7091,7 @@ function hitTestLinkDropSlot(clientX, clientY, draggedHrefKey) {
7056
7091
  return true;
7057
7092
  });
7058
7093
  for (const slot of slots) {
7059
- const cy = slot.top + slot.height / 2;
7060
- const dist = Math.abs(clientY - cy) + (inColX ? 0 : 1e3);
7094
+ const dist = slot.direction === "vertical" ? Math.abs(clientX - (slot.left + slot.width / 2)) : Math.abs(clientY - (slot.top + slot.height / 2));
7061
7095
  if (!best || dist < best.dist) best = { slot, dist };
7062
7096
  }
7063
7097
  }
@@ -7101,9 +7135,10 @@ function lockItemDuringDrag() {
7101
7135
  clearTextSelection();
7102
7136
  }
7103
7137
  function unlockItemDragInteraction() {
7138
+ const wasDragging = document.documentElement.hasAttribute("data-ohw-item-dragging");
7104
7139
  document.documentElement.removeAttribute("data-ohw-footer-press-drag");
7105
7140
  document.documentElement.removeAttribute("data-ohw-item-dragging");
7106
- clearTextSelection();
7141
+ if (wasDragging) clearTextSelection();
7107
7142
  }
7108
7143
  var armFooterPressDrag = armItemPressDrag;
7109
7144
  var lockFooterDuringDrag = lockItemDuringDrag;
@@ -7480,7 +7515,6 @@ function useNavItemDrag({
7480
7515
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
7481
7516
  if (!anchor || !isNavbarHrefKey(hrefKey)) return;
7482
7517
  if (isNavbarButton3(anchor) || isDragHandleDisabled2(anchor)) return;
7483
- armItemPressDrag();
7484
7518
  navPointerDragRef.current = {
7485
7519
  el: anchor,
7486
7520
  startX: e.clientX,
@@ -7507,6 +7541,7 @@ function useNavItemDrag({
7507
7541
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
7508
7542
  e.preventDefault();
7509
7543
  pending.started = true;
7544
+ armItemPressDrag();
7510
7545
  clearTextSelection();
7511
7546
  try {
7512
7547
  document.body.setPointerCapture(pending.pointerId);
@@ -7534,7 +7569,8 @@ function useNavItemDrag({
7534
7569
  }
7535
7570
  } catch {
7536
7571
  }
7537
- if (!pending?.started) {
7572
+ if (!pending) return;
7573
+ if (!pending.started) {
7538
7574
  unlockItemDragInteraction();
7539
7575
  return;
7540
7576
  }
@@ -7586,7 +7622,6 @@ function useNavItemDrag({
7586
7622
  const hrefKey = selected.getAttribute("data-ohw-href-key");
7587
7623
  if (!hrefKey || !isNavbarHrefKey(hrefKey)) return false;
7588
7624
  if (isNavbarButton3(selected) || isDragHandleDisabled2(selected)) return false;
7589
- armItemPressDrag();
7590
7625
  navPointerDragRef.current = {
7591
7626
  el: selected,
7592
7627
  startX: clientX,
@@ -8431,9 +8466,16 @@ function sanitizeHtml(html) {
8431
8466
  if (!SAFE_TAGS.has(el.tagName)) {
8432
8467
  parent.replaceChild(document.createTextNode(el.textContent ?? ""), el);
8433
8468
  } else {
8434
- const textAlign = el.style?.textAlign || el.getAttribute("align") || "";
8469
+ const htmlEl = el;
8470
+ const textAlign = htmlEl.style?.textAlign || el.getAttribute("align") || "";
8471
+ const fontWeight = htmlEl.style?.fontWeight || "";
8472
+ const fontStyle = htmlEl.style?.fontStyle || "";
8473
+ const textDecorationLine = htmlEl.style?.textDecorationLine || htmlEl.style?.textDecoration || "";
8435
8474
  for (const attr of Array.from(el.attributes)) el.removeAttribute(attr.name);
8436
- if (textAlign) el.style.textAlign = textAlign;
8475
+ if (textAlign) htmlEl.style.textAlign = textAlign;
8476
+ if (fontWeight) htmlEl.style.fontWeight = fontWeight;
8477
+ if (fontStyle) htmlEl.style.fontStyle = fontStyle;
8478
+ if (textDecorationLine) htmlEl.style.textDecorationLine = textDecorationLine;
8437
8479
  walk(el);
8438
8480
  }
8439
8481
  }
@@ -8621,6 +8663,79 @@ function resolveItemInteractionState(rect, parentScroll) {
8621
8663
  const { transform } = calcToolbarPos(rect, parentScroll, 120);
8622
8664
  return transform.includes("translateY(-100%)") ? "active-top" : "active-bottom";
8623
8665
  }
8666
+ function resolveSelectionStartElement(sel) {
8667
+ if (!sel || sel.rangeCount === 0) return null;
8668
+ const range = sel.getRangeAt(0);
8669
+ let startNode = range.startContainer;
8670
+ if (startNode.nodeType === Node.ELEMENT_NODE) {
8671
+ const el = startNode;
8672
+ startNode = el.childNodes[range.startOffset] ?? el.childNodes[range.startOffset - 1] ?? el;
8673
+ }
8674
+ return startNode.nodeType === Node.TEXT_NODE ? startNode.parentElement : startNode;
8675
+ }
8676
+ function detectActiveFormats(startEl, activeRoot) {
8677
+ const formats = /* @__PURE__ */ new Set();
8678
+ const cs = getComputedStyle(startEl);
8679
+ const weight = parseInt(cs.fontWeight, 10);
8680
+ let hasBold = cs.fontWeight === "bold" || !Number.isNaN(weight) && weight >= 600;
8681
+ let hasItalic = cs.fontStyle === "italic" || cs.fontStyle === "oblique";
8682
+ let hasUnderline = false;
8683
+ let hasStrike = false;
8684
+ let hasUl = false;
8685
+ let hasOl = false;
8686
+ for (let el = startEl; el; el = el.parentElement) {
8687
+ if (el.tagName === "B" || el.tagName === "STRONG") hasBold = true;
8688
+ if (el.tagName === "I" || el.tagName === "EM") hasItalic = true;
8689
+ const decoration = getComputedStyle(el).textDecorationLine;
8690
+ if (decoration.includes("underline") || el.tagName === "U" || el.tagName === "INS") hasUnderline = true;
8691
+ if (decoration.includes("line-through") || el.tagName === "S" || el.tagName === "STRIKE" || el.tagName === "DEL") hasStrike = true;
8692
+ if (el.tagName === "UL") hasUl = true;
8693
+ if (el.tagName === "OL") hasOl = true;
8694
+ if (el === activeRoot) break;
8695
+ }
8696
+ if (hasBold) formats.add("bold");
8697
+ if (hasItalic) formats.add("italic");
8698
+ if (hasUnderline) formats.add("underline");
8699
+ if (hasStrike) formats.add("strikeThrough");
8700
+ if (hasUl) formats.add("insertUnorderedList");
8701
+ if (hasOl) formats.add("insertOrderedList");
8702
+ const block = startEl.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? startEl;
8703
+ const align = getComputedStyle(block).textAlign;
8704
+ if (align === "center") formats.add("justifyCenter");
8705
+ else if (align === "right" || align === "end") formats.add("justifyRight");
8706
+ else formats.add("justifyLeft");
8707
+ return formats;
8708
+ }
8709
+ function setsEqual(a, b) {
8710
+ if (a.size !== b.size) return false;
8711
+ for (const item of a) {
8712
+ if (!b.has(item)) return false;
8713
+ }
8714
+ return true;
8715
+ }
8716
+ function textOffsetInRoot(root, node, offset) {
8717
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
8718
+ let total = 0;
8719
+ let current;
8720
+ while (current = walker.nextNode()) {
8721
+ if (current === node) return total + offset;
8722
+ total += (current.textContent ?? "").length;
8723
+ }
8724
+ return total;
8725
+ }
8726
+ function pointAtTextOffset(root, targetOffset) {
8727
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
8728
+ let total = 0;
8729
+ let current;
8730
+ let last = null;
8731
+ while (current = walker.nextNode()) {
8732
+ const len = (current.textContent ?? "").length;
8733
+ if (total + len >= targetOffset) return { node: current, offset: targetOffset - total };
8734
+ total += len;
8735
+ last = current;
8736
+ }
8737
+ return last ? { node: last, offset: (last.textContent ?? "").length } : null;
8738
+ }
8624
8739
  function FloatingToolbar({
8625
8740
  rect,
8626
8741
  parentScroll,
@@ -8813,6 +8928,7 @@ function OhhwellsBridge() {
8813
8928
  const [fetchState, setFetchState] = useState7("idle");
8814
8929
  const autoSaveTimers = useRef7(/* @__PURE__ */ new Map());
8815
8930
  const activeElRef = useRef7(null);
8931
+ const pointerHeldRef = useRef7(false);
8816
8932
  const selectedElRef = useRef7(null);
8817
8933
  const selectedHrefKeyRef = useRef7(null);
8818
8934
  const selectedFooterColAttrRef = useRef7(null);
@@ -9499,7 +9615,6 @@ function OhhwellsBridge() {
9499
9615
  if (e.button !== 0) return;
9500
9616
  const selected = selectedElRef.current;
9501
9617
  if (!selected) return;
9502
- armFooterPressDrag();
9503
9618
  const hrefKey = selected.getAttribute("data-ohw-href-key");
9504
9619
  if (hrefKey && isFooterHrefKey(hrefKey)) {
9505
9620
  footerPointerDragRef.current = {
@@ -9611,6 +9726,8 @@ function OhhwellsBridge() {
9611
9726
  setSiblingHintRect(null);
9612
9727
  setSiblingHintRects([]);
9613
9728
  setIsItemDragging(false);
9729
+ const preActivationSelection = window.getSelection();
9730
+ const preservedRange = preActivationSelection && preActivationSelection.rangeCount > 0 && !preActivationSelection.isCollapsed && el.contains(preActivationSelection.getRangeAt(0).commonAncestorContainer) ? preActivationSelection.getRangeAt(0).cloneRange() : null;
9614
9731
  el.setAttribute("contenteditable", "true");
9615
9732
  el.setAttribute("data-ohw-editing", "");
9616
9733
  el.removeAttribute("data-ohw-hovered");
@@ -9618,7 +9735,11 @@ function OhhwellsBridge() {
9618
9735
  activeElRef.current = el;
9619
9736
  originalContentRef.current = el.innerHTML;
9620
9737
  el.focus({ preventScroll: true });
9621
- if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9738
+ if (preservedRange) {
9739
+ const selection = window.getSelection();
9740
+ selection?.removeAllRanges();
9741
+ selection?.addRange(preservedRange);
9742
+ } else if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9622
9743
  const { caretX, caretY } = options;
9623
9744
  placeCaretAtPoint(el, caretX, caretY);
9624
9745
  requestAnimationFrame(() => {
@@ -10074,7 +10195,9 @@ function OhhwellsBridge() {
10074
10195
  if (inActive || inActiveNav) {
10075
10196
  e.preventDefault();
10076
10197
  e.stopPropagation();
10077
- if (e.detail < 2) {
10198
+ const selection = window.getSelection();
10199
+ const hasRangeSelection = Boolean(selection && !selection.isCollapsed);
10200
+ if (e.detail < 2 && !hasRangeSelection) {
10078
10201
  placeCaretAtPoint(active, e.clientX, e.clientY);
10079
10202
  refreshActiveCommandsRef.current();
10080
10203
  }
@@ -11227,30 +11350,30 @@ function OhhwellsBridge() {
11227
11350
  const h = document.documentElement.scrollHeight;
11228
11351
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
11229
11352
  };
11353
+ let selectionChangeRaf = null;
11354
+ const runSelectionChange = () => {
11355
+ selectionChangeRaf = null;
11356
+ const activeRoot = activeElRef.current;
11357
+ if (!activeRoot) return;
11358
+ const startEl = resolveSelectionStartElement(window.getSelection());
11359
+ const next = startEl ? detectActiveFormats(startEl, activeRoot) : /* @__PURE__ */ new Set();
11360
+ setActiveCommands((prev) => setsEqual(prev, next) ? prev : next);
11361
+ };
11230
11362
  const handleSelectionChange = () => {
11231
- if (!activeElRef.current) return;
11232
- const next = /* @__PURE__ */ new Set();
11233
- for (const cmd of ["bold", "italic", "underline", "strikeThrough", "insertUnorderedList", "insertOrderedList"]) {
11234
- try {
11235
- if (document.queryCommandState(cmd)) next.add(cmd);
11236
- } catch {
11237
- }
11238
- }
11239
- const sel = window.getSelection();
11240
- const anchor = sel?.anchorNode;
11241
- if (anchor) {
11242
- const el = anchor.nodeType === Node.TEXT_NODE ? anchor.parentElement : anchor;
11243
- const block = el?.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? el;
11244
- if (block) {
11245
- const align = getComputedStyle(block).textAlign;
11246
- if (align === "center") next.add("justifyCenter");
11247
- else if (align === "right" || align === "end") next.add("justifyRight");
11248
- else next.add("justifyLeft");
11249
- }
11250
- }
11251
- setActiveCommands(next);
11363
+ if (pointerHeldRef.current) return;
11364
+ if (selectionChangeRaf !== null) return;
11365
+ selectionChangeRaf = requestAnimationFrame(runSelectionChange);
11366
+ };
11367
+ const markPointerHeld = (e) => {
11368
+ if (e.button !== 0) return;
11369
+ pointerHeldRef.current = true;
11252
11370
  };
11253
- refreshActiveCommandsRef.current = handleSelectionChange;
11371
+ const markPointerReleased = () => {
11372
+ if (!pointerHeldRef.current) return;
11373
+ pointerHeldRef.current = false;
11374
+ handleSelectionChange();
11375
+ };
11376
+ refreshActiveCommandsRef.current = runSelectionChange;
11254
11377
  const handleDocMouseLeave = () => {
11255
11378
  hoveredImageRef.current = null;
11256
11379
  setMediaHover(null);
@@ -11490,7 +11613,6 @@ function OhhwellsBridge() {
11490
11613
  const anchor = getNavigationItemAnchor(target);
11491
11614
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
11492
11615
  if (anchor && isFooterHrefKey(hrefKey)) {
11493
- armFooterPressDrag();
11494
11616
  footerPointerDragRef.current = {
11495
11617
  el: anchor,
11496
11618
  kind: "link",
@@ -11504,7 +11626,6 @@ function OhhwellsBridge() {
11504
11626
  }
11505
11627
  const col = target.closest("[data-ohw-footer-col]");
11506
11628
  if (col && !getNavigationItemAnchor(target)) {
11507
- armFooterPressDrag();
11508
11629
  footerPointerDragRef.current = {
11509
11630
  el: col,
11510
11631
  kind: "column",
@@ -11533,6 +11654,7 @@ function OhhwellsBridge() {
11533
11654
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
11534
11655
  e.preventDefault();
11535
11656
  pending.started = true;
11657
+ armFooterPressDrag();
11536
11658
  clearTextSelection();
11537
11659
  try {
11538
11660
  document.body.setPointerCapture(pending.pointerId);
@@ -11586,7 +11708,8 @@ function OhhwellsBridge() {
11586
11708
  }
11587
11709
  } catch {
11588
11710
  }
11589
- if (!pending?.started) {
11711
+ if (!pending) return;
11712
+ if (!pending.started) {
11590
11713
  unlockFooterDragInteraction();
11591
11714
  return;
11592
11715
  }
@@ -11679,9 +11802,39 @@ function OhhwellsBridge() {
11679
11802
  return () => window.removeEventListener("hashchange", onHashChange);
11680
11803
  }, [pathname]);
11681
11804
  const handleCommand = useCallback4((cmd) => {
11805
+ const el = activeElRef.current;
11806
+ const selBefore = window.getSelection();
11807
+ let savedOffsets = null;
11808
+ if (el && selBefore && selBefore.rangeCount > 0 && !selBefore.isCollapsed) {
11809
+ const r2 = selBefore.getRangeAt(0);
11810
+ if (el.contains(r2.commonAncestorContainer)) {
11811
+ savedOffsets = {
11812
+ start: textOffsetInRoot(el, r2.startContainer, r2.startOffset),
11813
+ end: textOffsetInRoot(el, r2.endContainer, r2.endOffset)
11814
+ };
11815
+ }
11816
+ }
11817
+ document.execCommand("styleWithCSS", false, true);
11682
11818
  document.execCommand(cmd, false);
11683
- activeElRef.current?.focus();
11684
- if (activeElRef.current) setToolbarRect(getEditMeasureEl(activeElRef.current).getBoundingClientRect());
11819
+ if (el && document.activeElement !== el) {
11820
+ el.focus();
11821
+ }
11822
+ const selAfter = window.getSelection();
11823
+ if (el && savedOffsets && selAfter?.isCollapsed) {
11824
+ const start = pointAtTextOffset(el, savedOffsets.start);
11825
+ const end = pointAtTextOffset(el, savedOffsets.end);
11826
+ if (start && end) {
11827
+ try {
11828
+ const range = document.createRange();
11829
+ range.setStart(start.node, start.offset);
11830
+ range.setEnd(end.node, end.offset);
11831
+ selAfter.removeAllRanges();
11832
+ selAfter.addRange(range);
11833
+ } catch {
11834
+ }
11835
+ }
11836
+ }
11837
+ if (el) setToolbarRect(getEditMeasureEl(el).getBoundingClientRect());
11685
11838
  refreshActiveCommandsRef.current();
11686
11839
  }, []);
11687
11840
  const handleStateChange = useCallback4((state) => {