@trafica/editor 1.0.19 → 1.0.21

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.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { forwardRef, useRef, useEffect, useCallback, useMemo, useImperativeHandle, useState, useLayoutEffect, useSyncExternalStore } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { createPortal } from 'react-dom';
3
4
 
4
5
  // src/editor/core/DocumentModel.ts
5
6
  function isTextNode(node) {
@@ -5530,9 +5531,11 @@ function SpecialCharactersButton({
5530
5531
  null
5531
5532
  );
5532
5533
  const [focusedIdx, setFocusedIdx] = useState(-1);
5533
- const containerRef = useRef(null);
5534
+ const buttonRef = useRef(null);
5535
+ const popupRef = useRef(null);
5534
5536
  const searchRef = useRef(null);
5535
5537
  const gridRef = useRef(null);
5538
+ const [popupPos, setPopupPos] = useState({ top: 0, left: 0 });
5536
5539
  const filtered = useMemo(() => {
5537
5540
  const q = search.toLowerCase();
5538
5541
  return SPECIAL_CHARACTERS.filter(
@@ -5540,6 +5543,12 @@ function SpecialCharactersButton({
5540
5543
  );
5541
5544
  }, [category, search]);
5542
5545
  const openModal = () => {
5546
+ if (buttonRef.current) {
5547
+ const rect = buttonRef.current.getBoundingClientRect();
5548
+ const popupWidth = 340;
5549
+ const left = Math.min(rect.left, window.innerWidth - popupWidth - 8);
5550
+ setPopupPos({ top: rect.bottom + 4, left: Math.max(8, left) });
5551
+ }
5543
5552
  setOpen(true);
5544
5553
  setSearch("");
5545
5554
  setFocusedIdx(-1);
@@ -5552,9 +5561,11 @@ function SpecialCharactersButton({
5552
5561
  useEffect(() => {
5553
5562
  if (!open) return;
5554
5563
  const handler = (e) => {
5555
- if (containerRef.current && !containerRef.current.contains(e.target)) {
5556
- closeModal();
5557
- }
5564
+ var _a, _b;
5565
+ const target = e.target;
5566
+ const insidePopup = (_a = popupRef.current) == null ? void 0 : _a.contains(target);
5567
+ const insideButton = (_b = buttonRef.current) == null ? void 0 : _b.contains(target);
5568
+ if (!insidePopup && !insideButton) closeModal();
5558
5569
  };
5559
5570
  document.addEventListener("mousedown", handler);
5560
5571
  return () => document.removeEventListener("mousedown", handler);
@@ -5602,10 +5613,11 @@ function SpecialCharactersButton({
5602
5613
  const cell = gridRef.current.children[focusedIdx];
5603
5614
  cell == null ? void 0 : cell.focus();
5604
5615
  }, [focusedIdx]);
5605
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative", children: [
5616
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
5606
5617
  /* @__PURE__ */ jsx(
5607
5618
  "button",
5608
5619
  {
5620
+ ref: buttonRef,
5609
5621
  type: "button",
5610
5622
  title: "Special Characters",
5611
5623
  "aria-label": "Insert special character",
@@ -5613,7 +5625,11 @@ function SpecialCharactersButton({
5613
5625
  "aria-expanded": open,
5614
5626
  onMouseDown: (e) => {
5615
5627
  e.preventDefault();
5616
- open ? closeModal() : openModal();
5628
+ if (open) {
5629
+ closeModal();
5630
+ } else {
5631
+ openModal();
5632
+ }
5617
5633
  },
5618
5634
  className: [
5619
5635
  "flex items-center justify-center w-8 h-8 rounded text-sm font-medium transition-colors",
@@ -5622,127 +5638,131 @@ function SpecialCharactersButton({
5622
5638
  children: /* @__PURE__ */ jsx(OmegaIcon, {})
5623
5639
  }
5624
5640
  ),
5625
- open && /* @__PURE__ */ jsxs(
5626
- "div",
5627
- {
5628
- role: "dialog",
5629
- "aria-label": "Special characters",
5630
- "aria-modal": "true",
5631
- className: "absolute top-full left-0 mt-1 z-50 bg-white border border-gray-200 rounded-lg shadow-2xl flex flex-col",
5632
- style: { width: 340, maxHeight: 420 },
5633
- onKeyDown: (e) => {
5634
- if (e.key === "Escape") closeModal();
5635
- },
5636
- children: [
5637
- /* @__PURE__ */ jsxs("div", { className: "px-3 pt-3 pb-2 border-b border-gray-100 ", children: [
5638
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
5639
- /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Special Characters" }),
5640
- /* @__PURE__ */ jsx(
5641
- "button",
5642
- {
5643
- type: "button",
5644
- onClick: closeModal,
5645
- "aria-label": "Close",
5646
- className: "text-gray-400 hover:text-gray-600 p-0.5 rounded",
5647
- children: /* @__PURE__ */ jsx(CloseIcon, {})
5648
- }
5649
- )
5650
- ] }),
5651
- /* @__PURE__ */ jsx(
5652
- "input",
5653
- {
5654
- ref: searchRef,
5655
- type: "search",
5656
- placeholder: "Search characters\u2026",
5657
- value: search,
5658
- onChange: (e) => {
5659
- setSearch(e.target.value);
5660
- setFocusedIdx(-1);
5661
- },
5662
- onKeyDown: (e) => {
5663
- var _a, _b;
5664
- if (e.key === "Escape") closeModal();
5665
- if (e.key === "ArrowDown") {
5666
- e.preventDefault();
5667
- setFocusedIdx(0);
5668
- (_b = (_a = gridRef.current) == null ? void 0 : _a.children[0]) == null ? void 0 : _b.dispatchEvent(
5669
- new Event("focus")
5670
- );
5641
+ open && createPortal(
5642
+ /* @__PURE__ */ jsxs(
5643
+ "div",
5644
+ {
5645
+ ref: popupRef,
5646
+ role: "dialog",
5647
+ "aria-label": "Special characters",
5648
+ "aria-modal": "true",
5649
+ className: "bg-white border border-gray-200 rounded-lg shadow-2xl flex flex-col",
5650
+ style: { position: "fixed", top: popupPos.top, left: popupPos.left, width: 340, maxHeight: 420, zIndex: 9999 },
5651
+ onKeyDown: (e) => {
5652
+ if (e.key === "Escape") closeModal();
5653
+ },
5654
+ children: [
5655
+ /* @__PURE__ */ jsxs("div", { className: "px-3 pt-3 pb-2 border-b border-gray-100 ", children: [
5656
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
5657
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Special Characters" }),
5658
+ /* @__PURE__ */ jsx(
5659
+ "button",
5660
+ {
5661
+ type: "button",
5662
+ onClick: closeModal,
5663
+ "aria-label": "Close",
5664
+ className: "text-gray-400 hover:text-gray-600 p-0.5 rounded",
5665
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
5671
5666
  }
5672
- },
5673
- className: "w-full border border-gray-200 rounded px-2.5 py-1.5 text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
5674
- }
5675
- ),
5676
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mt-2", children: CATEGORIES.map((cat) => /* @__PURE__ */ jsx(
5677
- "button",
5678
- {
5679
- type: "button",
5680
- onMouseDown: (e) => {
5681
- e.preventDefault();
5682
- setCategory(cat);
5683
- setFocusedIdx(-1);
5684
- },
5685
- className: [
5686
- "px-2 py-0.5 rounded text-xs font-medium transition-colors",
5687
- category === cat ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200 "
5688
- ].join(" "),
5689
- children: cat
5690
- },
5691
- cat
5692
- )) })
5693
- ] }),
5694
- /* @__PURE__ */ jsx(
5695
- "div",
5696
- {
5697
- ref: gridRef,
5698
- role: "grid",
5699
- "aria-label": "Character grid",
5700
- className: "flex-1 overflow-y-auto p-2",
5701
- style: {
5702
- display: "grid",
5703
- gridTemplateColumns: "repeat(8, 1fr)",
5704
- gap: 2
5705
- },
5706
- onKeyDown: handleGridKeyDown,
5707
- children: filtered.length === 0 ? /* @__PURE__ */ jsx(
5708
- "div",
5667
+ )
5668
+ ] }),
5669
+ /* @__PURE__ */ jsx(
5670
+ "input",
5709
5671
  {
5710
- className: "col-span-8 text-center py-8 text-sm text-gray-400 ",
5711
- role: "status",
5712
- children: "No characters found"
5672
+ ref: searchRef,
5673
+ type: "search",
5674
+ placeholder: "Search characters\u2026",
5675
+ value: search,
5676
+ onChange: (e) => {
5677
+ setSearch(e.target.value);
5678
+ setFocusedIdx(-1);
5679
+ },
5680
+ onKeyDown: (e) => {
5681
+ var _a, _b;
5682
+ if (e.key === "Escape") closeModal();
5683
+ if (e.key === "ArrowDown") {
5684
+ e.preventDefault();
5685
+ setFocusedIdx(0);
5686
+ (_b = (_a = gridRef.current) == null ? void 0 : _a.children[0]) == null ? void 0 : _b.dispatchEvent(
5687
+ new Event("focus")
5688
+ );
5689
+ }
5690
+ },
5691
+ className: "w-full border border-gray-200 rounded px-2.5 py-1.5 text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
5713
5692
  }
5714
- ) : filtered.map((c, idx) => /* @__PURE__ */ jsx(
5693
+ ),
5694
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mt-2", children: CATEGORIES.map((cat) => /* @__PURE__ */ jsx(
5715
5695
  "button",
5716
5696
  {
5717
5697
  type: "button",
5718
- role: "gridcell",
5719
- tabIndex: focusedIdx === idx ? 0 : -1,
5720
- "aria-label": c.name,
5721
- title: c.name,
5722
5698
  onMouseDown: (e) => {
5723
5699
  e.preventDefault();
5724
- insertChar(c.char);
5700
+ setCategory(cat);
5701
+ setFocusedIdx(-1);
5725
5702
  },
5726
- onMouseEnter: () => setHovered({ char: c.char, name: c.name }),
5727
- onMouseLeave: () => setHovered(null),
5728
- onFocus: () => setHovered({ char: c.char, name: c.name }),
5729
- onBlur: () => setHovered(null),
5730
5703
  className: [
5731
- "flex items-center justify-center rounded text-base h-8 w-full transition-colors cursor-pointer select-none",
5732
- focusedIdx === idx ? "bg-blue-100 text-blue-700 ring-2 ring-blue-500" : "text-gray-800 hover:bg-blue-50 hover:text-blue-700 "
5704
+ "px-2 py-0.5 rounded text-xs font-medium transition-colors",
5705
+ category === cat ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200 "
5733
5706
  ].join(" "),
5734
- children: c.char
5707
+ children: cat
5735
5708
  },
5736
- c.char + c.name
5737
- ))
5738
- }
5739
- ),
5740
- /* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-t border-gray-100 flex items-center gap-3 min-h-[40px]", children: hovered ? /* @__PURE__ */ jsxs(Fragment, { children: [
5741
- /* @__PURE__ */ jsx("span", { className: "text-2xl leading-none text-gray-800 w-8 text-center", children: hovered.char }),
5742
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 truncate", children: hovered.name })
5743
- ] }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 ", children: "Hover or use arrow keys to preview" }) })
5744
- ]
5745
- }
5709
+ cat
5710
+ )) })
5711
+ ] }),
5712
+ /* @__PURE__ */ jsx(
5713
+ "div",
5714
+ {
5715
+ ref: gridRef,
5716
+ role: "grid",
5717
+ "aria-label": "Character grid",
5718
+ className: "flex-1 overflow-y-auto p-2",
5719
+ style: {
5720
+ display: "grid",
5721
+ gridTemplateColumns: "repeat(8, 1fr)",
5722
+ gap: 2
5723
+ },
5724
+ onKeyDown: handleGridKeyDown,
5725
+ children: filtered.length === 0 ? /* @__PURE__ */ jsx(
5726
+ "div",
5727
+ {
5728
+ className: "col-span-8 text-center py-8 text-sm text-gray-400 ",
5729
+ role: "status",
5730
+ children: "No characters found"
5731
+ }
5732
+ ) : filtered.map((c, idx) => /* @__PURE__ */ jsx(
5733
+ "button",
5734
+ {
5735
+ type: "button",
5736
+ role: "gridcell",
5737
+ tabIndex: focusedIdx === idx ? 0 : -1,
5738
+ "aria-label": c.name,
5739
+ title: c.name,
5740
+ onMouseDown: (e) => {
5741
+ e.preventDefault();
5742
+ insertChar(c.char);
5743
+ },
5744
+ onMouseEnter: () => setHovered({ char: c.char, name: c.name }),
5745
+ onMouseLeave: () => setHovered(null),
5746
+ onFocus: () => setHovered({ char: c.char, name: c.name }),
5747
+ onBlur: () => setHovered(null),
5748
+ className: [
5749
+ "flex items-center justify-center rounded text-base h-8 w-full transition-colors cursor-pointer select-none",
5750
+ focusedIdx === idx ? "bg-blue-100 text-blue-700 ring-2 ring-blue-500" : "text-gray-800 hover:bg-blue-50 hover:text-blue-700 "
5751
+ ].join(" "),
5752
+ children: c.char
5753
+ },
5754
+ c.char + c.name
5755
+ ))
5756
+ }
5757
+ ),
5758
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-t border-gray-100 flex items-center gap-3 min-h-[40px]", children: hovered ? /* @__PURE__ */ jsxs(Fragment, { children: [
5759
+ /* @__PURE__ */ jsx("span", { className: "text-2xl leading-none text-gray-800 w-8 text-center", children: hovered.char }),
5760
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 truncate", children: hovered.name })
5761
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 ", children: "Hover or use arrow keys to preview" }) })
5762
+ ]
5763
+ }
5764
+ ),
5765
+ document.body
5746
5766
  )
5747
5767
  ] });
5748
5768
  }
@@ -7917,7 +7937,18 @@ function EditorCore({
7917
7937
  container.querySelectorAll(".editor-cell-active").forEach((el) => {
7918
7938
  el.classList.remove("editor-cell-active");
7919
7939
  });
7920
- if (!inTableCellPos) return;
7940
+ if (!inTableCellPos) {
7941
+ setTableSelection(null);
7942
+ return;
7943
+ }
7944
+ if (tableSelection) {
7945
+ const { anchorCell, focusCell, tablePath } = tableSelection;
7946
+ const sameTable = JSON.stringify(tablePath) === JSON.stringify(inTableCellPos.tablePath);
7947
+ const singleCell = anchorCell[0] === focusCell[0] && anchorCell[1] === focusCell[1];
7948
+ if (!sameTable || singleCell) {
7949
+ setTableSelection(null);
7950
+ }
7951
+ }
7921
7952
  const cellPath = [...inTableCellPos.tablePath, inTableCellPos.row, inTableCellPos.col];
7922
7953
  const td = container.querySelector(
7923
7954
  `[data-block-path="${JSON.stringify(cellPath)}"]`