@bobfrankston/rmfmail 1.2.222 → 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/bin/mailx.js +6 -9
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +6 -9
- package/client/app.bundle.js +68 -1
- package/client/app.bundle.js.map +2 -2
- package/client/components/context-menu.js +76 -0
- package/client/components/context-menu.js.map +1 -1
- package/client/components/context-menu.ts +77 -0
- package/client/compose/compose.bundle.js +274 -114
- package/client/compose/compose.bundle.js.map +4 -4
- package/client/compose/compose.js +4 -149
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +4 -104
- package/client/compose/edit-commands.js +207 -0
- package/client/compose/edit-commands.js.map +1 -0
- package/client/compose/edit-commands.ts +181 -0
- package/client/compose/editor.js +6 -0
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +6 -0
- package/client/compose/spellcheck.js +19 -0
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +11 -0
- package/package.json +3 -3
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-104584 → node_modules.npmglobalize-stash-65000}/.package-lock.json +0 -0
|
@@ -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":"AAAA;;;GAGG;AAEH,IAAI,UAAU,GAAuB,IAAI,CAAC;AAC1C,IAAI,eAAe,GAAgC,IAAI,CAAC;AACxD,IAAI,cAAc,GAAwC,IAAI,CAAC;
|
|
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
|
}
|