@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.
@@ -1983,6 +1983,188 @@ var init_spellcheck_core = __esm({
1983
1983
  }
1984
1984
  });
1985
1985
 
1986
+ // client/compose/edit-commands.js
1987
+ var edit_commands_exports = {};
1988
+ __export(edit_commands_exports, {
1989
+ runClipboardCommand: () => runClipboardCommand,
1990
+ runFormatCommand: () => runFormatCommand,
1991
+ standardEditItems: () => standardEditItems
1992
+ });
1993
+ function isTinyEditor(ne) {
1994
+ return !!ne && typeof ne.execCommand === "function" && !!ne.selection;
1995
+ }
1996
+ function expandToWord(ne) {
1997
+ if (isTinyEditor(ne)) {
1998
+ try {
1999
+ if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") {
2000
+ ne.selection.expand({ type: "word" });
2001
+ }
2002
+ } catch {
2003
+ }
2004
+ return;
2005
+ }
2006
+ if (ne && typeof ne.getSelection === "function" && typeof ne.setSelection === "function") {
2007
+ const sel = ne.getSelection();
2008
+ if (!sel || sel.length > 0)
2009
+ return;
2010
+ const text = ne.getText();
2011
+ let a = sel.index, b = sel.index;
2012
+ while (a > 0 && /\S/.test(text[a - 1]))
2013
+ a--;
2014
+ while (b < text.length && /\S/.test(text[b]))
2015
+ b++;
2016
+ if (b > a)
2017
+ ne.setSelection(a, b - a);
2018
+ }
2019
+ }
2020
+ function escapeHtml(s) {
2021
+ return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
2022
+ }
2023
+ async function runClipboardCommand(ne, id) {
2024
+ const isTiny = isTinyEditor(ne);
2025
+ if (id === "paste") {
2026
+ let html2 = "";
2027
+ let text2 = "";
2028
+ if (navigator.clipboard?.read) {
2029
+ for (const item of await navigator.clipboard.read()) {
2030
+ if (item.types.includes("text/html"))
2031
+ html2 = await (await item.getType("text/html")).text();
2032
+ if (item.types.includes("text/plain"))
2033
+ text2 = await (await item.getType("text/plain")).text();
2034
+ }
2035
+ } else {
2036
+ text2 = await navigator.clipboard.readText();
2037
+ }
2038
+ const content = html2 || (text2 ? escapeHtml(text2).replace(/\r?\n/g, "<br>") : "");
2039
+ if (!content)
2040
+ return;
2041
+ if (isTiny) {
2042
+ ne.execCommand("mceInsertContent", false, content);
2043
+ return;
2044
+ }
2045
+ if (ne?.clipboard?.dangerouslyPasteHTML) {
2046
+ const sel = ne.getSelection(true);
2047
+ ne.clipboard.dangerouslyPasteHTML(sel?.index ?? 0, content);
2048
+ return;
2049
+ }
2050
+ throw new Error("editor doesn't support paste");
2051
+ }
2052
+ expandToWord(ne);
2053
+ let html = "";
2054
+ let text = "";
2055
+ if (isTiny) {
2056
+ html = ne.selection.getContent({ format: "html" });
2057
+ text = ne.selection.getContent({ format: "text" });
2058
+ } else {
2059
+ text = window.getSelection()?.toString() || "";
2060
+ }
2061
+ if (!text && !html)
2062
+ return;
2063
+ if (html && typeof ClipboardItem === "function" && navigator.clipboard?.write) {
2064
+ await navigator.clipboard.write([new ClipboardItem({
2065
+ "text/html": new Blob([html], { type: "text/html" }),
2066
+ "text/plain": new Blob([text], { type: "text/plain" })
2067
+ })]);
2068
+ } else {
2069
+ await navigator.clipboard.writeText(text);
2070
+ }
2071
+ if (id !== "cut")
2072
+ return;
2073
+ if (isTiny) {
2074
+ ne.execCommand("Delete");
2075
+ return;
2076
+ }
2077
+ if (typeof ne?.deleteText === "function") {
2078
+ const sel = ne.getSelection();
2079
+ if (sel?.length)
2080
+ ne.deleteText(sel.index, sel.length);
2081
+ }
2082
+ }
2083
+ function runFormatCommand(ne, id) {
2084
+ if (!ne)
2085
+ return;
2086
+ expandToWord(ne);
2087
+ if (isTinyEditor(ne)) {
2088
+ const cmd = {
2089
+ bold: "Bold",
2090
+ italic: "Italic",
2091
+ underline: "Underline",
2092
+ strike: "Strikethrough",
2093
+ link: "mceLink",
2094
+ clear: "RemoveFormat"
2095
+ };
2096
+ if (cmd[id])
2097
+ ne.execCommand(cmd[id]);
2098
+ return;
2099
+ }
2100
+ if (typeof ne.format !== "function" || typeof ne.getSelection !== "function")
2101
+ return;
2102
+ const sel = ne.getSelection();
2103
+ if (!sel || sel.length === 0)
2104
+ return;
2105
+ const cur = ne.getFormat(sel);
2106
+ switch (id) {
2107
+ case "bold":
2108
+ ne.format("bold", !cur.bold);
2109
+ break;
2110
+ case "italic":
2111
+ ne.format("italic", !cur.italic);
2112
+ break;
2113
+ case "underline":
2114
+ ne.format("underline", !cur.underline);
2115
+ break;
2116
+ case "strike":
2117
+ ne.format("strike", !cur.strike);
2118
+ break;
2119
+ case "clear":
2120
+ ne.removeFormat(sel.index, sel.length);
2121
+ break;
2122
+ // Quill's link dialog lives in its keyboard binding — Ctrl+K.
2123
+ case "link":
2124
+ try {
2125
+ ne.root.dispatchEvent(new KeyboardEvent("keydown", { key: "K", ctrlKey: true, bubbles: true, cancelable: true }));
2126
+ } catch {
2127
+ }
2128
+ break;
2129
+ }
2130
+ }
2131
+ function standardEditItems(getEditor, opts = {}) {
2132
+ const report = opts.onError || ((m) => console.warn(`[edit-commands] ${m}`));
2133
+ const clip = (id) => () => {
2134
+ void runClipboardCommand(getEditor(), id).catch((e) => {
2135
+ const key = id === "cut" ? "Ctrl+X" : id === "copy" ? "Ctrl+C" : "Ctrl+V";
2136
+ report(`${id[0].toUpperCase()}${id.slice(1)} failed: ${e?.message || e}. ${key} still works.`);
2137
+ });
2138
+ };
2139
+ const fmt = (id) => () => runFormatCommand(getEditor(), id);
2140
+ const items = [
2141
+ { label: "", action: () => {
2142
+ }, separator: true },
2143
+ { label: "Cut", action: clip("cut") },
2144
+ { label: "Copy", action: clip("copy") },
2145
+ { label: "Paste", action: clip("paste") },
2146
+ { label: "", action: () => {
2147
+ }, separator: true },
2148
+ { label: "Bold", action: fmt("bold") },
2149
+ { label: "Italic", action: fmt("italic") },
2150
+ { label: "Underline", action: fmt("underline") },
2151
+ { label: "Strikethrough", action: fmt("strike") },
2152
+ { label: "Link\u2026", action: fmt("link") },
2153
+ { label: "Clear formatting", action: fmt("clear") }
2154
+ ];
2155
+ if (opts.showSource) {
2156
+ items.push({ label: "", action: () => {
2157
+ }, separator: true });
2158
+ items.push({ label: "View HTML source\u2026", action: opts.showSource });
2159
+ }
2160
+ return items;
2161
+ }
2162
+ var init_edit_commands = __esm({
2163
+ "client/compose/edit-commands.js"() {
2164
+ "use strict";
2165
+ }
2166
+ });
2167
+
1986
2168
  // client/lib/rmf-tiny.js
