@bobfrankston/rmfmail 1.2.221 → 1.2.223

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/client/app.ts CHANGED
@@ -1449,6 +1449,27 @@ function showComposeOverlay(title = "Compose"): HTMLIFrameElement {
1449
1449
  return frame;
1450
1450
  }
1451
1451
 
1452
+ // Native context-menu commands land in the TOP frame: msger dispatches them
1453
+ // with CoreWebView2.ExecuteScript, which always runs against the top-level
1454
+ // document. But the in-window compose is an IFRAME, and the handler that owns
1455
+ // the semantics (__msgerContextCommand) is defined inside it — so every
1456
+ // right-click item in the in-window compose hit msger's `window.__msgerContext
1457
+ // Command && …` guard, found nothing, and silently did nothing. The items were
1458
+ // registered for this window in v1.2.155; the dispatcher never was. Forward to
1459
+ // the frontmost compose overlay (same origin, so reaching into it is legal).
1460
+ (window as any).__msgerContextCommand = (id: string): void => {
1461
+ const frames = [...document.querySelectorAll<HTMLIFrameElement>(".compose-overlay iframe")];
1462
+ // Frontmost = highest z-index on the wrapper; mousedown maintains it, so
1463
+ // the one the user just right-clicked is the one on top.
1464
+ const target = frames.sort((a, b) => {
1465
+ const z = (el: HTMLIFrameElement) => Number((el.closest(".compose-overlay") as HTMLElement)?.style.zIndex || 0);
1466
+ return z(b) - z(a);
1467
+ })[0];
1468
+ try {
1469
+ (target?.contentWindow as any)?.__msgerContextCommand?.(id);
1470
+ } catch { /* frame torn down mid-click */ }
1471
+ };
1472
+
1452
1473
  /** Drop a transparent full-viewport shield in front of every other element
1453
1474
  * so mousemove events stay in the document during a drag. Setting
1454
1475
  * pointer-events:none on the compose iframe alone wasn't enough — the
@@ -5,8 +5,18 @@
5
5
  let activeMenu = null;
6
6
  let dismissListener = null;
7
7
  let escapeListener = null;
8
+ /** Open submenu element, if any. Tracked separately from `activeMenu` so the
9
+ * outside-click dismisser doesn't treat the child as "outside" the parent. */
10
+ let activeSubmenu = null;
11
+ function closeSubmenu() {
12
+ if (activeSubmenu) {
13
+ activeSubmenu.remove();
14
+ activeSubmenu = null;
15
+ }
16
+ }
8
17
  /** Close any open context menu and remove dismiss listeners */
