@bobfrankston/rmfmail 1.2.229 → 1.2.230

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,18 +1983,74 @@ 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) {
1986
+ // client/components/edit-menu.js
1987
+ function isTinyEngine(ne) {
1994
1988
  return !!ne && typeof ne.execCommand === "function" && !!ne.selection;
1995
1989
  }
1990
+ function describeEditTarget(target) {
1991
+ const none = { el: null, editable: false, selection: "", canRead: true, isField: false };
1992
+ const node = target;
1993
+ if (!node)
1994
+ return { ...none, selection: pageSelection() };
1995
+ const start = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1996
+ if (!start)
1997
+ return { ...none, selection: pageSelection() };
1998
+ const field = start.closest("input, textarea");
1999
+ if (field) {
2000
+ const type = (field instanceof HTMLInputElement ? field.type : "text").toLowerCase();
2001
+ if (!TEXTUAL_INPUT_TYPES.has(type))
2002
+ return { ...none, selection: pageSelection() };
2003
+ const s = field.selectionStart ?? 0, e = field.selectionEnd ?? 0;
2004
+ return {
2005
+ el: field,
2006
+ editable: !field.readOnly && !field.disabled,
2007
+ selection: e > s ? field.value.slice(s, e) : "",
2008
+ canRead: type !== "password",
2009
+ isField: true
2010
+ };
2011
+ }
2012
+ const ce = start.closest("[contenteditable]");
2013
+ const editable = !!ce && ce.isContentEditable;
2014
+ return { el: ce || start, editable, selection: pageSelection(), canRead: true, isField: false };
2015
+ }
2016
+ function pageSelection() {
2017
+ try {
2018
+ return window.getSelection()?.toString() || "";
2019
+ } catch {
2020
+ return "";
2021
+ }
2022
+ }
2023
+ function escapeHtml(s) {
2024
+ return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
2025
+ }
2026
+ async function readClipboard() {
2027
+ let html = "", text = "";
2028
+ if (navigator.clipboard?.read) {
2029
+ for (const item of await navigator.clipboard.read()) {
2030
+ if (item.types.includes("text/html"))
2031
+ html = await (await item.getType("text/html")).text();
2032
+ if (item.types.includes("text/plain"))
2033
+ text = await (await item.getType("text/plain")).text();
2034
+ }
2035
+ if (!html && !text)
2036
+ text = await navigator.clipboard.readText();
2037
+ } else {
2038
+ text = await navigator.clipboard.readText();
2039
+ }
2040
+ return { html, text };
2041
+ }
2042
+ async function writeClipboard(text, html) {
2043
+ if (html && typeof ClipboardItem === "function" && navigator.clipboard?.write) {
2044
+ await navigator.clipboard.write([new ClipboardItem({
2045
+ "text/html": new Blob([html], { type: "text/html" }),
2046
+ "text/plain": new Blob([text], { type: "text/plain" })
2047
+ })]);
2048
+ return;
2049
+ }
2050
+ await navigator.clipboard.writeText(text);
2051
+ }
1996
2052
  function expandToWord(ne) {
1997
- if (isTinyEditor(ne)) {
2053
+ if (isTinyEngine(ne)) {
1998
2054
  try {
1999
2055
  if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") {
2000
2056
  ne.selection.expand({ type: "word" });
@@ -2017,24 +2073,59 @@ function expandToWord(ne) {
2017
2073
  ne.setSelection(a, b - a);
2018
2074
  }
2019
2075
  }
2020
- function escapeHtml(s) {
2021
- return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
2076
+ function replaceFieldSelection(field, text) {
2077
+ const s = field.selectionStart ?? field.value.length;
2078
+ const e = field.selectionEnd ?? field.value.length;
2079
+ field.value = field.value.slice(0, s) + text + field.value.slice(e);
2080
+ field.selectionStart = field.selectionEnd = s + text.length;
2081
+ field.dispatchEvent(new Event("input", { bubbles: true }));
2022
2082
  }
2023
- async function runClipboardCommand(ne, id) {
2024
- const isTiny = isTinyEditor(ne);
2083
+ async function runClipboard(ctx, id) {
2084
+ if (ctx.engine)
2085
+ return runEngineClipboard(ctx.engine, id);
2086
+ const info = describeEditTarget(ctx.target);
2025
2087
  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();
2088
+ if (!info.editable || !info.el)
2089
+ throw new Error("nothing here accepts a paste");
2090
+ const { html, text } = await readClipboard();
2091
+ if (info.isField) {
2092
+ if (!text && !html)
2093
+ return;
2094
+ replaceFieldSelection(info.el, text || stripHtml(html));
2095
+ return;
2037
2096
  }
2097
+ const content = html || (text ? escapeHtml(text).replace(/\r?\n/g, "<br>") : "");
2098
+ if (!content)
2099
+ return;
2100
+ insertHtmlAtSelection(info.el, content);
2101
+ return;
2102
+ }
2103
+ if (!info.canRead)
2104
+ throw new Error("this field's contents can't be copied");
2105
+ if (!info.selection)
2106
+ return;
2107
+ if (info.isField) {
2108
+ await writeClipboard(info.selection);
2109
+ } else {
2110
+ await writeClipboard(info.selection, selectionHtml());
2111
+ }
2112
+ if (id !== "cut")
2113
+ return;
2114
+ if (!info.editable)
2115
+ throw new Error("this text is read-only \u2014 copied instead of cut");
2116
+ if (info.isField) {
2117
+ replaceFieldSelection(info.el, "");
2118
+ return;
2119
+ }
2120
+ try {
2121
+ window.getSelection()?.deleteFromDocument();
2122
+ } catch {
2123
+ }
2124
+ }
2125
+ async function runEngineClipboard(ne, id) {
2126
+ const isTiny = isTinyEngine(ne);
2127
+ if (id === "paste") {
2128
+ const { html: html2, text: text2 } = await readClipboard();
2038
2129
  const content = html2 || (text2 ? escapeHtml(text2).replace(/\r?\n/g, "<br>") : "");
2039
2130
  if (!content)
2040
2131
  return;
@@ -2050,24 +2141,17 @@ async function runClipboardCommand(ne, id) {
2050
2141
  throw new Error("editor doesn't support paste");
2051
2142
  }
2052
2143
  expandToWord(ne);
2053
- let html = "";
2054
- let text = "";
2144
+ let html = "", text = "";
2055
2145
  if (isTiny) {
2056
2146
  html = ne.selection.getContent({ format: "html" });
2057
2147
  text = ne.selection.getContent({ format: "text" });
2058
2148
  } else {
2059
- text = window.getSelection()?.toString() || "";
2149
+ text = pageSelection();
2150
+ html = selectionHtml();
2060
2151
  }
2061
2152
  if (!text && !html)
2062
2153
  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
- }
2154
+ await writeClipboard(text, html || void 0);
2071
2155
  if (id !== "cut")
2072
2156
  return;
2073
2157
  if (isTiny) {
@@ -2080,10 +2164,136 @@ async function runClipboardCommand(ne, id) {
2080
2164
  ne.deleteText(sel.index, sel.length);
2081
2165
  }
2082
2166
  }
2167
+ function selectionHtml() {
2168
+ try {
2169
+ const sel = window.getSelection();
2170
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed)
2171
+ return "";
2172
+ const div = document.createElement("div");
2173
+ div.appendChild(sel.getRangeAt(0).cloneContents());
2174
+ return div.innerHTML;
2175
+ } catch {
2176
+ return "";
2177
+ }
2178
+ }
2179
+ function stripHtml(html) {
2180
+ const div = document.createElement("div");
2181
+ div.innerHTML = html;
2182
+ return div.textContent || "";
2183
+ }
2184
+ function insertHtmlAtSelection(host, html) {
2185
+ const sel = window.getSelection();
2186
+ const frag = document.createRange().createContextualFragment(html);
2187
+ if (!sel || sel.rangeCount === 0) {
2188
+ host?.appendChild(frag);
2189
+ return;
2190
+ }
2191
+ const range = sel.getRangeAt(0);
2192
+ range.deleteContents();
2193
+ const last = frag.lastChild;
2194
+ range.insertNode(frag);
2195
+ if (last) {
2196
+ range.setStartAfter(last);
2197
+ range.collapse(true);
2198
+ sel.removeAllRanges();
2199
+ sel.addRange(range);
2200
+ }
2201
+ }
2202
+ function selectAll(info) {
2203
+ if (info.isField && info.el) {
2204
+ info.el.select();
2205
+ return;
2206
+ }
2207
+ const host = info.el;
2208
+ if (!host)
2209
+ return;
2210
+ const range = document.createRange();
2211
+ range.selectNodeContents(host);
2212
+ const sel = window.getSelection();
2213
+ sel?.removeAllRanges();
2214
+ sel?.addRange(range);
2215
+ }
2216
+ function editMenuItems(ctx, opts = {}) {
2217
+ const report = opts.onError || ((m) => console.warn(`[edit-menu] ${m}`));
2218
+ const info = ctx.engine ? { el: null, editable: true, selection: pageSelection(), canRead: true, isField: false } : describeEditTarget(ctx.target);
2219
+ if (!ctx.engine && !info.editable && !info.selection)
2220
+ return [];
2221
+ const run = (id) => () => {
2222
+ void runClipboard(ctx, id).catch((e) => {
2223
+ const key = id === "cut" ? "Ctrl+X" : id === "copy" ? "Ctrl+C" : "Ctrl+V";
2224
+ const what = id[0].toUpperCase() + id.slice(1);
2225
+ report(`${what} failed: ${e?.message || e}. ${key} still works.`);
2226
+ });
2227
+ };
2228
+ const hasSel = !!ctx.engine || !!info.selection;
2229
+ const items = [
2230
+ { label: "Cut", action: run("cut"), disabled: !info.editable || !hasSel || !info.canRead },
2231
+ { label: "Copy", action: run("copy"), disabled: !hasSel || !info.canRead },
2232
+ { label: "Paste", action: run("paste"), disabled: !info.editable }
2233
+ ];
2234
+ if (opts.selectAll !== false && !ctx.engine) {
2235
+ items.push({ label: "Select all", action: () => selectAll(info) });
2236
+ }
2237
+ return items;
2238
+ }
2239
+ var TEXTUAL_INPUT_TYPES;
2240
+ var init_edit_menu = __esm({
2241
+ "client/components/edit-menu.js"() {
2242
+ "use strict";
2243
+ TEXTUAL_INPUT_TYPES = /* @__PURE__ */ new Set([
2244
+ "text",
2245
+ "search",
2246
+ "email",
2247
+ "url",
2248
+ "tel",
2249
+ "number",
2250
+ "password",
2251
+ ""
2252
+ ]);
2253
+ }
2254
+ });
2255
+
2256
+ // client/compose/edit-commands.js
2257
+ var edit_commands_exports = {};
2258
+ __export(edit_commands_exports, {
2259
+ runClipboardCommand: () => runClipboardCommand,
2260
+ runFormatCommand: () => runFormatCommand,
2261
+ standardEditItems: () => standardEditItems
2262
+ });
2263
+ function runClipboardCommand(ne, id) {
2264
+ return runClipboard({ engine: ne }, id);
2265
+ }
2266
+ function isTinyEditor(ne) {
2267
+ return !!ne && typeof ne.execCommand === "function" && !!ne.selection;
2268
+ }
2269
+ function expandToWord2(ne) {
2270
+ if (isTinyEditor(ne)) {
2271
+ try {
2272
+ if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") {
2273
+ ne.selection.expand({ type: "word" });
2274
+ }
2275
+ } catch {
2276
+ }
2277
+ return;
2278
+ }
2279
+ if (ne && typeof ne.getSelection === "function" && typeof ne.setSelection === "function") {
2280
+ const sel = ne.getSelection();
2281
+ if (!sel || sel.length > 0)
2282
+ return;
2283
+ const text = ne.getText();
2284
+ let a = sel.index, b = sel.index;
2285
+ while (a > 0 && /\S/.test(text[a - 1]))
2286
+ a--;
2287
+ while (b < text.length && /\S/.test(text[b]))
2288
+ b++;
2289
+ if (b > a)
2290
+ ne.setSelection(a, b - a);
2291
+ }
2292
+ }
2083
2293
  function runFormatCommand(ne, id) {
2084
2294
  if (!ne)
2085
2295
  return;
2086
- expandToWord(ne);
2296
+ expandToWord2(ne);
2087
2297
  if (isTinyEditor(ne)) {
2088
2298
  const cmd = {
2089
2299
  bold: "Bold",
@@ -2129,20 +2339,11 @@ function runFormatCommand(ne, id) {
2129
2339
  }
2130
2340
  }
2131
2341
  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
2342
  const fmt = (id) => () => runFormatCommand(getEditor(), id);
2140
2343
  const items = [
2141
2344
  { label: "", action: () => {
2142
2345
  }, separator: true },
2143
- { label: "Cut", action: clip("cut") },
2144
- { label: "Copy", action: clip("copy") },
2145
- { label: "Paste", action: clip("paste") },
2346
+ ...editMenuItems({ engine: getEditor() }, { onError: opts.onError }),
2146
2347
  { label: "", action: () => {
2147
2348
  }, separator: true },
2148
2349
  { label: "Bold", action: fmt("bold") },
@@ -2162,6 +2363,7 @@ function standardEditItems(getEditor, opts = {}) {
2162
2363
  var init_edit_commands = __esm({
2163
2364
  "client/compose/edit-commands.js"() {
2164
2365
  "use strict";
2366
+ init_edit_menu();
2165
2367
  }
2166
2368
  });
2167
2369
 
@@ -4620,6 +4822,7 @@ init_api_client();
4620
4822
  init_edit_commands();
4621
4823
 
4622
4824
  // client/components/context-menu.js
4825
+ init_edit_menu();
4623
4826
  var activeMenu = null;
4624
4827
  var dismissListener = null;
4625
4828
  var escapeListener = null;
@@ -4684,8 +4887,15 @@ function openSubmenu(parentRow, items) {
4684
4887
  sub.style.top = `${Math.max(4, top)}px`;
4685
4888
  activeSubmenu = sub;
4686
4889
  }
4687
- function showContextMenu(x, y, items) {
4890
+ function showContextMenu(x, y, items, opts = {}) {
4688
4891
  closeContextMenu();
4892
+ if (opts.editTarget !== void 0 || opts.editEngine) {
4893
+ const edit = editMenuItems({ target: opts.editTarget, engine: opts.editEngine }, { onError: opts.onEditError });
4894
+ if (edit.length > 0) {
4895
+ items = items.length > 0 ? [...edit, { label: "", action: () => {
4896
+ }, separator: true }, ...items] : edit;
4897
+ }
4898
+ }
4689
4899
  const menu = document.createElement("div");
4690
4900
  menu.className = "ctx-menu";
4691
4901
  for (const item of items) {
@@ -5129,43 +5339,8 @@ for (const el of [toInput, ccInput, bccInput]) {
5129
5339
  items.push({ label: `Copy address: ${addr.email}`, action: () => {
5130
5340
  void navigator.clipboard.writeText(addr.email);
5131
5341
  } });
5132
- items.push({ label: "", action: () => {
5133
- }, separator: true });
5134
5342
  }
5135
- const selText = () => el.value.slice(el.selectionStart ?? 0, el.selectionEnd ?? 0);
5136
- const replaceSelection = (text) => {
5137
- const s = el.selectionStart ?? el.value.length, en = el.selectionEnd ?? el.value.length;
5138
- el.value = el.value.slice(0, s) + text + el.value.slice(en);
5139
- el.selectionStart = el.selectionEnd = s + text.length;
5140
- el.dispatchEvent(new Event("input", { bubbles: true }));
5141
- };
5142
- items.push({ label: "Cut", action: async () => {
5143
- const t = selText();
5144
- if (t) {
5145
- try {
5146
- await navigator.clipboard.writeText(t);
5147
- } catch {
5148
- }
5149
- replaceSelection("");
5150
- }
5151
- } });
5152
- items.push({ label: "Copy", action: async () => {
5153
- const t = selText();
5154
- if (t) {
5155
- try {
5156
- await navigator.clipboard.writeText(t);
5157
- } catch {
5158
- }
5159
- }
5160
- } });
5161
- items.push({ label: "Paste", action: async () => {
5162
- try {
5163
- replaceSelection(await navigator.clipboard.readText());
5164
- } catch {
5165
- }
5166
- } });
5167
- items.push({ label: "Select all", action: () => el.select() });
5168
- showContextMenu(me.clientX, me.clientY, items);
5343
+ showContextMenu(me.clientX, me.clientY, items, { editTarget: el });
5169
5344
  });
5170
5345
  }
5171
5346
  var knownAccounts = [];