@bobfrankston/rmfmail 1.1.243 → 1.1.244
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.
|
@@ -948,8 +948,8 @@ var require_lib = __commonJS({
|
|
|
948
948
|
"use strict";
|
|
949
949
|
var buffer = require_is_buffer();
|
|
950
950
|
var affix = require_affix();
|
|
951
|
-
module.exports =
|
|
952
|
-
var proto =
|
|
951
|
+
module.exports = NSpell2;
|
|
952
|
+
var proto = NSpell2.prototype;
|
|
953
953
|
proto.correct = require_correct();
|
|
954
954
|
proto.suggest = require_suggest();
|
|
955
955
|
proto.spell = require_spell();
|
|
@@ -958,11 +958,11 @@ var require_lib = __commonJS({
|
|
|
958
958
|
proto.wordCharacters = require_word_characters();
|
|
959
959
|
proto.dictionary = require_dictionary2();
|
|
960
960
|
proto.personal = require_personal();
|
|
961
|
-
function
|
|
961
|
+
function NSpell2(aff, dic) {
|
|
962
962
|
var index = -1;
|
|
963
963
|
var dictionaries;
|
|
964
|
-
if (!(this instanceof
|
|
965
|
-
return new
|
|
964
|
+
if (!(this instanceof NSpell2)) {
|
|
965
|
+
return new NSpell2(aff, dic);
|
|
966
966
|
}
|
|
967
967
|
if (typeof aff === "string" || buffer(aff)) {
|
|
968
968
|
if (typeof dic === "string" || buffer(dic)) {
|
|
@@ -2329,438 +2329,12 @@ var spellcheck_exports = {};
|
|
|
2329
2329
|
__export(spellcheck_exports, {
|
|
2330
2330
|
wireSpellcheck: () => wireSpellcheck
|
|
2331
2331
|
});
|
|
2332
|
-
async function getSpell2() {
|
|
2333
|
-
if (spellPromise2)
|
|
2334
|
-
return spellPromise2;
|
|
2335
|
-
spellPromise2 = (async () => {
|
|
2336
|
-
const [affRes, dicRes] = await Promise.all([
|
|
2337
|
-
fetch("../lib/dict/en.aff"),
|
|
2338
|
-
fetch("../lib/dict/en.dic")
|
|
2339
|
-
]);
|
|
2340
|
-
if (!affRes.ok || !dicRes.ok) {
|
|
2341
|
-
throw new Error(`spellcheck: dict fetch failed (aff=${affRes.status} dic=${dicRes.status})`);
|
|
2342
|
-
}
|
|
2343
|
-
const [aff, dic] = await Promise.all([affRes.text(), dicRes.text()]);
|
|
2344
|
-
const sp = new import_nspell2.default({ aff, dic });
|
|
2345
|
-
try {
|
|
2346
|
-
const raw = localStorage.getItem(USER_DICT_KEY2);
|
|
2347
|
-
if (raw)
|
|
2348
|
-
for (const w of JSON.parse(raw))
|
|
2349
|
-
sp.add(w);
|
|
2350
|
-
} catch {
|
|
2351
|
-
}
|
|
2352
|
-
getUserDict().then((cloud) => {
|
|
2353
|
-
const cloudArr = Array.isArray(cloud) ? cloud : [];
|
|
2354
|
-
for (const w of cloudArr)
|
|
2355
|
-
sp.add(w);
|
|
2356
|
-
let local = [];
|
|
2357
|
-
try {
|
|
2358
|
-
const raw = localStorage.getItem(USER_DICT_KEY2);
|
|
2359
|
-
local = raw ? JSON.parse(raw) : [];
|
|
2360
|
-
} catch {
|
|
2361
|
-
local = [];
|
|
2362
|
-
}
|
|
2363
|
-
const cloudSet = new Set(cloudArr);
|
|
2364
|
-
const localOnly = local.filter((w) => !cloudSet.has(w));
|
|
2365
|
-
if (localOnly.length > 0) {
|
|
2366
|
-
addUserDictWords(localOnly).catch((e) => console.error("[spell] reconcile:", e));
|
|
2367
|
-
}
|
|
2368
|
-
try {
|
|
2369
|
-
const merged = [.../* @__PURE__ */ new Set([...local, ...cloudArr])];
|
|
2370
|
-
localStorage.setItem(USER_DICT_KEY2, JSON.stringify(merged));
|
|
2371
|
-
} catch {
|
|
2372
|
-
}
|
|
2373
|
-
}).catch(() => {
|
|
2374
|
-
});
|
|
2375
|
-
return sp;
|
|
2376
|
-
})();
|
|
2377
|
-
return spellPromise2;
|
|
2378
|
-
}
|
|
2379
|
-
function addToUserDict2(word, sp) {
|
|
2380
|
-
try {
|
|
2381
|
-
const raw = localStorage.getItem(USER_DICT_KEY2);
|
|
2382
|
-
const arr = raw ? JSON.parse(raw) : [];
|
|
2383
|
-
if (!arr.includes(word)) {
|
|
2384
|
-
arr.push(word);
|
|
2385
|
-
localStorage.setItem(USER_DICT_KEY2, JSON.stringify(arr));
|
|
2386
|
-
}
|
|
2387
|
-
} catch {
|
|
2388
|
-
}
|
|
2389
|
-
sp.add(word);
|
|
2390
|
-
addUserDictWord(word).catch((e) => console.error("[spell] addUserDictWord:", e));
|
|
2391
|
-
}
|
|
2392
|
-
function decorate(editor2, sp) {
|
|
2393
|
-
const body = editor2.getBody?.();
|
|
2394
|
-
const doc = editor2.getDoc?.();
|
|
2395
|
-
if (!body || !doc)
|
|
2396
|
-
return;
|
|
2397
|
-
const _activeSel = doc.getSelection();
|
|
2398
|
-
if (_activeSel && _activeSel.rangeCount > 0 && !_activeSel.isCollapsed)
|
|
2399
|
-
return;
|
|
2400
|
-
let savedFocusNode = null;
|
|
2401
|
-
let savedFocusOffset = 0;
|
|
2402
|
-
const _preSel = doc.getSelection();
|
|
2403
|
-
if (_preSel && _preSel.rangeCount > 0) {
|
|
2404
|
-
savedFocusNode = _preSel.focusNode;
|
|
2405
|
-
savedFocusOffset = _preSel.focusOffset;
|
|
2406
|
-
}
|
|
2407
|
-
const savedAbs = caretAbsOffsetFromBody(body);
|
|
2408
|
-
const scroller = doc.scrollingElement || doc.documentElement;
|
|
2409
|
-
const savedScrollTop = scroller?.scrollTop ?? 0;
|
|
2410
|
-
const savedBodyScrollTop = body.scrollTop;
|
|
2411
|
-
try {
|
|
2412
|
-
editor2.undoManager?.ignore?.(() => {
|
|
2413
|
-
const old = body.querySelectorAll(`span[${MARKER_ATTR2}]`);
|
|
2414
|
-
for (const m of old) {
|
|
2415
|
-
const parent2 = m.parentNode;
|
|
2416
|
-
if (!parent2)
|
|
2417
|
-
continue;
|
|
2418
|
-
while (m.firstChild)
|
|
2419
|
-
parent2.insertBefore(m.firstChild, m);
|
|
2420
|
-
parent2.removeChild(m);
|
|
2421
|
-
}
|
|
2422
|
-
body.normalize();
|
|
2423
|
-
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
2424
|
-
acceptNode(node) {
|
|
2425
|
-
let p = node.parentNode;
|
|
2426
|
-
while (p && p !== body) {
|
|
2427
|
-
if (p.nodeType === Node.ELEMENT_NODE && SKIP_TAGS2.has(p.tagName)) {
|
|
2428
|
-
return NodeFilter.FILTER_REJECT;
|
|
2429
|
-
}
|
|
2430
|
-
p = p.parentNode;
|
|
2431
|
-
}
|
|
2432
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
2433
|
-
}
|
|
2434
|
-
});
|
|
2435
|
-
let caretNode = null;
|
|
2436
|
-
let caretOffset = 0;
|
|
2437
|
-
const liveSel = doc.getSelection();
|
|
2438
|
-
if (liveSel && liveSel.rangeCount > 0) {
|
|
2439
|
-
const f = liveSel.focusNode;
|
|
2440
|
-
if (f && f.nodeType === Node.TEXT_NODE) {
|
|
2441
|
-
caretNode = f;
|
|
2442
|
-
caretOffset = liveSel.focusOffset;
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
const hits = [];
|
|
2446
|
-
let n = walker.nextNode();
|
|
2447
|
-
const WORD_RE = /[\p{L}][\p{L}'’\-]*/gu;
|
|
2448
|
-
const EMAIL_RE = /[^\s@<>()]+@[^\s@<>()]+\.[^\s@<>()]+/g;
|
|
2449
|
-
while (n) {
|
|
2450
|
-
const tn = n;
|
|
2451
|
-
const text = tn.data;
|
|
2452
|
-
const emailRanges = [];
|
|
2453
|
-
EMAIL_RE.lastIndex = 0;
|
|
2454
|
-
let em;
|
|
2455
|
-
while ((em = EMAIL_RE.exec(text)) !== null) {
|
|
2456
|
-
emailRanges.push([em.index, em.index + em[0].length]);
|
|
2457
|
-
}
|
|
2458
|
-
let m;
|
|
2459
|
-
WORD_RE.lastIndex = 0;
|
|
2460
|
-
while ((m = WORD_RE.exec(text)) !== null) {
|
|
2461
|
-
const word = m[0];
|
|
2462
|
-
if (word.length < MIN_WORD_LEN2)
|
|
2463
|
-
continue;
|
|
2464
|
-
const wStart = m.index, wEnd = m.index + word.length;
|
|
2465
|
-
if (emailRanges.some(([s, e]) => wStart < e && wEnd > s))
|
|
2466
|
-
continue;
|
|
2467
|
-
if (caretNode === tn && caretOffset >= m.index && caretOffset <= m.index + word.length) {
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
if (sp.correct(word))
|
|
2471
|
-
continue;
|
|
2472
|
-
hits.push({ node: tn, start: m.index, end: m.index + word.length });
|
|
2473
|
-
}
|
|
2474
|
-
n = walker.nextNode();
|
|
2475
|
-
}
|
|
2476
|
-
hits.reverse();
|
|
2477
|
-
for (const h of hits) {
|
|
2478
|
-
const range = doc.createRange();
|
|
2479
|
-
range.setStart(h.node, h.start);
|
|
2480
|
-
range.setEnd(h.node, h.end);
|
|
2481
|
-
const span = doc.createElement("span");
|
|
2482
|
-
span.setAttribute(MARKER_ATTR2, "1");
|
|
2483
|
-
try {
|
|
2484
|
-
range.surroundContents(span);
|
|
2485
|
-
} catch {
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
});
|
|
2489
|
-
} finally {
|
|
2490
|
-
let restored = false;
|
|
2491
|
-
if (savedFocusNode && body.contains(savedFocusNode)) {
|
|
2492
|
-
try {
|
|
2493
|
-
const range = doc.createRange();
|
|
2494
|
-
const maxOffset = savedFocusNode.nodeType === Node.TEXT_NODE ? savedFocusNode.data.length : savedFocusNode.childNodes.length;
|
|
2495
|
-
range.setStart(savedFocusNode, Math.min(savedFocusOffset, maxOffset));
|
|
2496
|
-
range.collapse(true);
|
|
2497
|
-
const sel = doc.getSelection();
|
|
2498
|
-
if (sel) {
|
|
2499
|
-
sel.removeAllRanges();
|
|
2500
|
-
sel.addRange(range);
|
|
2501
|
-
restored = true;
|
|
2502
|
-
}
|
|
2503
|
-
} catch {
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
if (!restored && savedAbs != null)
|
|
2507
|
-
restoreCaretFromAbsOffset(body, savedAbs);
|
|
2508
|
-
if (scroller && scroller.scrollTop !== savedScrollTop)
|
|
2509
|
-
scroller.scrollTop = savedScrollTop;
|
|
2510
|
-
if (body.scrollTop !== savedBodyScrollTop)
|
|
2511
|
-
body.scrollTop = savedBodyScrollTop;
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
function caretAbsOffsetFromBody(body) {
|
|
2515
|
-
const doc = body.ownerDocument;
|
|
2516
|
-
const sel = doc.getSelection();
|
|
2517
|
-
if (!sel || sel.rangeCount === 0)
|
|
2518
|
-
return null;
|
|
2519
|
-
const focusNode = sel.focusNode;
|
|
2520
|
-
const focusOffset = sel.focusOffset;
|
|
2521
|
-
if (!focusNode)
|
|
2522
|
-
return null;
|
|
2523
|
-
if (focusNode.nodeType !== Node.TEXT_NODE) {
|
|
2524
|
-
let abs2 = 0;
|
|
2525
|
-
const walker2 = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
2526
|
-
let n2 = walker2.nextNode();
|
|
2527
|
-
while (n2) {
|
|
2528
|
-
if (focusNode.contains(n2))
|
|
2529
|
-
break;
|
|
2530
|
-
const cmp = focusNode.compareDocumentPosition(n2);
|
|
2531
|
-
if (cmp & Node.DOCUMENT_POSITION_PRECEDING)
|
|
2532
|
-
abs2 += n2.data.length;
|
|
2533
|
-
n2 = walker2.nextNode();
|
|
2534
|
-
}
|
|
2535
|
-
return abs2;
|
|
2536
|
-
}
|
|
2537
|
-
let abs = 0;
|
|
2538
|
-
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
2539
|
-
let n = walker.nextNode();
|
|
2540
|
-
while (n) {
|
|
2541
|
-
if (n === focusNode)
|
|
2542
|
-
return abs + focusOffset;
|
|
2543
|
-
abs += n.data.length;
|
|
2544
|
-
n = walker.nextNode();
|
|
2545
|
-
}
|
|
2546
|
-
return null;
|
|
2547
|
-
}
|
|
2548
|
-
function restoreCaretFromAbsOffset(body, abs) {
|
|
2549
|
-
const doc = body.ownerDocument;
|
|
2550
|
-
const sel = doc.getSelection();
|
|
2551
|
-
if (!sel)
|
|
2552
|
-
return;
|
|
2553
|
-
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
2554
|
-
let acc = 0;
|
|
2555
|
-
let n = walker.nextNode();
|
|
2556
|
-
while (n) {
|
|
2557
|
-
const len = n.data.length;
|
|
2558
|
-
if (acc + len >= abs) {
|
|
2559
|
-
const range = doc.createRange();
|
|
2560
|
-
range.setStart(n, Math.max(0, abs - acc));
|
|
2561
|
-
range.collapse(true);
|
|
2562
|
-
sel.removeAllRanges();
|
|
2563
|
-
sel.addRange(range);
|
|
2564
|
-
return;
|
|
2565
|
-
}
|
|
2566
|
-
acc += len;
|
|
2567
|
-
n = walker.nextNode();
|
|
2568
|
-
}
|
|
2569
|
-
const last = walker.previousNode();
|
|
2570
|
-
if (last) {
|
|
2571
|
-
const range = doc.createRange();
|
|
2572
|
-
range.setStart(last, last.data.length);
|
|
2573
|
-
range.collapse(true);
|
|
2574
|
-
sel.removeAllRanges();
|
|
2575
|
-
sel.addRange(range);
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
function installDecorationStyle(editor2) {
|
|
2579
|
-
const doc = editor2.getDoc?.();
|
|
2580
|
-
if (!doc)
|
|
2581
|
-
return;
|
|
2582
|
-
if (doc.getElementById("mailx-spell-style"))
|
|
2583
|
-
return;
|
|
2584
|
-
const style = doc.createElement("style");
|
|
2585
|
-
style.id = "mailx-spell-style";
|
|
2586
|
-
style.textContent = `
|
|
2587
|
-
span[${MARKER_ATTR2}] {
|
|
2588
|
-
text-decoration: underline wavy #d33;
|
|
2589
|
-
text-decoration-skip-ink: none;
|
|
2590
|
-
text-underline-offset: 2px;
|
|
2591
|
-
/* No background \u2014 keeps the styling subtle, like a native
|
|
2592
|
-
* spell underline, not a Find-highlight. */
|
|
2593
|
-
background: transparent;
|
|
2594
|
-
}
|
|
2595
|
-
`;
|
|
2596
|
-
doc.head.appendChild(style);
|
|
2597
|
-
}
|
|
2598
|
-
function installSerializerFilter(editor2) {
|
|
2599
|
-
if (editor2.__mailxSpellSerializerWired)
|
|
2600
|
-
return;
|
|
2601
|
-
editor2.__mailxSpellSerializerWired = true;
|
|
2602
|
-
try {
|
|
2603
|
-
editor2.serializer.addAttributeFilter(MARKER_ATTR2, (nodes) => {
|
|
2604
|
-
for (const node of nodes) {
|
|
2605
|
-
if (typeof node.unwrap === "function")
|
|
2606
|
-
node.unwrap();
|
|
2607
|
-
}
|
|
2608
|
-
});
|
|
2609
|
-
} catch (e) {
|
|
2610
|
-
console.warn("[spellcheck] serializer filter setup failed:", e);
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
function showSuggestionsMenu2(parentDoc, x, y, items) {
|
|
2614
|
-
parentDoc.getElementById("mailx-spell-menu")?.remove();
|
|
2615
|
-
const menu = parentDoc.createElement("div");
|
|
2616
|
-
menu.id = "mailx-spell-menu";
|
|
2617
|
-
menu.style.cssText = `
|
|
2618
|
-
position: fixed;
|
|
2619
|
-
left: ${x}px; top: ${y}px;
|
|
2620
|
-
z-index: 10000;
|
|
2621
|
-
background: var(--color-bg, #fff);
|
|
2622
|
-
color: var(--color-text, #222);
|
|
2623
|
-
border: 1px solid var(--color-border, #ccc);
|
|
2624
|
-
border-radius: 6px;
|
|
2625
|
-
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
2626
|
-
padding: 4px 0;
|
|
2627
|
-
font: 13px system-ui, sans-serif;
|
|
2628
|
-
min-width: 180px;
|
|
2629
|
-
max-width: 320px;
|
|
2630
|
-
`;
|
|
2631
|
-
for (const it of items) {
|
|
2632
|
-
if (it.separator) {
|
|
2633
|
-
const sep = parentDoc.createElement("div");
|
|
2634
|
-
sep.style.cssText = "border-top:1px solid var(--color-border,#ddd); margin: 4px 0;";
|
|
2635
|
-
menu.appendChild(sep);
|
|
2636
|
-
continue;
|
|
2637
|
-
}
|
|
2638
|
-
const btn = parentDoc.createElement("button");
|
|
2639
|
-
btn.type = "button";
|
|
2640
|
-
btn.textContent = it.label;
|
|
2641
|
-
btn.style.cssText = `
|
|
2642
|
-
display: block; width: 100%; text-align: left;
|
|
2643
|
-
padding: 5px 12px; border: none; background: none;
|
|
2644
|
-
color: inherit; cursor: pointer; font: inherit;
|
|
2645
|
-
${it.emphasized ? "font-weight: 600;" : ""}
|
|
2646
|
-
`;
|
|
2647
|
-
btn.addEventListener("mouseenter", () => {
|
|
2648
|
-
btn.style.background = "var(--color-bg-hover, #eef)";
|
|
2649
|
-
});
|
|
2650
|
-
btn.addEventListener("mouseleave", () => {
|
|
2651
|
-
btn.style.background = "none";
|
|
2652
|
-
});
|
|
2653
|
-
btn.addEventListener("click", () => {
|
|
2654
|
-
try {
|
|
2655
|
-
it.action();
|
|
2656
|
-
} finally {
|
|
2657
|
-
menu.remove();
|
|
2658
|
-
}
|
|
2659
|
-
});
|
|
2660
|
-
menu.appendChild(btn);
|
|
2661
|
-
}
|
|
2662
|
-
parentDoc.body.appendChild(menu);
|
|
2663
|
-
const r = menu.getBoundingClientRect();
|
|
2664
|
-
if (r.right > window.innerWidth)
|
|
2665
|
-
menu.style.left = `${Math.max(8, window.innerWidth - r.width - 8)}px`;
|
|
2666
|
-
if (r.bottom > window.innerHeight)
|
|
2667
|
-
menu.style.top = `${Math.max(8, window.innerHeight - r.height - 8)}px`;
|
|
2668
|
-
const docs = [parentDoc];
|
|
2669
|
-
try {
|
|
2670
|
-
const composeWin = parentDoc.defaultView;
|
|
2671
|
-
if (composeWin?.frameElement && composeWin.parent?.document && composeWin.parent.document !== parentDoc) {
|
|
2672
|
-
docs.push(composeWin.parent.document);
|
|
2673
|
-
}
|
|
2674
|
-
} catch {
|
|
2675
|
-
}
|
|
2676
|
-
try {
|
|
2677
|
-
const editorIframe = parentDoc.querySelector("iframe.tox-edit-area__iframe") || parentDoc.querySelector("iframe");
|
|
2678
|
-
const editorDoc = editorIframe?.contentDocument;
|
|
2679
|
-
if (editorDoc && editorDoc !== parentDoc)
|
|
2680
|
-
docs.push(editorDoc);
|
|
2681
|
-
} catch {
|
|
2682
|
-
}
|
|
2683
|
-
const dismiss2 = (e) => {
|
|
2684
|
-
if (e.type === "keydown" && e.key !== "Escape")
|
|
2685
|
-
return;
|
|
2686
|
-
if (e.type === "mousedown" && menu.contains(e.target))
|
|
2687
|
-
return;
|
|
2688
|
-
menu.remove();
|
|
2689
|
-
for (const d of docs) {
|
|
2690
|
-
d.removeEventListener("mousedown", dismiss2, true);
|
|
2691
|
-
d.removeEventListener("keydown", dismiss2, true);
|
|
2692
|
-
}
|
|
2693
|
-
};
|
|
2694
|
-
setTimeout(() => {
|
|
2695
|
-
for (const d of docs) {
|
|
2696
|
-
d.addEventListener("mousedown", dismiss2, true);
|
|
2697
|
-
d.addEventListener("keydown", dismiss2, true);
|
|
2698
|
-
}
|
|
2699
|
-
}, 0);
|
|
2700
|
-
}
|
|
2701
|
-
function replaceMarker(editor2, marker, replacement) {
|
|
2702
|
-
try {
|
|
2703
|
-
editor2.focus();
|
|
2704
|
-
editor2.selection.select(marker);
|
|
2705
|
-
editor2.insertContent(editor2.dom.encode(replacement));
|
|
2706
|
-
} catch {
|
|
2707
|
-
const doc = editor2.getDoc();
|
|
2708
|
-
const range = doc.createRange();
|
|
2709
|
-
range.selectNode(marker);
|
|
2710
|
-
range.deleteContents();
|
|
2711
|
-
range.insertNode(doc.createTextNode(replacement));
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
function cleanupCorrected(editor2, sp) {
|
|
2715
|
-
const body = editor2.getBody?.();
|
|
2716
|
-
const doc = editor2.getDoc?.();
|
|
2717
|
-
if (!body || !doc)
|
|
2718
|
-
return;
|
|
2719
|
-
const activeSel = doc.getSelection();
|
|
2720
|
-
if (activeSel && activeSel.rangeCount > 0 && !activeSel.isCollapsed)
|
|
2721
|
-
return;
|
|
2722
|
-
const markers = body.querySelectorAll(`span[${MARKER_ATTR2}]`);
|
|
2723
|
-
if (markers.length === 0)
|
|
2724
|
-
return;
|
|
2725
|
-
let caretMarker = null;
|
|
2726
|
-
const sel = doc.getSelection();
|
|
2727
|
-
if (sel && sel.rangeCount > 0) {
|
|
2728
|
-
let p = sel.focusNode;
|
|
2729
|
-
while (p && p !== body) {
|
|
2730
|
-
if (p.nodeType === Node.ELEMENT_NODE && p.hasAttribute?.(MARKER_ATTR2)) {
|
|
2731
|
-
caretMarker = p;
|
|
2732
|
-
break;
|
|
2733
|
-
}
|
|
2734
|
-
p = p.parentNode;
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
const stale = [];
|
|
2738
|
-
for (const m of markers) {
|
|
2739
|
-
const word = m.textContent || "";
|
|
2740
|
-
const isStale = !word || /\s/.test(word) || sp.correct(word);
|
|
2741
|
-
if (!isStale && m === caretMarker)
|
|
2742
|
-
continue;
|
|
2743
|
-
if (isStale)
|
|
2744
|
-
stale.push(m);
|
|
2745
|
-
}
|
|
2746
|
-
if (stale.length === 0)
|
|
2747
|
-
return;
|
|
2748
|
-
editor2.undoManager?.ignore?.(() => {
|
|
2749
|
-
for (const m of stale) {
|
|
2750
|
-
const parent2 = m.parentNode;
|
|
2751
|
-
if (!parent2)
|
|
2752
|
-
continue;
|
|
2753
|
-
while (m.firstChild)
|
|
2754
|
-
parent2.insertBefore(m.firstChild, m);
|
|
2755
|
-
parent2.removeChild(m);
|
|
2756
|
-
}
|
|
2757
|
-
});
|
|
2758
|
-
}
|
|
2759
2332
|
function wireSpellcheck(editor2) {
|
|
2760
2333
|
if (editor2.__mailxSpellWired)
|
|
2761
2334
|
return;
|
|
2762
2335
|
editor2.__mailxSpellWired = true;
|
|
2763
|
-
|
|
2336
|
+
let sp = null;
|
|
2337
|
+
const killNative = () => {
|
|
2764
2338
|
try {
|
|
2765
2339
|
const body = editor2.getBody?.();
|
|
2766
2340
|
if (body && body.getAttribute("spellcheck") !== "false") {
|
|
@@ -2769,129 +2343,182 @@ function wireSpellcheck(editor2) {
|
|
|
2769
2343
|
} catch {
|
|
2770
2344
|
}
|
|
2771
2345
|
};
|
|
2772
|
-
|
|
2346
|
+
killNative();
|
|
2773
2347
|
try {
|
|
2774
2348
|
const body = editor2.getBody?.();
|
|
2775
2349
|
if (body)
|
|
2776
|
-
new MutationObserver(
|
|
2350
|
+
new MutationObserver(killNative).observe(body, { attributes: true, attributeFilter: ["spellcheck"] });
|
|
2777
2351
|
} catch {
|
|
2778
2352
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2353
|
+
const ensureOverlay = () => {
|
|
2354
|
+
const body = editor2.getBody?.();
|
|
2355
|
+
const doc = editor2.getDoc?.();
|
|
2356
|
+
if (!body || !doc)
|
|
2357
|
+
return null;
|
|
2358
|
+
let ov = doc.getElementById(OVERLAY_ID);
|
|
2359
|
+
if (!ov || ov.parentNode !== body) {
|
|
2360
|
+
ov?.remove();
|
|
2361
|
+
ov = doc.createElement("div");
|
|
2362
|
+
ov.id = OVERLAY_ID;
|
|
2363
|
+
ov.setAttribute("contenteditable", "false");
|
|
2364
|
+
ov.setAttribute("data-mce-bogus", "all");
|
|
2365
|
+
ov.style.cssText = "position:absolute;top:0;left:0;pointer-events:none;user-select:none;";
|
|
2366
|
+
body.appendChild(ov);
|
|
2367
|
+
}
|
|
2368
|
+
return ov;
|
|
2369
|
+
};
|
|
2370
|
+
const scan = () => {
|
|
2782
2371
|
if (!sp)
|
|
2783
2372
|
return;
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2373
|
+
const body = editor2.getBody?.();
|
|
2374
|
+
const doc = editor2.getDoc?.();
|
|
2375
|
+
const ov = ensureOverlay();
|
|
2376
|
+
if (!body || !doc || !ov)
|
|
2377
|
+
return;
|
|
2378
|
+
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
2379
|
+
acceptNode(node) {
|
|
2380
|
+
let p = node.parentNode;
|
|
2381
|
+
while (p && p !== body) {
|
|
2382
|
+
if (p.nodeType === Node.ELEMENT_NODE) {
|
|
2383
|
+
const el = p;
|
|
2384
|
+
if (el.id === OVERLAY_ID)
|
|
2385
|
+
return NodeFilter.FILTER_REJECT;
|
|
2386
|
+
if (SKIP_TAGS.has(el.tagName))
|
|
2387
|
+
return NodeFilter.FILTER_REJECT;
|
|
2388
|
+
}
|
|
2389
|
+
p = p.parentNode;
|
|
2390
|
+
}
|
|
2391
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
const bodyRect = body.getBoundingClientRect();
|
|
2395
|
+
const sx = body.scrollLeft;
|
|
2396
|
+
const sy = body.scrollTop;
|
|
2397
|
+
const frag = doc.createDocumentFragment();
|
|
2398
|
+
const wordRe = /[\p{L}][\p{L}'’-]*/gu;
|
|
2399
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
2400
|
+
const text = n.data;
|
|
2401
|
+
if (!text || text.length < MIN_WORD_LEN)
|
|
2402
|
+
continue;
|
|
2403
|
+
wordRe.lastIndex = 0;
|
|
2404
|
+
let m;
|
|
2405
|
+
while (m = wordRe.exec(text)) {
|
|
2406
|
+
const w = m[0];
|
|
2407
|
+
if (w.length < MIN_WORD_LEN)
|
|
2408
|
+
continue;
|
|
2409
|
+
if (sp.correct(w))
|
|
2410
|
+
continue;
|
|
2411
|
+
const r = doc.createRange();
|
|
2412
|
+
r.setStart(n, m.index);
|
|
2413
|
+
r.setEnd(n, m.index + w.length);
|
|
2414
|
+
const rects = r.getClientRects();
|
|
2415
|
+
for (let i = 0; i < rects.length; i++) {
|
|
2416
|
+
const rect = rects[i];
|
|
2417
|
+
if (rect.width < 1)
|
|
2418
|
+
continue;
|
|
2419
|
+
const sq = doc.createElement("div");
|
|
2420
|
+
sq.style.cssText = `position:absolute;left:${(rect.left - bodyRect.left + sx).toFixed(1)}px;top:${(rect.bottom - bodyRect.top + sy - 3).toFixed(1)}px;width:${rect.width.toFixed(1)}px;height:3px;background:${WAVE} repeat-x left bottom;`;
|
|
2421
|
+
frag.appendChild(sq);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
ov.textContent = "";
|
|
2426
|
+
ov.appendChild(frag);
|
|
2791
2427
|
};
|
|
2792
|
-
let
|
|
2793
|
-
const
|
|
2428
|
+
let scanTimer = null;
|
|
2429
|
+
const scheduleScan = () => {
|
|
2794
2430
|
if (!sp)
|
|
2795
2431
|
return;
|
|
2796
|
-
if (
|
|
2797
|
-
clearTimeout(
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}, CLEANUP_DEBOUNCE_MS);
|
|
2432
|
+
if (scanTimer)
|
|
2433
|
+
clearTimeout(scanTimer);
|
|
2434
|
+
scanTimer = setTimeout(() => {
|
|
2435
|
+
scanTimer = null;
|
|
2436
|
+
scan();
|
|
2437
|
+
}, SCAN_DEBOUNCE_MS);
|
|
2803
2438
|
};
|
|
2804
|
-
|
|
2439
|
+
getSpell().then((loaded) => {
|
|
2805
2440
|
sp = loaded;
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2441
|
+
scan();
|
|
2442
|
+
}).catch((e) => console.error("[spellcheck] dict load failed:", e));
|
|
2443
|
+
editor2.on("input keyup paste SetContent Undo Redo", scheduleScan);
|
|
2444
|
+
editor2.on("ResizeEditor", scheduleScan);
|
|
2445
|
+
try {
|
|
2446
|
+
editor2.getDoc()?.addEventListener("scroll", scheduleScan, { passive: true });
|
|
2447
|
+
} catch {
|
|
2448
|
+
}
|
|
2449
|
+
const apply = (node, start, end, replacement) => {
|
|
2450
|
+
try {
|
|
2451
|
+
const doc = editor2.getDoc();
|
|
2452
|
+
const range = doc.createRange();
|
|
2453
|
+
range.setStart(node, start);
|
|
2454
|
+
range.setEnd(node, end);
|
|
2455
|
+
editor2.focus();
|
|
2456
|
+
editor2.selection.setRng(range);
|
|
2457
|
+
editor2.insertContent(editor2.dom.encode(replacement));
|
|
2458
|
+
} catch {
|
|
2459
|
+
try {
|
|
2460
|
+
const doc = editor2.getDoc();
|
|
2461
|
+
const range = doc.createRange();
|
|
2462
|
+
range.setStart(node, start);
|
|
2463
|
+
range.setEnd(node, end);
|
|
2464
|
+
range.deleteContents();
|
|
2465
|
+
range.insertNode(doc.createTextNode(replacement));
|
|
2466
|
+
} catch {
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
scheduleScan();
|
|
2470
|
+
};
|
|
2814
2471
|
const iframeDoc = editor2.getDoc();
|
|
2815
2472
|
iframeDoc.addEventListener("contextmenu", (ev) => {
|
|
2816
2473
|
const e = ev;
|
|
2817
|
-
const
|
|
2818
|
-
if (!
|
|
2474
|
+
const body = editor2.getBody?.();
|
|
2475
|
+
if (!body || !sp)
|
|
2819
2476
|
return;
|
|
2820
|
-
const
|
|
2821
|
-
if (!
|
|
2477
|
+
const hit = getWordAtPoint(body, e.clientX, e.clientY);
|
|
2478
|
+
if (!hit)
|
|
2822
2479
|
return;
|
|
2823
|
-
|
|
2824
|
-
if (!word || !sp)
|
|
2480
|
+
if (sp.correct(hit.word))
|
|
2825
2481
|
return;
|
|
2826
2482
|
e.preventDefault();
|
|
2827
2483
|
e.stopPropagation();
|
|
2828
|
-
const
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
}
|
|
2835
|
-
const nspellSugs = sp.suggest(word);
|
|
2836
|
-
const sugs = [];
|
|
2837
|
-
for (const s of [...transposed, ...nspellSugs]) {
|
|
2838
|
-
if (!sugs.includes(s))
|
|
2839
|
-
sugs.push(s);
|
|
2840
|
-
if (sugs.length >= 7)
|
|
2841
|
-
break;
|
|
2842
|
-
}
|
|
2843
|
-
const iframeEl = editor2.iframeElement;
|
|
2844
|
-
const iframeRect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
|
|
2845
|
-
const items = [];
|
|
2846
|
-
if (sugs.length === 0) {
|
|
2847
|
-
items.push({ label: "(no suggestions)", action: () => {
|
|
2848
|
-
} });
|
|
2849
|
-
} else {
|
|
2850
|
-
for (const s of sugs) {
|
|
2851
|
-
items.push({
|
|
2852
|
-
label: s,
|
|
2853
|
-
emphasized: true,
|
|
2854
|
-
action: () => {
|
|
2855
|
-
replaceMarker(editor2, marker, s);
|
|
2856
|
-
scheduleDecorate();
|
|
2857
|
-
}
|
|
2858
|
-
});
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2484
|
+
const sugs = buildSuggestionList(hit.word, sp);
|
|
2485
|
+
const items = sugs.length === 0 ? [{ label: "(no suggestions)", action: () => {
|
|
2486
|
+
} }] : sugs.map((s) => ({
|
|
2487
|
+
label: s,
|
|
2488
|
+
emphasized: true,
|
|
2489
|
+
action: () => apply(hit.node, hit.start, hit.end, s)
|
|
2490
|
+
}));
|
|
2861
2491
|
items.push({ label: "", action: () => {
|
|
2862
2492
|
}, separator: true });
|
|
2863
2493
|
items.push({
|
|
2864
|
-
label: `Add "${word}" to dictionary`,
|
|
2494
|
+
label: `Add "${hit.word}" to dictionary`,
|
|
2865
2495
|
action: () => {
|
|
2866
2496
|
if (sp)
|
|
2867
|
-
|
|
2868
|
-
|
|
2497
|
+
addToUserDict(hit.word, sp);
|
|
2498
|
+
scheduleScan();
|
|
2869
2499
|
}
|
|
2870
2500
|
});
|
|
2871
2501
|
items.push({
|
|
2872
2502
|
label: "Ignore (this session)",
|
|
2873
2503
|
action: () => {
|
|
2874
2504
|
if (sp)
|
|
2875
|
-
sp.add(word);
|
|
2876
|
-
|
|
2505
|
+
sp.add(hit.word);
|
|
2506
|
+
scheduleScan();
|
|
2877
2507
|
}
|
|
2878
2508
|
});
|
|
2879
|
-
|
|
2509
|
+
const iframeEl = editor2.iframeElement;
|
|
2510
|
+
const rect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
|
|
2511
|
+
showSuggestionsMenu(document, rect.left + e.clientX, rect.top + e.clientY, items, [iframeDoc]);
|
|
2880
2512
|
}, true);
|
|
2881
2513
|
}
|
|
2882
|
-
var
|
|
2514
|
+
var SCAN_DEBOUNCE_MS, WAVE, OVERLAY_ID;
|
|
2883
2515
|
var init_spellcheck = __esm({
|
|
2884
2516
|
"client/compose/spellcheck.js"() {
|
|
2885
2517
|
"use strict";
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
DECORATE_DEBOUNCE_MS = 1200;
|
|
2891
|
-
CLEANUP_DEBOUNCE_MS = 300;
|
|
2892
|
-
MIN_WORD_LEN2 = 3;
|
|
2893
|
-
SKIP_TAGS2 = /* @__PURE__ */ new Set(["BLOCKQUOTE", "CODE", "PRE", "A", "SCRIPT", "STYLE", "KBD", "SAMP", "VAR"]);
|
|
2894
|
-
spellPromise2 = null;
|
|
2518
|
+
init_spellcheck_core();
|
|
2519
|
+
SCAN_DEBOUNCE_MS = 600;
|
|
2520
|
+
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>')}")`;
|
|
2521
|
+
OVERLAY_ID = "mailx-spell-overlay";
|
|
2895
2522
|
}
|
|
2896
2523
|
});
|
|
2897
2524
|
|