9
18
  export function closeContextMenu() {
19
+ closeSubmenu();
10
20
  if (activeMenu) {
11
21
  activeMenu.remove();
12
22
  activeMenu = null;
@@ -20,6 +30,50 @@ export function closeContextMenu() {
20
30
  escapeListener = null;
21
31
  }
22
32
  }
33
+ /** Render `items` as a child menu anchored to the right of `parentRow`,
34
+ * flipping to its left when there isn't room. Shares the .ctx-menu styling
35
+ * so it can't drift from the parent visually. */
36
+ function openSubmenu(parentRow, items) {
37
+ closeSubmenu();
38
+ const sub = document.createElement("div");
39
+ sub.className = "ctx-menu";
40
+ for (const item of items) {
41
+ if (item.separator) {
42
+ const sep = document.createElement("div");
43
+ sep.className = "ctx-sep";
44
+ sub.appendChild(sep);
45
+ continue;
46
+ }
47
+ const el = document.createElement("div");
48
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
49
+ el.textContent = item.label;
50
+ if (item.tooltip)
51
+ el.title = item.tooltip;
52
+ if (!item.disabled) {
53
+ el.addEventListener("click", () => {
54
+ closeContextMenu();
55
+ item.action();
56
+ });
57
+ }
58
+ sub.appendChild(el);
59
+ }
60
+ document.body.appendChild(sub);
61
+ const anchor = parentRow.getBoundingClientRect();
62
+ const vw = window.visualViewport?.width ?? window.innerWidth;
63
+ const vh = window.visualViewport?.height ?? window.innerHeight;
64
+ const rect = sub.getBoundingClientRect();
65
+ // -2px overlap keeps the pointer inside a menu while travelling from the
66
+ // parent row into the child; a gap would fire the outside-click dismiss.
67
+ let left = anchor.right - 2;
68
+ if (left + rect.width > vw)
69
+ left = anchor.left - rect.width + 2;
70
+ let top = anchor.top;
71
+ if (top + rect.height > vh)
72
+ top = vh - rect.height - 4;
73
+ sub.style.left = `${Math.max(4, left)}px`;
74
+ sub.style.top = `${Math.max(4, top)}px`;
75
+ activeSubmenu = sub;
76
+ }
23
77
  /** Show a context menu at the given position */
24
78
  export function showContextMenu(x, y, items) {
25
79
  closeContextMenu();
@@ -39,6 +93,24 @@ export function showContextMenu(x, y, items) {
39
93
  el.textContent = item.label;
40
94
  if (item.tooltip)
41
95
  el.title = item.tooltip;
96
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
97
+ // ▸ pushed to the right edge so parent rows read as parent rows.
98
+ el.style.display = "flex";
99
+ el.style.justifyContent = "space-between";
100
+ el.style.gap = "12px";
101
+ const caret = document.createElement("span");
102
+ caret.textContent = "▸";
103
+ caret.style.opacity = "0.6";
104
+ el.appendChild(caret);
105
+ const open = () => openSubmenu(el, item.submenu);
106
+ el.addEventListener("mouseenter", open);
107
+ el.addEventListener("click", (e) => { e.stopPropagation(); open(); });
108
+ menu.appendChild(el);
109
+ continue;
110
+ }
111
+ // Any non-parent row is hovered ⇒ whatever submenu was open belongs to
112
+ // a different row and should go.
113
+ el.addEventListener("mouseenter", closeSubmenu);
42
114
  if (!item.disabled) {
43
115
  el.addEventListener("click", () => {
44
116
  closeContextMenu();
@@ -78,6 +150,10 @@ export function showContextMenu(x, y, items) {
78
150
  // Deferred by one frame so the opening pointerdown doesn't immediately close it.
79
151
  requestAnimationFrame(() => {
80
152
  dismissListener = (e) => {
153
+ // A click in the SUBMENU is not a click outside — it lives in its
154
+ // own top-level element, so `activeMenu.contains` says otherwise.
155
+ if (activeSubmenu?.contains(e.target))
156
+ return;
81
157
  if (activeMenu && !activeMenu.contains(e.target)) {
82
158
  closeContextMenu();
83
159
  }
@@ -1 +1 @@
1
- {"version":3,"file":"context-menu.js","sourceRoot":"","sources":["context-menu.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-menu.js","sourceRoot":"","sources":["context-menu.ts"],"names":[],"mappings":"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"}
@@ -20,10 +20,24 @@ export interface MenuItem {
20
20
  * Trash, no undo" so the user can tell two near-identical entries
21
21
  * apart without trial-and-error. */
22
22
  tooltip?: string;
23
+ /** Nested items. Rendered with a ▸ affordance; opens on hover (and on
24
+ * click, so touch works). `action` is ignored when this is set — a
25
+ * parent row only opens its child menu. One level deep is deliberate:
26
+ * deeper nesting in a context menu is a usability tax, not a feature. */
27
+ submenu?: MenuItem[];
28
+ }
29
+
30
+ /** Open submenu element, if any. Tracked separately from `activeMenu` so the
31
+ * outside-click dismisser doesn't treat the child as "outside" the parent. */
32
+ let activeSubmenu: HTMLElement | null = null;
33
+
34
+ function closeSubmenu(): void {
35
+ if (activeSubmenu) { activeSubmenu.remove(); activeSubmenu = null; }
23
36
  }
24
37
 
25
38
  /** Close any open context menu and remove dismiss listeners */
26
39
  export function closeContextMenu(): void {
40
+ closeSubmenu();
27
41
  if (activeMenu) {
28
42
  activeMenu.remove();
29
43
  activeMenu = null;
@@ -38,6 +52,48 @@ export function closeContextMenu(): void {
38
52
  }
39
53
  }
40
54
 
55
+ /** Render `items` as a child menu anchored to the right of `parentRow`,
56
+ * flipping to its left when there isn't room. Shares the .ctx-menu styling
57
+ * so it can't drift from the parent visually. */
58
+ function openSubmenu(parentRow: HTMLElement, items: MenuItem[]): void {
59
+ closeSubmenu();
60
+ const sub = document.createElement("div");
61
+ sub.className = "ctx-menu";
62
+ for (const item of items) {
63
+ if (item.separator) {
64
+ const sep = document.createElement("div");
65
+ sep.className = "ctx-sep";
66
+ sub.appendChild(sep);
67
+ continue;
68
+ }
69
+ const el = document.createElement("div");
70
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
71
+ el.textContent = item.label;
72
+ if (item.tooltip) el.title = item.tooltip;
73
+ if (!item.disabled) {
74
+ el.addEventListener("click", () => {
75
+ closeContextMenu();
76
+ item.action();
77
+ });
78
+ }
79
+ sub.appendChild(el);
80
+ }
81
+ document.body.appendChild(sub);
82
+ const anchor = parentRow.getBoundingClientRect();
83
+ const vw = window.visualViewport?.width ?? window.innerWidth;
84
+ const vh = window.visualViewport?.height ?? window.innerHeight;
85
+ const rect = sub.getBoundingClientRect();
86
+ // -2px overlap keeps the pointer inside a menu while travelling from the
87
+ // parent row into the child; a gap would fire the outside-click dismiss.
88
+ let left = anchor.right - 2;
89
+ if (left + rect.width > vw) left = anchor.left - rect.width + 2;
90
+ let top = anchor.top;
91
+ if (top + rect.height > vh) top = vh - rect.height - 4;
92
+ sub.style.left = `${Math.max(4, left)}px`;
93
+ sub.style.top = `${Math.max(4, top)}px`;
94
+ activeSubmenu = sub;
95
+ }
96
+
41
97
  /** Show a context menu at the given position */
42
98
  export function showContextMenu(x: number, y: number, items: MenuItem[]): void {
43
99
  closeContextMenu();
@@ -57,6 +113,24 @@ export function showContextMenu(x: number, y: number, items: MenuItem[]): void {
57
113
  if (item.emphasized) el.style.fontWeight = "600";
58
114
  el.textContent = item.label;
59
115
  if (item.tooltip) el.title = item.tooltip;
116
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
117
+ // ▸ pushed to the right edge so parent rows read as parent rows.
118
+ el.style.display = "flex";
119
+ el.style.justifyContent = "space-between";
120
+ el.style.gap = "12px";
121
+ const caret = document.createElement("span");
122
+ caret.textContent = "▸";
123
+ caret.style.opacity = "0.6";
124
+ el.appendChild(caret);
125
+ const open = () => openSubmenu(el, item.submenu!);
126
+ el.addEventListener("mouseenter", open);
127
+ el.addEventListener("click", (e) => { e.stopPropagation(); open(); });
128
+ menu.appendChild(el);
129
+ continue;
130
+ }
131
+ // Any non-parent row is hovered ⇒ whatever submenu was open belongs to
132
+ // a different row and should go.
133
+ el.addEventListener("mouseenter", closeSubmenu);
60
134
  if (!item.disabled) {
61
135
  el.addEventListener("click", () => {
62
136
  closeContextMenu();
@@ -98,6 +172,9 @@ export function showContextMenu(x: number, y: number, items: MenuItem[]): void {
98
172
  // Deferred by one frame so the opening pointerdown doesn't immediately close it.
99
173
  requestAnimationFrame(() => {
100
174
  dismissListener = (e: Event) => {
175
+ // A click in the SUBMENU is not a click outside — it lives in its
176
+ // own top-level element, so `activeMenu.contains` says otherwise.
177
+ if (activeSubmenu?.contains(e.target as Node)) return;
101
178
  if (activeMenu && !activeMenu.contains(e.target as Node)) {
102
179
  closeContextMenu();
103
180
  }