@bobfrankston/rmfmail 1.2.221 → 1.2.222

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.
@@ -2178,6 +2178,77 @@ async function popoutHandoff(): Promise<void> {
2178
2178
  }
2179
2179
  window.addEventListener("compose-popout", () => { void popoutHandoff(); });
2180
2180
 
2181
+ // ── Native context-menu clipboard commands ──
2182
+ // Chromium drops Cut/Copy from its default context menu whenever the caret is
2183
+ // collapsed — and right-clicking a word IS a collapsed caret — so the menu you
2184
+ // get while editing has no Cut and no Copy (Bob 2026-08-07). These entries
2185
+ // expand to the word under the caret first, matching what the formatting
2186
+ // entries already do, and go through the async Clipboard API rather than the
2187
+ // deprecated document.execCommand (which needs a user gesture the native menu
2188
+ // callback doesn't carry).
2189
+ //
2190
+ // Clipboard READ is permission-gated in a way write is not, so paste can
2191
+ // legitimately be refused. Say so out loud rather than no-op'ing — a menu item
2192
+ // that silently does nothing is worse than one that isn't there.
2193
+ async function runClipboardCommand(id: "cut" | "copy" | "paste"): Promise<void> {
2194
+ const ne: any = (editor as any)?.nativeEditor;
2195
+ const isTiny = !!ne && typeof ne.execCommand === "function" && !!ne.selection;
2196
+ const label = id === "cut" ? "Cut" : id === "copy" ? "Copy" : "Paste";
2197
+ const shortcut = id === "cut" ? "Ctrl+X" : id === "copy" ? "Ctrl+C" : "Ctrl+V";
2198
+ try {
2199
+ if (id === "paste") {
2200
+ let html = "";
2201
+ let text = "";
2202
+ if (navigator.clipboard?.read) {
2203
+ for (const item of await navigator.clipboard.read()) {
2204
+ if (item.types.includes("text/html")) html = await (await item.getType("text/html")).text();
2205
+ if (item.types.includes("text/plain")) text = await (await item.getType("text/plain")).text();
2206
+ }
2207
+ } else {
2208
+ text = await navigator.clipboard.readText();
2209
+ }
2210
+ // Prefer HTML so formatting survives, exactly as Ctrl+V would.
2211
+ // Plain text has to be escaped and line-broken by hand — inserted
2212
+ // raw it would be parsed as markup.
2213
+ const content = html || (text
2214
+ ? text.replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c] as string)).replace(/\r?\n/g, "<br>")
2215
+ : "");
2216
+ if (!content) return;
2217
+ if (isTiny) ne.execCommand("mceInsertContent", false, content);
2218
+ else if (ne && typeof ne.getSelection === "function") {
2219
+ const sel = ne.getSelection(true);
2220
+ ne.clipboard?.dangerouslyPasteHTML?.(sel?.index ?? 0, content);
2221
+ } else throw new Error("no editor");
2222
+ return;
2223
+ }
2224
+ // Cut / Copy. Expand a collapsed caret to the word under it so the
2225
+ // command has something to act on.
2226
+ if (isTiny) {
2227
+ try { if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") ne.selection.expand({ type: "word" }); } catch { /* older TinyMCE */ }
2228
+ }
2229
+ const html = isTiny ? ne.selection.getContent({ format: "html" }) : "";
2230
+ const text = isTiny ? ne.selection.getContent({ format: "text" }) : (window.getSelection()?.toString() || "");
2231
+ if (!text && !html) return;
2232
+ if (html && typeof ClipboardItem === "function" && navigator.clipboard?.write) {
2233
+ await navigator.clipboard.write([new ClipboardItem({
2234
+ "text/html": new Blob([html], { type: "text/html" }),
2235
+ "text/plain": new Blob([text], { type: "text/plain" }),
2236
+ })]);
2237
+ } else {
2238
+ await navigator.clipboard.writeText(text);
2239
+ }
2240
+ if (id === "cut") {
2241
+ if (isTiny) ne.execCommand("Delete");
2242
+ else if (ne && typeof ne.deleteText === "function") {
2243
+ const sel = ne.getSelection();
2244
+ if (sel?.length) ne.deleteText(sel.index, sel.length);
2245
+ }
2246
+ }
2247
+ } catch (e: any) {
2248
+ showDraftStatus(`${label} failed: ${e?.message || e}. ${shortcut} still works.`, true);
2249
+ }
2250
+ }
2251
+
2181
2252
  // ── Native context-menu app commands ──
2182
2253
  // msger appends app items to the WEBVIEW's native right-click menu (the
2183
2254
  // native menu must stay — Chromium's spellcheck suggestions exist only
@@ -2193,6 +2264,7 @@ window.addEventListener("compose-popout", () => { void popoutHandoff(); });
2193
2264
  try { (editor as any)?.showSource?.(); } catch { /* non-TinyMCE engine */ }
2194
2265
  return;
2195
2266
  }
2267
+ if (id === "cut" || id === "copy" || id === "paste") { void runClipboardCommand(id); return; }
2196
2268
  const ne: any = (editor as any)?.nativeEditor;
2197
2269
  if (!ne) return;
2198
2270
  const isTiny = typeof ne.execCommand === "function" && ne.selection;
@@ -124,7 +124,13 @@ export async function createTinyMceEditor(container, opts = {}) {
124
124
  // 2026-06-01). The paste_* OPTIONS below remain valid (core).
125
125
  plugins: "lists advlist link table code codesample image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality help",
126
126
  toolbar: [
127
- "undo redo | bold italic underline strikethrough | forecolor backcolor",
127
+ // fontsize on the main bar (Bob 2026-08-07: "there should also
128
+ // be font sizing on the tool bar"). `fontsize` is the v6+ name
129
+ // for v5's `fontsizeselect`; it renders the size dropdown and
130
+ // reflects the size at the caret. The Format ▸ Font sizes
131
+ // submenu had it all along, which is two clicks deeper than a
132
+ // size control has any business being.
133
+ "undo redo | fontsize | bold italic underline strikethrough | forecolor backcolor",
128
134
  // blockquote on the main bar (was quickbar-only) — Bob
129
135
  // 2026-07-28: pasted/inserted text needs a one-click quote.
130
136
  "blockquote bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.221",
3
+ "version": "1.2.222",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -43,7 +43,7 @@
43
43
  "@bobfrankston/msger": "^0.1.422",
44
44
  "@bobfrankston/node-tcp-transport": "^0.1.10",
45
45
  "@bobfrankston/oauthsupport": "^1.0.34",
46
- "@bobfrankston/rmf-tiny": "^0.1.43",
46
+ "@bobfrankston/rmf-tiny": "^0.1.44",
47
47
  "@bobfrankston/smtp-direct": "^0.1.9",
48
48
  "@bobfrankston/tcp-transport": "^0.1.8",
49
49
  "@capacitor/android": "^8.3.0",
@@ -123,7 +123,7 @@
123
123
  "@bobfrankston/msger": "^0.1.422",
124
124
  "@bobfrankston/node-tcp-transport": "^0.1.10",
125
125
  "@bobfrankston/oauthsupport": "^1.0.34",
126
- "@bobfrankston/rmf-tiny": "^0.1.43",
126
+ "@bobfrankston/rmf-tiny": "^0.1.44",
127
127
  "@bobfrankston/smtp-direct": "^0.1.9",
128
128
  "@bobfrankston/tcp-transport": "^0.1.8",
129
129
  "@capacitor/android": "^8.3.0",