fable-editor 1.2.6 → 1.2.8

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.
@@ -1828,7 +1828,12 @@ class FableEditor {
1828
1828
  foreColor = '#000000';
1829
1829
  backColor = '#FACC15';
1830
1830
  openPop = null;
1831
- openSubEl = null;
1831
+ /** Stack of currently-open flyout submenus, shallowest first (e.g. for
1832
+ * Table > Cell > Cell background: [Cell flyout, Cell background flyout]).
1833
+ * A single slot isn't enough past one level deep - opening a 2nd-level
1834
+ * flyout would remove the 1st-level one still needed to bridge visually
1835
+ * back to the top-level popup, leaving a gap. */
1836
+ subStack = [];
1832
1837
  popAnchor = null;
1833
1838
  dlgOvl = null;
1834
1839
  tblActive = null;
@@ -2018,7 +2023,7 @@ class FableEditor {
2018
2023
  if (this.openPop &&
2019
2024
  !this.openPop.contains(e.target) &&
2020
2025
  !this.popAnchor.contains(e.target) &&
2021
- !(this.openSubEl && this.openSubEl.contains(e.target))) {
2026
+ !this.subContains(e.target)) {
2022
2027
  this.closePop();
2023
2028
  }
2024
2029
  });
@@ -2175,7 +2180,17 @@ class FableEditor {
2175
2180
  this.positionVidCtx();
2176
2181
  this.positionCodeCtx();
2177
2182
  });
