@bobfrankston/rmfmail 1.1.151 → 1.1.152
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/client/lib/rmf-tiny.js
CHANGED
|
@@ -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
|
|
253
|
-
//
|
|
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"
|
|
259
|
-
// autolink, pasted URLs,
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/rmfmail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.152",
|
|
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.
|
|
48
|
+
"@bobfrankston/rmf-tiny": "^0.1.24",
|
|
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.
|
|
128
|
+
"@bobfrankston/rmf-tiny": "^0.1.24",
|
|
129
129
|
"@bobfrankston/smtp-direct": "^0.1.8",
|
|
130
130
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
131
131
|
"@capacitor/android": "^8.3.0",
|