1987
2169
  var rmf_tiny_exports = {};
1988
2170
  __export(rmf_tiny_exports, {
@@ -2080,7 +2262,13 @@ async function createTinyMceEditor(container2, opts = {}) {
2080
2262
  // 2026-06-01). The paste_* OPTIONS below remain valid (core).
2081
2263
  plugins: "lists advlist link table code codesample image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality help",
2082
2264
  toolbar: [
2083
- "undo redo | bold italic underline strikethrough | forecolor backcolor",
2265
+ // fontsize on the main bar (Bob 2026-08-07: "there should also
2266
+ // be font sizing on the tool bar"). `fontsize` is the v6+ name
2267
+ // for v5's `fontsizeselect`; it renders the size dropdown and
2268
+ // reflects the size at the caret. The Format ▸ Font sizes
2269
+ // submenu had it all along, which is two clicks deeper than a
2270
+ // size control has any business being.
2271
+ "undo redo | fontsize | bold italic underline strikethrough | forecolor backcolor",
2084
2272
  // blockquote on the main bar (was quickbar-only) — Bob
2085
2273
  // 2026-07-28: pasted/inserted text needs a one-click quote.
2086
2274
  "blockquote bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help"
@@ -2931,6 +3119,21 @@ function wireSpellcheck(editor2) {
2931
3119
  scheduleScan();
2932
3120
  }
2933
3121
  });
3122
+ items.push(...standardEditItems(() => editor2, {
3123
+ onError: (msg) => {
3124
+ try {
3125
+ editor2.notificationManager?.open({ text: msg, type: "error", timeout: 6e3 });
3126
+ } catch {
3127
+ console.warn(msg);
3128
+ }
3129
+ },
3130
+ showSource: () => {
3131
+ try {
3132
+ editor2.execCommand("mceCodeEditor");
3133
+ } catch {
3134
+ }
3135
+ }
3136
+ }));
2934
3137
  const iframeEl = editor2.iframeElement;
2935
3138
  const rect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
2936
3139
  showSuggestionsMenu(document, rect.left + e.clientX, rect.top + e.clientY, items, [iframeDoc]);
@@ -2941,6 +3144,7 @@ var init_spellcheck = __esm({
2941
3144
  "client/compose/spellcheck.js"() {
2942
3145
  "use strict";
2943
3146
  init_spellcheck_core();
3147
+ init_edit_commands();
2944
3148
  SCAN_DEBOUNCE_MS = 600;
2945
3149
  WAVE = `url("data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3"><path d="M0 2 Q1.5 0 3 2 T6 2" stroke="#d33" fill="none" stroke-width="1"/></svg>')}")`;
2946
3150
  OVERLAY_ID = "mailx-spell-overlay";
@@ -3935,6 +4139,8 @@ function createQuillEditor(container2) {
3935
4139
  label: "Ignore (this session)",
3936
4140
  action: () => _spellSp.add(word)
3937
4141
  });
4142
+ const { standardEditItems: standardEditItems2 } = await Promise.resolve().then(() => (init_edit_commands(), edit_commands_exports));
4143
+ items.push(...standardEditItems2(() => q));
3938
4144
  core.showSuggestionsMenu(document, e.clientX, e.clientY, items);
3939
4145
  } catch (err) {
3940
4146
  console.warn("[spellcheck] right-click handler error:", err?.message || err);
@@ -4411,12 +4617,21 @@ async function createTinyMceEditor2(container2, opts = {}) {
4411
4617
 
4412
4618
  // client/compose/compose.ts
4413
4619
  init_api_client();
4620
+ init_edit_commands();
4414
4621
 
4415
4622
  // client/components/context-menu.js
4416
4623
  var activeMenu = null;
4417
4624
  var dismissListener = null;
4418
4625
  var escapeListener = null;
4626
+ var activeSubmenu = null;
4627
+ function closeSubmenu() {
4628
+ if (activeSubmenu) {
4629
+ activeSubmenu.remove();
4630
+ activeSubmenu = null;
4631
+ }
4632
+ }
4419
4633
  function closeContextMenu() {
4634
+ closeSubmenu();
4420
4635
  if (activeMenu) {
4421
4636
  activeMenu.remove();
4422
4637
  activeMenu = null;
@@ -4430,6 +4645,45 @@ function closeContextMenu() {
4430
4645
  escapeListener = null;
4431
4646
  }
4432
4647
  }
4648
+ function openSubmenu(parentRow, items) {
4649
+ closeSubmenu();
4650
+ const sub = document.createElement("div");
4651
+ sub.className = "ctx-menu";
4652
+ for (const item of items) {
4653
+ if (item.separator) {
4654
+ const sep = document.createElement("div");
4655
+ sep.className = "ctx-sep";
4656
+ sub.appendChild(sep);
4657
+ continue;
4658
+ }
4659
+ const el = document.createElement("div");
4660
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
4661
+ el.textContent = item.label;
4662
+ if (item.tooltip)
4663
+ el.title = item.tooltip;
4664
+ if (!item.disabled) {
4665
+ el.addEventListener("click", () => {
4666
+ closeContextMenu();
4667
+ item.action();
4668
+ });
4669
+ }
4670
+ sub.appendChild(el);
4671
+ }
4672
+ document.body.appendChild(sub);
4673
+ const anchor = parentRow.getBoundingClientRect();
4674
+ const vw = window.visualViewport?.width ?? window.innerWidth;
4675
+ const vh = window.visualViewport?.height ?? window.innerHeight;
4676
+ const rect = sub.getBoundingClientRect();
4677
+ let left = anchor.right - 2;
4678
+ if (left + rect.width > vw)
4679
+ left = anchor.left - rect.width + 2;
4680
+ let top = anchor.top;
4681
+ if (top + rect.height > vh)
4682
+ top = vh - rect.height - 4;
4683
+ sub.style.left = `${Math.max(4, left)}px`;
4684
+ sub.style.top = `${Math.max(4, top)}px`;
4685
+ activeSubmenu = sub;
4686
+ }
4433
4687
  function showContextMenu(x, y, items) {
4434
4688
  closeContextMenu();
4435
4689
  const menu = document.createElement("div");
@@ -4448,6 +4702,24 @@ function showContextMenu(x, y, items) {
4448
4702
  el.textContent = item.label;
4449
4703
  if (item.tooltip)
4450
4704
  el.title = item.tooltip;
4705
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
4706
+ el.style.display = "flex";
4707
+ el.style.justifyContent = "space-between";
4708
+ el.style.gap = "12px";
4709
+ const caret = document.createElement("span");
4710
+ caret.textContent = "\u25B8";
4711
+ caret.style.opacity = "0.6";
4712
+ el.appendChild(caret);
4713
+ const open = () => openSubmenu(el, item.submenu);
4714
+ el.addEventListener("mouseenter", open);
4715
+ el.addEventListener("click", (e) => {
4716
+ e.stopPropagation();
4717
+ open();
4718
+ });
4719
+ menu.appendChild(el);
4720
+ continue;
4721
+ }
4722
+ el.addEventListener("mouseenter", closeSubmenu);
4451
4723
  if (!item.disabled) {
4452
4724
  el.addEventListener("click", () => {
4453
4725
  closeContextMenu();
@@ -4479,6 +4751,8 @@ function showContextMenu(x, y, items) {
4479
4751
  activeMenu = menu;
4480
4752
  requestAnimationFrame(() => {
4481
4753
  dismissListener = (e) => {
4754
+ if (activeSubmenu?.contains(e.target))
4755
+ return;
4482
4756
  if (activeMenu && !activeMenu.contains(e.target)) {
4483
4757
  closeContextMenu();
4484
4758
  }
@@ -5407,7 +5681,7 @@ function applyInit(init) {
5407
5681
  if (init.mode !== "draft" && !init.draftUid) {
5408
5682
  let sigHtml = "";
5409
5683
  if (acct?.sig?.text) {
5410
- sigHtml = acct.sig.html ? acct.sig.text : escapeHtml(acct.sig.text).replace(/\n/g, "<br>");
5684
+ sigHtml = acct.sig.html ? acct.sig.text : escapeHtml2(acct.sig.text).replace(/\n/g, "<br>");
5411
5685
  } else if (acct?.signature) {
5412
5686
  sigHtml = acct.signature;
5413
5687
  }
@@ -6102,57 +6376,7 @@ window.__msgerContextCommand = (id) => {
6102
6376
  }
6103
6377
  return;
6104
6378
  }
6105
- const ne = editor?.nativeEditor;
6106
- if (!ne) return;
6107
- const isTiny = typeof ne.execCommand === "function" && ne.selection;
6108
- if (isTiny) {
6109
- try {
6110
- if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") ne.selection.expand({ type: "word" });
6111
- } catch {
6112
- }
6113
- const cmd = { bold: "Bold", italic: "Italic", underline: "Underline", strike: "Strikethrough", link: "mceLink", clear: "RemoveFormat" };
6114
- if (cmd[id]) ne.execCommand(cmd[id]);
6115
- return;
6116
- }
6117
- if (typeof ne.format === "function" && typeof ne.getSelection === "function") {
6118
- let sel = ne.getSelection();
6119
- if (sel && sel.length === 0) {
6120
- const text = ne.getText();
6121
- let a = sel.index, b = sel.index;
6122
- while (a > 0 && /S/.test(text[a - 1])) a--;
6123
- while (b < text.length && /S/.test(text[b])) b++;
6124
- if (b > a) {
6125
- ne.setSelection(a, b - a);
6126
- sel = { index: a, length: b - a };
6127
- }
6128
- }
6129
- if (!sel || sel.length === 0) return;
6130
- const cur = ne.getFormat(sel);
6131
- switch (id) {
6132
- case "bold":
6133
- ne.format("bold", !cur.bold);
6134
- break;
6135
- case "italic":
6136
- ne.format("italic", !cur.italic);
6137
- break;
6138
- case "underline":
6139
- ne.format("underline", !cur.underline);
6140
- break;
6141
- case "strike":
6142
- ne.format("strike", !cur.strike);
6143
- break;
6144
- case "clear":
6145
- ne.removeFormat(sel.index, sel.length);
6146
- break;
6147
- // link: Quill's dialog lives in its keyboard binding — Ctrl+K.
6148
- case "link":
6149
- try {
6150
- ne.root.dispatchEvent(new KeyboardEvent("keydown", { key: "K", ctrlKey: true, bubbles: true, cancelable: true }));
6151
- } catch {
6152
- }
6153
- break;
6154
- }
6155
- }
6379
+ runFormatCommand(editor?.nativeEditor, id);
6156
6380
  };
6157
6381
  var ccRow = document.getElementById("compose-cc-row");
6158
6382
  var bccRow = document.getElementById("compose-bcc-row");
@@ -6185,7 +6409,7 @@ function renderAttachmentChips() {
6185
6409
  const a = attachments[i];
6186
6410
  const chip = document.createElement("span");
6187
6411
  chip.className = "compose-att-chip";
6188
- chip.innerHTML = `\u{1F4CE} ${escapeHtml(a.filename)} (${formatSize(a.size)}) `;
6412
+ chip.innerHTML = `\u{1F4CE} ${escapeHtml2(a.filename)} (${formatSize(a.size)}) `;
6189
6413
  const rm = document.createElement("button");
6190
6414
  rm.type = "button";
6191
6415
  rm.title = "Remove attachment";
@@ -6198,7 +6422,7 @@ function renderAttachmentChips() {
6198
6422
  attEl.appendChild(chip);
6199
6423
  }
6200
6424
  }
6201
- function escapeHtml(s) {
6425
+ function escapeHtml2(s) {
6202
6426
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6203
6427
  }
6204
6428
  function formatSize(n) {
@@ -6340,9 +6564,9 @@ function showExternalEditHint(editorLabel) {
6340
6564
  const panel = document.createElement("div");
6341
6565
  panel.style.cssText = "background:var(--color-bg, #fff);color:var(--color-text, #000);border:1px solid var(--color-border, #ccc);border-radius:8px;padding:18px 22px;max-width:480px;box-shadow:0 8px 32px rgba(0,0,0,0.4);font:14px/1.5 system-ui;";
6342
6566
  panel.innerHTML = `
6343
- <div style="font-weight:600;font-size:16px;margin-bottom:10px;">Editing in ${escapeHtml(editorLabel)}</div>
6567
+ <div style="font-weight:600;font-size:16px;margin-bottom:10px;">Editing in ${escapeHtml2(editorLabel)}</div>
6344
6568
  <ol style="margin:0 0 12px 18px;padding:0;">
6345
- <li>Edit your message in <b>${escapeHtml(editorLabel)}</b>.</li>
6569
+ <li>Edit your message in <b>${escapeHtml2(editorLabel)}</b>.</li>
6346
6570
  <li>Save (<b>Ctrl+S</b>). Choose <b>"Web Page, Filtered (.htm)"</b> if asked for a format \u2014 keep the same filename.</li>
6347
6571
  <li>Switch back to this window (<b>Alt+Tab</b>). The body will reload here.</li>
6348
6572
  <li>Click <b>Send</b> in this window when ready.</li>