@bobfrankston/rmfmail 1.1.151 → 1.1.153

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.
@@ -249,41 +249,74 @@ export async function createTinyMceEditor(container, opts = {}) {
249
249
  // Ctrl+- for its built-in page zoom, so we have to call
250
250
  // preventDefault in the capture phase to win the race.
251
251
  // Bound below in the "init" handler once the iframe doc exists.
252
- // Typing right after a link must not extend the link. When a
253
- // printable character is about to be inserted with the caret
252
+ // Typing right after a link must not extend the link. When
253
+ // any text insertion is about to happen with the caret
254
254
  // collapsed at the very end of an <a>, hop the caret to just
255
255
  // after the <a> first — otherwise contenteditable keeps
256
256
  // appending into the link and the whole sentence turns into
257
257
  // link text (Bob 2026-05-18: "I typed a URL but it then
258
- // included everything else I typed in the URL text"). Covers
259
- // autolink, pasted URLs, and hand-made links alike.
260
- ed.on("keypress", (e) => {
261
- if (e.ctrlKey || e.altKey || e.metaKey)
258
+ // included everything else I typed in the URL text"; Bob
259
+ // 2026-05-25 reconfirmed). Covers autolink, pasted URLs,
260
+ // hand-made links, IME composition, and clipboard paste.
261
+ //
262
+ // Uses `beforeinput` (modern, fires for ALL text insertion
263
+ // kinds — keypress is deprecated and silently skips IME /
264
+ // paste / autocomplete on Chromium). Done as a raw DOM
265
+ // listener so it runs BEFORE TinyMCE's own beforeinput
266
+ // handling, which is what reaches into the <a> and extends
267
+ // it. Re-bound on every `init` (the iframe doc is recreated
268
+ // on setContent, fullscreen, etc.).
269
+ const installLinkEscape = () => {
270
+ const doc = ed.getDoc();
271
+ if (!doc || doc.__rmfLinkEscapeInstalled)
262
272
  return;
263
- const sel = ed.selection;
264
- const rng = sel.getRng();
265
- if (!rng || !rng.collapsed)
266
- return;
267
- const a = ed.dom.getParent(sel.getNode(), "a");
268
- if (!a)
269
- return;
270
- // Range from the caret to just after the <a>: no text in
271
- // it the caret sits at the link's trailing edge.
272
- let tail;
273
- try {
274
- tail = rng.cloneRange();
275
- tail.setEndAfter(a);
276
- }
277
- catch {
278
- return;
279
- }
280
- if (tail.toString().length > 0)
281
- return;
282
- const after = ed.getDoc().createRange();
283
- after.setStartAfter(a);
284
- after.collapse(true);
285
- sel.setRng(after);
286
- });
273
+ doc.__rmfLinkEscapeInstalled = true;
274
+ doc.addEventListener("beforeinput", (e) => {
275
+ // Only intercept text-insertion kinds. Deletes,
276
+ // formatting, history nav don't extend links.
277
+ const kind = e.inputType || "";
278
+ const isInsert = kind === "insertText"
279
+ || kind === "insertCompositionText"
280
+ || kind === "insertFromPaste"
281
+ || kind === "insertFromDrop"
282
+ || kind === "insertFromComposition";
283
+ if (!isInsert)
284
+ return;
285
+ const sel = doc.getSelection();
286
+ if (!sel || sel.rangeCount === 0)
287
+ return;
288
+ const rng = sel.getRangeAt(0);
289
+ if (!rng.collapsed)
290
+ return;
291
+ const node = rng.startContainer;
292
+ const a = (node.nodeType === Node.ELEMENT_NODE ? node : node.parentNode);
293
+ if (!a)
294
+ return;
295
+ const link = a.closest?.("a");
296
+ if (!link)
297
+ return;
298
+ // Range from caret to end-of-link: empty ⇒ caret is
299
+ // at the link's trailing edge. Anything else means
300
+ // mid-link edit, which we want to leave alone.
301
+ let tail;
302
+ try {
303
+ tail = rng.cloneRange();
304
+ tail.setEndAfter(link);
305
+ }
306
+ catch {
307
+ return;
308
+ }
309
+ if (tail.toString().length > 0)
310
+ return;
311
+ const after = doc.createRange();
312
+ after.setStartAfter(link);
313
+ after.collapse(true);
314
+ sel.removeAllRanges();
315
+ sel.addRange(after);
316
+ }, true);
317
+ };
318
+ ed.on("init", installLinkEscape);
319
+ ed.on("SetContent", installLinkEscape);
287
320
  ed.on("init", () => {
288
321
  // Engage WebView2's native spellcheck. TinyMCE's own
289
322
  // `browser_spellcheck: true` is a no-op — its code path
@@ -1718,6 +1718,15 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
1718
1718
  line-height: 1.5;
1719
1719
  white-space: pre-wrap;
1720
1720
  word-break: break-word;
1721
+ /* The parent `.mv-body` is `overflow:hidden` because the full-body case
1722
+ * is an iframe that scrolls internally. The snippet placeholder lives
1723
+ * directly in `.mv-body` (no iframe), so without its own scroll it's
1724
+ * stuck — wheel events go nowhere until the user clicks away and back
1725
+ * (Bob 2026-05-25 "trying to scroll the summary but I seem stuck").
1726
+ * Take the full flex height and scroll our own content. */
1727
+ height: 100%;
1728
+ overflow: auto;
1729
+ box-sizing: border-box;
1721
1730
  }
1722
1731
  .mv-tear-line {
1723
1732
  position: relative;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.1.151",
3
+ "version": "1.1.153",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -45,7 +45,7 @@
45
45
  "@bobfrankston/msger": "^0.1.383",
46
46
  "@bobfrankston/node-tcp-transport": "^0.1.8",
47
47
  "@bobfrankston/oauthsupport": "^1.0.27",
48
- "@bobfrankston/rmf-tiny": "^0.1.23",
48
+ "@bobfrankston/rmf-tiny": "^0.1.25",
49
49
  "@bobfrankston/smtp-direct": "^0.1.8",
50
50
  "@bobfrankston/tcp-transport": "^0.1.6",
51
51
  "@capacitor/android": "^8.3.0",
@@ -125,7 +125,7 @@
125
125
  "@bobfrankston/msger": "^0.1.383",
126
126
  "@bobfrankston/node-tcp-transport": "^0.1.8",
127
127
  "@bobfrankston/oauthsupport": "^1.0.27",
128
- "@bobfrankston/rmf-tiny": "^0.1.23",
128
+ "@bobfrankston/rmf-tiny": "^0.1.25",
129
129
  "@bobfrankston/smtp-direct": "^0.1.8",
130
130
  "@bobfrankston/tcp-transport": "^0.1.6",
131
131
  "@capacitor/android": "^8.3.0",