2178
- this.onWin('scroll', () => {
2183
+ this.onWin('scroll', (e) => {
2184
+ /* this fires for ANY scroll in the document (capture:true is what
2185
+ lets it catch a scrollable ancestor, not just the window) -
2186
+ including the popup's own overflow:auto scrolling through a long
2187
+ menu. Only close it for a scroll that happened outside the open
2188
+ menu/flyouts; otherwise scrolling a long dropdown would close it
2189
+ on the very first scroll tick instead of scrolling its content. */
2190
+ const t = e.target;
2191
+ const withinOpenMenu = t instanceof Node && ((this.openPop?.contains(t) ?? false) || this.subContains(t));
2192
+ if (!withinOpenMenu)
2193
+ this.closePop();
2179
2194
  this.positionTableHandles();
2180
2195
  this.positionImageHandles();
2181
2196
  this.positionImgPhCtx();
@@ -2259,7 +2274,7 @@ class FableEditor {
2259
2274
  (this.vidCtx && this.vidCtx.contains(e.target)) ||
2260
2275
  (this.codeCtx && this.codeCtx.contains(e.target)) ||
2261
2276
  (this.openPop && this.openPop.contains(e.target)) ||
2262
- (this.openSubEl && this.openSubEl.contains(e.target)))
2277
+ this.subContains(e.target))
2263
2278
  return;
2264
2279
  this.clearTableHandles();
2265
2280
  this.clearImageHandles();
@@ -2600,8 +2615,28 @@ class FableEditor {
2600
2615
  }
2601
2616
  /* ---------------------------------------------------------- popups / menus */
2602
2617
  closeSub() {
2603
- this.openSubEl?.remove();
2604
- this.openSubEl = null;
2618
+ this.closeSubFrom(0);
2619
+ }
2620
+ /** Removes stacked flyouts from `level` (0 = shallowest) onward, keeping
2621
+ * anything shallower - e.g. hovering a sibling item inside the "Cell"
2622
+ * flyout should drop a deeper "Cell background" flyout but keep "Cell"
2623
+ * itself and the top-level popup it bridges to. */
2624
+ closeSubFrom(level) {
2625
+ while (this.subStack.length > level)
2626
+ this.subStack.pop().remove();
2627
+ }
2628
+ /** True if `node` is inside any currently-open flyout submenu (any depth). */
2629
+ subContains(node) {
2630
+ return this.subStack.some((el) => el.contains(node));
2631
+ }
2632
+ /** 0 for the top-level popup itself; otherwise this container's depth in
2633
+ * the flyout stack + 1. Used to know how many deeper flyouts to close
2634
+ * when opening or hovering within `container`. */
2635
+ subLevelOf(container) {
2636
+ if (container === this.openPop)
2637
+ return 0;
2638
+ const idx = this.subStack.indexOf(container);
2639
+ return idx === -1 ? this.subStack.length : idx + 1;
2605
2640
  }
2606
2641
  closePop() {
2607
2642
  this.closeSub();
@@ -2616,7 +2651,17 @@ class FableEditor {
2616
2651
  }
2617
2652
  }
2618
2653
  openSubFor(item, anchor) {
2619
- this.closeSub();
2654
+ /* captured before truncating the stack - for a flyout nested inside
2655
+ another flyout (e.g. Table > Cell > Cell background), anchor is itself
2656
+ a descendant of the currently-open sub-popup; measuring after removing
2657
+ it would read a detached, zeroed-out rect and land the new flyout at
2658
+ the top-left corner of the screen */
2659
+ const r = anchor.getBoundingClientRect();
2660
+ /* only close flyouts deeper than the one anchor lives in - closing
2661
+ everything (the old behavior) would also remove that same containing
2662
+ flyout, leaving a visual gap back to the top-level popup */
2663
+ const parentPop = anchor.closest('.pop');
2664
+ this.closeSubFrom(parentPop ? this.subLevelOf(parentPop) : 0);
2620
2665
  const sub = document.createElement('div');
2621
2666
  sub.className = 'pop sub';
2622
2667
  sub.dir = this.dir();
@@ -2629,7 +2674,6 @@ class FableEditor {
2629
2674
  e.preventDefault();
2630
2675
  });
2631
2676
  document.body.appendChild(sub);
2632
- const r = anchor.getBoundingClientRect();
2633
2677
  const isRtl = this.dir() === 'rtl';
2634
2678
  // overlap the parent item slightly so the pointer can travel into the flyout
2635
2679
  let x = isRtl ? r.left - sub.offsetWidth + 2 : r.right - 2;
@@ -2638,7 +2682,7 @@ class FableEditor {
2638
2682
  y = Math.max(8 + scrollY, Math.min(y, scrollY + innerHeight - sub.offsetHeight - 8));
2639
2683
  sub.style.left = x + 'px';
2640
2684
  sub.style.top = y + 'px';
2641
- this.openSubEl = sub;
2685
+ this.subStack.push(sub);
2642
2686
  }
2643
2687
  popup(anchor, build, cls) {
2644
2688
  if (this.openPop && this.popAnchor === anchor) {
@@ -2654,7 +2698,11 @@ class FableEditor {
2654
2698
  const r = anchor.getBoundingClientRect();
2655
2699
  // align the menu with its control: at least as wide as the anchor
2656
2700
  el.style.minWidth = Math.max(160, Math.round(r.width)) + 'px';
2657
- el.style.top = r.bottom + scrollY + 2 + 'px';
2701
+ // clamp so a long menu (its own max-height/overflow makes it scrollable)
2702
+ // stays fully on-screen instead of running off the bottom of the viewport
2703
+ let y = r.bottom + scrollY + 2;
2704
+ y = Math.max(8 + scrollY, Math.min(y, scrollY + innerHeight - el.offsetHeight - 8));
2705
+ el.style.top = y + 'px';
2658
2706
  const isRtl = this.dir() === 'rtl';
2659
2707
  let x = isRtl ? r.right - el.offsetWidth : r.left;
2660
2708
  x = Math.max(8, Math.min(x + scrollX, scrollX + innerWidth - el.offsetWidth - 8));
@@ -2686,8 +2734,11 @@ class FableEditor {
2686
2734
  b.addEventListener('mouseenter', () => {
2687
2735
  if (hasSub)
2688
2736
  this.openSubFor(it, b);
2689
- else if (this.openSubEl && el !== this.openSubEl)
2690
- this.closeSub();
2737
+ /* hovering a no-submenu item drops whatever deeper flyout was open
2738
+ from a previously-hovered sibling, but keeps el itself and any
2739
+ shallower ancestors intact */
2740
+ else
2741
+ this.closeSubFrom(this.subLevelOf(el));
2691
2742
  it.hover?.(true);
2692
2743
  });
2693
2744
  b.addEventListener('mouseleave', () => it.hover?.(false));
@@ -3273,8 +3324,8 @@ class FableEditor {
3273
3324
  buildToolbarRegistry() {
3274
3325
  const tableBtn = this.tbtn(IC.tableic, this.t('quicktable'), () => this.tableGrid(tableBtn));
3275
3326
  const registry = {
3276
- undo: () => this.tbtn(IC.undo, this.t('undo'), () => this.exec('undo')),
3277
- redo: () => this.tbtn(IC.redo, this.t('redo'), () => this.exec('redo')),
3327
+ undo: () => this.tbtn(IC.undo, this.t('undo'), () => this.exec('undo'), 'undo'),
3328
+ redo: () => this.tbtn(IC.redo, this.t('redo'), () => this.exec('redo'), 'redo'),
3278
3329
  preview: () => this.tbtn(IC.prevw, this.t('preview'), () => this.previewDlg()),
3279
3330
  print: () => this.tbtn(IC.printic, this.t('print'), () => this.printDoc()),
3280
3331
  importword: () => this.tbtn(IC.wordic, this.t('importword'), () => this.pickWordDoc()),