@richscripts/richtexteditor 2.4.2 → 2.5.1
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/package.json +2 -2
- package/richtexteditor/all_plugins.js +2837 -935
- package/richtexteditor/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/accessibilitychecker.js +186 -1
- package/richtexteditor/plugins/aitoolkit.js +1 -1
- package/richtexteditor/plugins/all_plugins.js +2837 -935
- package/richtexteditor/plugins/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/gapcursor.js +365 -0
- package/richtexteditor/plugins/imageeditor.js +30 -3
- package/richtexteditor/plugins/importstyles.js +312 -0
- package/richtexteditor/plugins/insertcode.js +81 -4
- package/richtexteditor/plugins/insertemoji.js +302 -229
- package/richtexteditor/plugins/insertgallery.js +34 -6
- package/richtexteditor/plugins/keyboarda11y.js +27 -1
- package/richtexteditor/plugins/multiroot.js +269 -0
- package/richtexteditor/plugins/revisionhistory.js +5 -0
- package/richtexteditor/plugins/textpartlanguage.js +504 -0
- package/richtexteditor/plugins/trackedchanges.js +716 -689
- package/richtexteditor/plugins/wordexport.js +1 -1
- package/richtexteditor/rte-config.js +2 -2
- package/richtexteditor/rte.js +63 -55
- package/richtexteditor/rte_theme_default.css +172 -107
- package/richtexteditor/runtime/richtexteditor_content.css +21 -14
|
@@ -323,7 +323,14 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
323
323
|
function repairIssue(issueIndex, options) {
|
|
324
324
|
if (!lastResult || !lastResult.issues || !lastResult.issues[issueIndex]) return runAudit();
|
|
325
325
|
var ctor = window.RichTextEditor;
|
|
326
|
-
|
|
326
|
+
// An issue carrying `_target` came from the DOM audit and is addressed
|
|
327
|
+
// by a DOM node, not by a JSON path. Handing it to the structured-content
|
|
328
|
+
// repairer looks like it works — no error, a document comes back — but
|
|
329
|
+
// that repairer cannot resolve the node and returns the input unchanged,
|
|
330
|
+
// so the fix silently does nothing.
|
|
331
|
+
var issue = lastResult.issues[issueIndex];
|
|
332
|
+
if (issue._target || lastResult.source === "dom") return repairDomIssue(issue, options || {});
|
|
333
|
+
if (!ctor || typeof ctor.repairAccessibilityIssue !== "function") return repairDomIssue(issue, options || {});
|
|
327
334
|
var nextDocument = ctor.repairAccessibilityIssue(editor.getJSON(), lastResult.issues[issueIndex], options || {});
|
|
328
335
|
editor.setJSON(nextDocument);
|
|
329
336
|
selectedIssueIndex = issueIndex;
|
|
@@ -355,6 +362,101 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
355
362
|
editor.focus();
|
|
356
363
|
}
|
|
357
364
|
|
|
365
|
+
// ---- WCAG 3.1.2 Language of Parts -----------------------------------
|
|
366
|
+
//
|
|
367
|
+
// A passage in a language other than the document's must be marked, or a
|
|
368
|
+
// screen reader pronounces it with the wrong phoneme set. Detecting
|
|
369
|
+
// "French inside English" is a language-identification problem we are not
|
|
370
|
+
// going to pretend to solve; detecting a different WRITING SYSTEM is
|
|
371
|
+
// deterministic, and it catches the cases that actually break speech
|
|
372
|
+
// synthesis outright (Hebrew, Arabic, Greek, Cyrillic, CJK in a Latin
|
|
373
|
+
// document, and the reverse). We flag only what we can be certain of —
|
|
374
|
+
// a checker that cries wolf gets switched off.
|
|
375
|
+
var SCRIPTS = [
|
|
376
|
+
{ name: "Hebrew", lang: "he", re: /[-]/g },
|
|
377
|
+
{ name: "Arabic", lang: "ar", re: /[-ۿݐ-ݿﭐ-﷿ﹰ-]/g },
|
|
378
|
+
{ name: "Greek", lang: "el", re: /[Ͱ-Ͽἀ-]/g },
|
|
379
|
+
{ name: "Cyrillic", lang: "ru", re: /[Ѐ-ӿ]/g },
|
|
380
|
+
{ name: "Han", lang: "zh", re: /[㐀-䶿一-鿿]/g },
|
|
381
|
+
{ name: "Kana", lang: "ja", re: /[-ゟ゠-ヿ]/g },
|
|
382
|
+
{ name: "Hangul", lang: "ko", re: /[가-ᄀ-ᇿ]/g },
|
|
383
|
+
{ name: "Devanagari", lang: "hi", re: /[ऀ-ॿ]/g },
|
|
384
|
+
{ name: "Thai", lang: "th", re: /[-]/g },
|
|
385
|
+
{ name: "Latin", lang: "en", re: /[A-Za-zÀ-ɏ]/g }
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
// Below this, a "run" is more likely a stray glyph, a maths symbol or a
|
|
389
|
+
// single borrowed character than a passage worth annotating.
|
|
390
|
+
var MIN_SCRIPT_RUN = 4;
|
|
391
|
+
|
|
392
|
+
// Every script present in the text with enough characters to be a passage.
|
|
393
|
+
//
|
|
394
|
+
// Deliberately NOT "the dominant script": a foreign phrase is by definition
|
|
395
|
+
// the minority of its paragraph, so picking the most common script finds
|
|
396
|
+
// the document's own language and reports nothing. "A Hebrew proverb,
|
|
397
|
+
// שלום עליכם, appears mid-sentence" is 40 Latin characters and 9 Hebrew —
|
|
398
|
+
// the 9 are the entire point.
|
|
399
|
+
function scriptsIn(text) {
|
|
400
|
+
var found = [];
|
|
401
|
+
for (var i = 0; i < SCRIPTS.length; i++) {
|
|
402
|
+
var m = String(text).match(SCRIPTS[i].re);
|
|
403
|
+
if (m && m.length >= MIN_SCRIPT_RUN) found.push(SCRIPTS[i]);
|
|
404
|
+
}
|
|
405
|
+
return found;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function documentScript() {
|
|
409
|
+
var editable = editor.getEditable ? editor.getEditable() : null;
|
|
410
|
+
var code = (editable && editable.getAttribute("lang")) ||
|
|
411
|
+
(typeof document !== "undefined" && document.documentElement.getAttribute("lang")) || "en";
|
|
412
|
+
code = String(code).toLowerCase().split("-")[0];
|
|
413
|
+
for (var i = 0; i < SCRIPTS.length; i++) if (SCRIPTS[i].lang === code) return SCRIPTS[i];
|
|
414
|
+
// An unlisted language tag (nl, pt, sv…) is Latin-script; defaulting to
|
|
415
|
+
// Latin keeps us from flagging every English word in a Dutch document.
|
|
416
|
+
return SCRIPTS[SCRIPTS.length - 1];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function hasLangAncestor(node, editable) {
|
|
420
|
+
var n = (node && node.nodeType === 3) ? node.parentNode : node;
|
|
421
|
+
while (n && n !== editable && n.nodeType === 1) {
|
|
422
|
+
if (n.hasAttribute && n.hasAttribute("lang")) return true;
|
|
423
|
+
n = n.parentNode;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function collectUnmarkedLanguageRuns(block, editable, path, issues) {
|
|
429
|
+
var docScript = documentScript();
|
|
430
|
+
var doc = block.ownerDocument;
|
|
431
|
+
if (!doc || !doc.createTreeWalker) return;
|
|
432
|
+
var walker = doc.createTreeWalker(block, 4 /* SHOW_TEXT */, null, false);
|
|
433
|
+
var textNode, seen = {};
|
|
434
|
+
while ((textNode = walker.nextNode())) {
|
|
435
|
+
var value = textNode.nodeValue || "";
|
|
436
|
+
if (!value.replace(/\s+/g, "")) continue;
|
|
437
|
+
if (hasLangAncestor(textNode, editable)) continue;
|
|
438
|
+
var scripts = scriptsIn(value);
|
|
439
|
+
for (var s = 0; s < scripts.length; s++) {
|
|
440
|
+
var script = scripts[s];
|
|
441
|
+
if (script === docScript) continue;
|
|
442
|
+
// One issue per script per block: a paragraph with six Hebrew
|
|
443
|
+
// words is one thing to fix, not six.
|
|
444
|
+
if (seen[script.name]) continue;
|
|
445
|
+
seen[script.name] = true;
|
|
446
|
+
issues.push({
|
|
447
|
+
code: "language-of-parts",
|
|
448
|
+
severity: "warning",
|
|
449
|
+
message: script.name + " text is not marked with a language. Select the phrase and set its " +
|
|
450
|
+
"language so assistive technology pronounces it correctly (WCAG 3.1.2).",
|
|
451
|
+
path: path + ".lang",
|
|
452
|
+
_target: block,
|
|
453
|
+
_lang: script.lang,
|
|
454
|
+
_script: script.name
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
358
460
|
function auditDomAccessibility() {
|
|
359
461
|
var editable = editor.getEditable ? editor.getEditable() : null;
|
|
360
462
|
var issues = [];
|
|
@@ -424,6 +526,8 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
424
526
|
});
|
|
425
527
|
}
|
|
426
528
|
}
|
|
529
|
+
|
|
530
|
+
collectUnmarkedLanguageRuns(node, editable, path, issues);
|
|
427
531
|
}
|
|
428
532
|
|
|
429
533
|
return { document: null, issues: issues, valid: !issues.length, source: "dom" };
|
|
@@ -449,12 +553,74 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
449
553
|
else if (issue.code === "table-missing-header") {
|
|
450
554
|
promoteFirstTableRow(target);
|
|
451
555
|
}
|
|
556
|
+
else if (issue.code === "language-of-parts") {
|
|
557
|
+
markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
|
|
558
|
+
}
|
|
452
559
|
|
|
453
560
|
selectedIssueIndex = 0;
|
|
454
561
|
scheduleEditorChange();
|
|
455
562
|
return runAudit();
|
|
456
563
|
}
|
|
457
564
|
|
|
565
|
+
// Wrap the offending script's runs in <span lang> — the same markup
|
|
566
|
+
// textpartlanguage.js produces, so the two features agree on one shape and
|
|
567
|
+
// the author can retarget the result from the Language dropdown afterwards.
|
|
568
|
+
//
|
|
569
|
+
// Only the matching characters (plus the spaces and punctuation BETWEEN
|
|
570
|
+
// them) are wrapped: wrapping the whole text node would relabel the
|
|
571
|
+
// surrounding English as Hebrew, which is a worse lie than leaving it
|
|
572
|
+
// unmarked.
|
|
573
|
+
function markLanguageRuns(block, lang, scriptName) {
|
|
574
|
+
if (!block || !lang) return;
|
|
575
|
+
var script = null;
|
|
576
|
+
for (var i = 0; i < SCRIPTS.length; i++) if (SCRIPTS[i].name === scriptName) script = SCRIPTS[i];
|
|
577
|
+
if (!script) return;
|
|
578
|
+
var doc = block.ownerDocument;
|
|
579
|
+
var editable = editor.getEditable ? editor.getEditable() : null;
|
|
580
|
+
var rtl = (lang === "he" || lang === "ar" || lang === "fa" || lang === "ur");
|
|
581
|
+
|
|
582
|
+
var walker = doc.createTreeWalker(block, 4 /* SHOW_TEXT */, null, false);
|
|
583
|
+
var pending = [], n;
|
|
584
|
+
while ((n = walker.nextNode())) {
|
|
585
|
+
if (hasLangAncestor(n, editable)) continue;
|
|
586
|
+
pending.push(n);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (var t = 0; t < pending.length; t++) {
|
|
590
|
+
var node = pending[t];
|
|
591
|
+
var value = node.nodeValue || "";
|
|
592
|
+
// Character classes are per-char; build a run matcher that also
|
|
593
|
+
// absorbs the neutral characters sitting inside a passage.
|
|
594
|
+
var runRe = new RegExp("(?:" + script.re.source + "|[\\s.,;:!?'\"()\\u2010-\\u2027\\u00AB\\u00BB\\u201C\\u201D])*" +
|
|
595
|
+
script.re.source +
|
|
596
|
+
"(?:" + script.re.source + "|[\\s.,;:!?'\"()\\u2010-\\u2027\\u00AB\\u00BB\\u201C\\u201D])*", "g");
|
|
597
|
+
var frag = doc.createDocumentFragment();
|
|
598
|
+
var last = 0, m, wrapped = false;
|
|
599
|
+
while ((m = runRe.exec(value))) {
|
|
600
|
+
var text = m[0];
|
|
601
|
+
// Trim the neutrals the greedy match pulled in at the edges;
|
|
602
|
+
// they belong to the surrounding sentence, not the phrase.
|
|
603
|
+
var lead = text.length - text.replace(/^[\s.,;:!?'"()‐-‧«»“”]+/, "").length;
|
|
604
|
+
var trail = text.length - text.replace(/[\s.,;:!?'"()‐-‧«»“”]+$/, "").length;
|
|
605
|
+
var start = m.index + lead;
|
|
606
|
+
var end = m.index + text.length - trail;
|
|
607
|
+
if (end - start < MIN_SCRIPT_RUN) continue;
|
|
608
|
+
if (start > last) frag.appendChild(doc.createTextNode(value.slice(last, start)));
|
|
609
|
+
var span = doc.createElement("span");
|
|
610
|
+
span.setAttribute("lang", lang);
|
|
611
|
+
if (rtl) span.setAttribute("dir", "rtl");
|
|
612
|
+
span.appendChild(doc.createTextNode(value.slice(start, end)));
|
|
613
|
+
frag.appendChild(span);
|
|
614
|
+
last = end;
|
|
615
|
+
wrapped = true;
|
|
616
|
+
if (runRe.lastIndex === m.index) runRe.lastIndex++; // zero-width guard
|
|
617
|
+
}
|
|
618
|
+
if (!wrapped) continue;
|
|
619
|
+
if (last < value.length) frag.appendChild(doc.createTextNode(value.slice(last)));
|
|
620
|
+
node.parentNode.replaceChild(frag, node);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
458
624
|
function promoteFirstTableRow(table) {
|
|
459
625
|
var firstRow = table && table.rows && table.rows.length ? table.rows[0] : null;
|
|
460
626
|
if (!firstRow) return;
|
|
@@ -603,6 +769,25 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
603
769
|
return;
|
|
604
770
|
}
|
|
605
771
|
|
|
772
|
+
if (issue.code === "language-of-parts") {
|
|
773
|
+
var langRow = detail.ownerDocument.createElement("div");
|
|
774
|
+
langRow.className = "rte-a11y-row";
|
|
775
|
+
var mark = detail.ownerDocument.createElement("button");
|
|
776
|
+
mark.type = "button";
|
|
777
|
+
mark.className = "rte-a11y-button";
|
|
778
|
+
mark.innerText = "Mark as " + issue._script;
|
|
779
|
+
mark.onclick = function () { repairIssue(selectedIssueIndex, { lang: issue._lang }); };
|
|
780
|
+
var look = detail.ownerDocument.createElement("button");
|
|
781
|
+
look.type = "button";
|
|
782
|
+
look.className = "rte-a11y-button rte-a11y-button-secondary";
|
|
783
|
+
look.innerText = "Focus issue";
|
|
784
|
+
look.onclick = function () { focusIssue(issue); };
|
|
785
|
+
langRow.appendChild(mark);
|
|
786
|
+
langRow.appendChild(look);
|
|
787
|
+
detail.appendChild(langRow);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
606
791
|
detail.innerHTML += '<div class="rte-a11y-detail-copy">This issue is visible here, but does not have an automatic repair action yet.</div>';
|
|
607
792
|
}
|
|
608
793
|
|
|
@@ -711,7 +896,7 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
|
711
896
|
|
|
712
897
|
if (!RTE_DefaultConfig.svgCode_aiassist) {
|
|
713
898
|
RTE_DefaultConfig.svgCode_aiassist = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true"><defs><linearGradient id="rte-ai-robot-shell" x1="5" y1="6" x2="19" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#1fb6ff"/><stop offset=".52" stop-color="#2563eb"/><stop offset="1" stop-color="#7c3aed"/></linearGradient><linearGradient id="rte-ai-robot-glow" x1="8" y1="10" x2="16" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#fef9c3"/><stop offset="1" stop-color="#fde68a"/></linearGradient></defs><path d="M12 2.4v2.4" stroke="#f59e0b" stroke-width="1.7" stroke-linecap="round"/><circle cx="12" cy="2.4" r="1" fill="#fbbf24"/><rect x="5.1" y="6.1" width="13.8" height="11.2" rx="3.4" fill="url(#rte-ai-robot-shell)" stroke="#1e40af" stroke-width="1.2"/><path d="M8.2 6.1V5.8A1.8 1.8 0 0110 4h4a1.8 1.8 0 011.8 1.8v.3" stroke="#93c5fd" stroke-width="1.2" stroke-linecap="round"/><rect x="7.7" y="9.2" width="8.6" height="4.8" rx="2.4" fill="#0f172a" opacity=".26"/><circle cx="10" cy="11.6" r="1.15" fill="url(#rte-ai-robot-glow)"/><circle cx="14" cy="11.6" r="1.15" fill="url(#rte-ai-robot-glow)"/><path d="M9.4 14.9c.8.55 1.66.83 2.6.83s1.8-.28 2.6-.83" stroke="#ffffff" stroke-width="1.45" stroke-linecap="round"/><path d="M8.1 19.2l1-2.1M15.9 19.2l-1-2.1" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/><path d="M18.8 6.4l.55-1.25.55 1.25 1.25.55-1.25.55-.55 1.25-.55-1.25-1.25-.55z" fill="#fef08a"/></svg>';
|
|
714
|
-
RTE_DefaultConfig.svgCode_aiassist = '<svg viewBox="
|
|
899
|
+
RTE_DefaultConfig.svgCode_aiassist = '<svg viewBox="1.78 0.3 21.8 21.8" width="20" height="20" fill="none" aria-hidden="true"><defs><linearGradient id="rte-ai-bot-v5-shell" x1="3" y1="5" x2="21" y2="20" gradientUnits="userSpaceOnUse"><stop stop-color="#22d3ee"/><stop offset=".52" stop-color="#2563eb"/><stop offset="1" stop-color="#4338ca"/></linearGradient><linearGradient id="rte-ai-bot-v5-visor" x1="7" y1="8" x2="17" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#f8fafc"/><stop offset="1" stop-color="#bfdbfe"/></linearGradient></defs><rect x="2.6" y="5.2" width="16.4" height="16" rx="4.8" fill="url(#rte-ai-bot-v5-shell)" stroke="#1e40af" stroke-width="1.6"/><rect x="5.9" y="9.1" width="9.8" height="6.8" rx="3.4" fill="url(#rte-ai-bot-v5-visor)"/><circle cx="8.6" cy="12.5" r="1.35" fill="#0f172a"/><circle cx="13" cy="12.5" r="1.35" fill="#0f172a"/><path d="M8.3 18.2c.9.7 1.8 1 2.5 1s1.6-.3 2.5-1" stroke="#eff6ff" stroke-width="1.7" stroke-linecap="round"/><path d="M19.6 .4l1.25 2.7 2.7 1.25-2.7 1.25-1.25 2.7-1.25-2.7-2.7-1.25 2.7-1.25z" fill="#fbbf24"/></svg>';
|
|
715
900
|
}
|
|
716
901
|
|
|
717
902
|
if (!RTE_DefaultConfig.svgCode_aiassist_open_dialog) {
|
|
@@ -27519,6 +27704,372 @@ function RTE_Plugin_FormattingMarks() {
|
|
|
27519
27704
|
|
|
27520
27705
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
27521
27706
|
|
|
27707
|
+
// 2026-08-15 Gap cursor — reach the unreachable places in a document.
|
|
27708
|
+
//
|
|
27709
|
+
// A document that ENDS with a table cannot be added to. The caret goes into the
|
|
27710
|
+
// last cell and stays there: ArrowDown, ArrowRight and End all leave it where it
|
|
27711
|
+
// was, because there is no node after the table to put it in. Same story for a
|
|
27712
|
+
// document that STARTS with a table, and for two adjacent tables with no
|
|
27713
|
+
// paragraph between them — there is no caret position in the gap, so the author
|
|
27714
|
+
// cannot type there at all. Verified on the running editor before writing this:
|
|
27715
|
+
// the caret stayed at TABLE>TBODY>TR>TD through every key.
|
|
27716
|
+
//
|
|
27717
|
+
// This is the single most-reported "the editor is broken" issue in every
|
|
27718
|
+
// WYSIWYG that lacks it. Froala ships it as `lineBreaker`, Tiptap as
|
|
27719
|
+
// `gapcursor`, and ProseMirror has it in core. We had it under no name.
|
|
27720
|
+
//
|
|
27721
|
+
// Design notes:
|
|
27722
|
+
// - ON DEMAND, never eager. The obvious fix — always append a trailing empty
|
|
27723
|
+
// paragraph — mutates every document that ends in a table, so getHTMLCode()
|
|
27724
|
+
// starts returning markup the author did not write and a round-trip through
|
|
27725
|
+
// the editor is no longer lossless. A paragraph is created only when the
|
|
27726
|
+
// author actually asks for one by pressing a key or clicking the gap.
|
|
27727
|
+
// - Clicking the blank space below the content is the gesture people already
|
|
27728
|
+
// have from Google Docs and Notion, and it needs no overlay DOM: a click
|
|
27729
|
+
// that lands on the editable ITSELF (rather than on a child) is by
|
|
27730
|
+
// definition a click in a gap. The child list then says which gap.
|
|
27731
|
+
// - Only TRAPPING blocks count. A trailing <p> is already reachable, so
|
|
27732
|
+
// offering to add another one after it would just spam empty paragraphs.
|
|
27733
|
+
// - The inserted paragraph carries a <br> filler. An empty <p> has zero
|
|
27734
|
+
// height in most engines, so without it the caret lands somewhere invisible
|
|
27735
|
+
// and the author thinks nothing happened.
|
|
27736
|
+
RTE_DefaultConfig.plugin_gapcursor = RTE_Plugin_GapCursor;
|
|
27737
|
+
|
|
27738
|
+
// Click the empty space below/around content to create a paragraph there.
|
|
27739
|
+
if (typeof RTE_DefaultConfig.gapCursorClick === "undefined") RTE_DefaultConfig.gapCursorClick = true;
|
|
27740
|
+
// Arrow/Home/End keys escape a trapping block at the document edge.
|
|
27741
|
+
if (typeof RTE_DefaultConfig.gapCursorKeys === "undefined") RTE_DefaultConfig.gapCursorKeys = true;
|
|
27742
|
+
// Show a thin insertion hint when hovering a gap between two trapping blocks.
|
|
27743
|
+
if (typeof RTE_DefaultConfig.gapCursorHint === "undefined") RTE_DefaultConfig.gapCursorHint = true;
|
|
27744
|
+
|
|
27745
|
+
function RTE_Plugin_GapCursor() {
|
|
27746
|
+
var obj = this;
|
|
27747
|
+
var config, editor;
|
|
27748
|
+
var boundDoc = null;
|
|
27749
|
+
|
|
27750
|
+
obj.PluginName = "GapCursor";
|
|
27751
|
+
|
|
27752
|
+
// Blocks that cannot host a caret directly beside them. A caret can always
|
|
27753
|
+
// be placed inside a paragraph or heading, so those are never "trapping".
|
|
27754
|
+
var TRAPPING = {
|
|
27755
|
+
TABLE: 1, HR: 1, IMG: 1, FIGURE: 1, IFRAME: 1, VIDEO: 1, AUDIO: 1,
|
|
27756
|
+
OBJECT: 1, EMBED: 1, CANVAS: 1, SVG: 1, PRE: 1
|
|
27757
|
+
};
|
|
27758
|
+
|
|
27759
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
27760
|
+
|
|
27761
|
+
obj.InitEditor = function (argeditor) {
|
|
27762
|
+
editor = argeditor;
|
|
27763
|
+
|
|
27764
|
+
bind();
|
|
27765
|
+
try { editor.attachEvent("ready", bind); } catch (e) {}
|
|
27766
|
+
try { editor.attachEvent("aftersethtml", bind); } catch (e) {}
|
|
27767
|
+
setTimeout(bind, 0);
|
|
27768
|
+
|
|
27769
|
+
// Public API.
|
|
27770
|
+
editor.insertParagraphAt = function (index) { return insertAt(index); };
|
|
27771
|
+
editor.insertParagraphAfter = function (el) { return insertAround(el, true); };
|
|
27772
|
+
editor.insertParagraphBefore = function (el) { return insertAround(el, false); };
|
|
27773
|
+
editor.getGapPositions = function () { return gaps(); };
|
|
27774
|
+
};
|
|
27775
|
+
|
|
27776
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
27777
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
27778
|
+
|
|
27779
|
+
function bind() {
|
|
27780
|
+
var doc = getDoc();
|
|
27781
|
+
var ed = getEditable();
|
|
27782
|
+
if (!doc || !ed || doc === boundDoc) return;
|
|
27783
|
+
boundDoc = doc;
|
|
27784
|
+
injectStyles(doc);
|
|
27785
|
+
|
|
27786
|
+
if (config.gapCursorKeys !== false) ed.addEventListener("keydown", onKeyDown, true);
|
|
27787
|
+
if (config.gapCursorClick !== false) ed.addEventListener("click", onClick, true);
|
|
27788
|
+
if (config.gapCursorHint !== false) ed.addEventListener("mousemove", onMove, true);
|
|
27789
|
+
}
|
|
27790
|
+
|
|
27791
|
+
function isTrapping(el) {
|
|
27792
|
+
return !!(el && el.nodeType === 1 && TRAPPING[el.nodeName]);
|
|
27793
|
+
}
|
|
27794
|
+
|
|
27795
|
+
function isHint(el) {
|
|
27796
|
+
return !!(el && el.nodeType === 1 && el.className === "rte-gap-hint");
|
|
27797
|
+
}
|
|
27798
|
+
|
|
27799
|
+
// The editable's real children, with the hover hint filtered out.
|
|
27800
|
+
//
|
|
27801
|
+
// The hint is itself a child of the editable, so any code that reasons about
|
|
27802
|
+
// positions via ed.children sees it as a block: it shifts every index after
|
|
27803
|
+
// it, and because a <div> is not "trapping" its presence as the last child
|
|
27804
|
+
// made the trailing gap disappear entirely — hovering below a table removed
|
|
27805
|
+
// the very gap the hover was advertising, and the click then did nothing.
|
|
27806
|
+
function blocks() {
|
|
27807
|
+
var ed = getEditable();
|
|
27808
|
+
var out = [];
|
|
27809
|
+
if (!ed) return out;
|
|
27810
|
+
for (var i = 0; i < ed.children.length; i++) {
|
|
27811
|
+
if (!isHint(ed.children[i])) out.push(ed.children[i]);
|
|
27812
|
+
}
|
|
27813
|
+
return out;
|
|
27814
|
+
}
|
|
27815
|
+
|
|
27816
|
+
// Every index where a paragraph could usefully be inserted: before a leading
|
|
27817
|
+
// trapping block, after a trailing one, and between two adjacent ones.
|
|
27818
|
+
function gaps() {
|
|
27819
|
+
var out = [];
|
|
27820
|
+
var kids = blocks();
|
|
27821
|
+
if (!kids.length) return out;
|
|
27822
|
+
if (isTrapping(kids[0])) out.push(0);
|
|
27823
|
+
for (var i = 0; i < kids.length - 1; i++) {
|
|
27824
|
+
if (isTrapping(kids[i]) && isTrapping(kids[i + 1])) out.push(i + 1);
|
|
27825
|
+
}
|
|
27826
|
+
if (isTrapping(kids[kids.length - 1])) out.push(kids.length);
|
|
27827
|
+
return out;
|
|
27828
|
+
}
|
|
27829
|
+
|
|
27830
|
+
// ---- insertion -------------------------------------------------------
|
|
27831
|
+
|
|
27832
|
+
function makeParagraph(doc) {
|
|
27833
|
+
var p = doc.createElement("p");
|
|
27834
|
+
// An empty <p> collapses to zero height, so the caret would land
|
|
27835
|
+
// somewhere the author cannot see.
|
|
27836
|
+
p.appendChild(doc.createElement("br"));
|
|
27837
|
+
return p;
|
|
27838
|
+
}
|
|
27839
|
+
|
|
27840
|
+
function insertAt(index) {
|
|
27841
|
+
var ed = getEditable();
|
|
27842
|
+
var doc = getDoc();
|
|
27843
|
+
if (!ed || !doc) return null;
|
|
27844
|
+
// Resolve against the hint-free list, then insert relative to that node.
|
|
27845
|
+
hideHint();
|
|
27846
|
+
var p = makeParagraph(doc);
|
|
27847
|
+
var before = blocks()[index] || null;
|
|
27848
|
+
ed.insertBefore(p, before);
|
|
27849
|
+
focusParagraph(p);
|
|
27850
|
+
fireChange();
|
|
27851
|
+
return p;
|
|
27852
|
+
}
|
|
27853
|
+
|
|
27854
|
+
function insertAround(el, after) {
|
|
27855
|
+
var ed = getEditable();
|
|
27856
|
+
var doc = getDoc();
|
|
27857
|
+
if (!ed || !doc || !el) return null;
|
|
27858
|
+
var p = makeParagraph(doc);
|
|
27859
|
+
if (after) el.parentNode.insertBefore(p, el.nextSibling);
|
|
27860
|
+
else el.parentNode.insertBefore(p, el);
|
|
27861
|
+
focusParagraph(p);
|
|
27862
|
+
fireChange();
|
|
27863
|
+
return p;
|
|
27864
|
+
}
|
|
27865
|
+
|
|
27866
|
+
function focusParagraph(p) {
|
|
27867
|
+
try {
|
|
27868
|
+
var doc = p.ownerDocument;
|
|
27869
|
+
var r = doc.createRange();
|
|
27870
|
+
r.setStart(p, 0);
|
|
27871
|
+
r.collapse(true);
|
|
27872
|
+
var sel = editor.getSelection();
|
|
27873
|
+
sel.removeAllRanges();
|
|
27874
|
+
sel.addRange(r);
|
|
27875
|
+
editor.focus();
|
|
27876
|
+
} catch (e) {}
|
|
27877
|
+
}
|
|
27878
|
+
|
|
27879
|
+
// ---- keyboard --------------------------------------------------------
|
|
27880
|
+
|
|
27881
|
+
// The top-level child of the editable that contains this node.
|
|
27882
|
+
function topBlockOf(node) {
|
|
27883
|
+
var ed = getEditable();
|
|
27884
|
+
var n = (node && node.nodeType === 3) ? node.parentNode : node;
|
|
27885
|
+
while (n && n.parentNode && n.parentNode !== ed) n = n.parentNode;
|
|
27886
|
+
return (n && n.parentNode === ed) ? n : null;
|
|
27887
|
+
}
|
|
27888
|
+
|
|
27889
|
+
function onKeyDown(e) {
|
|
27890
|
+
var key = e.key;
|
|
27891
|
+
var forward = (key === "ArrowDown" || key === "ArrowRight" || key === "End" || key === "PageDown");
|
|
27892
|
+
var back = (key === "ArrowUp" || key === "ArrowLeft" || key === "Home" || key === "PageUp");
|
|
27893
|
+
if (!forward && !back) return;
|
|
27894
|
+
|
|
27895
|
+
var ed = getEditable();
|
|
27896
|
+
if (!ed || !ed.children.length) return;
|
|
27897
|
+
|
|
27898
|
+
var sel = editor.getSelection();
|
|
27899
|
+
if (!sel || !sel.rangeCount) return;
|
|
27900
|
+
var block = topBlockOf(sel.getRangeAt(0).startContainer);
|
|
27901
|
+
if (!isTrapping(block)) return;
|
|
27902
|
+
|
|
27903
|
+
var kids = blocks();
|
|
27904
|
+
var index = kids.indexOf(block);
|
|
27905
|
+
if (index < 0) return;
|
|
27906
|
+
|
|
27907
|
+
if (forward) {
|
|
27908
|
+
var next = kids[index + 1];
|
|
27909
|
+
// Escape only where the author is genuinely stuck: nothing after the
|
|
27910
|
+
// block, or another trapping block immediately after it.
|
|
27911
|
+
if (next && !isTrapping(next)) return;
|
|
27912
|
+
e.preventDefault();
|
|
27913
|
+
e.stopPropagation();
|
|
27914
|
+
insertAt(index + 1);
|
|
27915
|
+
} else {
|
|
27916
|
+
var prev = kids[index - 1];
|
|
27917
|
+
if (prev && !isTrapping(prev)) return;
|
|
27918
|
+
e.preventDefault();
|
|
27919
|
+
e.stopPropagation();
|
|
27920
|
+
insertAt(index);
|
|
27921
|
+
}
|
|
27922
|
+
}
|
|
27923
|
+
|
|
27924
|
+
// ---- click in a gap --------------------------------------------------
|
|
27925
|
+
|
|
27926
|
+
// A click that lands on the editable itself, not on any child, is a click in
|
|
27927
|
+
// the space between or after blocks.
|
|
27928
|
+
function onClick(e) {
|
|
27929
|
+
var ed = getEditable();
|
|
27930
|
+
if (!ed || e.target !== ed) return;
|
|
27931
|
+
// Measure the gaps as they are without the hint, or the hint's own box
|
|
27932
|
+
// shifts the geometry the click is being resolved against.
|
|
27933
|
+
hideHint();
|
|
27934
|
+
var index = gapIndexAt(e.clientY);
|
|
27935
|
+
if (index === null) return;
|
|
27936
|
+
e.preventDefault();
|
|
27937
|
+
e.stopPropagation();
|
|
27938
|
+
insertAt(index);
|
|
27939
|
+
}
|
|
27940
|
+
|
|
27941
|
+
// Which gap does this viewport Y fall in? Below every block means the end.
|
|
27942
|
+
function gapIndexAt(clientY) {
|
|
27943
|
+
var kids = blocks();
|
|
27944
|
+
if (!kids.length) return null;
|
|
27945
|
+
var available = gaps();
|
|
27946
|
+
if (!available.length) return null;
|
|
27947
|
+
|
|
27948
|
+
for (var i = 0; i < kids.length; i++) {
|
|
27949
|
+
var rect = kids[i].getBoundingClientRect();
|
|
27950
|
+
if (clientY < rect.top) return allowed(available, i);
|
|
27951
|
+
if (clientY <= rect.bottom) return null; // on the block itself
|
|
27952
|
+
}
|
|
27953
|
+
return allowed(available, kids.length);
|
|
27954
|
+
}
|
|
27955
|
+
|
|
27956
|
+
function allowed(available, index) {
|
|
27957
|
+
return available.indexOf(index) >= 0 ? index : null;
|
|
27958
|
+
}
|
|
27959
|
+
|
|
27960
|
+
// ---- hover hint ------------------------------------------------------
|
|
27961
|
+
|
|
27962
|
+
var hint = null;
|
|
27963
|
+
var hintIndex = null;
|
|
27964
|
+
|
|
27965
|
+
// How close to a block boundary counts as "aiming at the gap".
|
|
27966
|
+
var EDGE_TOLERANCE = 10;
|
|
27967
|
+
|
|
27968
|
+
function onMove(e) {
|
|
27969
|
+
var ed = getEditable();
|
|
27970
|
+
if (!ed) return;
|
|
27971
|
+
if (isHint(e.target)) return; // already on the hint; leave it alone
|
|
27972
|
+
|
|
27973
|
+
// Blank space below/around the content: the click will land on the
|
|
27974
|
+
// editable itself and the geometry alone is enough.
|
|
27975
|
+
if (e.target === ed) {
|
|
27976
|
+
var index = gapIndexAt(e.clientY);
|
|
27977
|
+
if (index === null) { hideHint(); return; }
|
|
27978
|
+
showHint(index);
|
|
27979
|
+
return;
|
|
27980
|
+
}
|
|
27981
|
+
|
|
27982
|
+
// Two adjacent tables have ZERO pixels between them, so a click there
|
|
27983
|
+
// can never reach the editable — it always lands on one of the tables.
|
|
27984
|
+
// The gap is unreachable by mouse unless we put something in it, so
|
|
27985
|
+
// hovering near the boundary materialises the hint and the hint itself
|
|
27986
|
+
// becomes the click target.
|
|
27987
|
+
var near = gapNearEdge(e.clientY);
|
|
27988
|
+
if (near === null) { hideHint(); return; }
|
|
27989
|
+
showHint(near);
|
|
27990
|
+
}
|
|
27991
|
+
|
|
27992
|
+
// A gap whose boundary is within EDGE_TOLERANCE of this Y.
|
|
27993
|
+
function gapNearEdge(clientY) {
|
|
27994
|
+
var kids = blocks();
|
|
27995
|
+
var available = gaps();
|
|
27996
|
+
for (var g = 0; g < available.length; g++) {
|
|
27997
|
+
var i = available[g];
|
|
27998
|
+
var edge;
|
|
27999
|
+
if (i === 0) edge = kids[0].getBoundingClientRect().top;
|
|
28000
|
+
else edge = kids[i - 1].getBoundingClientRect().bottom;
|
|
28001
|
+
if (Math.abs(clientY - edge) <= EDGE_TOLERANCE) return i;
|
|
28002
|
+
}
|
|
28003
|
+
return null;
|
|
28004
|
+
}
|
|
28005
|
+
|
|
28006
|
+
function showHint(index) {
|
|
28007
|
+
var ed = getEditable();
|
|
28008
|
+
var doc = getDoc();
|
|
28009
|
+
if (!ed || !doc) return;
|
|
28010
|
+
if (!hint) {
|
|
28011
|
+
hint = doc.createElement("div");
|
|
28012
|
+
hint.className = "rte-gap-hint";
|
|
28013
|
+
hint.setAttribute("contenteditable", "false");
|
|
28014
|
+
hint.setAttribute("aria-hidden", "true");
|
|
28015
|
+
// The hint is the only clickable thing in a zero-height gap.
|
|
28016
|
+
hint.addEventListener("mousedown", function (ev) {
|
|
28017
|
+
ev.preventDefault();
|
|
28018
|
+
ev.stopPropagation();
|
|
28019
|
+
if (hintIndex !== null) insertAt(hintIndex);
|
|
28020
|
+
}, true);
|
|
28021
|
+
}
|
|
28022
|
+
var before = blocks()[index] || null;
|
|
28023
|
+
// Re-inserting in place would thrash the DOM on every mousemove.
|
|
28024
|
+
if (hint.parentNode === ed && hint.nextSibling === before) { hintIndex = index; return; }
|
|
28025
|
+
ed.insertBefore(hint, before);
|
|
28026
|
+
hintIndex = index;
|
|
28027
|
+
}
|
|
28028
|
+
|
|
28029
|
+
function hideHint() {
|
|
28030
|
+
if (hint && hint.parentNode) hint.parentNode.removeChild(hint);
|
|
28031
|
+
hintIndex = null;
|
|
28032
|
+
}
|
|
28033
|
+
|
|
28034
|
+
function fireChange() {
|
|
28035
|
+
// The hint is decoration; it must never be in the document when the
|
|
28036
|
+
// content is read.
|
|
28037
|
+
hideHint();
|
|
28038
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
28039
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
28040
|
+
}
|
|
28041
|
+
|
|
28042
|
+
// ---- styles ----------------------------------------------------------
|
|
28043
|
+
|
|
28044
|
+
function css() {
|
|
28045
|
+
// A real hit area, not just a line: in a zero-height gap between two
|
|
28046
|
+
// tables this element IS the click target, so it needs height.
|
|
28047
|
+
return ".rte-gap-hint{height:6px;margin:1px 0;position:relative;cursor:text;" +
|
|
28048
|
+
"pointer-events:auto;background:transparent;}" +
|
|
28049
|
+
".rte-gap-hint::before{content:'';position:absolute;left:0;right:0;top:2px;" +
|
|
28050
|
+
"border-top:2px solid #2563eb;opacity:.65;}" +
|
|
28051
|
+
".rte-gap-hint::after{content:'';position:absolute;left:-3px;top:0;width:6px;height:6px;" +
|
|
28052
|
+
"border-radius:50%;background:#2563eb;}";
|
|
28053
|
+
}
|
|
28054
|
+
|
|
28055
|
+
function injectStyles(doc) {
|
|
28056
|
+
if (!doc) return;
|
|
28057
|
+
var text = css();
|
|
28058
|
+
var existing = doc.getElementById("rte-gapcursor-styles");
|
|
28059
|
+
if (existing) {
|
|
28060
|
+
if (existing.getAttribute("data-css") === text) return;
|
|
28061
|
+
existing.parentNode && existing.parentNode.removeChild(existing);
|
|
28062
|
+
}
|
|
28063
|
+
var st = doc.createElement("style");
|
|
28064
|
+
st.id = "rte-gapcursor-styles";
|
|
28065
|
+
st.setAttribute("data-css", text);
|
|
28066
|
+
st.appendChild(doc.createTextNode(text));
|
|
28067
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
28068
|
+
}
|
|
28069
|
+
}
|
|
28070
|
+
|
|
28071
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
28072
|
+
|
|
27522
28073
|
// 2026-06-06 AI ghost-text autocomplete. GitHub-Copilot / Notion-AI-style inline
|
|
27523
28074
|
// completion: after you pause typing, a greyed suggestion appears after the caret
|
|
27524
28075
|
// continuing your sentence; press Tab to accept it, Esc (or just keep typing) to
|
|
@@ -28123,7 +28674,22 @@ function RTE_Plugin_ImageEditor() {
|
|
|
28123
28674
|
return tag;
|
|
28124
28675
|
}
|
|
28125
28676
|
|
|
28126
|
-
|
|
28677
|
+
var MIME_EXTENSION = {
|
|
28678
|
+
"image/png": ".png",
|
|
28679
|
+
"image/jpeg": ".jpg",
|
|
28680
|
+
"image/jpg": ".jpg",
|
|
28681
|
+
"image/webp": ".webp",
|
|
28682
|
+
"image/gif": ".gif",
|
|
28683
|
+
"image/bmp": ".bmp",
|
|
28684
|
+
"image/avif": ".avif"
|
|
28685
|
+
};
|
|
28686
|
+
|
|
28687
|
+
// Returns a File, not a bare Blob. Upload handlers identify the file by its
|
|
28688
|
+
// .name - a Blob has none, so the edited image was posted with an undefined
|
|
28689
|
+
// name and every server rejected it ("Invalid file extension"), surfacing as
|
|
28690
|
+
// "Upload failed. Check your connection and try again." when the network was
|
|
28691
|
+
// perfectly fine.
|
|
28692
|
+
function dataURLToBlob(dataurl, filename) {
|
|
28127
28693
|
var arr = dataurl.split(',');
|
|
28128
28694
|
var mime = arr[0].match(/:(.*?);/)[1];
|
|
28129
28695
|
var bstr = atob(arr[1]);
|
|
@@ -28132,7 +28698,16 @@ function RTE_Plugin_ImageEditor() {
|
|
|
28132
28698
|
while (n--) {
|
|
28133
28699
|
u8arr[n] = bstr.charCodeAt(n);
|
|
28134
28700
|
}
|
|
28135
|
-
|
|
28701
|
+
var name = filename || ("image-" + new Date().getTime() + (MIME_EXTENSION[mime] || ".png"));
|
|
28702
|
+
try {
|
|
28703
|
+
return new File([u8arr], name, { type: mime });
|
|
28704
|
+
} catch (e) {
|
|
28705
|
+
// Very old browsers have no File constructor. Keep the name reachable
|
|
28706
|
+
// so a handler reading file.name still works.
|
|
28707
|
+
var blob = new Blob([u8arr], { type: mime });
|
|
28708
|
+
blob.name = name;
|
|
28709
|
+
return blob;
|
|
28710
|
+
}
|
|
28136
28711
|
}
|
|
28137
28712
|
|
|
28138
28713
|
obj.DoImageEditor = function () {
|
|
@@ -28239,8 +28814,11 @@ function RTE_Plugin_ImageEditor() {
|
|
|
28239
28814
|
}
|
|
28240
28815
|
savebtn.disabled = false;
|
|
28241
28816
|
savebtn.innerText = "Save";
|
|
28817
|
+
// Show what the server actually said. Blaming the connection sent
|
|
28818
|
+
// people chasing a network problem when the real answer was a
|
|
28819
|
+
// rejected file type or a size limit.
|
|
28242
28820
|
status.innerText = error
|
|
28243
|
-
? "Upload failed
|
|
28821
|
+
? ("Upload failed: " + error)
|
|
28244
28822
|
: "The upload did not return a file URL. Please try again.";
|
|
28245
28823
|
});
|
|
28246
28824
|
|
|
@@ -28268,6 +28846,319 @@ function RTE_Plugin_ImageEditor() {
|
|
|
28268
28846
|
|
|
28269
28847
|
|
|
28270
28848
|
|
|
28849
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
28850
|
+
|
|
28851
|
+
// 2026-08-15 Import styles from the page's own stylesheets.
|
|
28852
|
+
//
|
|
28853
|
+
// The four Quick Styles dropdowns (inlineStyles / paragraphStyles / imageStyles
|
|
28854
|
+
// / linkStyles) have always been hand-configured, and the shipped defaults are
|
|
28855
|
+
// placeholders — "my-cls-mark", "my-cls-quote" — that mean nothing in a real
|
|
28856
|
+
// integration. Every team wiring this editor into an existing design system
|
|
28857
|
+
// therefore retypes their own class list into config before the dropdowns are
|
|
28858
|
+
// worth opening. TinyMCE removes that step with `importcss`; this is our
|
|
28859
|
+
// equivalent.
|
|
28860
|
+
//
|
|
28861
|
+
// Design notes:
|
|
28862
|
+
// - OPT-IN (`importStyles = true`). Silently rewriting the Styles menu out of
|
|
28863
|
+
// whatever CSS happens to be on the page would be a surprising default, and
|
|
28864
|
+
// on a utility-CSS site it would be a catastrophic one.
|
|
28865
|
+
// - The ELEMENT QUALIFIER decides the bucket: `img.hero` is an image style,
|
|
28866
|
+
// `a.cta` a link style, `p.lede` a paragraph style. An unqualified `.foo`
|
|
28867
|
+
// cannot be placed, so it goes in both the inline and paragraph menus —
|
|
28868
|
+
// the author picks by what they have selected.
|
|
28869
|
+
// - Cross-origin stylesheets throw a SecurityError on `.cssRules`, and it is
|
|
28870
|
+
// thrown on ACCESS, not returned as null. One un-caught sheet from a CDN
|
|
28871
|
+
// kills the whole scan, which is the classic way this feature fails.
|
|
28872
|
+
// - `@media` / `@supports` blocks hold their own `cssRules`; a design system's
|
|
28873
|
+
// responsive variants live in there, so the walk recurses.
|
|
28874
|
+
// - There is a HARD CAP. A Tailwind or Bootstrap page exposes thousands of
|
|
28875
|
+
// atomic classes, and a dropdown with 4,000 entries is not a feature. When
|
|
28876
|
+
// the cap truncates, it says so via console + `getImportedStyles().truncated`
|
|
28877
|
+
// rather than quietly showing the first hundred as if that were all of them.
|
|
28878
|
+
RTE_DefaultConfig.plugin_importstyles = RTE_Plugin_ImportStyles;
|
|
28879
|
+
|
|
28880
|
+
// Master switch.
|
|
28881
|
+
if (typeof RTE_DefaultConfig.importStyles === "undefined") RTE_DefaultConfig.importStyles = false;
|
|
28882
|
+
|
|
28883
|
+
// Which stylesheets to read. null = all readable ones. A string/RegExp is
|
|
28884
|
+
// matched against the sheet's href ("site.css", /theme/), a function gets the
|
|
28885
|
+
// CSSStyleSheet.
|
|
28886
|
+
if (typeof RTE_DefaultConfig.importStylesSheets === "undefined") RTE_DefaultConfig.importStylesSheets = null;
|
|
28887
|
+
|
|
28888
|
+
// Classes never imported: the editor's own chrome, and the other editors'
|
|
28889
|
+
// (a page migrating from CKEditor/TinyMCE still has their CSS loaded).
|
|
28890
|
+
if (typeof RTE_DefaultConfig.importStylesExclude === "undefined") {
|
|
28891
|
+
RTE_DefaultConfig.importStylesExclude = /^(rte[-_]|ck[-_]|cke[-_]|mce[-_]|tox[-_]|js[-_]|is[-_]|has[-_])/i;
|
|
28892
|
+
}
|
|
28893
|
+
|
|
28894
|
+
// Per-class predicate. Return false to skip, a string to override the label.
|
|
28895
|
+
// function (className, selectorText, bucket) {}
|
|
28896
|
+
if (typeof RTE_DefaultConfig.importStylesFilter === "undefined") RTE_DefaultConfig.importStylesFilter = null;
|
|
28897
|
+
|
|
28898
|
+
// Replace the configured styles instead of appending to them.
|
|
28899
|
+
if (typeof RTE_DefaultConfig.importStylesReplace === "undefined") RTE_DefaultConfig.importStylesReplace = false;
|
|
28900
|
+
|
|
28901
|
+
// Ceiling per dropdown.
|
|
28902
|
+
if (typeof RTE_DefaultConfig.importStylesLimit === "undefined") RTE_DefaultConfig.importStylesLimit = 100;
|
|
28903
|
+
|
|
28904
|
+
function RTE_Plugin_ImportStyles() {
|
|
28905
|
+
var obj = this;
|
|
28906
|
+
var config, editor;
|
|
28907
|
+
var imported = null;
|
|
28908
|
+
var scannedDocs = [];
|
|
28909
|
+
|
|
28910
|
+
obj.PluginName = "ImportStyles";
|
|
28911
|
+
|
|
28912
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
28913
|
+
|
|
28914
|
+
obj.InitEditor = function (argeditor) {
|
|
28915
|
+
editor = argeditor;
|
|
28916
|
+
|
|
28917
|
+
// Public API — usable even with importStyles off, so an integrator can
|
|
28918
|
+
// inspect what WOULD be imported before switching it on.
|
|
28919
|
+
editor.importStyles = function (options) { return run(options || {}); };
|
|
28920
|
+
editor.getImportedStyles = function () { return imported; };
|
|
28921
|
+
editor.scanStylesheetClasses = function (options) {
|
|
28922
|
+
activeOptions = options || {};
|
|
28923
|
+
try { return collect(); } finally { activeOptions = {}; }
|
|
28924
|
+
};
|
|
28925
|
+
|
|
28926
|
+
if (!config.importStyles) return;
|
|
28927
|
+
|
|
28928
|
+
// The host document's sheets are ready now; the editing document's are
|
|
28929
|
+
// not, so scan again once it exists. The dropdown panels read config
|
|
28930
|
+
// when they OPEN, so a later scan still lands.
|
|
28931
|
+
run({});
|
|
28932
|
+
try { editor.attachEvent("ready", function () { run({}); }); } catch (e) {}
|
|
28933
|
+
setTimeout(function () { run({}); }, 0);
|
|
28934
|
+
};
|
|
28935
|
+
|
|
28936
|
+
// ---- buckets ---------------------------------------------------------
|
|
28937
|
+
|
|
28938
|
+
var IMAGE_TAGS = { img: 1, figure: 1, picture: 1 };
|
|
28939
|
+
var LINK_TAGS = { a: 1 };
|
|
28940
|
+
var INLINE_TAGS = { span: 1, strong: 1, b: 1, em: 1, i: 1, code: 1, mark: 1, small: 1, sub: 1, sup: 1, abbr: 1, q: 1, cite: 1 };
|
|
28941
|
+
var BLOCK_TAGS = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, blockquote: 1, li: 1, ul: 1, ol: 1, pre: 1, section: 1, article: 1, aside: 1, figcaption: 1, td: 1, th: 1, table: 1 };
|
|
28942
|
+
|
|
28943
|
+
var TARGETS = [
|
|
28944
|
+
{ key: "imageStyles", tags: IMAGE_TAGS },
|
|
28945
|
+
{ key: "linkStyles", tags: LINK_TAGS },
|
|
28946
|
+
{ key: "inlineStyles", tags: INLINE_TAGS },
|
|
28947
|
+
{ key: "paragraphStyles", tags: BLOCK_TAGS }
|
|
28948
|
+
];
|
|
28949
|
+
|
|
28950
|
+
// The last class in the selector is the one the author would apply; the
|
|
28951
|
+
// element qualifier immediately before it says what it applies TO.
|
|
28952
|
+
// "article .card p.lede:hover" -> tag "p", class "lede".
|
|
28953
|
+
var SELECTOR_RE = /^([a-zA-Z][\w-]*)?\.([\w-]+)$/;
|
|
28954
|
+
|
|
28955
|
+
function parseSelector(selector) {
|
|
28956
|
+
// Only the rightmost compound matters; strip pseudo-classes/elements
|
|
28957
|
+
// and attribute filters, which are states of a class, not new classes.
|
|
28958
|
+
var simple = String(selector)
|
|
28959
|
+
.replace(/\[[^\]]*\]/g, "")
|
|
28960
|
+
.replace(/::?[\w-]+(\([^)]*\))?/g, "")
|
|
28961
|
+
.trim();
|
|
28962
|
+
var parts = simple.split(/[\s>+~]+/);
|
|
28963
|
+
var last = parts[parts.length - 1] || "";
|
|
28964
|
+
// A compound like "p.lede.wide" has no single applicable class.
|
|
28965
|
+
if ((last.match(/\./g) || []).length !== 1) return null;
|
|
28966
|
+
var m = last.match(SELECTOR_RE);
|
|
28967
|
+
if (!m) return null;
|
|
28968
|
+
return { tag: (m[1] || "").toLowerCase(), cls: m[2] };
|
|
28969
|
+
}
|
|
28970
|
+
|
|
28971
|
+
function bucketsFor(tag) {
|
|
28972
|
+
if (!tag) return ["inlineStyles", "paragraphStyles"]; // unqualified: offer both
|
|
28973
|
+
for (var i = 0; i < TARGETS.length; i++) if (TARGETS[i].tags[tag]) return [TARGETS[i].key];
|
|
28974
|
+
return [];
|
|
28975
|
+
}
|
|
28976
|
+
|
|
28977
|
+
// ---- stylesheet walk -------------------------------------------------
|
|
28978
|
+
|
|
28979
|
+
// The editor's own CSS must never reach the Styles menu, and the reliable
|
|
28980
|
+
// axis for that is ORIGIN, not class name. A first cut filtered by class
|
|
28981
|
+
// prefix and imported 77 classes — the theme's `richtexteditor`/`colordiv`,
|
|
28982
|
+
// and 1,788 rules of `aitoolkit.css` — because plugin CSS does not agree on
|
|
28983
|
+
// one prefix. Where a sheet came from is unambiguous:
|
|
28984
|
+
// - anything served out of the editor's own asset folder,
|
|
28985
|
+
// - any <style> the editor injected (its nodes carry rte-/__rte ids),
|
|
28986
|
+
// - any anonymous <style> inside the EDITING document, which is only ever
|
|
28987
|
+
// editor-injected — a site's content CSS arrives there via contentCssUrl
|
|
28988
|
+
// and therefore has an href.
|
|
28989
|
+
function isEditorSheet(sheet, isEditingDoc) {
|
|
28990
|
+
var href = sheet.href || "";
|
|
28991
|
+
if (/\/richtexteditor\//i.test(href)) return true;
|
|
28992
|
+
var node = sheet.ownerNode;
|
|
28993
|
+
var id = (node && node.id) || "";
|
|
28994
|
+
if (/^(rte[-_]|__rte)/i.test(id)) return true;
|
|
28995
|
+
if (isEditingDoc && !href) return true;
|
|
28996
|
+
return false;
|
|
28997
|
+
}
|
|
28998
|
+
|
|
28999
|
+
// Options passed to editor.importStyles() win over config. The editor copies
|
|
29000
|
+
// config at construction time, so mutating the caller's config object later
|
|
29001
|
+
// does NOT reach this plugin — an API that quietly ignored its arguments
|
|
29002
|
+
// would be worse than no API at all.
|
|
29003
|
+
var activeOptions = {};
|
|
29004
|
+
function setting(name) {
|
|
29005
|
+
return (activeOptions && name in activeOptions) ? activeOptions[name] : config["importStyles" + name.charAt(0).toUpperCase() + name.slice(1)];
|
|
29006
|
+
}
|
|
29007
|
+
|
|
29008
|
+
function wantSheet(sheet, isEditingDoc) {
|
|
29009
|
+
if (isEditorSheet(sheet, isEditingDoc)) return false;
|
|
29010
|
+
var want = setting("sheets");
|
|
29011
|
+
if (!want) return true;
|
|
29012
|
+
var href = sheet.href || "";
|
|
29013
|
+
if (typeof want === "function") return !!want(sheet);
|
|
29014
|
+
if (want instanceof RegExp) return want.test(href);
|
|
29015
|
+
return href.indexOf(String(want)) >= 0;
|
|
29016
|
+
}
|
|
29017
|
+
|
|
29018
|
+
function excluded(cls) {
|
|
29019
|
+
var ex = setting("exclude");
|
|
29020
|
+
if (!ex) return false;
|
|
29021
|
+
if (ex instanceof RegExp) return ex.test(cls);
|
|
29022
|
+
if (typeof ex === "function") return !!ex(cls);
|
|
29023
|
+
return cls.indexOf(String(ex)) === 0;
|
|
29024
|
+
}
|
|
29025
|
+
|
|
29026
|
+
function walkRules(rules, out) {
|
|
29027
|
+
if (!rules) return;
|
|
29028
|
+
for (var i = 0; i < rules.length; i++) {
|
|
29029
|
+
var rule = rules[i];
|
|
29030
|
+
// @media / @supports carry their own rule list.
|
|
29031
|
+
if (rule.cssRules && !rule.selectorText) { walkRules(rule.cssRules, out); continue; }
|
|
29032
|
+
if (!rule.selectorText) continue;
|
|
29033
|
+
var selectors = rule.selectorText.split(",");
|
|
29034
|
+
for (var s = 0; s < selectors.length; s++) {
|
|
29035
|
+
var parsed = parseSelector(selectors[s]);
|
|
29036
|
+
if (!parsed || excluded(parsed.cls)) continue;
|
|
29037
|
+
out.push({ cls: parsed.cls, tag: parsed.tag, selector: selectors[s].trim() });
|
|
29038
|
+
}
|
|
29039
|
+
}
|
|
29040
|
+
}
|
|
29041
|
+
|
|
29042
|
+
function documents() {
|
|
29043
|
+
var docs = [];
|
|
29044
|
+
if (typeof document !== "undefined") docs.push({ doc: document, editing: false });
|
|
29045
|
+
try {
|
|
29046
|
+
var d = editor.getDocument && editor.getDocument();
|
|
29047
|
+
if (d && d !== document) docs.push({ doc: d, editing: true });
|
|
29048
|
+
} catch (e) {}
|
|
29049
|
+
return docs;
|
|
29050
|
+
}
|
|
29051
|
+
|
|
29052
|
+
function collect() {
|
|
29053
|
+
var found = [];
|
|
29054
|
+
var unreadable = 0;
|
|
29055
|
+
var docs = documents();
|
|
29056
|
+
for (var d = 0; d < docs.length; d++) {
|
|
29057
|
+
var sheets = docs[d].doc.styleSheets || [];
|
|
29058
|
+
for (var i = 0; i < sheets.length; i++) {
|
|
29059
|
+
var sheet = sheets[i];
|
|
29060
|
+
if (!wantSheet(sheet, docs[d].editing)) continue;
|
|
29061
|
+
var rules = null;
|
|
29062
|
+
try {
|
|
29063
|
+
// Throws SecurityError for a cross-origin sheet. This must
|
|
29064
|
+
// not abort the scan — a single CDN font sheet would
|
|
29065
|
+
// otherwise silently empty the whole Styles menu.
|
|
29066
|
+
rules = sheet.cssRules || sheet.rules;
|
|
29067
|
+
} catch (e) { unreadable++; continue; }
|
|
29068
|
+
walkRules(rules, found);
|
|
29069
|
+
}
|
|
29070
|
+
}
|
|
29071
|
+
return { classes: found, unreadableSheets: unreadable, documents: docs.length };
|
|
29072
|
+
}
|
|
29073
|
+
|
|
29074
|
+
// "my-cls-large-center" -> "Large Center"; "lede" -> "Lede".
|
|
29075
|
+
function humanize(cls) {
|
|
29076
|
+
var text = String(cls).replace(/^(my[-_]cls[-_]|u[-_]|c[-_])/i, "").replace(/[-_]+/g, " ").trim();
|
|
29077
|
+
if (!text) text = cls;
|
|
29078
|
+
return text.replace(/\b\w/g, function (ch) { return ch.toUpperCase(); });
|
|
29079
|
+
}
|
|
29080
|
+
|
|
29081
|
+
function alreadyListed(list, value) {
|
|
29082
|
+
for (var i = 0; i < list.length; i++) if (list[i] && list[i][1] === value) return true;
|
|
29083
|
+
return false;
|
|
29084
|
+
}
|
|
29085
|
+
|
|
29086
|
+
// ---- apply -----------------------------------------------------------
|
|
29087
|
+
|
|
29088
|
+
function run(options) {
|
|
29089
|
+
activeOptions = options || {};
|
|
29090
|
+
var scan = collect();
|
|
29091
|
+
var limit = Math.max(0, parseInt(setting("limit"), 10) || 0) || Infinity;
|
|
29092
|
+
var replace = !!setting("replace");
|
|
29093
|
+
var filter = setting("filter");
|
|
29094
|
+
|
|
29095
|
+
var buckets = {};
|
|
29096
|
+
var truncated = {};
|
|
29097
|
+
for (var t = 0; t < TARGETS.length; t++) buckets[TARGETS[t].key] = [];
|
|
29098
|
+
|
|
29099
|
+
var seen = {};
|
|
29100
|
+
for (var i = 0; i < scan.classes.length; i++) {
|
|
29101
|
+
var item = scan.classes[i];
|
|
29102
|
+
var targets = bucketsFor(item.tag);
|
|
29103
|
+
for (var b = 0; b < targets.length; b++) {
|
|
29104
|
+
var key = targets[b];
|
|
29105
|
+
var dedupeKey = key + "|" + item.cls;
|
|
29106
|
+
if (seen[dedupeKey]) continue;
|
|
29107
|
+
|
|
29108
|
+
var label = humanize(item.cls);
|
|
29109
|
+
if (typeof filter === "function") {
|
|
29110
|
+
var verdict = filter(item.cls, item.selector, key);
|
|
29111
|
+
if (verdict === false) continue;
|
|
29112
|
+
if (typeof verdict === "string" && verdict) label = verdict;
|
|
29113
|
+
}
|
|
29114
|
+
|
|
29115
|
+
seen[dedupeKey] = true;
|
|
29116
|
+
if (buckets[key].length >= limit) { truncated[key] = (truncated[key] || 0) + 1; continue; }
|
|
29117
|
+
// Two-element form = apply as a class (the three-element form is
|
|
29118
|
+
// for inline CSS); this is what the dropdowns already expect.
|
|
29119
|
+
buckets[key].push([label, item.cls]);
|
|
29120
|
+
}
|
|
29121
|
+
}
|
|
29122
|
+
|
|
29123
|
+
var applied = {};
|
|
29124
|
+
for (var k = 0; k < TARGETS.length; k++) {
|
|
29125
|
+
var name = TARGETS[k].key;
|
|
29126
|
+
var incoming = buckets[name];
|
|
29127
|
+
if (replace) {
|
|
29128
|
+
config[name] = incoming;
|
|
29129
|
+
} else {
|
|
29130
|
+
if (!config[name]) config[name] = [];
|
|
29131
|
+
for (var j = 0; j < incoming.length; j++) {
|
|
29132
|
+
if (!alreadyListed(config[name], incoming[j][1])) config[name].push(incoming[j]);
|
|
29133
|
+
}
|
|
29134
|
+
}
|
|
29135
|
+
applied[name] = incoming.length;
|
|
29136
|
+
}
|
|
29137
|
+
|
|
29138
|
+
// A cap that hides work is a lie about coverage. Say what was dropped.
|
|
29139
|
+
var dropped = 0;
|
|
29140
|
+
for (var key2 in truncated) if (truncated.hasOwnProperty(key2)) dropped += truncated[key2];
|
|
29141
|
+
if (dropped) {
|
|
29142
|
+
try {
|
|
29143
|
+
console.warn("[RichTextEditor] importStyles: " + dropped + " class(es) omitted — " +
|
|
29144
|
+
"importStylesLimit is " + limit + " per dropdown. Raise the limit or narrow " +
|
|
29145
|
+
"importStylesSheets / importStylesFilter.");
|
|
29146
|
+
} catch (e) {}
|
|
29147
|
+
}
|
|
29148
|
+
|
|
29149
|
+
imported = {
|
|
29150
|
+
applied: applied,
|
|
29151
|
+
truncated: truncated,
|
|
29152
|
+
droppedCount: dropped,
|
|
29153
|
+
unreadableSheets: scan.unreadableSheets,
|
|
29154
|
+
documentsScanned: scan.documents,
|
|
29155
|
+
replaced: !!replace
|
|
29156
|
+
};
|
|
29157
|
+
activeOptions = {};
|
|
29158
|
+
return imported;
|
|
29159
|
+
}
|
|
29160
|
+
}
|
|
29161
|
+
|
|
28271
29162
|
|
|
28272
29163
|
if (!RTE_DefaultConfig.svgCode_insertcode) {
|
|
28273
29164
|
RTE_DefaultConfig.svgCode_insertcode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 7 3 12 8 17"/><polyline points="16 7 21 12 16 17"/><line x1="14" y1="5" x2="10" y2="19"/></svg>';
|
|
@@ -28518,6 +29409,72 @@ function RTE_Plugin_InsertCode() {
|
|
|
28518
29409
|
|
|
28519
29410
|
obj.DoShowDialog = function () {
|
|
28520
29411
|
|
|
29412
|
+
// Capture the caret BEFORE the dialog opens. The dialog focuses its own
|
|
29413
|
+
// textarea, which clears the editor's selection - so by the time Insert
|
|
29414
|
+
// is clicked there is no caret left to insert at, and the code block was
|
|
29415
|
+
// appended as a new root paragraph instead of landing where the user was
|
|
29416
|
+
// typing. Same failure mode as the 2026-05-08 image-upload fix in core.
|
|
29417
|
+
var savedRange = null;
|
|
29418
|
+
try {
|
|
29419
|
+
var edoc = editor.getDocument();
|
|
29420
|
+
var esel = edoc.defaultView.getSelection();
|
|
29421
|
+
if (esel && esel.rangeCount) savedRange = esel.getRangeAt(0).cloneRange();
|
|
29422
|
+
} catch (e) { savedRange = null; }
|
|
29423
|
+
|
|
29424
|
+
function restoreCaret() {
|
|
29425
|
+
if (!savedRange) return false;
|
|
29426
|
+
try {
|
|
29427
|
+
var edoc = editor.getDocument();
|
|
29428
|
+
var esel = edoc.defaultView.getSelection();
|
|
29429
|
+
esel.removeAllRanges();
|
|
29430
|
+
esel.addRange(savedRange);
|
|
29431
|
+
return true;
|
|
29432
|
+
} catch (e) { return false; }
|
|
29433
|
+
}
|
|
29434
|
+
|
|
29435
|
+
// A <div class="dp-highlighter"> dropped at a caret INSIDE a <p> is
|
|
29436
|
+
// invalid nesting. It looks right in the live DOM, but the moment the
|
|
29437
|
+
// saved HTML is parsed again the browser hoists the div out and tears the
|
|
29438
|
+
// paragraph in two -- 3 blocks become 5, so the document a customer
|
|
29439
|
+
// reloads is not the one they saved. Split the paragraph ourselves and
|
|
29440
|
+
// place the block between the halves, which round-trips byte-stable.
|
|
29441
|
+
function insertCodeBlock(html) {
|
|
29442
|
+
if (!restoreCaret()) return false;
|
|
29443
|
+
var edoc = editor.getDocument();
|
|
29444
|
+
var esel = edoc.defaultView.getSelection();
|
|
29445
|
+
if (!esel || !esel.rangeCount) return false;
|
|
29446
|
+
var range = esel.getRangeAt(0);
|
|
29447
|
+
var n = range.startContainer, block = null;
|
|
29448
|
+
while (n && n !== edoc.body) {
|
|
29449
|
+
if (n.nodeType === 1 && /^(P|H[1-6]|LI|TD|TH|DIV|BLOCKQUOTE)$/.test(n.tagName)) { block = n; break; }
|
|
29450
|
+
n = n.parentNode;
|
|
29451
|
+
}
|
|
29452
|
+
var holder = edoc.createElement("div");
|
|
29453
|
+
holder.innerHTML = html;
|
|
29454
|
+
var node = holder.firstChild;
|
|
29455
|
+
if (!node) return false;
|
|
29456
|
+
// DIV/LI/TD/BLOCKQUOTE may legally contain the block: insert in place.
|
|
29457
|
+
if (!block || !/^(P|H[1-6])$/.test(block.tagName)) {
|
|
29458
|
+
range.deleteContents();
|
|
29459
|
+
range.insertNode(node);
|
|
29460
|
+
return true;
|
|
29461
|
+
}
|
|
29462
|
+
range.deleteContents();
|
|
29463
|
+
var tail = range.cloneRange();
|
|
29464
|
+
tail.setEndAfter(block.lastChild || block);
|
|
29465
|
+
var tailFrag = tail.extractContents();
|
|
29466
|
+
block.parentNode.insertBefore(node, block.nextSibling);
|
|
29467
|
+
var tailBlock = edoc.createElement(block.tagName);
|
|
29468
|
+
tailBlock.appendChild(tailFrag);
|
|
29469
|
+
if (tailBlock.textContent.replace(/^\s+|\s+$/g, "") !== "" || tailBlock.querySelector("*")) {
|
|
29470
|
+
block.parentNode.insertBefore(tailBlock, node.nextSibling);
|
|
29471
|
+
}
|
|
29472
|
+
if (block.textContent.replace(/^\s+|\s+$/g, "") === "" && !block.querySelector("img,table")) {
|
|
29473
|
+
block.parentNode.removeChild(block);
|
|
29474
|
+
}
|
|
29475
|
+
return true;
|
|
29476
|
+
}
|
|
29477
|
+
|
|
28521
29478
|
var dialoginner = editor.createDialog(editor.getLangText("insertcode"), "rte-dialog-insertcode");
|
|
28522
29479
|
|
|
28523
29480
|
var div2 = __Append(dialoginner, "div", "position:relative;text-align:center;");
|
|
@@ -28570,12 +29527,23 @@ function RTE_Plugin_InsertCode() {
|
|
|
28570
29527
|
|
|
28571
29528
|
var tag = textarea.previousSibling
|
|
28572
29529
|
|
|
28573
|
-
|
|
28574
|
-
|
|
29530
|
+
// insertHTML honours the caret; insertRootParagraph always
|
|
29531
|
+
// appended at document level, which is why the block never
|
|
29532
|
+
// landed at the focus position.
|
|
29533
|
+
if (insertCodeBlock('<div class="dp-highlighter">' + tag.innerHTML + "</div>")) {
|
|
29534
|
+
// placed at the caret, at block level
|
|
29535
|
+
} else {
|
|
29536
|
+
var p = editor.insertRootParagraph();
|
|
29537
|
+
p.innerHTML = '<div class="dp-highlighter">' + tag.innerHTML + "</div>";
|
|
29538
|
+
}
|
|
28575
29539
|
}
|
|
28576
29540
|
else {
|
|
28577
|
-
|
|
28578
|
-
|
|
29541
|
+
if (restoreCaret()) {
|
|
29542
|
+
editor.insertText(textarea.value);
|
|
29543
|
+
} else {
|
|
29544
|
+
var p = editor.insertRootParagraph();
|
|
29545
|
+
p.innerText = textarea.value;
|
|
29546
|
+
}
|
|
28579
29547
|
}
|
|
28580
29548
|
|
|
28581
29549
|
editor.focus();
|
|
@@ -28588,235 +29556,308 @@ function RTE_Plugin_InsertCode() {
|
|
|
28588
29556
|
|
|
28589
29557
|
|
|
28590
29558
|
|
|
28591
|
-
|
|
28592
|
-
RTE_DefaultConfig.plugin_insertemoji = RTE_Plugin_InsertEmoji;
|
|
28593
|
-
|
|
28594
|
-
function RTE_Plugin_InsertEmoji() {
|
|
28595
|
-
|
|
28596
|
-
function CharToHTMLCode(ch) {
|
|
28597
|
-
if (ch.length > 2)
|
|
28598
|
-
return ch;
|
|
28599
|
-
|
|
28600
|
-
if (ch.length == 1 || ch.charCodeAt(1) == 0xfe0f)
|
|
28601
|
-
return "&#" + ch.charCodeAt(0) + ";"
|
|
28602
|
-
|
|
28603
|
-
if (ch.charCodeAt(1) == 0xfe0f) {
|
|
28604
|
-
return "&#" + ch.charCodeAt(0) + ";"
|
|
28605
|
-
}
|
|
28606
|
-
|
|
28607
|
-
var offset = ch.charCodeAt(0) - 0xd83c;
|
|
28608
|
-
if (offset < 0 && offset > 3)//not support
|
|
28609
|
-
return ch;
|
|
28610
|
-
|
|
28611
|
-
var second = ch.charCodeAt(1) - 0xdc04;
|
|
28612
|
-
var f = offset * 0x400 + second + 0x1f004;
|
|
28613
|
-
return "&#x" + f.toString(16) + ";"
|
|
28614
|
-
}
|
|
28615
|
-
|
|
28616
|
-
|
|
28617
|
-
var groupnames = ["smileys", "people", "animals", "food", "travel", "activities", "objects", "symbols"]
|
|
28618
|
-
var emojistrs = ["😀#grinning face|😃#grinning face with big eyes|😄#grinning face with smiling eyes|😁#beaming face with smiling eyes|😆#grinning squinting face|😅#grinning face with sweat|🤣#rolling on the floor laughing|😂#face with tears of joy|🙂#slightly smiling face|🙃#upside-down face|😉#winking face|😊#smiling face with smiling eyes|😇#smiling face with halo|😍#smiling face with heart-eyes|🤩#star-struck|😘#face blowing a kiss|😗#kissing face|😚#kissing face with closed eyes|😙#kissing face with smiling eyes|😋#face savoring food|😛#face with tongue|😜#winking face with tongue|🤪#zany face|😝#squinting face with tongue|🤑#money-mouth face|🤗#hugging face|🤭#face with hand over mouth|🤫#shushing face|🤔#thinking face|🤐#zipper-mouth face|🤨#face with raised eyebrow|😐#neutral face|😑#expressionless face|😶#face without mouth|😏#smirking face|😒#unamused face|🙄#face with rolling eyes|😬#grimacing face|🤥#lying face|😌#relieved face|😔#pensive face|😪#sleepy face|🤤#drooling face|😴#sleeping face|😷#face with medical mask|🤒#face with thermometer|🤕#face with head-bandage|🤢#nauseated face|🤮#face vomiting|🤧#sneezing face|😵#dizzy face|🤯#exploding head|🤠#cowboy hat face|😎#smiling face with sunglasses|🤓#nerd face|🧐#face with monocle|😕#confused face|😟#worried face|🙁#slightly frowning face|☹️#frowning face|😮#face with open mouth|😯#hushed face|😲#astonished face|😳#flushed face|😦#frowning face with open mouth|😧#anguished face|😨#fearful face|😰#anxious face with sweat|😥#sad but relieved face|😢#crying face|😭#loudly crying face|😱#face screaming in fear|😖#confounded face|😣#persevering face|😞#disappointed face|😓#downcast face with sweat|😩#weary face|😫#tired face|😤#face with steam from nose|😡#pouting face|😠#angry face|🤬#face with symbols on mouth|😈#smiling face with horns|👿#angry face with horns|💀#skull|☠️#skull and crossbones|💩#pile of poo|🤡#clown face|👹#ogre|👺#goblin|👻#ghost|👽#alien|👾#alien monster|🤖#robot|😺#grinning cat|😸#grinning cat with smiling eyes|😹#cat with tears of joy|😻#smiling cat with heart-eyes|😼#cat with wry smile|😽#kissing cat|🙀#weary cat|😿#crying cat|😾#pouting cat|🙈#see-no-evil monkey|🙉#hear-no-evil monkey|🙊#speak-no-evil monkey|💋#kiss mark|💌#love letter|💘#heart with arrow|💝#heart with ribbon|💖#sparkling heart|💗#growing heart|💓#beating heart|💞#revolving hearts|💕#two hearts|💟#heart decoration|❣️#heart exclamation|💔#broken heart|❤️#red heart|🧡#orange heart|💛#yellow heart|💚#green heart|💙#blue heart|💜#purple heart|🖤#black heart|💯#hundred points|💢#anger symbol|💥#collision|💫#dizzy|💦#sweat droplets|💨#dashing away|💣#bomb|💬#speech balloon|💭#thought balloon|💤#zzz", "👋#waving hand|🤚#raised back of hand|✋#raised hand|🖖#vulcan salute|👌#OK hand|✌️#victory hand|🤞#crossed fingers|🤟#love-you gesture|🤘#sign of the horns|🤙#call me hand|👈#backhand index pointing left|👉#backhand index pointing right|👆#backhand index pointing up|🖕#middle finger|👇#backhand index pointing down|☝️#index pointing up|👍#thumbs up|👎#thumbs down|✊#raised fist|👊#oncoming fist|🤛#left-facing fist|🤜#right-facing fist|👏#clapping hands|🙌#raising hands|👐#open hands|🤲#palms up together|🤝#handshake|🙏#folded hands|✍️#writing hand|💅#nail polish|🤳#selfie|💪#flexed biceps|👂#ear|👃#nose|🧠#brain|👀#eyes|👅#tongue|👄#mouth|👶#baby|🧒#child|👦#boy|👧#girl|🧑#person|👱#person with blond hair|👨#man|🧔#man with beard|👩#woman|🧓#older person|👴#old man|👵#old woman|🙍#person frowning|🙎#person pouting|🙅#person gesturing NO|🙆#person gesturing OK|💁#person tipping hand|🙋#person raising hand|🙇#person bowing|🤦#person facepalming|🤷#person shrugging|👮#police officer|💂#guard|👷#construction worker|🤴#prince|👸#princess|👳#person wearing turban|👲#person with skullcap|🧕#woman with headscarf|🤵#person in tuxedo|👰#person with veil|🤰#pregnant woman|🤱#breast-feeding|👼#baby angel|🎅#Santa Claus|🤶#Mrs. Claus|🧙#mage|🧚#fairy|🧛#vampire|🧜#merperson|🧝#elf|🧞#genie|🧟#zombie|💆#person getting massage|💇#person getting haircut|🚶#person walking|🏃#person running|💃#woman dancing|🕺#man dancing|👯#people with bunny ears|🧖#person in steamy room|🧗#person climbing|🤺#person fencing|🏇#horse racing|⛷️#skier|🏂#snowboarder|🏄#person surfing|🚣#person rowing boat|🏊#person swimming|⛹️#person bouncing ball|🚴#person biking|🚵#person mountain biking|🤸#person cartwheeling|🤼#people wrestling|🤽#person playing water polo|🤾#person playing handball|🤹#person juggling|🧘#person in lotus position|🛀#person taking bath|🛌#person in bed|👭#women holding hands|👫#woman and man holding hands|👬#men holding hands|💏#kiss|💑#couple with heart|👪#family|👤#bust in silhouette|👥#busts in silhouette|👣#footprints", "🐵#monkey face|🐒#monkey|🦍#gorilla|🐶#dog face|🐕#dog|🐩#poodle|🐺#wolf|🦊#fox|🐱#cat face|🐈#cat|🦁#lion|🐯#tiger face|🐅#tiger|🐆#leopard|🐴#horse face|🐎#horse|🦄#unicorn|🦓#zebra|🦌#deer|🐮#cow face|🐂#ox|🐃#water buffalo|🐄#cow|🐷#pig face|🐖#pig|🐗#boar|🐽#pig nose|🐏#ram|🐑#ewe|🐐#goat|🐪#camel|🐫#two-hump camel|🦒#giraffe|🐘#elephant|🦏#rhinoceros|🐭#mouse face|🐁#mouse|🐀#rat|🐹#hamster|🐰#rabbit face|🐇#rabbit|🦔#hedgehog|🦇#bat|🐻#bear|🐨#koala|🐼#panda|🐾#paw prints|🦃#turkey|🐔#chicken|🐓#rooster|🐣#hatching chick|🐤#baby chick|🐥#front-facing baby chick|🐦#bird|🐧#penguin|🦅#eagle|🦆#duck|🦉#owl|🐸#frog|🐊#crocodile|🐢#turtle|🦎#lizard|🐍#snake|🐲#dragon face|🐉#dragon|🦕#sauropod|🦖#T-Rex|🐳#spouting whale|🐋#whale|🐬#dolphin|🐟#fish|🐠#tropical fish|🐡#blowfish|🦈#shark|🐙#octopus|🐚#spiral shell|🐌#snail|🦋#butterfly|🐛#bug|🐜#ant|🐝#honeybee|🐞#lady beetle|🦗#cricket|🦂#scorpion|💐#bouquet|🌸#cherry blossom|💮#white flower|🌹#rose|🥀#wilted flower|🌺#hibiscus|🌻#sunflower|🌼#blossom|🌷#tulip|🌱#seedling|🌲#evergreen tree|🌳#deciduous tree|🌴#palm tree|🌵#cactus|🌾#sheaf of rice|🌿#herb|☘️#shamrock|🍀#four leaf clover|🍁#maple leaf|🍂#fallen leaf|🍃#leaf fluttering in wind", "🍇#grapes|🍈#melon|🍉#watermelon|🍊#tangerine|🍋#lemon|🍌#banana|🍍#pineapple|🍎#red apple|🍏#green apple|🍐#pear|🍑#peach|🍒#cherries|🍓#strawberry|🥝#kiwi fruit|🍅#tomato|🥥#coconut|🥑#avocado|🍆#eggplant|🥔#potato|🥕#carrot|🌽#ear of corn|🥒#cucumber|🥦#broccoli|🍄#mushroom|🥜#peanuts|🌰#chestnut|🍞#bread|🥐#croissant|🥖#baguette bread|🥨#pretzel|🥞#pancakes|🧀#cheese wedge|🍖#meat on bone|🍗#poultry leg|🥩#cut of meat|🥓#bacon|🍔#hamburger|🍟#french fries|🍕#pizza|🌭#hot dog|🥪#sandwich|🌮#taco|🌯#burrito|🥙#stuffed flatbread|🥚#egg|🍳#cooking|🥘#shallow pan of food|🍲#pot of food|🥣#bowl with spoon|🥗#green salad|🍿#popcorn|🥫#canned food|🍱#bento box|🍘#rice cracker|🍙#rice ball|🍚#cooked rice|🍛#curry rice|🍜#steaming bowl|🍝#spaghetti|🍠#roasted sweet potato|🍢#oden|🍣#sushi|🍤#fried shrimp|🍥#fish cake with swirl|🍡#dango|🥟#dumpling|🥠#fortune cookie|🥡#takeout box|🦀#crab|🦐#shrimp|🦑#squid|🍦#soft ice cream|🍧#shaved ice|🍨#ice cream|🍩#doughnut|🍪#cookie|🎂#birthday cake|🍰#shortcake|🥧#pie|🍫#chocolate bar|🍬#candy|🍭#lollipop|🍮#custard|🍯#honey pot|🍼#baby bottle|🥛#glass of milk|☕#hot beverage|🍵#teacup without handle|🍶#sake|🍾#bottle with popping cork|🍷#wine glass|🍸#cocktail glass|🍹#tropical drink|🍺#beer mug|🍻#clinking beer mugs|🥂#clinking glasses|🥃#tumbler glass|🥤#cup with straw|🥢#chopsticks|🍴#fork and knife|🥄#spoon|🔪#kitchen knife|🏺#amphora", "🌍#globe showing Europe-Africa|🌎#globe showing Americas|🌏#globe showing Asia-Australia|🌐#globe with meridians|🗾#map of Japan|⛰️#mountain|🌋#volcano|🗻#mount fuji|🏠#house|🏡#house with garden|🏢#office building|🏣#Japanese post office|🏤#post office|🏥#hospital|🏦#bank|🏨#hotel|🏩#love hotel|🏪#convenience store|🏫#school|🏬#department store|🏭#factory|🏯#Japanese castle|🏰#castle|💒#wedding|🗼#Tokyo tower|🗽#Statue of Liberty|⛪#church|🕌#mosque|🕍#synagogue|⛩️#shinto shrine|🕋#kaaba|⛲#fountain|⛺#tent|🌁#foggy|🌃#night with stars|🌄#sunrise over mountains|🌅#sunrise|🌆#cityscape at dusk|🌇#sunset|🌉#bridge at night|♨️#hot springs|🎠#carousel horse|🎡#ferris wheel|🎢#roller coaster|💈#barber pole|🎪#circus tent|🚂#locomotive|🚃#railway car|🚄#high-speed train|🚅#bullet train|🚆#train|🚇#metro|🚈#light rail|🚉#station|🚊#tram|🚝#monorail|🚞#mountain railway|🚋#tram car|🚌#bus|🚍#oncoming bus|🚎#trolleybus|🚐#minibus|🚑#ambulance|🚒#fire engine|🚓#police car|🚔#oncoming police car|🚕#taxi|🚖#oncoming taxi|🚗#automobile|🚘#oncoming automobile|🚙#sport utility vehicle|🚚#delivery truck|🚛#articulated lorry|🚜#tractor|🛵#motor scooter|🚲#bicycle|🛴#kick scooter|🚏#bus stop|⛽#fuel pump|🚨#police car light|🚥#horizontal traffic light|🚦#vertical traffic light|🛑#stop sign|🚧#construction|⚓#anchor|⛵#sailboat|🛶#canoe|🚤#speedboat|⛴️#ferry|🚢#ship|✈️#airplane|🛫#airplane departure|🛬#airplane arrival|💺#seat|🚁#helicopter|🚟#suspension railway|🚠#mountain cableway|🚡#aerial tramway|🚀#rocket|🛸#flying saucer|⌛#hourglass done|⏳#hourglass not done|⌚#watch|⏰#alarm clock|⏱️#stopwatch|⏲️#timer clock|🕛#twelve o’clock|🕧#twelve-thirty|🕐#one o’clock|🕜#one-thirty|🕑#two o’clock|🕝#two-thirty|🕒#three o’clock|🕞#three-thirty|🕓#four o’clock|🕟#four-thirty|🕔#five o’clock|🕠#five-thirty|🕕#six o’clock|🕡#six-thirty|🕖#seven o’clock|🕢#seven-thirty|🕗#eight o’clock|🕣#eight-thirty|🕘#nine o’clock|🕤#nine-thirty|🕙#ten o’clock|🕥#ten-thirty|🕚#eleven o’clock|🕦#eleven-thirty|🌑#new moon|🌒#waxing crescent moon|🌓#first quarter moon|🌔#waxing gibbous moon|🌕#full moon|🌖#waning gibbous moon|🌗#last quarter moon|🌘#waning crescent moon|🌙#crescent moon|🌚#new moon face|🌛#first quarter moon face|🌜#last quarter moon face|☀️#sun|🌝#full moon face|🌞#sun with face|⭐#star|🌟#glowing star|🌠#shooting star|🌌#milky way|☁️#cloud|⛅#sun behind cloud|⛈️#cloud with lightning and rain|🌀#cyclone|🌈#rainbow|🌂#closed umbrella|☂️#umbrella|☔#umbrella with rain drops|⛱️#umbrella on ground|⚡#high voltage|❄️#snowflake|☃️#snowman|⛄#snowman without snow|☄️#comet|🔥#fire|💧#droplet|🌊#water wave", "🎃#jack-o-lantern|🎄#Christmas tree|🎆#fireworks|🎇#sparkler|✨#sparkles|🎈#balloon|🎉#party popper|🎊#confetti ball|🎋#tanabata tree|🎍#pine decoration|🎎#Japanese dolls|🎏#carp streamer|🎐#wind chime|🎑#moon viewing ceremony|🎀#ribbon|🎁#wrapped gift|🎫#ticket|🏆#trophy|🏅#sports medal|🥇#1st place medal|🥈#2nd place medal|🥉#3rd place medal|⚽#soccer ball|⚾#baseball|🏀#basketball|🏐#volleyball|🏈#american football|🏉#rugby football|🎾#tennis|🎳#bowling|🏏#cricket game|🏑#field hockey|🏒#ice hockey|🏓#ping pong|🏸#badminton|🥊#boxing glove|🥋#martial arts uniform|🥅#goal net|⛳#flag in hole|⛸️#ice skate|🎣#fishing pole|🎽#running shirt|🎿#skis|🛷#sled|🥌#curling stone|🎯#direct hit|🎱#pool 8 ball|🔮#crystal ball|🎮#video game|🎰#slot machine|🎲#game die|♠️#spade suit|♥️#heart suit|♦️#diamond suit|♣️#club suit|🃏#joker|🀄#mahjong red dragon|🎴#flower playing cards|🎭#performing arts|🎨#artist palette", "👓#glasses|👔#necktie|👕#t-shirt|👖#jeans|🧣#scarf|🧤#gloves|🧥#coat|🧦#socks|👗#dress|👘#kimono|👙#bikini|👚#woman’s clothes|👛#purse|👜#handbag|👝#clutch bag|🎒#backpack|👞#man’s shoe|👟#running shoe|👠#high-heeled shoe|👡#woman’s sandal|👢#woman’s boot|👑#crown|👒#woman’s hat|🎩#top hat|🎓#graduation cap|🧢#billed cap|⛑️#rescue worker’s helmet|📿#prayer beads|💄#lipstick|💍#ring|💎#gem stone|🔇#muted speaker|🔈#speaker low volume|🔉#speaker medium volume|🔊#speaker high volume|📢#loudspeaker|📣#megaphone|📯#postal horn|🔔#bell|🔕#bell with slash|🎼#musical score|🎵#musical note|🎶#musical notes|🎤#microphone|🎧#headphone|📻#radio|🎷#saxophone|🎸#guitar|🎹#musical keyboard|🎺#trumpet|🎻#violin|🥁#drum|📱#mobile phone|📲#mobile phone with arrow|☎️#telephone|📞#telephone receiver|📟#pager|📠#fax machine|🔋#battery|🔌#electric plug|💻#laptop|⌨️#keyboard|💽#computer disk|💾#floppy disk|💿#optical disk|📀#dvd|🎥#movie camera|🎬#clapper board|📺#television|📷#camera|📸#camera with flash|📹#video camera|📼#videocassette|🔍#magnifying glass tilted left|🔎#magnifying glass tilted right|💡#light bulb|🔦#flashlight|🏮#red paper lantern|📔#notebook with decorative cover|📕#closed book|📖#open book|📗#green book|📘#blue book|📙#orange book|📚#books|📓#notebook|📒#ledger|📃#page with curl|📜#scroll|📄#page facing up|📰#newspaper|📑#bookmark tabs|🔖#bookmark|💰#money bag|💴#yen banknote|💵#dollar banknote|💶#euro banknote|💷#pound banknote|💸#money with wings|💳#credit card|💹#chart increasing with yen|✉️#envelope|📧#e-mail|📨#incoming envelope|📩#envelope with arrow|📤#outbox tray|📥#inbox tray|📦#package|📫#closed mailbox with raised flag|📪#closed mailbox with lowered flag|📬#open mailbox with raised flag|📭#open mailbox with lowered flag|📮#postbox|✏️#pencil|✒️#black nib|📝#memo|💼#briefcase|📁#file folder|📂#open file folder|📅#calendar|📆#tear-off calendar|📇#card index|📈#chart increasing|📉#chart decreasing|📊#bar chart|📋#clipboard|📌#pushpin|📍#round pushpin|📎#paperclip|📏#straight ruler|📐#triangular ruler|✂️#scissors|🔒#locked|🔓#unlocked|🔏#locked with pen|🔐#locked with key|🔑#key|🔨#hammer|⛏️#pick|⚒️#hammer and pick|⚔️#crossed swords|🔫#pistol|🏹#bow and arrow|🔧#wrench|🔩#nut and bolt|⚙️#gear|⚖️#balance scale|🔗#link|⛓️#chains|⚗️#alembic|🔬#microscope|🔭#telescope|📡#satellite antenna|💉#syringe|💊#pill|🚪#door|🚽#toilet|🚿#shower|🛁#bathtub|🛒#shopping cart|🚬#cigarette|⚰️#coffin|⚱️#funeral urn|🗿#moai", "🏧#ATM sign|🚮#litter in bin sign|🚰#potable water|♿#wheelchair symbol|🚹#men’s room|🚺#women’s room|🚻#restroom|🚼#baby symbol|🚾#water closet|🛂#passport control|🛃#customs|🛄#baggage claim|🛅#left luggage|⚠️#warning|🚸#children crossing|⛔#no entry|🚫#prohibited|🚳#no bicycles|🚭#no smoking|🚯#no littering|🚱#non-potable water|🚷#no pedestrians|📵#no mobile phones|🔞#no one under eighteen|☢️#radioactive|☣️#biohazard|⬆️#up arrow|↗️#up-right arrow|➡️#right arrow|↘️#down-right arrow|⬇️#down arrow|↙️#down-left arrow|⬅️#left arrow|↖️#up-left arrow|↕️#up-down arrow|↔️#left-right arrow|↩️#right arrow curving left|↪️#left arrow curving right|⤴️#right arrow curving up|⤵️#right arrow curving down|🔃#clockwise vertical arrows|🔄#counterclockwise arrows button|🔙#BACK arrow|🔚#END arrow|🔛#ON! arrow|🔜#SOON arrow|🔝#TOP arrow|🛐#place of worship|⚛️#atom symbol|✡️#star of David|☸️#wheel of dharma|☯️#yin yang|✝️#latin cross|☦️#orthodox cross|☪️#star and crescent|☮️#peace symbol|🕎#menorah|🔯#dotted six-pointed star|♈#Aries|♉#Taurus|♊#Gemini|♋#Cancer|♌#Leo|♍#Virgo|♎#Libra|♏#Scorpio|♐#Sagittarius|♑#Capricorn|♒#Aquarius|♓#Pisces|⛎#Ophiuchus|🔀#shuffle tracks button|🔁#repeat button|🔂#repeat single button|▶️#play button|⏩#fast-forward button|⏭️#next track button|⏯️#play or pause button|◀️#reverse button|⏪#fast reverse button|⏮️#last track button|🔼#upwards button|⏫#fast up button|🔽#downwards button|⏬#fast down button|⏸️#pause button|⏹️#stop button|⏺️#record button|⏏️#eject button|🎦#cinema|🔅#dim button|🔆#bright button|📶#antenna bars|📳#vibration mode|📴#mobile phone off|♀️#female sign|♂️#male sign|✖️#multiply|➕#plus|➖#minus|➗#divide|‼️#double exclamation mark|⁉️#exclamation question mark|❓#question mark|❔#white question mark|❕#white exclamation mark|❗#exclamation mark|〰️#wavy dash|💱#currency exchange|💲#heavy dollar sign|⚕️#medical symbol|♻️#recycling symbol|⚜️#fleur-de-lis|🔱#trident emblem|📛#name badge|🔰#Japanese symbol for beginner|⭕#hollow red circle|✅#check mark button|☑️#check box with check|✔️#check mark|❌#cross mark|❎#cross mark button|➰#curly loop|➿#double curly loop|〽️#part alternation mark|✳️#eight-spoked asterisk|✴️#eight-pointed star|❇️#sparkle|©️#copyright|®️#registered|™️#trade mark|🔟#keycap: 10|🔠#input latin uppercase|🔡#input latin lowercase|🔢#input numbers|🔣#input symbols|🔤#input latin letters|🆎#AB button (blood type)|🆑#CL button|🆒#COOL button|🆓#FREE button|ℹ️#information|🆔#ID button|Ⓜ️#circled M|🆕#NEW button|🆖#NG button|🆗#OK button|🆘#SOS button|🆙#UP! button|🆚#VS button|🈁#Japanese “here” button|🈶#Japanese “not free of charge” button|🈯#Japanese “reserved” button|🉐#Japanese “bargain” button|🈹#Japanese “discount” button|🈚#Japanese “free of charge” button|🈲#Japanese “prohibited” button|🉑#Japanese “acceptable” button|🈸#Japanese “application” button|🈴#Japanese “passing grade” button|🈳#Japanese “vacancy” button|㊗️#Japanese “congratulations” button|㊙️#Japanese “secret” button|🈺#Japanese “open for business” button|🈵#Japanese “no vacancy” button|🔴#red circle|🔵#blue circle|⚫#black circle|⚪#white circle|⬛#black large square|⬜#white large square|◼️#black medium square|◻️#white medium square|◾#black medium-small square|◽#white medium-small square|▪️#black small square|▫️#white small square|🔶#large orange diamond|🔷#large blue diamond|🔸#small orange diamond|🔹#small blue diamond|🔺#red triangle pointed up|🔻#red triangle pointed down|💠#diamond with a dot|🔘#radio button|🔳#white square button|🔲#black square button"]
|
|
28619
|
-
var emojidata = null;
|
|
28620
|
-
|
|
28621
|
-
function MakeEmojiData() {
|
|
28622
|
-
if (emojidata)
|
|
28623
|
-
return;
|
|
28624
|
-
emojidata = [];
|
|
28625
|
-
for (var groupindex = 0; groupindex < groupnames.length; groupindex++) {
|
|
28626
|
-
var emojiitems = [];
|
|
28627
|
-
var emojigroup = { index: groupindex, name: groupnames[groupindex], items: emojiitems };
|
|
28628
|
-
var emojiarr = emojistrs[groupindex].split('|');
|
|
28629
|
-
for (var ei = 0; ei < emojiarr.length; ei++) {
|
|
28630
|
-
var emojistr = emojiarr[ei];
|
|
28631
|
-
var pair = emojistr.split('#')
|
|
28632
|
-
emojiitems.push({ emoji: pair[0], keyword: pair[1] });
|
|
28633
|
-
}
|
|
28634
|
-
emojidata.push(emojigroup);
|
|
28635
|
-
}
|
|
28636
|
-
//console.log(emojidata);
|
|
28637
|
-
}
|
|
28638
|
-
|
|
28639
|
-
var obj = this;
|
|
28640
|
-
|
|
28641
|
-
var config, editor;
|
|
28642
|
-
|
|
28643
|
-
obj.PluginName = "InsertEmoji";
|
|
28644
|
-
|
|
28645
|
-
obj.InitConfig = function (argconfig) {
|
|
28646
|
-
config = argconfig;
|
|
28647
|
-
}
|
|
28648
|
-
obj.InitEditor = function (argeditor) {
|
|
28649
|
-
editor = argeditor;
|
|
28650
|
-
|
|
28651
|
-
editor.toolbarFactoryMap["insertemoji"] = function (cmd) {
|
|
28652
|
-
return editor.createToolbarItemDropDownPanel(cmd, function (panel) {
|
|
28653
|
-
|
|
28654
|
-
MakeEmojiData()
|
|
28655
|
-
|
|
28656
|
-
panel.style.width = "360px";
|
|
28657
|
-
panel.style.height = "420px";
|
|
28658
|
-
panel.style.display = "flex";
|
|
28659
|
-
panel.style.flexDirection = "column";
|
|
28660
|
-
|
|
28661
|
-
panel.onclick = function (e) {
|
|
28662
|
-
if (e.target.nodeName == "GSPAN") {
|
|
28663
|
-
editor.closeCurrentPopup();
|
|
28664
|
-
var htmlcode = e.target.getAttribute("htmlcode");
|
|
28665
|
-
editor.insertHTML(htmlcode);
|
|
28666
|
-
editor.collapse(false);
|
|
28667
|
-
editor.focus();
|
|
28668
|
-
}
|
|
28669
|
-
}
|
|
28670
|
-
|
|
28671
|
-
|
|
28672
|
-
|
|
28673
|
-
|
|
28674
|
-
|
|
28675
|
-
|
|
28676
|
-
|
|
28677
|
-
|
|
28678
|
-
|
|
28679
|
-
|
|
28680
|
-
|
|
28681
|
-
|
|
28682
|
-
|
|
28683
|
-
}
|
|
28684
|
-
|
|
28685
|
-
|
|
28686
|
-
|
|
28687
|
-
|
|
28688
|
-
|
|
28689
|
-
|
|
28690
|
-
|
|
28691
|
-
|
|
28692
|
-
|
|
28693
|
-
|
|
28694
|
-
|
|
28695
|
-
|
|
28696
|
-
|
|
28697
|
-
|
|
28698
|
-
|
|
28699
|
-
|
|
28700
|
-
|
|
28701
|
-
|
|
28702
|
-
|
|
28703
|
-
|
|
28704
|
-
|
|
28705
|
-
|
|
28706
|
-
|
|
28707
|
-
|
|
28708
|
-
|
|
28709
|
-
|
|
28710
|
-
|
|
28711
|
-
|
|
28712
|
-
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
28717
|
-
|
|
28718
|
-
|
|
28719
|
-
|
|
28720
|
-
|
|
28721
|
-
|
|
28722
|
-
|
|
28723
|
-
|
|
28724
|
-
|
|
28725
|
-
|
|
28726
|
-
|
|
28727
|
-
|
|
28728
|
-
|
|
28729
|
-
|
|
28730
|
-
|
|
28731
|
-
|
|
28732
|
-
|
|
28733
|
-
|
|
28734
|
-
|
|
28735
|
-
|
|
28736
|
-
|
|
28737
|
-
|
|
28738
|
-
|
|
28739
|
-
|
|
28740
|
-
|
|
28741
|
-
|
|
28742
|
-
|
|
28743
|
-
|
|
28744
|
-
|
|
28745
|
-
|
|
28746
|
-
|
|
28747
|
-
|
|
28748
|
-
|
|
28749
|
-
|
|
28750
|
-
|
|
28751
|
-
|
|
28752
|
-
|
|
28753
|
-
|
|
28754
|
-
|
|
28755
|
-
|
|
28756
|
-
|
|
28757
|
-
|
|
28758
|
-
|
|
28759
|
-
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
28763
|
-
|
|
28764
|
-
|
|
28765
|
-
|
|
28766
|
-
|
|
28767
|
-
|
|
28768
|
-
|
|
28769
|
-
|
|
28770
|
-
|
|
28771
|
-
|
|
28772
|
-
|
|
28773
|
-
|
|
28774
|
-
|
|
28775
|
-
|
|
28776
|
-
|
|
28777
|
-
|
|
28778
|
-
|
|
28779
|
-
|
|
28780
|
-
|
|
28781
|
-
var
|
|
28782
|
-
|
|
28783
|
-
|
|
28784
|
-
|
|
28785
|
-
|
|
28786
|
-
|
|
28787
|
-
|
|
28788
|
-
|
|
28789
|
-
|
|
28790
|
-
|
|
28791
|
-
|
|
28792
|
-
|
|
28793
|
-
|
|
28794
|
-
|
|
28795
|
-
|
|
28796
|
-
|
|
28797
|
-
|
|
28798
|
-
|
|
28799
|
-
|
|
28800
|
-
|
|
28801
|
-
|
|
28802
|
-
|
|
28803
|
-
|
|
28804
|
-
|
|
28805
|
-
|
|
28806
|
-
|
|
28807
|
-
|
|
28808
|
-
|
|
28809
|
-
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
28816
|
-
|
|
28817
|
-
|
|
28818
|
-
|
|
28819
|
-
|
|
29559
|
+
|
|
29560
|
+
RTE_DefaultConfig.plugin_insertemoji = RTE_Plugin_InsertEmoji;
|
|
29561
|
+
|
|
29562
|
+
function RTE_Plugin_InsertEmoji() {
|
|
29563
|
+
|
|
29564
|
+
function CharToHTMLCode(ch) {
|
|
29565
|
+
if (ch.length > 2)
|
|
29566
|
+
return ch;
|
|
29567
|
+
|
|
29568
|
+
if (ch.length == 1 || ch.charCodeAt(1) == 0xfe0f)
|
|
29569
|
+
return "&#" + ch.charCodeAt(0) + ";"
|
|
29570
|
+
|
|
29571
|
+
if (ch.charCodeAt(1) == 0xfe0f) {
|
|
29572
|
+
return "&#" + ch.charCodeAt(0) + ";"
|
|
29573
|
+
}
|
|
29574
|
+
|
|
29575
|
+
var offset = ch.charCodeAt(0) - 0xd83c;
|
|
29576
|
+
if (offset < 0 && offset > 3)//not support
|
|
29577
|
+
return ch;
|
|
29578
|
+
|
|
29579
|
+
var second = ch.charCodeAt(1) - 0xdc04;
|
|
29580
|
+
var f = offset * 0x400 + second + 0x1f004;
|
|
29581
|
+
return "&#x" + f.toString(16) + ";"
|
|
29582
|
+
}
|
|
29583
|
+
|
|
29584
|
+
|
|
29585
|
+
var groupnames = ["smileys", "people", "animals", "food", "travel", "activities", "objects", "symbols"]
|
|
29586
|
+
var emojistrs = ["😀#grinning face|😃#grinning face with big eyes|😄#grinning face with smiling eyes|😁#beaming face with smiling eyes|😆#grinning squinting face|😅#grinning face with sweat|🤣#rolling on the floor laughing|😂#face with tears of joy|🙂#slightly smiling face|🙃#upside-down face|😉#winking face|😊#smiling face with smiling eyes|😇#smiling face with halo|😍#smiling face with heart-eyes|🤩#star-struck|😘#face blowing a kiss|😗#kissing face|😚#kissing face with closed eyes|😙#kissing face with smiling eyes|😋#face savoring food|😛#face with tongue|😜#winking face with tongue|🤪#zany face|😝#squinting face with tongue|🤑#money-mouth face|🤗#hugging face|🤭#face with hand over mouth|🤫#shushing face|🤔#thinking face|🤐#zipper-mouth face|🤨#face with raised eyebrow|😐#neutral face|😑#expressionless face|😶#face without mouth|😏#smirking face|😒#unamused face|🙄#face with rolling eyes|😬#grimacing face|🤥#lying face|😌#relieved face|😔#pensive face|😪#sleepy face|🤤#drooling face|😴#sleeping face|😷#face with medical mask|🤒#face with thermometer|🤕#face with head-bandage|🤢#nauseated face|🤮#face vomiting|🤧#sneezing face|😵#dizzy face|🤯#exploding head|🤠#cowboy hat face|😎#smiling face with sunglasses|🤓#nerd face|🧐#face with monocle|😕#confused face|😟#worried face|🙁#slightly frowning face|☹️#frowning face|😮#face with open mouth|😯#hushed face|😲#astonished face|😳#flushed face|😦#frowning face with open mouth|😧#anguished face|😨#fearful face|😰#anxious face with sweat|😥#sad but relieved face|😢#crying face|😭#loudly crying face|😱#face screaming in fear|😖#confounded face|😣#persevering face|😞#disappointed face|😓#downcast face with sweat|😩#weary face|😫#tired face|😤#face with steam from nose|😡#pouting face|😠#angry face|🤬#face with symbols on mouth|😈#smiling face with horns|👿#angry face with horns|💀#skull|☠️#skull and crossbones|💩#pile of poo|🤡#clown face|👹#ogre|👺#goblin|👻#ghost|👽#alien|👾#alien monster|🤖#robot|😺#grinning cat|😸#grinning cat with smiling eyes|😹#cat with tears of joy|😻#smiling cat with heart-eyes|😼#cat with wry smile|😽#kissing cat|🙀#weary cat|😿#crying cat|😾#pouting cat|🙈#see-no-evil monkey|🙉#hear-no-evil monkey|🙊#speak-no-evil monkey|💋#kiss mark|💌#love letter|💘#heart with arrow|💝#heart with ribbon|💖#sparkling heart|💗#growing heart|💓#beating heart|💞#revolving hearts|💕#two hearts|💟#heart decoration|❣️#heart exclamation|💔#broken heart|❤️#red heart|🧡#orange heart|💛#yellow heart|💚#green heart|💙#blue heart|💜#purple heart|🖤#black heart|💯#hundred points|💢#anger symbol|💥#collision|💫#dizzy|💦#sweat droplets|💨#dashing away|💣#bomb|💬#speech balloon|💭#thought balloon|💤#zzz", "👋#waving hand|🤚#raised back of hand|✋#raised hand|🖖#vulcan salute|👌#OK hand|✌️#victory hand|🤞#crossed fingers|🤟#love-you gesture|🤘#sign of the horns|🤙#call me hand|👈#backhand index pointing left|👉#backhand index pointing right|👆#backhand index pointing up|🖕#middle finger|👇#backhand index pointing down|☝️#index pointing up|👍#thumbs up|👎#thumbs down|✊#raised fist|👊#oncoming fist|🤛#left-facing fist|🤜#right-facing fist|👏#clapping hands|🙌#raising hands|👐#open hands|🤲#palms up together|🤝#handshake|🙏#folded hands|✍️#writing hand|💅#nail polish|🤳#selfie|💪#flexed biceps|👂#ear|👃#nose|🧠#brain|👀#eyes|👅#tongue|👄#mouth|👶#baby|🧒#child|👦#boy|👧#girl|🧑#person|👱#person with blond hair|👨#man|🧔#man with beard|👩#woman|🧓#older person|👴#old man|👵#old woman|🙍#person frowning|🙎#person pouting|🙅#person gesturing NO|🙆#person gesturing OK|💁#person tipping hand|🙋#person raising hand|🙇#person bowing|🤦#person facepalming|🤷#person shrugging|👮#police officer|💂#guard|👷#construction worker|🤴#prince|👸#princess|👳#person wearing turban|👲#person with skullcap|🧕#woman with headscarf|🤵#person in tuxedo|👰#person with veil|🤰#pregnant woman|🤱#breast-feeding|👼#baby angel|🎅#Santa Claus|🤶#Mrs. Claus|🧙#mage|🧚#fairy|🧛#vampire|🧜#merperson|🧝#elf|🧞#genie|🧟#zombie|💆#person getting massage|💇#person getting haircut|🚶#person walking|🏃#person running|💃#woman dancing|🕺#man dancing|👯#people with bunny ears|🧖#person in steamy room|🧗#person climbing|🤺#person fencing|🏇#horse racing|⛷️#skier|🏂#snowboarder|🏄#person surfing|🚣#person rowing boat|🏊#person swimming|⛹️#person bouncing ball|🚴#person biking|🚵#person mountain biking|🤸#person cartwheeling|🤼#people wrestling|🤽#person playing water polo|🤾#person playing handball|🤹#person juggling|🧘#person in lotus position|🛀#person taking bath|🛌#person in bed|👭#women holding hands|👫#woman and man holding hands|👬#men holding hands|💏#kiss|💑#couple with heart|👪#family|👤#bust in silhouette|👥#busts in silhouette|👣#footprints", "🐵#monkey face|🐒#monkey|🦍#gorilla|🐶#dog face|🐕#dog|🐩#poodle|🐺#wolf|🦊#fox|🐱#cat face|🐈#cat|🦁#lion|🐯#tiger face|🐅#tiger|🐆#leopard|🐴#horse face|🐎#horse|🦄#unicorn|🦓#zebra|🦌#deer|🐮#cow face|🐂#ox|🐃#water buffalo|🐄#cow|🐷#pig face|🐖#pig|🐗#boar|🐽#pig nose|🐏#ram|🐑#ewe|🐐#goat|🐪#camel|🐫#two-hump camel|🦒#giraffe|🐘#elephant|🦏#rhinoceros|🐭#mouse face|🐁#mouse|🐀#rat|🐹#hamster|🐰#rabbit face|🐇#rabbit|🦔#hedgehog|🦇#bat|🐻#bear|🐨#koala|🐼#panda|🐾#paw prints|🦃#turkey|🐔#chicken|🐓#rooster|🐣#hatching chick|🐤#baby chick|🐥#front-facing baby chick|🐦#bird|🐧#penguin|🦅#eagle|🦆#duck|🦉#owl|🐸#frog|🐊#crocodile|🐢#turtle|🦎#lizard|🐍#snake|🐲#dragon face|🐉#dragon|🦕#sauropod|🦖#T-Rex|🐳#spouting whale|🐋#whale|🐬#dolphin|🐟#fish|🐠#tropical fish|🐡#blowfish|🦈#shark|🐙#octopus|🐚#spiral shell|🐌#snail|🦋#butterfly|🐛#bug|🐜#ant|🐝#honeybee|🐞#lady beetle|🦗#cricket|🦂#scorpion|💐#bouquet|🌸#cherry blossom|💮#white flower|🌹#rose|🥀#wilted flower|🌺#hibiscus|🌻#sunflower|🌼#blossom|🌷#tulip|🌱#seedling|🌲#evergreen tree|🌳#deciduous tree|🌴#palm tree|🌵#cactus|🌾#sheaf of rice|🌿#herb|☘️#shamrock|🍀#four leaf clover|🍁#maple leaf|🍂#fallen leaf|🍃#leaf fluttering in wind", "🍇#grapes|🍈#melon|🍉#watermelon|🍊#tangerine|🍋#lemon|🍌#banana|🍍#pineapple|🍎#red apple|🍏#green apple|🍐#pear|🍑#peach|🍒#cherries|🍓#strawberry|🥝#kiwi fruit|🍅#tomato|🥥#coconut|🥑#avocado|🍆#eggplant|🥔#potato|🥕#carrot|🌽#ear of corn|🥒#cucumber|🥦#broccoli|🍄#mushroom|🥜#peanuts|🌰#chestnut|🍞#bread|🥐#croissant|🥖#baguette bread|🥨#pretzel|🥞#pancakes|🧀#cheese wedge|🍖#meat on bone|🍗#poultry leg|🥩#cut of meat|🥓#bacon|🍔#hamburger|🍟#french fries|🍕#pizza|🌭#hot dog|🥪#sandwich|🌮#taco|🌯#burrito|🥙#stuffed flatbread|🥚#egg|🍳#cooking|🥘#shallow pan of food|🍲#pot of food|🥣#bowl with spoon|🥗#green salad|🍿#popcorn|🥫#canned food|🍱#bento box|🍘#rice cracker|🍙#rice ball|🍚#cooked rice|🍛#curry rice|🍜#steaming bowl|🍝#spaghetti|🍠#roasted sweet potato|🍢#oden|🍣#sushi|🍤#fried shrimp|🍥#fish cake with swirl|🍡#dango|🥟#dumpling|🥠#fortune cookie|🥡#takeout box|🦀#crab|🦐#shrimp|🦑#squid|🍦#soft ice cream|🍧#shaved ice|🍨#ice cream|🍩#doughnut|🍪#cookie|🎂#birthday cake|🍰#shortcake|🥧#pie|🍫#chocolate bar|🍬#candy|🍭#lollipop|🍮#custard|🍯#honey pot|🍼#baby bottle|🥛#glass of milk|☕#hot beverage|🍵#teacup without handle|🍶#sake|🍾#bottle with popping cork|🍷#wine glass|🍸#cocktail glass|🍹#tropical drink|🍺#beer mug|🍻#clinking beer mugs|🥂#clinking glasses|🥃#tumbler glass|🥤#cup with straw|🥢#chopsticks|🍴#fork and knife|🥄#spoon|🔪#kitchen knife|🏺#amphora", "🌍#globe showing Europe-Africa|🌎#globe showing Americas|🌏#globe showing Asia-Australia|🌐#globe with meridians|🗾#map of Japan|⛰️#mountain|🌋#volcano|🗻#mount fuji|🏠#house|🏡#house with garden|🏢#office building|🏣#Japanese post office|🏤#post office|🏥#hospital|🏦#bank|🏨#hotel|🏩#love hotel|🏪#convenience store|🏫#school|🏬#department store|🏭#factory|🏯#Japanese castle|🏰#castle|💒#wedding|🗼#Tokyo tower|🗽#Statue of Liberty|⛪#church|🕌#mosque|🕍#synagogue|⛩️#shinto shrine|🕋#kaaba|⛲#fountain|⛺#tent|🌁#foggy|🌃#night with stars|🌄#sunrise over mountains|🌅#sunrise|🌆#cityscape at dusk|🌇#sunset|🌉#bridge at night|♨️#hot springs|🎠#carousel horse|🎡#ferris wheel|🎢#roller coaster|💈#barber pole|🎪#circus tent|🚂#locomotive|🚃#railway car|🚄#high-speed train|🚅#bullet train|🚆#train|🚇#metro|🚈#light rail|🚉#station|🚊#tram|🚝#monorail|🚞#mountain railway|🚋#tram car|🚌#bus|🚍#oncoming bus|🚎#trolleybus|🚐#minibus|🚑#ambulance|🚒#fire engine|🚓#police car|🚔#oncoming police car|🚕#taxi|🚖#oncoming taxi|🚗#automobile|🚘#oncoming automobile|🚙#sport utility vehicle|🚚#delivery truck|🚛#articulated lorry|🚜#tractor|🛵#motor scooter|🚲#bicycle|🛴#kick scooter|🚏#bus stop|⛽#fuel pump|🚨#police car light|🚥#horizontal traffic light|🚦#vertical traffic light|🛑#stop sign|🚧#construction|⚓#anchor|⛵#sailboat|🛶#canoe|🚤#speedboat|⛴️#ferry|🚢#ship|✈️#airplane|🛫#airplane departure|🛬#airplane arrival|💺#seat|🚁#helicopter|🚟#suspension railway|🚠#mountain cableway|🚡#aerial tramway|🚀#rocket|🛸#flying saucer|⌛#hourglass done|⏳#hourglass not done|⌚#watch|⏰#alarm clock|⏱️#stopwatch|⏲️#timer clock|🕛#twelve o’clock|🕧#twelve-thirty|🕐#one o’clock|🕜#one-thirty|🕑#two o’clock|🕝#two-thirty|🕒#three o’clock|🕞#three-thirty|🕓#four o’clock|🕟#four-thirty|🕔#five o’clock|🕠#five-thirty|🕕#six o’clock|🕡#six-thirty|🕖#seven o’clock|🕢#seven-thirty|🕗#eight o’clock|🕣#eight-thirty|🕘#nine o’clock|🕤#nine-thirty|🕙#ten o’clock|🕥#ten-thirty|🕚#eleven o’clock|🕦#eleven-thirty|🌑#new moon|🌒#waxing crescent moon|🌓#first quarter moon|🌔#waxing gibbous moon|🌕#full moon|🌖#waning gibbous moon|🌗#last quarter moon|🌘#waning crescent moon|🌙#crescent moon|🌚#new moon face|🌛#first quarter moon face|🌜#last quarter moon face|☀️#sun|🌝#full moon face|🌞#sun with face|⭐#star|🌟#glowing star|🌠#shooting star|🌌#milky way|☁️#cloud|⛅#sun behind cloud|⛈️#cloud with lightning and rain|🌀#cyclone|🌈#rainbow|🌂#closed umbrella|☂️#umbrella|☔#umbrella with rain drops|⛱️#umbrella on ground|⚡#high voltage|❄️#snowflake|☃️#snowman|⛄#snowman without snow|☄️#comet|🔥#fire|💧#droplet|🌊#water wave", "🎃#jack-o-lantern|🎄#Christmas tree|🎆#fireworks|🎇#sparkler|✨#sparkles|🎈#balloon|🎉#party popper|🎊#confetti ball|🎋#tanabata tree|🎍#pine decoration|🎎#Japanese dolls|🎏#carp streamer|🎐#wind chime|🎑#moon viewing ceremony|🎀#ribbon|🎁#wrapped gift|🎫#ticket|🏆#trophy|🏅#sports medal|🥇#1st place medal|🥈#2nd place medal|🥉#3rd place medal|⚽#soccer ball|⚾#baseball|🏀#basketball|🏐#volleyball|🏈#american football|🏉#rugby football|🎾#tennis|🎳#bowling|🏏#cricket game|🏑#field hockey|🏒#ice hockey|🏓#ping pong|🏸#badminton|🥊#boxing glove|🥋#martial arts uniform|🥅#goal net|⛳#flag in hole|⛸️#ice skate|🎣#fishing pole|🎽#running shirt|🎿#skis|🛷#sled|🥌#curling stone|🎯#direct hit|🎱#pool 8 ball|🔮#crystal ball|🎮#video game|🎰#slot machine|🎲#game die|♠️#spade suit|♥️#heart suit|♦️#diamond suit|♣️#club suit|🃏#joker|🀄#mahjong red dragon|🎴#flower playing cards|🎭#performing arts|🎨#artist palette", "👓#glasses|👔#necktie|👕#t-shirt|👖#jeans|🧣#scarf|🧤#gloves|🧥#coat|🧦#socks|👗#dress|👘#kimono|👙#bikini|👚#woman’s clothes|👛#purse|👜#handbag|👝#clutch bag|🎒#backpack|👞#man’s shoe|👟#running shoe|👠#high-heeled shoe|👡#woman’s sandal|👢#woman’s boot|👑#crown|👒#woman’s hat|🎩#top hat|🎓#graduation cap|🧢#billed cap|⛑️#rescue worker’s helmet|📿#prayer beads|💄#lipstick|💍#ring|💎#gem stone|🔇#muted speaker|🔈#speaker low volume|🔉#speaker medium volume|🔊#speaker high volume|📢#loudspeaker|📣#megaphone|📯#postal horn|🔔#bell|🔕#bell with slash|🎼#musical score|🎵#musical note|🎶#musical notes|🎤#microphone|🎧#headphone|📻#radio|🎷#saxophone|🎸#guitar|🎹#musical keyboard|🎺#trumpet|🎻#violin|🥁#drum|📱#mobile phone|📲#mobile phone with arrow|☎️#telephone|📞#telephone receiver|📟#pager|📠#fax machine|🔋#battery|🔌#electric plug|💻#laptop|⌨️#keyboard|💽#computer disk|💾#floppy disk|💿#optical disk|📀#dvd|🎥#movie camera|🎬#clapper board|📺#television|📷#camera|📸#camera with flash|📹#video camera|📼#videocassette|🔍#magnifying glass tilted left|🔎#magnifying glass tilted right|💡#light bulb|🔦#flashlight|🏮#red paper lantern|📔#notebook with decorative cover|📕#closed book|📖#open book|📗#green book|📘#blue book|📙#orange book|📚#books|📓#notebook|📒#ledger|📃#page with curl|📜#scroll|📄#page facing up|📰#newspaper|📑#bookmark tabs|🔖#bookmark|💰#money bag|💴#yen banknote|💵#dollar banknote|💶#euro banknote|💷#pound banknote|💸#money with wings|💳#credit card|💹#chart increasing with yen|✉️#envelope|📧#e-mail|📨#incoming envelope|📩#envelope with arrow|📤#outbox tray|📥#inbox tray|📦#package|📫#closed mailbox with raised flag|📪#closed mailbox with lowered flag|📬#open mailbox with raised flag|📭#open mailbox with lowered flag|📮#postbox|✏️#pencil|✒️#black nib|📝#memo|💼#briefcase|📁#file folder|📂#open file folder|📅#calendar|📆#tear-off calendar|📇#card index|📈#chart increasing|📉#chart decreasing|📊#bar chart|📋#clipboard|📌#pushpin|📍#round pushpin|📎#paperclip|📏#straight ruler|📐#triangular ruler|✂️#scissors|🔒#locked|🔓#unlocked|🔏#locked with pen|🔐#locked with key|🔑#key|🔨#hammer|⛏️#pick|⚒️#hammer and pick|⚔️#crossed swords|🔫#pistol|🏹#bow and arrow|🔧#wrench|🔩#nut and bolt|⚙️#gear|⚖️#balance scale|🔗#link|⛓️#chains|⚗️#alembic|🔬#microscope|🔭#telescope|📡#satellite antenna|💉#syringe|💊#pill|🚪#door|🚽#toilet|🚿#shower|🛁#bathtub|🛒#shopping cart|🚬#cigarette|⚰️#coffin|⚱️#funeral urn|🗿#moai", "🏧#ATM sign|🚮#litter in bin sign|🚰#potable water|♿#wheelchair symbol|🚹#men’s room|🚺#women’s room|🚻#restroom|🚼#baby symbol|🚾#water closet|🛂#passport control|🛃#customs|🛄#baggage claim|🛅#left luggage|⚠️#warning|🚸#children crossing|⛔#no entry|🚫#prohibited|🚳#no bicycles|🚭#no smoking|🚯#no littering|🚱#non-potable water|🚷#no pedestrians|📵#no mobile phones|🔞#no one under eighteen|☢️#radioactive|☣️#biohazard|⬆️#up arrow|↗️#up-right arrow|➡️#right arrow|↘️#down-right arrow|⬇️#down arrow|↙️#down-left arrow|⬅️#left arrow|↖️#up-left arrow|↕️#up-down arrow|↔️#left-right arrow|↩️#right arrow curving left|↪️#left arrow curving right|⤴️#right arrow curving up|⤵️#right arrow curving down|🔃#clockwise vertical arrows|🔄#counterclockwise arrows button|🔙#BACK arrow|🔚#END arrow|🔛#ON! arrow|🔜#SOON arrow|🔝#TOP arrow|🛐#place of worship|⚛️#atom symbol|✡️#star of David|☸️#wheel of dharma|☯️#yin yang|✝️#latin cross|☦️#orthodox cross|☪️#star and crescent|☮️#peace symbol|🕎#menorah|🔯#dotted six-pointed star|♈#Aries|♉#Taurus|♊#Gemini|♋#Cancer|♌#Leo|♍#Virgo|♎#Libra|♏#Scorpio|♐#Sagittarius|♑#Capricorn|♒#Aquarius|♓#Pisces|⛎#Ophiuchus|🔀#shuffle tracks button|🔁#repeat button|🔂#repeat single button|▶️#play button|⏩#fast-forward button|⏭️#next track button|⏯️#play or pause button|◀️#reverse button|⏪#fast reverse button|⏮️#last track button|🔼#upwards button|⏫#fast up button|🔽#downwards button|⏬#fast down button|⏸️#pause button|⏹️#stop button|⏺️#record button|⏏️#eject button|🎦#cinema|🔅#dim button|🔆#bright button|📶#antenna bars|📳#vibration mode|📴#mobile phone off|♀️#female sign|♂️#male sign|✖️#multiply|➕#plus|➖#minus|➗#divide|‼️#double exclamation mark|⁉️#exclamation question mark|❓#question mark|❔#white question mark|❕#white exclamation mark|❗#exclamation mark|〰️#wavy dash|💱#currency exchange|💲#heavy dollar sign|⚕️#medical symbol|♻️#recycling symbol|⚜️#fleur-de-lis|🔱#trident emblem|📛#name badge|🔰#Japanese symbol for beginner|⭕#hollow red circle|✅#check mark button|☑️#check box with check|✔️#check mark|❌#cross mark|❎#cross mark button|➰#curly loop|➿#double curly loop|〽️#part alternation mark|✳️#eight-spoked asterisk|✴️#eight-pointed star|❇️#sparkle|©️#copyright|®️#registered|™️#trade mark|🔟#keycap: 10|🔠#input latin uppercase|🔡#input latin lowercase|🔢#input numbers|🔣#input symbols|🔤#input latin letters|🆎#AB button (blood type)|🆑#CL button|🆒#COOL button|🆓#FREE button|ℹ️#information|🆔#ID button|Ⓜ️#circled M|🆕#NEW button|🆖#NG button|🆗#OK button|🆘#SOS button|🆙#UP! button|🆚#VS button|🈁#Japanese “here” button|🈶#Japanese “not free of charge” button|🈯#Japanese “reserved” button|🉐#Japanese “bargain” button|🈹#Japanese “discount” button|🈚#Japanese “free of charge” button|🈲#Japanese “prohibited” button|🉑#Japanese “acceptable” button|🈸#Japanese “application” button|🈴#Japanese “passing grade” button|🈳#Japanese “vacancy” button|㊗️#Japanese “congratulations” button|㊙️#Japanese “secret” button|🈺#Japanese “open for business” button|🈵#Japanese “no vacancy” button|🔴#red circle|🔵#blue circle|⚫#black circle|⚪#white circle|⬛#black large square|⬜#white large square|◼️#black medium square|◻️#white medium square|◾#black medium-small square|◽#white medium-small square|▪️#black small square|▫️#white small square|🔶#large orange diamond|🔷#large blue diamond|🔸#small orange diamond|🔹#small blue diamond|🔺#red triangle pointed up|🔻#red triangle pointed down|💠#diamond with a dot|🔘#radio button|🔳#white square button|🔲#black square button"]
|
|
29587
|
+
var emojidata = null;
|
|
29588
|
+
|
|
29589
|
+
function MakeEmojiData() {
|
|
29590
|
+
if (emojidata)
|
|
29591
|
+
return;
|
|
29592
|
+
emojidata = [];
|
|
29593
|
+
for (var groupindex = 0; groupindex < groupnames.length; groupindex++) {
|
|
29594
|
+
var emojiitems = [];
|
|
29595
|
+
var emojigroup = { index: groupindex, name: groupnames[groupindex], items: emojiitems };
|
|
29596
|
+
var emojiarr = emojistrs[groupindex].split('|');
|
|
29597
|
+
for (var ei = 0; ei < emojiarr.length; ei++) {
|
|
29598
|
+
var emojistr = emojiarr[ei];
|
|
29599
|
+
var pair = emojistr.split('#')
|
|
29600
|
+
emojiitems.push({ emoji: pair[0], keyword: pair[1] });
|
|
29601
|
+
}
|
|
29602
|
+
emojidata.push(emojigroup);
|
|
29603
|
+
}
|
|
29604
|
+
//console.log(emojidata);
|
|
29605
|
+
}
|
|
29606
|
+
|
|
29607
|
+
var obj = this;
|
|
29608
|
+
|
|
29609
|
+
var config, editor;
|
|
29610
|
+
|
|
29611
|
+
obj.PluginName = "InsertEmoji";
|
|
29612
|
+
|
|
29613
|
+
obj.InitConfig = function (argconfig) {
|
|
29614
|
+
config = argconfig;
|
|
29615
|
+
}
|
|
29616
|
+
obj.InitEditor = function (argeditor) {
|
|
29617
|
+
editor = argeditor;
|
|
29618
|
+
|
|
29619
|
+
editor.toolbarFactoryMap["insertemoji"] = function (cmd) {
|
|
29620
|
+
return editor.createToolbarItemDropDownPanel(cmd, function (panel) {
|
|
29621
|
+
|
|
29622
|
+
MakeEmojiData()
|
|
29623
|
+
|
|
29624
|
+
panel.style.width = "360px";
|
|
29625
|
+
panel.style.height = "420px";
|
|
29626
|
+
panel.style.display = "flex";
|
|
29627
|
+
panel.style.flexDirection = "column";
|
|
29628
|
+
|
|
29629
|
+
panel.onclick = function (e) {
|
|
29630
|
+
if (e.target.nodeName == "GSPAN") {
|
|
29631
|
+
editor.closeCurrentPopup();
|
|
29632
|
+
var htmlcode = e.target.getAttribute("htmlcode");
|
|
29633
|
+
editor.insertHTML(htmlcode);
|
|
29634
|
+
editor.collapse(false);
|
|
29635
|
+
editor.focus();
|
|
29636
|
+
}
|
|
29637
|
+
}
|
|
29638
|
+
|
|
29639
|
+
// Keyboard access for the emoji grid.
|
|
29640
|
+
//
|
|
29641
|
+
// The grid is <gspan> cells, which no browser makes focusable and
|
|
29642
|
+
// which the editor's own keyboard layer does not recognise (it
|
|
29643
|
+
// finds menu items by tag name, and these are not on that list).
|
|
29644
|
+
// Until the cells were given a role and a tab stop, the only
|
|
29645
|
+
// keyboard-reachable thing in this whole panel was the search box:
|
|
29646
|
+
// the picker announced itself as a menu and then could not be
|
|
29647
|
+
// operated without a mouse.
|
|
29648
|
+
function cells() {
|
|
29649
|
+
var visible = resultpanel.style.display !== "none" ? resultpanel : grouppanel;
|
|
29650
|
+
return [].slice.call(visible.querySelectorAll("gspan"));
|
|
29651
|
+
}
|
|
29652
|
+
|
|
29653
|
+
// The grid wraps, so "up" and "down" mean the nearest cell on the
|
|
29654
|
+
// adjacent visual row — computed from geometry rather than assuming
|
|
29655
|
+
// a fixed column count, which changes with the panel width.
|
|
29656
|
+
function step(list, from, dir) {
|
|
29657
|
+
var here = from.getBoundingClientRect();
|
|
29658
|
+
var candidates = list.filter(function (c) {
|
|
29659
|
+
var r = c.getBoundingClientRect();
|
|
29660
|
+
return dir < 0 ? r.bottom <= here.top + 1 : r.top >= here.bottom - 1;
|
|
29661
|
+
});
|
|
29662
|
+
if (!candidates.length) return null;
|
|
29663
|
+
var rowEdge = null;
|
|
29664
|
+
candidates.forEach(function (c) {
|
|
29665
|
+
var r = c.getBoundingClientRect();
|
|
29666
|
+
if (rowEdge === null) rowEdge = r.top;
|
|
29667
|
+
else if (dir < 0 ? r.top > rowEdge : r.top < rowEdge) rowEdge = r.top;
|
|
29668
|
+
});
|
|
29669
|
+
var row = candidates.filter(function (c) {
|
|
29670
|
+
return Math.abs(c.getBoundingClientRect().top - rowEdge) < 2;
|
|
29671
|
+
});
|
|
29672
|
+
var best = row[0], bestDx = Infinity;
|
|
29673
|
+
row.forEach(function (c) {
|
|
29674
|
+
var dx = Math.abs(c.getBoundingClientRect().left - here.left);
|
|
29675
|
+
if (dx < bestDx) { bestDx = dx; best = c; }
|
|
29676
|
+
});
|
|
29677
|
+
return best;
|
|
29678
|
+
}
|
|
29679
|
+
|
|
29680
|
+
panel.addEventListener("keydown", function (e) {
|
|
29681
|
+
var target = e.target;
|
|
29682
|
+
if (!target || target.nodeName !== "GSPAN") return;
|
|
29683
|
+
var list = cells();
|
|
29684
|
+
var at = list.indexOf(target);
|
|
29685
|
+
var next = null;
|
|
29686
|
+
|
|
29687
|
+
if (e.key === "ArrowRight") next = list[at + 1];
|
|
29688
|
+
else if (e.key === "ArrowLeft") next = list[at - 1];
|
|
29689
|
+
else if (e.key === "ArrowDown") next = step(list, target, 1);
|
|
29690
|
+
else if (e.key === "ArrowUp") next = step(list, target, -1);
|
|
29691
|
+
else if (e.key === "Home") next = list[0];
|
|
29692
|
+
else if (e.key === "End") next = list[list.length - 1];
|
|
29693
|
+
else if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
|
|
29694
|
+
e.preventDefault();
|
|
29695
|
+
e.stopPropagation();
|
|
29696
|
+
target.click();
|
|
29697
|
+
return;
|
|
29698
|
+
}
|
|
29699
|
+
else if (e.key === "Escape") {
|
|
29700
|
+
e.preventDefault();
|
|
29701
|
+
e.stopPropagation();
|
|
29702
|
+
editor.closeCurrentPopup();
|
|
29703
|
+
return;
|
|
29704
|
+
}
|
|
29705
|
+
else return;
|
|
29706
|
+
|
|
29707
|
+
e.preventDefault();
|
|
29708
|
+
e.stopPropagation();
|
|
29709
|
+
if (next) next.focus();
|
|
29710
|
+
}, true);
|
|
29711
|
+
|
|
29712
|
+
var selecteditem = null;
|
|
29713
|
+
var toselectitem = null;
|
|
29714
|
+
function clear_selecteditem() {
|
|
29715
|
+
if (selecteditem != null) {
|
|
29716
|
+
selecteditem.style.backgroundColor = "";
|
|
29717
|
+
selecteditem = null;
|
|
29718
|
+
}
|
|
29719
|
+
}
|
|
29720
|
+
function set_selecteditem() {
|
|
29721
|
+
clear_selecteditem();
|
|
29722
|
+
selecteditem = toselectitem; selecteditem.style.backgroundColor = "#e6e6e6";
|
|
29723
|
+
toselectitem = null;
|
|
29724
|
+
}
|
|
29725
|
+
var tid_sel = 0;
|
|
29726
|
+
|
|
29727
|
+
panel.onmouseover = function (e) {
|
|
29728
|
+
for (var node = e.target; node != panel; node = node.parentNode) {
|
|
29729
|
+
if (node.nodeName == "GITEM") {
|
|
29730
|
+
if (node == toselectitem)
|
|
29731
|
+
return;
|
|
29732
|
+
toselectitem = node;
|
|
29733
|
+
clearTimeout(tid_sel);
|
|
29734
|
+
tid_sel = setTimeout(set_selecteditem, 10);
|
|
29735
|
+
return;
|
|
29736
|
+
}
|
|
29737
|
+
}
|
|
29738
|
+
}
|
|
29739
|
+
panel.onmouseout = function () {
|
|
29740
|
+
clearTimeout(tid_sel);
|
|
29741
|
+
tid_sel = setTimeout(clear_selecteditem, 10)
|
|
29742
|
+
}
|
|
29743
|
+
|
|
29744
|
+
var searchbar = __Append(panel, "label", "margin:5px;position:relative;");
|
|
29745
|
+
searchbar.setAttribute("id", "emojis_searchbar");
|
|
29746
|
+
var searchbox = __Append(searchbar, "input", "width:100%;padding:5px 20px;border:solid 1px #ccc;border-radius:5px;");
|
|
29747
|
+
searchbox.setAttribute("placeholder", editor.getLangText("searchemojis"));
|
|
29748
|
+
|
|
29749
|
+
var tid_key = 0;
|
|
29750
|
+
searchbox.onchange = searchbox.onkeyup = searchbox.onkeypress = searchbox.onpaste = function () {
|
|
29751
|
+
clearTimeout(tid_key);
|
|
29752
|
+
tid_key = setTimeout(show_result, 100);
|
|
29753
|
+
}
|
|
29754
|
+
// 2026-05-11 quick-load rewrite: bulk-build via innerHTML and
|
|
29755
|
+
// only render the active category. Previous version eagerly
|
|
29756
|
+
// built ~4000 DOM nodes for 1,037 emojis on every panel open,
|
|
29757
|
+
// noticeably slow on low-end machines. New flow:
|
|
29758
|
+
// - panel opens → render first category only (~125 nodes)
|
|
29759
|
+
// - tab click → swap to that category's HTML
|
|
29760
|
+
// - search → bulk innerHTML build of filtered results
|
|
29761
|
+
function buildCategoryHTML(group) {
|
|
29762
|
+
var parts = [];
|
|
29763
|
+
parts.push('<div style="padding:3px;margin-top:5px;color:darkblue;">' + group.name[0].toUpperCase() + group.name.substring(1) + '</div>');
|
|
29764
|
+
parts.push('<div style="display:flex;flex-direction:row;flex-wrap:wrap;">');
|
|
29765
|
+
for (var i = 0; i < group.items.length; i++) {
|
|
29766
|
+
var item = group.items[i];
|
|
29767
|
+
var htmlcode = CharToHTMLCode(item.emoji);
|
|
29768
|
+
parts.push('<gitem class="rte-flex-column-center" style="width:32px;height:32px;margin:2px"><gspan role="menuitem" tabindex="0" aria-label="' + (item.keyword || item.emoji).replace(/"/g, '') + '" htmlcode="' + htmlcode + '" title="' + item.emoji + ' ' + (item.keyword || '').replace(/"/g, '') + '">' + htmlcode + '</gspan></gitem>');
|
|
29769
|
+
}
|
|
29770
|
+
parts.push('</div>');
|
|
29771
|
+
return parts.join('');
|
|
29772
|
+
}
|
|
29773
|
+
|
|
29774
|
+
function show_result() {
|
|
29775
|
+
var keyword = searchbox.value.trim().toLowerCase();
|
|
29776
|
+
if (!keyword) {
|
|
29777
|
+
tabpanel.style.display =
|
|
29778
|
+
grouppanel.style.display = "";
|
|
29779
|
+
resultpanel.style.display = "none";
|
|
29780
|
+
return;
|
|
29781
|
+
}
|
|
29782
|
+
|
|
29783
|
+
tabpanel.style.display =
|
|
29784
|
+
grouppanel.style.display = "none";
|
|
29785
|
+
resultpanel.style.display = "flex";
|
|
29786
|
+
|
|
29787
|
+
var hitsHtml = [];
|
|
29788
|
+
var itemindex = 0;
|
|
29789
|
+
for (var gi = 0; gi < emojidata.length; gi++) {
|
|
29790
|
+
var group = emojidata[gi];
|
|
29791
|
+
for (var ii = 0; ii < group.items.length; ii++) {
|
|
29792
|
+
var item = group.items[ii];
|
|
29793
|
+
if (!item.keyword || item.keyword.indexOf(keyword) == -1)
|
|
29794
|
+
continue;
|
|
29795
|
+
itemindex++;
|
|
29796
|
+
var htmlcode = CharToHTMLCode(item.emoji);
|
|
29797
|
+
hitsHtml.push('<gitem class="rte-flex-column-center" style="width:32px;height:32px;margin:2px"><gspan role="menuitem" tabindex="0" aria-label="' + (item.keyword || item.emoji).replace(/"/g, '') + '" htmlcode="' + htmlcode + '" title="' + item.emoji + ' ' + (item.keyword || '').replace(/"/g, '') + '">' + htmlcode + '</gspan></gitem>');
|
|
29798
|
+
}
|
|
29799
|
+
}
|
|
29800
|
+
resultpanel.innerHTML = '<div style="width:100%;padding:3px;margin-top:5px;color:darkblue;text-align:center;">' + itemindex + ' items</div>' + hitsHtml.join('');
|
|
29801
|
+
}
|
|
29802
|
+
|
|
29803
|
+
panel.setAttribute("id", "emoji-picker");
|
|
29804
|
+
|
|
29805
|
+
var tabpanel = __Append(panel, "div");
|
|
29806
|
+
|
|
29807
|
+
var resultpanel = __Append(panel, "div", "display:none;flex-direction:row;flex-wrap:wrap;overflow-y:scroll;padding-bottom:55px");
|
|
29808
|
+
|
|
29809
|
+
var grouppanel = __Append(panel, "div", "overflow-y:scroll;padding-bottom:55px;flex:999");
|
|
29810
|
+
|
|
29811
|
+
// Lazy-render: only build the active category. Each tab click swaps the html.
|
|
29812
|
+
var activeGroupIndex = 0;
|
|
29813
|
+
function renderCategory(gi) {
|
|
29814
|
+
activeGroupIndex = gi;
|
|
29815
|
+
grouppanel.innerHTML = buildCategoryHTML(emojidata[gi]);
|
|
29816
|
+
grouppanel.scrollTop = 0;
|
|
29817
|
+
for (var bi = 0; bi < tabuibtns.length; bi++) {
|
|
29818
|
+
tabuibtns[bi].className = bi === gi ? "rte-ui-active" : "";
|
|
29819
|
+
}
|
|
29820
|
+
}
|
|
29821
|
+
|
|
29822
|
+
var tabui = __Append(tabpanel, "rte-tabui");
|
|
29823
|
+
tabui.setAttribute("id", "emoji-picker");
|
|
29824
|
+
var tabuitoolbar = __Append(tabui, "rte-tabui-toolbar");
|
|
29825
|
+
var tabuibtns = [];
|
|
29826
|
+
for (var gi = 0; gi < emojidata.length; gi++) {
|
|
29827
|
+
(function (group) {
|
|
29828
|
+
var btn = __Append(tabuitoolbar, "rte-tabui-toolbar-button", "width:32px;text-align:center;margin:4px");
|
|
29829
|
+
btn.setAttribute("title", group.name);
|
|
29830
|
+
btn.innerHTML = group.items[0].emoji;
|
|
29831
|
+
btn.onclick = function () { renderCategory(group.index); };
|
|
29832
|
+
tabuibtns.push(btn);
|
|
29833
|
+
})(emojidata[gi]);
|
|
29834
|
+
}
|
|
29835
|
+
|
|
29836
|
+
// Initial render: just the first category (~125 emojis instead of all 1037).
|
|
29837
|
+
renderCategory(0);
|
|
29838
|
+
|
|
29839
|
+
searchbox.focus();
|
|
29840
|
+
|
|
29841
|
+
|
|
29842
|
+
})
|
|
29843
|
+
};
|
|
29844
|
+
|
|
29845
|
+
}
|
|
29846
|
+
|
|
29847
|
+
function __Append(parent, tagname, csstext, cssclass) {
|
|
29848
|
+
var tag = parent.ownerDocument.createElement(tagname);
|
|
29849
|
+
if (csstext) tag.style.cssText = csstext;
|
|
29850
|
+
if (cssclass) tag.className = cssclass;
|
|
29851
|
+
parent.appendChild(tag);
|
|
29852
|
+
return tag;
|
|
29853
|
+
}
|
|
29854
|
+
|
|
29855
|
+
|
|
29856
|
+
}
|
|
29857
|
+
|
|
29858
|
+
|
|
29859
|
+
|
|
29860
|
+
|
|
28820
29861
|
|
|
28821
29862
|
// 2026-08-05 Image gallery browser.
|
|
28822
29863
|
//
|
|
@@ -29051,6 +30092,15 @@ function RTE_Plugin_InsertGallery() {
|
|
|
29051
30092
|
|
|
29052
30093
|
var browser = append(dialoginner, "div", "", "rte-gallery-browser");
|
|
29053
30094
|
|
|
30095
|
+
// Exactly what the upload endpoint accepts, and nothing more. The file
|
|
30096
|
+
// picker used to advertise "image/*,...,.svg", so it offered files the
|
|
30097
|
+
// server then rejected with "Invalid file extension" and the user had no
|
|
30098
|
+
// way to know the real list until after the failure. Declared here
|
|
30099
|
+
// because both the header hint and the file input read it.
|
|
30100
|
+
// SVG is excluded on purpose: it can carry script, so it is not a safe
|
|
30101
|
+
// thing to store and serve back from our own origin.
|
|
30102
|
+
var GALLERY_ACCEPT = [".jpg", ".jpeg", ".jfif", ".png", ".gif", ".webp", ".bmp", ".avif"];
|
|
30103
|
+
|
|
29054
30104
|
// The dialog frame already draws a title bar and close button, so this is
|
|
29055
30105
|
// a one-line subtitle, not a second heading.
|
|
29056
30106
|
var header = append(browser, "div", "", "rte-dialog-browser-header");
|
|
@@ -29059,6 +30109,12 @@ function RTE_Plugin_InsertGallery() {
|
|
|
29059
30109
|
? "Browse folders, upload new files, and insert the selected image into the editor."
|
|
29060
30110
|
: "Browse the available images, filter by name, and insert the selected image into the editor.";
|
|
29061
30111
|
|
|
30112
|
+
// State the accepted formats up front. Previously the only way to learn
|
|
30113
|
+
// them was to pick an unsupported file and read "Invalid file extension".
|
|
30114
|
+
var acceptHint = append(header, "div", "", "rte-dialog-browser-accept");
|
|
30115
|
+
acceptHint.innerText = "Accepted formats: " +
|
|
30116
|
+
GALLERY_ACCEPT.map(function (ext) { return ext.replace(".", "").toUpperCase(); }).join(", ");
|
|
30117
|
+
|
|
29062
30118
|
var toolbar = append(browser, "div", "", "rte-gallery-browser-toolbar");
|
|
29063
30119
|
var toolbarLeft = append(toolbar, "div", "", "rte-gallery-browser-toolbar-group");
|
|
29064
30120
|
var toolbarRight = append(toolbar, "div", "", "rte-gallery-browser-toolbar-group rte-gallery-browser-toolbar-group-right");
|
|
@@ -29093,7 +30149,7 @@ function RTE_Plugin_InsertGallery() {
|
|
|
29093
30149
|
|
|
29094
30150
|
var fileInput = append(toolbarRight, "input", "display:none;");
|
|
29095
30151
|
fileInput.type = "file";
|
|
29096
|
-
fileInput.accept = "
|
|
30152
|
+
fileInput.accept = GALLERY_ACCEPT.join(",");
|
|
29097
30153
|
fileInput.multiple = true;
|
|
29098
30154
|
|
|
29099
30155
|
// Without a server there is no folder tree to walk and nothing to create
|
|
@@ -29119,6 +30175,12 @@ function RTE_Plugin_InsertGallery() {
|
|
|
29119
30175
|
grid.setAttribute("role", "listbox");
|
|
29120
30176
|
grid.setAttribute("aria-label", "Available images");
|
|
29121
30177
|
|
|
30178
|
+
// Non-blocking failure notice. Sits above the grid instead of replacing
|
|
30179
|
+
// it, so a rejected upload never hides the images you can still pick.
|
|
30180
|
+
var errorBar = append(surface, "div", "", "rte-gallery-browser-error");
|
|
30181
|
+
errorBar.setAttribute("role", "status");
|
|
30182
|
+
show(errorBar, false);
|
|
30183
|
+
|
|
29122
30184
|
var empty = append(surface, "div", "", "rte-gallery-browser-empty");
|
|
29123
30185
|
|
|
29124
30186
|
var footer = append(browser, "div", "", "rte-gallery-browser-footer");
|
|
@@ -29161,6 +30223,9 @@ function RTE_Plugin_InsertGallery() {
|
|
|
29161
30223
|
|
|
29162
30224
|
function setSelected(url) {
|
|
29163
30225
|
state.selectedUrl = url || "";
|
|
30226
|
+
// Picking an image is a fresh action, so a stale failure notice from
|
|
30227
|
+
// an earlier upload must not linger next to it.
|
|
30228
|
+
state.error = "";
|
|
29164
30229
|
render();
|
|
29165
30230
|
}
|
|
29166
30231
|
|
|
@@ -29314,12 +30379,16 @@ function RTE_Plugin_InsertGallery() {
|
|
|
29314
30379
|
return;
|
|
29315
30380
|
}
|
|
29316
30381
|
|
|
30382
|
+
// An upload failure must not wipe the browser. It used to replace the
|
|
30383
|
+
// whole grid, so one rejected file left the dialog reading "0 items"
|
|
30384
|
+
// with an error where the images should be - while the footer still
|
|
30385
|
+
// said the previously selected image was ready to insert. Two panels
|
|
30386
|
+
// contradicting each other. Show it as a banner and keep browsing.
|
|
29317
30387
|
if (state.error) {
|
|
29318
|
-
|
|
29319
|
-
show(
|
|
29320
|
-
|
|
29321
|
-
|
|
29322
|
-
return;
|
|
30388
|
+
errorBar.innerText = state.error;
|
|
30389
|
+
show(errorBar, true);
|
|
30390
|
+
} else {
|
|
30391
|
+
show(errorBar, false);
|
|
29323
30392
|
}
|
|
29324
30393
|
|
|
29325
30394
|
for (var folderIndex = 0; folderIndex < visibleFolders.length; folderIndex++) {
|
|
@@ -30055,7 +31124,33 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
30055
31124
|
// The editor already tracks active state — it just keeps it in a class.
|
|
30056
31125
|
// Mirroring rather than recomputing means the announced state can never
|
|
30057
31126
|
// disagree with the highlighted button.
|
|
30058
|
-
|
|
31127
|
+
// Command names here must match the TOOLBAR command exactly — the regex is
|
|
31128
|
+
// anchored, so a near-miss is a silent no-op that looks like coverage.
|
|
31129
|
+
// "blockquote" is one: the command is `insertblockquote`, so the entry
|
|
31130
|
+
// written specifically for it never matched and that button ships with no
|
|
31131
|
+
// aria-pressed. Found by checking every genuine toggle on the full toolbar
|
|
31132
|
+
// rather than the three the published verify page samples.
|
|
31133
|
+
//
|
|
31134
|
+
// This mirror only copies the editor's own active state, so a command may
|
|
31135
|
+
// only be listed here once core actually tracks it. Emitting the attribute
|
|
31136
|
+
// for an untracked command yields a permanent aria-pressed="false" —
|
|
31137
|
+
// confidently announcing "not pressed" while the formatting IS applied,
|
|
31138
|
+
// which makes announced state DISAGREE with the highlight. That is the exact
|
|
31139
|
+
// failure this mirror exists to prevent, so silence beats a wrong answer.
|
|
31140
|
+
// - `toggleborder` was already tracked (core checks the editable's
|
|
31141
|
+
// rte-toggleborder class); it reads inactive simply when borders are off.
|
|
31142
|
+
// - `insertblockquote` was NOT tracked and now is, via a core case added
|
|
31143
|
+
// alongside `indent`, which resolves the same way.
|
|
31144
|
+
// Both verified on the running editor rather than inferred.
|
|
31145
|
+
//
|
|
31146
|
+
// `inlinecode`, `pagination`, `typewriter` and `focusmode` match nothing in
|
|
31147
|
+
// this build, and now the reason is known rather than assumed: pagination.js
|
|
31148
|
+
// and typewriter.js are API-only (togglePageView / toggleTypewriterMode …)
|
|
31149
|
+
// and register no toolbar command, and insertcode.js registers `insertcode`,
|
|
31150
|
+
// which inserts a block rather than toggling the selection. They are kept —
|
|
31151
|
+
// harmless, and correct the moment a toolbar button is added — but they
|
|
31152
|
+
// cover nothing today, so do not read this list as evidence of coverage.
|
|
31153
|
+
var TOGGLE_CMD = /^(bold|italic|underline|strikethrough|subscript|superscript|justifyleft|justifycenter|justifyright|justifyfull|insertorderedlist|insertunorderedlist|outdent|indent|insertblockquote|toggleborder|inlinecode|trackchanges|typewriter|focusmode|pagination|formattingmarks|linenumbers|permanentpen|rtlui)$/;
|
|
30059
31154
|
|
|
30060
31155
|
function syncToggleStates(root) {
|
|
30061
31156
|
var btns = [].slice.call(root.querySelectorAll('[role="button"][rte-cmd-name]'));
|
|
@@ -32672,6 +33767,276 @@ function RTE_Plugin_MultiLevelList() {
|
|
|
32672
33767
|
|
|
32673
33768
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
32674
33769
|
|
|
33770
|
+
// 2026-08-15 Multi-root editing — one toolbar, several editable regions.
|
|
33771
|
+
//
|
|
33772
|
+
// The CMS / page-builder case: a page has a title, a summary and a body, and the
|
|
33773
|
+
// author wants one toolbar that acts on whichever region they are in, not three
|
|
33774
|
+
// stacked toolbars eating the viewport.
|
|
33775
|
+
//
|
|
33776
|
+
// SCOPE — read this before assuming parity with CKEditor's multi-root:
|
|
33777
|
+
// This editor edits <body> inside an IFRAME and has no div/inline mode. One
|
|
33778
|
+
// instance therefore owns exactly one editable document, and every plugin,
|
|
33779
|
+
// the selection model and the CRDT binding are written against that
|
|
33780
|
+
// assumption. Making a single instance own N editables is a core rewrite, not
|
|
33781
|
+
// a plugin.
|
|
33782
|
+
// What this does instead is COORDINATE several ordinary instances: they share
|
|
33783
|
+
// one visible toolbar and one group-level API. That covers the layout and the
|
|
33784
|
+
// workflow, which is what the feature is actually for.
|
|
33785
|
+
// What it does NOT do, and cannot without core work:
|
|
33786
|
+
// - a shared undo stack (Ctrl+Z undoes within the focused region only),
|
|
33787
|
+
// - a selection spanning two regions,
|
|
33788
|
+
// - one collaboration session across regions (each root syncs separately).
|
|
33789
|
+
// Those are stated in getCapabilities() as well as here, so an integrator
|
|
33790
|
+
// finds out from the API rather than from a bug report.
|
|
33791
|
+
//
|
|
33792
|
+
// Design notes:
|
|
33793
|
+
// - Toolbars are SHOWN/HIDDEN, not rebuilt or re-pointed. Each instance's
|
|
33794
|
+
// toolbar already drives its own editor correctly; re-routing commands
|
|
33795
|
+
// across instances would mean reimplementing every command's notion of
|
|
33796
|
+
// "current editor" and would break the moment a plugin cached one.
|
|
33797
|
+
// - Moving the active toolbar into a shared host is optional. DOM moves
|
|
33798
|
+
// preserve listeners, so the toolbar keeps working, but a host that is
|
|
33799
|
+
// positioned or scrolled differently can change the float-panel anchoring —
|
|
33800
|
+
// hence opt-in rather than default.
|
|
33801
|
+
// - Registration is by GROUP NAME in a module-level registry, because plugins
|
|
33802
|
+
// are constructed per instance and have no other way to see each other.
|
|
33803
|
+
RTE_DefaultConfig.plugin_multiroot = RTE_Plugin_MultiRoot;
|
|
33804
|
+
|
|
33805
|
+
// Instances sharing a group name act as one multi-root editor. null = off.
|
|
33806
|
+
if (typeof RTE_DefaultConfig.multiRootGroup === "undefined") RTE_DefaultConfig.multiRootGroup = null;
|
|
33807
|
+
// A name for this root in the group API. Defaults to the container's id.
|
|
33808
|
+
if (typeof RTE_DefaultConfig.multiRootName === "undefined") RTE_DefaultConfig.multiRootName = null;
|
|
33809
|
+
// Element (or its id) to move the active toolbar into. null = leave in place.
|
|
33810
|
+
if (typeof RTE_DefaultConfig.multiRootToolbarHost === "undefined") RTE_DefaultConfig.multiRootToolbarHost = null;
|
|
33811
|
+
|
|
33812
|
+
// group name -> { roots: [entry], active: entry }
|
|
33813
|
+
if (!window.__RTE_MultiRootGroups) window.__RTE_MultiRootGroups = {};
|
|
33814
|
+
|
|
33815
|
+
function RTE_Plugin_MultiRoot() {
|
|
33816
|
+
var obj = this;
|
|
33817
|
+
var config, editor;
|
|
33818
|
+
var entry = null;
|
|
33819
|
+
|
|
33820
|
+
obj.PluginName = "MultiRoot";
|
|
33821
|
+
|
|
33822
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
33823
|
+
|
|
33824
|
+
obj.InitEditor = function (argeditor) {
|
|
33825
|
+
editor = argeditor;
|
|
33826
|
+
|
|
33827
|
+
var group = config.multiRootGroup;
|
|
33828
|
+
if (!group) return; // opt-in
|
|
33829
|
+
|
|
33830
|
+
entry = {
|
|
33831
|
+
name: config.multiRootName || containerId() || ("root" + (groupOf(group).roots.length + 1)),
|
|
33832
|
+
editor: editor,
|
|
33833
|
+
container: container(),
|
|
33834
|
+
group: group
|
|
33835
|
+
};
|
|
33836
|
+
groupOf(group).roots.push(entry);
|
|
33837
|
+
|
|
33838
|
+
editor.multiRoot = api(group);
|
|
33839
|
+
|
|
33840
|
+
// Wait for the toolbar to exist before hiding anything.
|
|
33841
|
+
setTimeout(function () { bindFocus(); settle(group); }, 0);
|
|
33842
|
+
try { editor.attachEvent("ready", function () { bindFocus(); settle(group); }); } catch (e) {}
|
|
33843
|
+
};
|
|
33844
|
+
|
|
33845
|
+
function groupOf(name) {
|
|
33846
|
+
// `listeners` lives on the GROUP, not in this closure. Every root builds
|
|
33847
|
+
// its own copy of this plugin, so an instance-local list means a callback
|
|
33848
|
+
// registered through one root's API never hears an activation fired by a
|
|
33849
|
+
// different root — the subscriber silently lags by one event, or misses
|
|
33850
|
+
// everything, depending on which root the user clicks.
|
|
33851
|
+
if (!window.__RTE_MultiRootGroups[name]) {
|
|
33852
|
+
window.__RTE_MultiRootGroups[name] = { roots: [], active: null, listeners: [] };
|
|
33853
|
+
}
|
|
33854
|
+
var g = window.__RTE_MultiRootGroups[name];
|
|
33855
|
+
if (!g.listeners) g.listeners = [];
|
|
33856
|
+
return g;
|
|
33857
|
+
}
|
|
33858
|
+
|
|
33859
|
+
// The editor exposes its host element as `container` (a property, not a
|
|
33860
|
+
// getter — there is no getContainer()). Falling back through the iframe
|
|
33861
|
+
// covers any build where that property is absent, rather than silently
|
|
33862
|
+
// returning null and leaving every toolbar visible.
|
|
33863
|
+
function container() {
|
|
33864
|
+
try {
|
|
33865
|
+
if (editor.container) return editor.container;
|
|
33866
|
+
if (editor.iframe) {
|
|
33867
|
+
var n = editor.iframe.parentNode;
|
|
33868
|
+
while (n && !(n.classList && n.classList.contains("richtexteditor"))) n = n.parentNode;
|
|
33869
|
+
return n || null;
|
|
33870
|
+
}
|
|
33871
|
+
} catch (e) {}
|
|
33872
|
+
return null;
|
|
33873
|
+
}
|
|
33874
|
+
|
|
33875
|
+
function containerId() {
|
|
33876
|
+
var c = container();
|
|
33877
|
+
return c && c.id ? c.id : null;
|
|
33878
|
+
}
|
|
33879
|
+
|
|
33880
|
+
// ---- toolbar visibility ----------------------------------------------
|
|
33881
|
+
|
|
33882
|
+
function toolbarsOf(root) {
|
|
33883
|
+
var c = root.container;
|
|
33884
|
+
if (!c) return [];
|
|
33885
|
+
return [].slice.call(c.querySelectorAll("rte-toolbar"));
|
|
33886
|
+
}
|
|
33887
|
+
|
|
33888
|
+
function setVisible(root, visible) {
|
|
33889
|
+
var bars = toolbarsOf(root);
|
|
33890
|
+
for (var i = 0; i < bars.length; i++) {
|
|
33891
|
+
var bar = bars[i];
|
|
33892
|
+
if (visible) {
|
|
33893
|
+
// Restore whatever the editor's own responsive logic had chosen
|
|
33894
|
+
// (desktop vs mobile bar), rather than forcing display:flex on
|
|
33895
|
+
// both and showing two toolbars at once.
|
|
33896
|
+
if (bar.__rteMultiRootPrevDisplay !== undefined) {
|
|
33897
|
+
bar.style.display = bar.__rteMultiRootPrevDisplay;
|
|
33898
|
+
delete bar.__rteMultiRootPrevDisplay;
|
|
33899
|
+
}
|
|
33900
|
+
} else {
|
|
33901
|
+
if (bar.__rteMultiRootPrevDisplay === undefined) {
|
|
33902
|
+
bar.__rteMultiRootPrevDisplay = bar.style.display;
|
|
33903
|
+
}
|
|
33904
|
+
bar.style.display = "none";
|
|
33905
|
+
}
|
|
33906
|
+
}
|
|
33907
|
+
}
|
|
33908
|
+
|
|
33909
|
+
function settle(groupName) {
|
|
33910
|
+
var g = groupOf(groupName);
|
|
33911
|
+
if (!g.roots.length) return;
|
|
33912
|
+
if (!g.active) g.active = g.roots[0];
|
|
33913
|
+
for (var i = 0; i < g.roots.length; i++) {
|
|
33914
|
+
setVisible(g.roots[i], g.roots[i] === g.active);
|
|
33915
|
+
}
|
|
33916
|
+
hostToolbar(g.active);
|
|
33917
|
+
}
|
|
33918
|
+
|
|
33919
|
+
function hostToolbar(root) {
|
|
33920
|
+
var hostRef = config.multiRootToolbarHost;
|
|
33921
|
+
if (!hostRef || !root) return;
|
|
33922
|
+
var host = (typeof hostRef === "string") ? document.getElementById(hostRef) : hostRef;
|
|
33923
|
+
if (!host) return;
|
|
33924
|
+
var bars = toolbarsOf(root);
|
|
33925
|
+
for (var i = 0; i < bars.length; i++) {
|
|
33926
|
+
// Moving a node keeps its listeners, so the toolbar stays live.
|
|
33927
|
+
if (bars[i].parentNode !== host) host.appendChild(bars[i]);
|
|
33928
|
+
}
|
|
33929
|
+
}
|
|
33930
|
+
|
|
33931
|
+
// ---- focus tracking ---------------------------------------------------
|
|
33932
|
+
|
|
33933
|
+
function bindFocus() {
|
|
33934
|
+
var doc;
|
|
33935
|
+
try { doc = editor.getDocument(); } catch (e) { return; }
|
|
33936
|
+
if (!doc || doc.__rteMultiRootBound) return;
|
|
33937
|
+
doc.__rteMultiRootBound = true;
|
|
33938
|
+
|
|
33939
|
+
var activate = function () {
|
|
33940
|
+
if (!entry) return;
|
|
33941
|
+
var g = groupOf(entry.group);
|
|
33942
|
+
if (g.active === entry) return;
|
|
33943
|
+
g.active = entry;
|
|
33944
|
+
settle(entry.group);
|
|
33945
|
+
fire(entry.group, entry);
|
|
33946
|
+
};
|
|
33947
|
+
|
|
33948
|
+
// focusin bubbles; focus does not.
|
|
33949
|
+
doc.addEventListener("focusin", activate, true);
|
|
33950
|
+
doc.addEventListener("mousedown", activate, true);
|
|
33951
|
+
}
|
|
33952
|
+
|
|
33953
|
+
function fire(groupName, root) {
|
|
33954
|
+
var list = groupOf(groupName).listeners;
|
|
33955
|
+
for (var i = 0; i < list.length; i++) {
|
|
33956
|
+
try { list[i]({ active: root.name, editor: root.editor }); } catch (e) {}
|
|
33957
|
+
}
|
|
33958
|
+
}
|
|
33959
|
+
|
|
33960
|
+
// ---- group API --------------------------------------------------------
|
|
33961
|
+
|
|
33962
|
+
function api(groupName) {
|
|
33963
|
+
var g = groupOf(groupName);
|
|
33964
|
+
return {
|
|
33965
|
+
group: groupName,
|
|
33966
|
+
|
|
33967
|
+
getRoots: function () {
|
|
33968
|
+
return g.roots.map(function (r) { return r.name; });
|
|
33969
|
+
},
|
|
33970
|
+
|
|
33971
|
+
getActive: function () {
|
|
33972
|
+
return g.active ? g.active.name : null;
|
|
33973
|
+
},
|
|
33974
|
+
|
|
33975
|
+
getEditor: function (name) {
|
|
33976
|
+
for (var i = 0; i < g.roots.length; i++) if (g.roots[i].name === name) return g.roots[i].editor;
|
|
33977
|
+
return null;
|
|
33978
|
+
},
|
|
33979
|
+
|
|
33980
|
+
focusRoot: function (name) {
|
|
33981
|
+
var target = null;
|
|
33982
|
+
for (var i = 0; i < g.roots.length; i++) if (g.roots[i].name === name) target = g.roots[i];
|
|
33983
|
+
if (!target) return false;
|
|
33984
|
+
g.active = target;
|
|
33985
|
+
settle(groupName);
|
|
33986
|
+
try { target.editor.focus(); } catch (e) {}
|
|
33987
|
+
fire(groupName, target);
|
|
33988
|
+
return true;
|
|
33989
|
+
},
|
|
33990
|
+
|
|
33991
|
+
// The whole document, one entry per root — what a CMS saves.
|
|
33992
|
+
getHTML: function () {
|
|
33993
|
+
var out = {};
|
|
33994
|
+
for (var i = 0; i < g.roots.length; i++) {
|
|
33995
|
+
try { out[g.roots[i].name] = g.roots[i].editor.getHTMLCode(); } catch (e) { out[g.roots[i].name] = null; }
|
|
33996
|
+
}
|
|
33997
|
+
return out;
|
|
33998
|
+
},
|
|
33999
|
+
|
|
34000
|
+
setHTML: function (map) {
|
|
34001
|
+
if (!map) return false;
|
|
34002
|
+
for (var i = 0; i < g.roots.length; i++) {
|
|
34003
|
+
var r = g.roots[i];
|
|
34004
|
+
if (!(r.name in map)) continue;
|
|
34005
|
+
try { r.editor.setHTMLCode(map[r.name]); } catch (e) {}
|
|
34006
|
+
}
|
|
34007
|
+
return true;
|
|
34008
|
+
},
|
|
34009
|
+
|
|
34010
|
+
onActiveChange: function (fn) {
|
|
34011
|
+
if (typeof fn !== "function") return function () {};
|
|
34012
|
+
g.listeners.push(fn);
|
|
34013
|
+
return function () {
|
|
34014
|
+
var at = g.listeners.indexOf(fn);
|
|
34015
|
+
if (at >= 0) g.listeners.splice(at, 1);
|
|
34016
|
+
};
|
|
34017
|
+
},
|
|
34018
|
+
|
|
34019
|
+
// Say plainly what this does and does not do, so an integrator does
|
|
34020
|
+
// not discover the limits from a support ticket.
|
|
34021
|
+
getCapabilities: function () {
|
|
34022
|
+
return {
|
|
34023
|
+
sharedToolbar: true,
|
|
34024
|
+
sharedGroupApi: true,
|
|
34025
|
+
perRootUndo: true,
|
|
34026
|
+
sharedUndoStack: false,
|
|
34027
|
+
selectionAcrossRoots: false,
|
|
34028
|
+
sharedCollaborationSession: false,
|
|
34029
|
+
note: "Each root is a separate editor instance sharing one visible toolbar. " +
|
|
34030
|
+
"Undo, selection and collaboration are per-root; the editor edits an " +
|
|
34031
|
+
"iframe body, so a single instance cannot own several editables without core changes."
|
|
34032
|
+
};
|
|
34033
|
+
}
|
|
34034
|
+
};
|
|
34035
|
+
}
|
|
34036
|
+
}
|
|
34037
|
+
|
|
34038
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
34039
|
+
|
|
32675
34040
|
// 2026-07-17 Pagination / page view. Renders the editable as a stack of paper
|
|
32676
34041
|
// pages — the "print layout" view Word and Google Docs have, and the feature
|
|
32677
34042
|
// CKEditor and Tiptap both sell as a paid add-on.
|
|
@@ -35400,6 +36765,11 @@ function RTE_Plugin_RevisionHistory() {
|
|
|
35400
36765
|
}
|
|
35401
36766
|
|
|
35402
36767
|
function restore(id, opts) {
|
|
36768
|
+
// The dialog's own Restore button calls restore(id) with no options, so
|
|
36769
|
+
// reading opts.skipConfirm threw and the button did nothing at all. The
|
|
36770
|
+
// public API wrapper defaulted this, the internal caller did not - so
|
|
36771
|
+
// default it here, where every caller is covered.
|
|
36772
|
+
opts = opts || {};
|
|
35403
36773
|
var entry = findById(id);
|
|
35404
36774
|
if (!entry) return false;
|
|
35405
36775
|
if (!opts.skipConfirm) {
|
|
@@ -38754,6 +40124,511 @@ function RTE_Plugin_TextDirection() {
|
|
|
38754
40124
|
|
|
38755
40125
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
38756
40126
|
|
|
40127
|
+
// 2026-08-15 Text part language ("language of parts").
|
|
40128
|
+
//
|
|
40129
|
+
// WCAG 2.1 SC 3.1.2 (Level AA) requires that the human language of every
|
|
40130
|
+
// PASSAGE or PHRASE be programmatically determinable when it differs from the
|
|
40131
|
+
// document language. Until now this editor could only set a language on the
|
|
40132
|
+
// whole document (a11yenhance.js stamps <html lang>), so a French quotation
|
|
40133
|
+
// inside an English page was unmarkable and any AA conformance claim on
|
|
40134
|
+
// /docs/accessibility was overstated. A screen reader reads such a phrase with
|
|
40135
|
+
// the wrong phoneme set, which is the failure the SC exists to prevent.
|
|
40136
|
+
//
|
|
40137
|
+
// CKEditor 5 ships this as "Text part language". TinyMCE has no equivalent
|
|
40138
|
+
// plugin (8.5's `content_language` sets the default proofing language, not
|
|
40139
|
+
// per-phrase markup).
|
|
40140
|
+
//
|
|
40141
|
+
// Design notes:
|
|
40142
|
+
// - `lang` is CONTENT, exactly like `dir` in textdirection.js: it must survive
|
|
40143
|
+
// getHTMLCode(), and it is never stripped on serialize. A document whose
|
|
40144
|
+
// language annotations vanished on save would silently drop its own
|
|
40145
|
+
// conformance.
|
|
40146
|
+
// - The wrapper is a bare <span lang>. No class is emitted, so there is
|
|
40147
|
+
// nothing for check-css-contract.mjs to chase and nothing that depends on a
|
|
40148
|
+
// stylesheet travelling with the content. The in-editor visual marker is
|
|
40149
|
+
// injected into the EDITING DOCUMENT only, keyed off [lang], so it shows the
|
|
40150
|
+
// author what is marked without ever leaving the editor.
|
|
40151
|
+
// - RTL languages also get `dir`, because `lang` alone does not set direction.
|
|
40152
|
+
// A Hebrew phrase marked only with lang="he" still renders left-to-right and
|
|
40153
|
+
// reads as gibberish.
|
|
40154
|
+
// - Re-applying the SAME language to a selection REMOVES it (toggle), which is
|
|
40155
|
+
// how every other inline format in this editor behaves.
|
|
40156
|
+
// - Nested lang spans inside the selection are unwrapped before wrapping, so a
|
|
40157
|
+
// phrase never ends up with two competing languages. The inner one would win
|
|
40158
|
+
// in the accessibility tree and the author would have no way to see why.
|
|
40159
|
+
// - A collapsed selection deliberately does NOT create an empty wrapper. An
|
|
40160
|
+
// empty inline span is a caret trap (the caret falls in and cannot be typed
|
|
40161
|
+
// out of); instead a collapsed caret retargets the [lang] element it is
|
|
40162
|
+
// already inside, so "put the cursor in the phrase and pick Remove" works.
|
|
40163
|
+
RTE_DefaultConfig.plugin_textpartlanguage = RTE_Plugin_TextPartLanguage;
|
|
40164
|
+
|
|
40165
|
+
// The dropdown list. `dir` is only needed for right-to-left languages.
|
|
40166
|
+
if (typeof RTE_DefaultConfig.textPartLanguages === "undefined") {
|
|
40167
|
+
RTE_DefaultConfig.textPartLanguages = [
|
|
40168
|
+
{ code: "ar", label: "Arabic", dir: "rtl" },
|
|
40169
|
+
{ code: "zh-Hans", label: "Chinese (Simplified)" },
|
|
40170
|
+
{ code: "zh-Hant", label: "Chinese (Traditional)" },
|
|
40171
|
+
{ code: "cs", label: "Czech" },
|
|
40172
|
+
{ code: "nl", label: "Dutch" },
|
|
40173
|
+
{ code: "en", label: "English" },
|
|
40174
|
+
{ code: "fr", label: "French" },
|
|
40175
|
+
{ code: "de", label: "German" },
|
|
40176
|
+
{ code: "el", label: "Greek" },
|
|
40177
|
+
{ code: "he", label: "Hebrew", dir: "rtl" },
|
|
40178
|
+
{ code: "hi", label: "Hindi" },
|
|
40179
|
+
{ code: "it", label: "Italian" },
|
|
40180
|
+
{ code: "ja", label: "Japanese" },
|
|
40181
|
+
{ code: "ko", label: "Korean" },
|
|
40182
|
+
{ code: "fa", label: "Persian", dir: "rtl" },
|
|
40183
|
+
{ code: "pl", label: "Polish" },
|
|
40184
|
+
{ code: "pt", label: "Portuguese" },
|
|
40185
|
+
{ code: "ru", label: "Russian" },
|
|
40186
|
+
{ code: "es", label: "Spanish" },
|
|
40187
|
+
{ code: "sv", label: "Swedish" },
|
|
40188
|
+
{ code: "tr", label: "Turkish" },
|
|
40189
|
+
{ code: "uk", label: "Ukrainian" },
|
|
40190
|
+
{ code: "ur", label: "Urdu", dir: "rtl" },
|
|
40191
|
+
{ code: "vi", label: "Vietnamese" }
|
|
40192
|
+
];
|
|
40193
|
+
}
|
|
40194
|
+
|
|
40195
|
+
// Show marked phrases with a dotted underline while editing. Editor-only: the
|
|
40196
|
+
// rule lives in the editing document, never in the saved HTML.
|
|
40197
|
+
if (typeof RTE_DefaultConfig.textPartLanguageHighlight === "undefined") RTE_DefaultConfig.textPartLanguageHighlight = true;
|
|
40198
|
+
|
|
40199
|
+
function RTE_Plugin_TextPartLanguage() {
|
|
40200
|
+
var obj = this;
|
|
40201
|
+
var config, editor;
|
|
40202
|
+
var boundDoc = null;
|
|
40203
|
+
|
|
40204
|
+
obj.PluginName = "TextPartLanguage";
|
|
40205
|
+
|
|
40206
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
40207
|
+
|
|
40208
|
+
obj.InitEditor = function (argeditor) {
|
|
40209
|
+
editor = argeditor;
|
|
40210
|
+
|
|
40211
|
+
editor.attachEvent("exec_command_textpartlanguage", function (state) {
|
|
40212
|
+
state.returnValue = true;
|
|
40213
|
+
var v = state && state.value;
|
|
40214
|
+
if (!v || v === "none" || v === "remove") obj.Remove();
|
|
40215
|
+
else obj.Apply(v);
|
|
40216
|
+
});
|
|
40217
|
+
|
|
40218
|
+
if (editor.toolbarFactoryMap) {
|
|
40219
|
+
editor.toolbarFactoryMap["textpartlanguage"] = function (cmd) {
|
|
40220
|
+
return editor.createToolbarItemDropDownPanel(cmd, buildPanel);
|
|
40221
|
+
};
|
|
40222
|
+
}
|
|
40223
|
+
|
|
40224
|
+
setup();
|
|
40225
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
40226
|
+
try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
|
|
40227
|
+
setTimeout(setup, 0);
|
|
40228
|
+
|
|
40229
|
+
// Public API.
|
|
40230
|
+
editor.setTextLanguage = function (code) { return obj.Apply(code); };
|
|
40231
|
+
editor.removeTextLanguage = function () { return obj.Remove(); };
|
|
40232
|
+
editor.getTextLanguage = function () { return obj.Current(); };
|
|
40233
|
+
editor.listTextLanguages = function () { return languages().slice(); };
|
|
40234
|
+
editor.getMarkedLanguages = function () { return obj.List(); };
|
|
40235
|
+
};
|
|
40236
|
+
|
|
40237
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
40238
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
40239
|
+
|
|
40240
|
+
function languages() {
|
|
40241
|
+
var list = config && config.textPartLanguages;
|
|
40242
|
+
return (list && list.length) ? list : [];
|
|
40243
|
+
}
|
|
40244
|
+
|
|
40245
|
+
function entry(code) {
|
|
40246
|
+
var list = languages();
|
|
40247
|
+
for (var i = 0; i < list.length; i++) if (list[i].code === code) return list[i];
|
|
40248
|
+
return { code: code, label: code };
|
|
40249
|
+
}
|
|
40250
|
+
|
|
40251
|
+
// ---- state -----------------------------------------------------------
|
|
40252
|
+
|
|
40253
|
+
// The nearest ancestor carrying a lang, bounded by the editable so the host
|
|
40254
|
+
// page's own <html lang> is never reported as the phrase language.
|
|
40255
|
+
function langAncestor(node) {
|
|
40256
|
+
var ed = getEditable();
|
|
40257
|
+
var n = (node && node.nodeType === 3) ? node.parentNode : node;
|
|
40258
|
+
while (n && n !== ed && n.nodeType === 1) {
|
|
40259
|
+
if (n.hasAttribute && n.hasAttribute("lang")) return n;
|
|
40260
|
+
n = n.parentNode;
|
|
40261
|
+
}
|
|
40262
|
+
return null;
|
|
40263
|
+
}
|
|
40264
|
+
|
|
40265
|
+
function selectionRange() {
|
|
40266
|
+
try {
|
|
40267
|
+
var sel = editor.getSelection();
|
|
40268
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
40269
|
+
return sel.getRangeAt(0);
|
|
40270
|
+
} catch (e) { return null; }
|
|
40271
|
+
}
|
|
40272
|
+
|
|
40273
|
+
obj.Current = function () {
|
|
40274
|
+
var r = selectionRange();
|
|
40275
|
+
if (!r) return null;
|
|
40276
|
+
var el = langAncestor(r.startContainer);
|
|
40277
|
+
// A non-collapsed selection only counts as "in" a language if BOTH ends
|
|
40278
|
+
// sit in the same marked phrase; a selection straddling the boundary has
|
|
40279
|
+
// no single language and must not report one.
|
|
40280
|
+
if (el && !r.collapsed && langAncestor(r.endContainer) !== el) return null;
|
|
40281
|
+
return el ? el.getAttribute("lang") : null;
|
|
40282
|
+
};
|
|
40283
|
+
|
|
40284
|
+
// Every distinct language marked in the document, for auditing.
|
|
40285
|
+
obj.List = function () {
|
|
40286
|
+
var ed = getEditable();
|
|
40287
|
+
var out = [];
|
|
40288
|
+
if (!ed) return out;
|
|
40289
|
+
var seen = {};
|
|
40290
|
+
var nodes = ed.querySelectorAll("[lang]");
|
|
40291
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
40292
|
+
var code = nodes[i].getAttribute("lang");
|
|
40293
|
+
if (!code || seen[code]) continue;
|
|
40294
|
+
seen[code] = true;
|
|
40295
|
+
out.push({ code: code, label: entry(code).label });
|
|
40296
|
+
}
|
|
40297
|
+
return out;
|
|
40298
|
+
};
|
|
40299
|
+
|
|
40300
|
+
// ---- apply -----------------------------------------------------------
|
|
40301
|
+
|
|
40302
|
+
obj.Apply = function (code) {
|
|
40303
|
+
code = String(code == null ? "" : code).trim();
|
|
40304
|
+
if (!code) return false;
|
|
40305
|
+
|
|
40306
|
+
var doc = getDoc();
|
|
40307
|
+
var r = selectionRange();
|
|
40308
|
+
if (!doc || !r) return false;
|
|
40309
|
+
|
|
40310
|
+
// Re-picking the current language toggles it off.
|
|
40311
|
+
if (obj.Current() === code) return obj.Remove();
|
|
40312
|
+
|
|
40313
|
+
if (r.collapsed) {
|
|
40314
|
+
// No empty wrapper (caret trap). Retarget the phrase the caret is in.
|
|
40315
|
+
var host = langAncestor(r.startContainer);
|
|
40316
|
+
if (!host) return false;
|
|
40317
|
+
stamp(host, code);
|
|
40318
|
+
fireChange();
|
|
40319
|
+
return code;
|
|
40320
|
+
}
|
|
40321
|
+
|
|
40322
|
+
var span = doc.createElement("span");
|
|
40323
|
+
stamp(span, code);
|
|
40324
|
+
|
|
40325
|
+
try {
|
|
40326
|
+
// surroundContents is the clean path, but it throws whenever the
|
|
40327
|
+
// range partially selects a non-text node (half a <strong>), which
|
|
40328
|
+
// is the common case in real prose.
|
|
40329
|
+
r.surroundContents(span);
|
|
40330
|
+
} catch (e) {
|
|
40331
|
+
var frag = r.extractContents();
|
|
40332
|
+
span.appendChild(frag);
|
|
40333
|
+
r.insertNode(span);
|
|
40334
|
+
}
|
|
40335
|
+
|
|
40336
|
+
unwrapInner(span);
|
|
40337
|
+
pruneEmptyText(span);
|
|
40338
|
+
if (!span.firstChild) { // selection held nothing real
|
|
40339
|
+
if (span.parentNode) span.parentNode.removeChild(span);
|
|
40340
|
+
return false;
|
|
40341
|
+
}
|
|
40342
|
+
mergeSiblings(span);
|
|
40343
|
+
select(span);
|
|
40344
|
+
fireChange();
|
|
40345
|
+
return code;
|
|
40346
|
+
};
|
|
40347
|
+
|
|
40348
|
+
obj.Remove = function () {
|
|
40349
|
+
var r = selectionRange();
|
|
40350
|
+
if (!r) return false;
|
|
40351
|
+
var ed = getEditable();
|
|
40352
|
+
if (!ed) return false;
|
|
40353
|
+
|
|
40354
|
+
var removed = 0;
|
|
40355
|
+
|
|
40356
|
+
// The phrase the caret/selection sits inside.
|
|
40357
|
+
var host = langAncestor(r.startContainer);
|
|
40358
|
+
if (host) { unwrap(host); removed++; }
|
|
40359
|
+
|
|
40360
|
+
// Plus any fully- or partly-selected marked phrases in a wider range.
|
|
40361
|
+
if (!r.collapsed) {
|
|
40362
|
+
var nodes = ed.querySelectorAll("[lang]");
|
|
40363
|
+
for (var i = nodes.length - 1; i >= 0; i--) {
|
|
40364
|
+
var el = nodes[i];
|
|
40365
|
+
if (el === host) continue;
|
|
40366
|
+
if (intersects(r, el)) { unwrap(el); removed++; }
|
|
40367
|
+
}
|
|
40368
|
+
}
|
|
40369
|
+
|
|
40370
|
+
if (removed) fireChange();
|
|
40371
|
+
return removed > 0;
|
|
40372
|
+
};
|
|
40373
|
+
|
|
40374
|
+
function intersects(range, el) {
|
|
40375
|
+
try {
|
|
40376
|
+
var er = el.ownerDocument.createRange();
|
|
40377
|
+
er.selectNodeContents(el);
|
|
40378
|
+
return range.compareBoundaryPoints(Range.END_TO_START, er) < 0 &&
|
|
40379
|
+
range.compareBoundaryPoints(Range.START_TO_END, er) > 0;
|
|
40380
|
+
} catch (e) { return false; }
|
|
40381
|
+
}
|
|
40382
|
+
|
|
40383
|
+
function stamp(el, code) {
|
|
40384
|
+
el.setAttribute("lang", code);
|
|
40385
|
+
var dir = entry(code).dir;
|
|
40386
|
+
// lang does not imply direction; without dir an RTL phrase renders LTR.
|
|
40387
|
+
if (dir === "rtl" || dir === "ltr") el.setAttribute("dir", dir);
|
|
40388
|
+
else el.removeAttribute("dir");
|
|
40389
|
+
}
|
|
40390
|
+
|
|
40391
|
+
// A phrase must carry exactly one language. Anything marked inside the new
|
|
40392
|
+
// wrapper would otherwise win in the accessibility tree.
|
|
40393
|
+
function unwrapInner(span) {
|
|
40394
|
+
var inner = span.querySelectorAll("[lang]");
|
|
40395
|
+
for (var i = inner.length - 1; i >= 0; i--) unwrap(inner[i]);
|
|
40396
|
+
}
|
|
40397
|
+
|
|
40398
|
+
function unwrap(el) {
|
|
40399
|
+
if (!el || !el.parentNode) return;
|
|
40400
|
+
// A span we created carries nothing else; anything the author's own
|
|
40401
|
+
// markup carries (a styled <em lang>) must survive, so only the
|
|
40402
|
+
// language attributes come off.
|
|
40403
|
+
if (el.nodeName === "SPAN" && el.attributes.length <= 2 &&
|
|
40404
|
+
!el.getAttribute("class") && !el.getAttribute("style") && !el.getAttribute("id")) {
|
|
40405
|
+
var parent = el.parentNode;
|
|
40406
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
40407
|
+
parent.removeChild(el);
|
|
40408
|
+
try { parent.normalize(); } catch (e) {}
|
|
40409
|
+
} else {
|
|
40410
|
+
el.removeAttribute("lang");
|
|
40411
|
+
el.removeAttribute("dir");
|
|
40412
|
+
}
|
|
40413
|
+
}
|
|
40414
|
+
|
|
40415
|
+
// extractContents leaves zero-length text nodes behind; they make a wrapper
|
|
40416
|
+
// look non-empty and leave invisible caret stops in the phrase.
|
|
40417
|
+
function pruneEmptyText(el) {
|
|
40418
|
+
for (var i = el.childNodes.length - 1; i >= 0; i--) {
|
|
40419
|
+
var n = el.childNodes[i];
|
|
40420
|
+
if (n.nodeType === 3 && n.nodeValue === "") el.removeChild(n);
|
|
40421
|
+
}
|
|
40422
|
+
}
|
|
40423
|
+
|
|
40424
|
+
// Marking two adjacent halves of a phrase should read as one phrase, not two.
|
|
40425
|
+
function mergeSiblings(span) {
|
|
40426
|
+
var code = span.getAttribute("lang");
|
|
40427
|
+
var prev = span.previousSibling;
|
|
40428
|
+
if (prev && prev.nodeType === 1 && prev.nodeName === "SPAN" && prev.getAttribute("lang") === code) {
|
|
40429
|
+
while (span.firstChild) prev.appendChild(span.firstChild);
|
|
40430
|
+
span.parentNode.removeChild(span);
|
|
40431
|
+
span = prev;
|
|
40432
|
+
}
|
|
40433
|
+
var next = span.nextSibling;
|
|
40434
|
+
if (next && next.nodeType === 1 && next.nodeName === "SPAN" && next.getAttribute("lang") === code) {
|
|
40435
|
+
while (next.firstChild) span.appendChild(next.firstChild);
|
|
40436
|
+
next.parentNode.removeChild(next);
|
|
40437
|
+
}
|
|
40438
|
+
try { span.normalize(); } catch (e) {}
|
|
40439
|
+
return span;
|
|
40440
|
+
}
|
|
40441
|
+
|
|
40442
|
+
function select(span) {
|
|
40443
|
+
try {
|
|
40444
|
+
var sel = editor.getSelection();
|
|
40445
|
+
var r = span.ownerDocument.createRange();
|
|
40446
|
+
r.selectNodeContents(span);
|
|
40447
|
+
sel.removeAllRanges();
|
|
40448
|
+
sel.addRange(r);
|
|
40449
|
+
} catch (e) {}
|
|
40450
|
+
}
|
|
40451
|
+
|
|
40452
|
+
function fireChange() {
|
|
40453
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
40454
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
40455
|
+
}
|
|
40456
|
+
|
|
40457
|
+
// ---- toolbar panel ---------------------------------------------------
|
|
40458
|
+
|
|
40459
|
+
function buildPanel(panel) {
|
|
40460
|
+
var current = obj.Current();
|
|
40461
|
+
panel.style.width = "220px";
|
|
40462
|
+
panel.style.maxHeight = "340px";
|
|
40463
|
+
panel.style.overflowY = "auto";
|
|
40464
|
+
panel.style.padding = "4px 0";
|
|
40465
|
+
|
|
40466
|
+
var list = languages();
|
|
40467
|
+
|
|
40468
|
+
addRow(panel, editor.getLangText ? (editor.getLangText("removelanguage") || "Remove language") : "Remove language",
|
|
40469
|
+
null, !current, function () { obj.Remove(); });
|
|
40470
|
+
|
|
40471
|
+
var sep = panel.ownerDocument.createElement("div");
|
|
40472
|
+
sep.style.cssText = "height:1px;margin:4px 6px;background:var(--rte-border-color,#e0e0e0);";
|
|
40473
|
+
panel.appendChild(sep);
|
|
40474
|
+
|
|
40475
|
+
for (var i = 0; i < list.length; i++) {
|
|
40476
|
+
(function (item) {
|
|
40477
|
+
addRow(panel, item.label, item.code, current === item.code, function () { obj.Apply(item.code); });
|
|
40478
|
+
})(list[i]);
|
|
40479
|
+
}
|
|
40480
|
+
|
|
40481
|
+
// Opening a menu has to put focus inside it, or a keyboard user has
|
|
40482
|
+
// opened something they cannot reach. Start on the current language when
|
|
40483
|
+
// there is one — that is the item they are most likely acting on.
|
|
40484
|
+
setTimeout(function () {
|
|
40485
|
+
var rows = panel.querySelectorAll("rte-toolbar-dropdown-item");
|
|
40486
|
+
if (!rows.length) return;
|
|
40487
|
+
var target = rows[0];
|
|
40488
|
+
for (var i = 0; i < rows.length; i++) {
|
|
40489
|
+
if (rows[i].getAttribute("aria-checked") === "true") { target = rows[i]; break; }
|
|
40490
|
+
}
|
|
40491
|
+
try { target.focus(); } catch (e) {}
|
|
40492
|
+
}, 0);
|
|
40493
|
+
}
|
|
40494
|
+
|
|
40495
|
+
function addRow(panel, label, code, active, run) {
|
|
40496
|
+
var doc = panel.ownerDocument;
|
|
40497
|
+
// Two things are required for a menu row to be operable by keyboard, and
|
|
40498
|
+
// a plain <div role="menuitemradio"> has neither:
|
|
40499
|
+
// 1. The TAG. The core's keyboard layer finds items via
|
|
40500
|
+
// __actionElementSelector, which lists element NAMES
|
|
40501
|
+
// (rte-toolbar-dropdown-item, rte-dropdown-menuitem, rte-menuitem)
|
|
40502
|
+
// and ignores roles entirely.
|
|
40503
|
+
// 2. A tab stop and key handling. The core applies those inside its own
|
|
40504
|
+
// closure — __Append / __Make_ActionElementAccessible are not exposed
|
|
40505
|
+
// to plugins — so a plugin has to supply them itself rather than
|
|
40506
|
+
// hope an internal picks the row up.
|
|
40507
|
+
// Both are done here, so the menu works with a keyboard regardless of
|
|
40508
|
+
// what the core does or does not reach into (WCAG 2.1.1).
|
|
40509
|
+
var row = doc.createElement("rte-toolbar-dropdown-item");
|
|
40510
|
+
panel.appendChild(row);
|
|
40511
|
+
row.setAttribute("role", "menuitemradio");
|
|
40512
|
+
row.setAttribute("aria-checked", active ? "true" : "false");
|
|
40513
|
+
row.setAttribute("tabindex", "0");
|
|
40514
|
+
row.style.cssText = "padding:5px 12px;cursor:pointer;white-space:nowrap;display:flex;" +
|
|
40515
|
+
"justify-content:space-between;gap:12px;color:var(--rte-text-color,inherit);" +
|
|
40516
|
+
(active ? "font-weight:600;" : "");
|
|
40517
|
+
|
|
40518
|
+
var name = doc.createElement("span");
|
|
40519
|
+
name.appendChild(doc.createTextNode(label));
|
|
40520
|
+
row.appendChild(name);
|
|
40521
|
+
|
|
40522
|
+
if (code) {
|
|
40523
|
+
var tag = doc.createElement("span");
|
|
40524
|
+
tag.style.cssText = "opacity:.55;font-size:11px;";
|
|
40525
|
+
tag.appendChild(doc.createTextNode(code));
|
|
40526
|
+
row.appendChild(tag);
|
|
40527
|
+
}
|
|
40528
|
+
|
|
40529
|
+
row.onmouseover = function () { row.style.backgroundColor = "var(--rte-hover-bg,#e6e6e6)"; };
|
|
40530
|
+
row.onmouseout = function () { row.style.backgroundColor = ""; };
|
|
40531
|
+
row.onclick = function () {
|
|
40532
|
+
// Close first: the panel steals focus, and applying to a selection
|
|
40533
|
+
// the editor no longer owns silently does nothing.
|
|
40534
|
+
try { editor.closeCurrentPopup(); } catch (e) {}
|
|
40535
|
+
try { editor.focus(); } catch (e) {}
|
|
40536
|
+
run();
|
|
40537
|
+
};
|
|
40538
|
+
|
|
40539
|
+
row.onkeydown = function (e) {
|
|
40540
|
+
var key = e.key;
|
|
40541
|
+
if (key === "Enter" || key === " " || key === "Spacebar") {
|
|
40542
|
+
e.preventDefault();
|
|
40543
|
+
e.stopPropagation();
|
|
40544
|
+
row.onclick();
|
|
40545
|
+
return;
|
|
40546
|
+
}
|
|
40547
|
+
if (key === "ArrowDown" || key === "ArrowUp") {
|
|
40548
|
+
e.preventDefault();
|
|
40549
|
+
e.stopPropagation();
|
|
40550
|
+
var items = panel.querySelectorAll("rte-toolbar-dropdown-item");
|
|
40551
|
+
var list = Array.prototype.slice.call(items);
|
|
40552
|
+
var at = list.indexOf(row);
|
|
40553
|
+
// Wrap, so the list cannot dead-end and strand the user.
|
|
40554
|
+
var next = list[(at + (key === "ArrowDown" ? 1 : -1) + list.length) % list.length];
|
|
40555
|
+
if (next) next.focus();
|
|
40556
|
+
return;
|
|
40557
|
+
}
|
|
40558
|
+
if (key === "Home" || key === "End") {
|
|
40559
|
+
e.preventDefault();
|
|
40560
|
+
e.stopPropagation();
|
|
40561
|
+
var all = panel.querySelectorAll("rte-toolbar-dropdown-item");
|
|
40562
|
+
var target = key === "Home" ? all[0] : all[all.length - 1];
|
|
40563
|
+
if (target) target.focus();
|
|
40564
|
+
return;
|
|
40565
|
+
}
|
|
40566
|
+
if (key === "Escape") {
|
|
40567
|
+
e.preventDefault();
|
|
40568
|
+
e.stopPropagation();
|
|
40569
|
+
// Escape closes the menu; focus then lands in the EDITING AREA,
|
|
40570
|
+
// not back on the launcher.
|
|
40571
|
+
//
|
|
40572
|
+
// That is the core's behaviour for every dropdown — closing a
|
|
40573
|
+
// popup refocuses the editable asynchronously — and it cannot be
|
|
40574
|
+
// overridden from here. Measured: setting focus on the launcher
|
|
40575
|
+
// does take effect, and is then taken back ~16 ms later; a forced
|
|
40576
|
+
// re-focus is taken back again. Racing it with longer timeouts
|
|
40577
|
+
// would be fragile and would fight the "close the popup, carry on
|
|
40578
|
+
// typing" flow the rest of the editor depends on.
|
|
40579
|
+
//
|
|
40580
|
+
// This is not a trap and not a WCAG failure: the menu closes,
|
|
40581
|
+
// focus is visible and lands somewhere meaningful. Restoring
|
|
40582
|
+
// launcher focus properly belongs in the core's popup-close path,
|
|
40583
|
+
// where it would apply to every dropdown at once.
|
|
40584
|
+
try { editor.closeCurrentPopup(); } catch (ex) {}
|
|
40585
|
+
}
|
|
40586
|
+
};
|
|
40587
|
+
}
|
|
40588
|
+
|
|
40589
|
+
// ---- editor-only marker ----------------------------------------------
|
|
40590
|
+
|
|
40591
|
+
function setup() {
|
|
40592
|
+
var doc = getDoc();
|
|
40593
|
+
if (!doc || doc === boundDoc) return;
|
|
40594
|
+
boundDoc = doc;
|
|
40595
|
+
injectStyles(doc);
|
|
40596
|
+
}
|
|
40597
|
+
|
|
40598
|
+
function css() {
|
|
40599
|
+
if (config && config.textPartLanguageHighlight === false) return "";
|
|
40600
|
+
// Scoped to [lang] inside the editing document only. Never serialized,
|
|
40601
|
+
// so the marker cannot follow the content into a customer's page.
|
|
40602
|
+
//
|
|
40603
|
+
// :not(html):not(body) is REQUIRED, not defensive tidiness. a11yenhance.js
|
|
40604
|
+
// stamps the editing document's root with the document language, so a bare
|
|
40605
|
+
// [lang] selector matches <html> — which made hovering anywhere in the
|
|
40606
|
+
// editing area grey the ENTIRE surface, and drew the "marked phrase"
|
|
40607
|
+
// underline across the whole document. Reported from the live demos page.
|
|
40608
|
+
// Only a phrase the author actually marked should be decorated.
|
|
40609
|
+
return "[lang]:not(html):not(body){border-bottom:1px dotted rgba(120,120,120,.75);}" +
|
|
40610
|
+
"[lang]:not(html):not(body):hover{background:rgba(120,120,120,.10);}";
|
|
40611
|
+
}
|
|
40612
|
+
|
|
40613
|
+
function injectStyles(doc) {
|
|
40614
|
+
if (!doc) return;
|
|
40615
|
+
var text = css();
|
|
40616
|
+
var existing = doc.getElementById("rte-textpartlanguage-styles");
|
|
40617
|
+
if (existing) {
|
|
40618
|
+
if (existing.getAttribute("data-css") === text) return;
|
|
40619
|
+
existing.parentNode && existing.parentNode.removeChild(existing);
|
|
40620
|
+
}
|
|
40621
|
+
if (!text) return;
|
|
40622
|
+
var st = doc.createElement("style");
|
|
40623
|
+
st.id = "rte-textpartlanguage-styles";
|
|
40624
|
+
st.setAttribute("data-css", text);
|
|
40625
|
+
st.appendChild(doc.createTextNode(text));
|
|
40626
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
40627
|
+
}
|
|
40628
|
+
}
|
|
40629
|
+
|
|
40630
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
40631
|
+
|
|
38757
40632
|
// 2026-06-05 Interactive task / checklist. Notion / GitHub / Google-Docs style:
|
|
38758
40633
|
// a list whose items each carry a clickable checkbox; clicking toggles the item
|
|
38759
40634
|
// between open and done (a check + strike-through). Closes a real gap — the
|
|
@@ -38890,695 +40765,722 @@ function RTE_Plugin_TodoList() {
|
|
|
38890
40765
|
};
|
|
38891
40766
|
}
|
|
38892
40767
|
|
|
38893
|
-
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
38894
|
-
|
|
38895
|
-
if (!RTE_DefaultConfig.svgCode_trackchanges) {
|
|
38896
|
-
RTE_DefaultConfig.svgCode_trackchanges = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17l4-1 10-10-3-3L5 13l-1 4z"/><path d="M14 4l3 3"/><path d="M4 21h10"/></svg>';
|
|
38897
|
-
}
|
|
38898
|
-
|
|
38899
|
-
RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
|
|
38900
|
-
|
|
38901
|
-
function RTE_Plugin_TrackedChanges() {
|
|
38902
|
-
var obj = this;
|
|
38903
|
-
var config;
|
|
38904
|
-
var editor;
|
|
38905
|
-
var enabled = false;
|
|
38906
|
-
|
|
38907
|
-
obj.PluginName = "TrackedChanges";
|
|
38908
|
-
|
|
38909
|
-
obj.InitConfig = function (argconfig) {
|
|
38910
|
-
config = argconfig;
|
|
38911
|
-
if (config.trackChangesEnabled === false) return;
|
|
38912
|
-
|
|
38913
|
-
config.currentUser = config.currentUser || null;
|
|
38914
|
-
config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
|
|
38915
|
-
config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
|
|
38916
|
-
// Optional application policy for accept/reject. Return false to keep a
|
|
38917
|
-
// suggestion pending. The default remains permissive for compatibility.
|
|
38918
|
-
config.reviewCanDecide = config.reviewCanDecide || null;
|
|
38919
|
-
config.onReviewDecisionDenied = config.onReviewDecisionDenied || null;
|
|
38920
|
-
|
|
38921
|
-
// Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
|
|
38922
|
-
config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
|
|
38923
|
-
appendToolbarCommand("toolbar_default", "#{trackchanges}");
|
|
38924
|
-
appendToolbarCommand("toolbar_full", "#{trackchanges}");
|
|
38925
|
-
appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
|
|
38926
|
-
|
|
38927
|
-
if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
|
|
38928
|
-
config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
|
|
38929
|
-
}
|
|
38930
|
-
};
|
|
38931
|
-
|
|
38932
|
-
obj.InitEditor = function (argeditor) {
|
|
38933
|
-
editor = argeditor;
|
|
38934
|
-
if (config.trackChangesEnabled === false) return;
|
|
38935
|
-
|
|
38936
|
-
editor.trackedChanges = {
|
|
38937
|
-
enable: function (user) { enable(user); },
|
|
38938
|
-
disable: function () { disable(); },
|
|
38939
|
-
isEnabled: function () { return enabled; },
|
|
38940
|
-
acceptAll: function (filter) { return acceptAll(filter); },
|
|
38941
|
-
rejectAll: function (filter) { return rejectAll(filter); },
|
|
38942
|
-
accept: function (id) { return acceptEntry(id); },
|
|
38943
|
-
reject: function (id) { return rejectEntry(id); },
|
|
38944
|
-
canDecide: function (id, action) {
|
|
38945
|
-
var entry = editor.reviewLedger && editor.reviewLedger.get(id);
|
|
38946
|
-
return !!entry && canDecide(entry, action || "accept");
|
|
38947
|
-
},
|
|
38948
|
-
list: function (opts) {
|
|
38949
|
-
if (!editor.reviewLedger) return [];
|
|
38950
|
-
// Default to the PENDING review queue (matches the internal
|
|
38951
|
-
// accept/reject semantics). Pass {all:true} for full history
|
|
38952
|
-
// including accepted/rejected entries (also via reviewLedger.list()).
|
|
38953
|
-
var includeAll = opts && opts.all === true;
|
|
38954
|
-
return editor.reviewLedger.list().filter(function (e) {
|
|
38955
|
-
if (e.changeType !== "insert" && e.changeType !== "delete") return false;
|
|
38956
|
-
return includeAll || e.status === "pending";
|
|
38957
|
-
});
|
|
38958
|
-
}
|
|
38959
|
-
};
|
|
38960
|
-
|
|
38961
|
-
injectStyles();
|
|
38962
|
-
|
|
38963
|
-
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
38964
|
-
editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
|
|
38965
|
-
var btn = editor.createToolbarButton(cmd);
|
|
38966
|
-
btn.__tcSync = function () {
|
|
38967
|
-
if (enabled) btn.classList.add("rte-ui-active");
|
|
38968
|
-
else btn.classList.remove("rte-ui-active");
|
|
38969
|
-
btn.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
38970
|
-
|
|
38971
|
-
|
|
38972
|
-
|
|
38973
|
-
|
|
38974
|
-
|
|
38975
|
-
|
|
38976
|
-
|
|
38977
|
-
|
|
38978
|
-
|
|
38979
|
-
|
|
38980
|
-
|
|
38981
|
-
|
|
38982
|
-
|
|
38983
|
-
|
|
38984
|
-
|
|
38985
|
-
|
|
38986
|
-
|
|
38987
|
-
|
|
38988
|
-
|
|
38989
|
-
|
|
38990
|
-
|
|
38991
|
-
|
|
38992
|
-
|
|
38993
|
-
if (
|
|
38994
|
-
|
|
38995
|
-
|
|
38996
|
-
|
|
38997
|
-
|
|
38998
|
-
|
|
38999
|
-
|
|
39000
|
-
|
|
39001
|
-
|
|
39002
|
-
|
|
39003
|
-
enabled =
|
|
39004
|
-
syncToolbarButtons();
|
|
39005
|
-
}
|
|
39006
|
-
|
|
39007
|
-
function
|
|
39008
|
-
|
|
39009
|
-
|
|
39010
|
-
|
|
39011
|
-
|
|
39012
|
-
|
|
39013
|
-
|
|
39014
|
-
|
|
39015
|
-
|
|
39016
|
-
|
|
39017
|
-
|
|
39018
|
-
|
|
39019
|
-
|
|
39020
|
-
|
|
39021
|
-
|
|
39022
|
-
|
|
39023
|
-
|
|
39024
|
-
if (
|
|
39025
|
-
|
|
39026
|
-
|
|
39027
|
-
|
|
39028
|
-
|
|
39029
|
-
if (type === "
|
|
39030
|
-
e.preventDefault();
|
|
39031
|
-
|
|
39032
|
-
return;
|
|
39033
|
-
}
|
|
39034
|
-
|
|
39035
|
-
|
|
39036
|
-
|
|
39037
|
-
|
|
39038
|
-
|
|
39039
|
-
|
|
39040
|
-
|
|
39041
|
-
|
|
39042
|
-
|
|
39043
|
-
|
|
39044
|
-
|
|
39045
|
-
|
|
39046
|
-
|
|
39047
|
-
|
|
39048
|
-
|
|
39049
|
-
|
|
39050
|
-
|
|
39051
|
-
|
|
39052
|
-
|
|
39053
|
-
|
|
39054
|
-
|
|
39055
|
-
|
|
39056
|
-
}
|
|
39057
|
-
|
|
39058
|
-
|
|
39059
|
-
|
|
39060
|
-
|
|
39061
|
-
|
|
39062
|
-
|
|
39063
|
-
|
|
39064
|
-
|
|
39065
|
-
|
|
39066
|
-
|
|
39067
|
-
//
|
|
39068
|
-
|
|
39069
|
-
|
|
39070
|
-
|
|
39071
|
-
}
|
|
39072
|
-
// Caret
|
|
39073
|
-
if (node.nodeType ===
|
|
39074
|
-
|
|
39075
|
-
|
|
39076
|
-
|
|
39077
|
-
|
|
39078
|
-
|
|
39079
|
-
|
|
39080
|
-
|
|
39081
|
-
|
|
39082
|
-
|
|
39083
|
-
|
|
39084
|
-
|
|
39085
|
-
|
|
39086
|
-
|
|
39087
|
-
|
|
39088
|
-
|
|
39089
|
-
|
|
39090
|
-
|
|
39091
|
-
|
|
39092
|
-
|
|
39093
|
-
|
|
39094
|
-
|
|
39095
|
-
|
|
39096
|
-
|
|
39097
|
-
|
|
39098
|
-
|
|
39099
|
-
|
|
39100
|
-
|
|
39101
|
-
|
|
39102
|
-
var
|
|
39103
|
-
|
|
39104
|
-
|
|
39105
|
-
|
|
39106
|
-
|
|
39107
|
-
|
|
39108
|
-
|
|
39109
|
-
|
|
39110
|
-
|
|
39111
|
-
|
|
39112
|
-
span
|
|
39113
|
-
|
|
39114
|
-
|
|
39115
|
-
|
|
39116
|
-
|
|
39117
|
-
|
|
39118
|
-
|
|
39119
|
-
|
|
39120
|
-
|
|
39121
|
-
|
|
39122
|
-
|
|
39123
|
-
|
|
39124
|
-
|
|
39125
|
-
}
|
|
39126
|
-
|
|
39127
|
-
function
|
|
39128
|
-
var
|
|
39129
|
-
|
|
39130
|
-
|
|
39131
|
-
|
|
39132
|
-
|
|
39133
|
-
|
|
39134
|
-
|
|
39135
|
-
|
|
39136
|
-
|
|
39137
|
-
|
|
39138
|
-
|
|
39139
|
-
|
|
39140
|
-
|
|
39141
|
-
|
|
39142
|
-
|
|
39143
|
-
|
|
39144
|
-
|
|
39145
|
-
|
|
39146
|
-
|
|
39147
|
-
|
|
39148
|
-
|
|
39149
|
-
|
|
39150
|
-
|
|
39151
|
-
|
|
39152
|
-
|
|
39153
|
-
|
|
39154
|
-
|
|
39155
|
-
|
|
39156
|
-
|
|
39157
|
-
|
|
39158
|
-
|
|
39159
|
-
|
|
39160
|
-
|
|
39161
|
-
|
|
39162
|
-
|
|
39163
|
-
|
|
39164
|
-
|
|
39165
|
-
|
|
39166
|
-
|
|
39167
|
-
|
|
39168
|
-
|
|
39169
|
-
|
|
39170
|
-
|
|
39171
|
-
|
|
39172
|
-
|
|
39173
|
-
|
|
39174
|
-
|
|
39175
|
-
|
|
39176
|
-
|
|
39177
|
-
|
|
39178
|
-
|
|
39179
|
-
|
|
39180
|
-
|
|
39181
|
-
}
|
|
39182
|
-
|
|
39183
|
-
|
|
39184
|
-
|
|
39185
|
-
|
|
39186
|
-
|
|
39187
|
-
|
|
39188
|
-
|
|
39189
|
-
|
|
39190
|
-
|
|
39191
|
-
|
|
39192
|
-
|
|
39193
|
-
|
|
39194
|
-
|
|
39195
|
-
|
|
39196
|
-
|
|
39197
|
-
|
|
39198
|
-
|
|
39199
|
-
|
|
39200
|
-
|
|
39201
|
-
|
|
39202
|
-
|
|
39203
|
-
|
|
39204
|
-
|
|
39205
|
-
|
|
39206
|
-
|
|
39207
|
-
|
|
39208
|
-
|
|
39209
|
-
|
|
39210
|
-
|
|
39211
|
-
|
|
39212
|
-
|
|
39213
|
-
|
|
39214
|
-
|
|
39215
|
-
|
|
39216
|
-
|
|
39217
|
-
|
|
39218
|
-
|
|
39219
|
-
|
|
39220
|
-
|
|
39221
|
-
|
|
39222
|
-
|
|
39223
|
-
|
|
39224
|
-
|
|
39225
|
-
|
|
39226
|
-
|
|
39227
|
-
|
|
39228
|
-
|
|
39229
|
-
|
|
39230
|
-
|
|
39231
|
-
|
|
39232
|
-
|
|
39233
|
-
|
|
39234
|
-
|
|
39235
|
-
|
|
39236
|
-
|
|
39237
|
-
|
|
39238
|
-
return
|
|
39239
|
-
|
|
39240
|
-
|
|
39241
|
-
|
|
39242
|
-
|
|
39243
|
-
|
|
39244
|
-
|
|
39245
|
-
|
|
39246
|
-
|
|
39247
|
-
|
|
39248
|
-
|
|
39249
|
-
|
|
39250
|
-
|
|
39251
|
-
|
|
39252
|
-
|
|
39253
|
-
|
|
39254
|
-
|
|
39255
|
-
|
|
39256
|
-
|
|
39257
|
-
|
|
39258
|
-
|
|
39259
|
-
|
|
39260
|
-
|
|
39261
|
-
|
|
39262
|
-
|
|
39263
|
-
|
|
39264
|
-
|
|
39265
|
-
|
|
39266
|
-
|
|
39267
|
-
|
|
39268
|
-
|
|
39269
|
-
var
|
|
39270
|
-
|
|
39271
|
-
if (
|
|
39272
|
-
|
|
39273
|
-
}
|
|
39274
|
-
return null;
|
|
39275
|
-
}
|
|
39276
|
-
|
|
39277
|
-
function
|
|
39278
|
-
|
|
39279
|
-
|
|
39280
|
-
var
|
|
39281
|
-
|
|
39282
|
-
|
|
39283
|
-
|
|
39284
|
-
|
|
39285
|
-
return null;
|
|
39286
|
-
|
|
39287
|
-
|
|
39288
|
-
|
|
39289
|
-
|
|
39290
|
-
|
|
39291
|
-
|
|
39292
|
-
|
|
39293
|
-
|
|
39294
|
-
|
|
39295
|
-
|
|
39296
|
-
|
|
39297
|
-
|
|
39298
|
-
|
|
39299
|
-
|
|
39300
|
-
|
|
39301
|
-
|
|
39302
|
-
|
|
39303
|
-
|
|
39304
|
-
|
|
39305
|
-
|
|
39306
|
-
var
|
|
39307
|
-
var
|
|
39308
|
-
|
|
39309
|
-
|
|
39310
|
-
|
|
39311
|
-
|
|
39312
|
-
|
|
39313
|
-
|
|
39314
|
-
|
|
39315
|
-
|
|
39316
|
-
|
|
39317
|
-
|
|
39318
|
-
|
|
39319
|
-
|
|
39320
|
-
|
|
39321
|
-
|
|
39322
|
-
|
|
39323
|
-
|
|
39324
|
-
|
|
39325
|
-
|
|
39326
|
-
|
|
39327
|
-
|
|
39328
|
-
|
|
39329
|
-
|
|
39330
|
-
|
|
39331
|
-
|
|
39332
|
-
|
|
39333
|
-
|
|
39334
|
-
|
|
39335
|
-
|
|
39336
|
-
|
|
39337
|
-
|
|
39338
|
-
|
|
39339
|
-
if (
|
|
39340
|
-
|
|
39341
|
-
|
|
39342
|
-
|
|
39343
|
-
|
|
39344
|
-
|
|
39345
|
-
|
|
39346
|
-
|
|
39347
|
-
|
|
39348
|
-
|
|
39349
|
-
|
|
39350
|
-
|
|
39351
|
-
|
|
39352
|
-
|
|
39353
|
-
|
|
39354
|
-
|
|
39355
|
-
|
|
39356
|
-
|
|
39357
|
-
|
|
39358
|
-
|
|
39359
|
-
|
|
39360
|
-
|
|
39361
|
-
|
|
39362
|
-
|
|
39363
|
-
|
|
39364
|
-
|
|
39365
|
-
|
|
39366
|
-
|
|
39367
|
-
|
|
39368
|
-
|
|
39369
|
-
|
|
39370
|
-
|
|
39371
|
-
|
|
39372
|
-
|
|
39373
|
-
|
|
39374
|
-
|
|
39375
|
-
|
|
39376
|
-
|
|
39377
|
-
|
|
39378
|
-
|
|
39379
|
-
|
|
39380
|
-
|
|
39381
|
-
|
|
39382
|
-
|
|
39383
|
-
|
|
39384
|
-
|
|
39385
|
-
|
|
39386
|
-
|
|
39387
|
-
|
|
39388
|
-
|
|
39389
|
-
|
|
39390
|
-
|
|
39391
|
-
|
|
39392
|
-
|
|
39393
|
-
|
|
39394
|
-
|
|
39395
|
-
|
|
39396
|
-
|
|
39397
|
-
|
|
39398
|
-
|
|
39399
|
-
|
|
39400
|
-
|
|
39401
|
-
|
|
39402
|
-
|
|
39403
|
-
|
|
39404
|
-
|
|
39405
|
-
|
|
39406
|
-
|
|
39407
|
-
|
|
39408
|
-
|
|
39409
|
-
|
|
39410
|
-
|
|
39411
|
-
|
|
39412
|
-
|
|
39413
|
-
|
|
39414
|
-
|
|
39415
|
-
|
|
39416
|
-
|
|
39417
|
-
|
|
39418
|
-
|
|
39419
|
-
|
|
39420
|
-
|
|
39421
|
-
|
|
39422
|
-
|
|
39423
|
-
|
|
39424
|
-
|
|
39425
|
-
|
|
39426
|
-
|
|
39427
|
-
|
|
39428
|
-
|
|
39429
|
-
|
|
39430
|
-
|
|
39431
|
-
|
|
39432
|
-
|
|
39433
|
-
|
|
39434
|
-
|
|
39435
|
-
|
|
39436
|
-
|
|
39437
|
-
|
|
39438
|
-
|
|
39439
|
-
|
|
39440
|
-
|
|
39441
|
-
|
|
39442
|
-
|
|
39443
|
-
|
|
39444
|
-
|
|
39445
|
-
|
|
39446
|
-
|
|
39447
|
-
|
|
39448
|
-
|
|
39449
|
-
|
|
39450
|
-
|
|
39451
|
-
|
|
39452
|
-
|
|
39453
|
-
|
|
39454
|
-
|
|
39455
|
-
|
|
39456
|
-
|
|
39457
|
-
|
|
39458
|
-
|
|
39459
|
-
|
|
39460
|
-
|
|
39461
|
-
|
|
39462
|
-
|
|
39463
|
-
|
|
39464
|
-
|
|
39465
|
-
|
|
39466
|
-
|
|
39467
|
-
|
|
39468
|
-
|
|
39469
|
-
|
|
39470
|
-
|
|
39471
|
-
|
|
39472
|
-
|
|
39473
|
-
|
|
39474
|
-
|
|
39475
|
-
|
|
39476
|
-
|
|
39477
|
-
|
|
39478
|
-
var
|
|
39479
|
-
|
|
39480
|
-
|
|
39481
|
-
|
|
39482
|
-
|
|
39483
|
-
|
|
39484
|
-
|
|
39485
|
-
var
|
|
39486
|
-
|
|
39487
|
-
|
|
39488
|
-
|
|
39489
|
-
|
|
39490
|
-
|
|
39491
|
-
|
|
39492
|
-
|
|
39493
|
-
}
|
|
39494
|
-
|
|
39495
|
-
|
|
39496
|
-
|
|
39497
|
-
|
|
39498
|
-
|
|
39499
|
-
var
|
|
39500
|
-
|
|
39501
|
-
|
|
39502
|
-
|
|
39503
|
-
|
|
39504
|
-
|
|
39505
|
-
|
|
39506
|
-
|
|
39507
|
-
}
|
|
39508
|
-
|
|
39509
|
-
|
|
39510
|
-
|
|
39511
|
-
|
|
39512
|
-
|
|
39513
|
-
|
|
39514
|
-
|
|
39515
|
-
|
|
39516
|
-
|
|
39517
|
-
|
|
39518
|
-
|
|
39519
|
-
|
|
39520
|
-
|
|
39521
|
-
|
|
39522
|
-
|
|
39523
|
-
|
|
39524
|
-
|
|
39525
|
-
|
|
39526
|
-
|
|
39527
|
-
return
|
|
39528
|
-
|
|
39529
|
-
|
|
39530
|
-
|
|
39531
|
-
|
|
39532
|
-
if (
|
|
39533
|
-
|
|
39534
|
-
|
|
39535
|
-
|
|
39536
|
-
|
|
39537
|
-
|
|
39538
|
-
|
|
39539
|
-
|
|
39540
|
-
|
|
39541
|
-
|
|
39542
|
-
|
|
39543
|
-
|
|
39544
|
-
|
|
39545
|
-
|
|
39546
|
-
|
|
39547
|
-
|
|
39548
|
-
|
|
39549
|
-
|
|
39550
|
-
|
|
39551
|
-
|
|
39552
|
-
|
|
39553
|
-
|
|
39554
|
-
|
|
39555
|
-
|
|
39556
|
-
|
|
39557
|
-
|
|
39558
|
-
|
|
39559
|
-
|
|
39560
|
-
|
|
39561
|
-
|
|
39562
|
-
|
|
39563
|
-
|
|
39564
|
-
|
|
39565
|
-
|
|
39566
|
-
|
|
39567
|
-
|
|
39568
|
-
|
|
39569
|
-
var
|
|
39570
|
-
if (
|
|
39571
|
-
|
|
39572
|
-
|
|
39573
|
-
|
|
39574
|
-
|
|
39575
|
-
|
|
39576
|
-
|
|
39577
|
-
|
|
39578
|
-
|
|
39579
|
-
|
|
39580
|
-
|
|
39581
|
-
|
|
40768
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
40769
|
+
|
|
40770
|
+
if (!RTE_DefaultConfig.svgCode_trackchanges) {
|
|
40771
|
+
RTE_DefaultConfig.svgCode_trackchanges = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17l4-1 10-10-3-3L5 13l-1 4z"/><path d="M14 4l3 3"/><path d="M4 21h10"/></svg>';
|
|
40772
|
+
}
|
|
40773
|
+
|
|
40774
|
+
RTE_DefaultConfig.plugin_trackedchanges = RTE_Plugin_TrackedChanges;
|
|
40775
|
+
|
|
40776
|
+
function RTE_Plugin_TrackedChanges() {
|
|
40777
|
+
var obj = this;
|
|
40778
|
+
var config;
|
|
40779
|
+
var editor;
|
|
40780
|
+
var enabled = false;
|
|
40781
|
+
|
|
40782
|
+
obj.PluginName = "TrackedChanges";
|
|
40783
|
+
|
|
40784
|
+
obj.InitConfig = function (argconfig) {
|
|
40785
|
+
config = argconfig;
|
|
40786
|
+
if (config.trackChangesEnabled === false) return;
|
|
40787
|
+
|
|
40788
|
+
config.currentUser = config.currentUser || null;
|
|
40789
|
+
config.trackChangesInsertClass = config.trackChangesInsertClass || "rte-tc rte-tc-insert";
|
|
40790
|
+
config.trackChangesDeleteClass = config.trackChangesDeleteClass || "rte-tc rte-tc-delete";
|
|
40791
|
+
// Optional application policy for accept/reject. Return false to keep a
|
|
40792
|
+
// suggestion pending. The default remains permissive for compatibility.
|
|
40793
|
+
config.reviewCanDecide = config.reviewCanDecide || null;
|
|
40794
|
+
config.onReviewDecisionDenied = config.onReviewDecisionDenied || null;
|
|
40795
|
+
|
|
40796
|
+
// Toolbar command — only surfaces if the editor page adds it to a toolbar slot.
|
|
40797
|
+
config.text_trackchanges = config.text_trackchanges || "Suggesting mode";
|
|
40798
|
+
appendToolbarCommand("toolbar_default", "#{trackchanges}");
|
|
40799
|
+
appendToolbarCommand("toolbar_full", "#{trackchanges}");
|
|
40800
|
+
appendToolbarCommand("toolbar_mobile", "#{trackchanges}");
|
|
40801
|
+
|
|
40802
|
+
if ((config.controltoolbar_TEXT || "").indexOf("trackchanges") === -1) {
|
|
40803
|
+
config.controltoolbar_TEXT = (config.controltoolbar_TEXT || "") + "|{trackchanges}";
|
|
40804
|
+
}
|
|
40805
|
+
};
|
|
40806
|
+
|
|
40807
|
+
obj.InitEditor = function (argeditor) {
|
|
40808
|
+
editor = argeditor;
|
|
40809
|
+
if (config.trackChangesEnabled === false) return;
|
|
40810
|
+
|
|
40811
|
+
editor.trackedChanges = {
|
|
40812
|
+
enable: function (user) { enable(user); },
|
|
40813
|
+
disable: function () { disable(); },
|
|
40814
|
+
isEnabled: function () { return enabled; },
|
|
40815
|
+
acceptAll: function (filter) { return acceptAll(filter); },
|
|
40816
|
+
rejectAll: function (filter) { return rejectAll(filter); },
|
|
40817
|
+
accept: function (id) { return acceptEntry(id); },
|
|
40818
|
+
reject: function (id) { return rejectEntry(id); },
|
|
40819
|
+
canDecide: function (id, action) {
|
|
40820
|
+
var entry = editor.reviewLedger && editor.reviewLedger.get(id);
|
|
40821
|
+
return !!entry && canDecide(entry, action || "accept");
|
|
40822
|
+
},
|
|
40823
|
+
list: function (opts) {
|
|
40824
|
+
if (!editor.reviewLedger) return [];
|
|
40825
|
+
// Default to the PENDING review queue (matches the internal
|
|
40826
|
+
// accept/reject semantics). Pass {all:true} for full history
|
|
40827
|
+
// including accepted/rejected entries (also via reviewLedger.list()).
|
|
40828
|
+
var includeAll = opts && opts.all === true;
|
|
40829
|
+
return editor.reviewLedger.list().filter(function (e) {
|
|
40830
|
+
if (e.changeType !== "insert" && e.changeType !== "delete") return false;
|
|
40831
|
+
return includeAll || e.status === "pending";
|
|
40832
|
+
});
|
|
40833
|
+
}
|
|
40834
|
+
};
|
|
40835
|
+
|
|
40836
|
+
injectStyles();
|
|
40837
|
+
|
|
40838
|
+
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
40839
|
+
editor.toolbarFactoryMap["trackchanges"] = function (cmd) {
|
|
40840
|
+
var btn = editor.createToolbarButton(cmd);
|
|
40841
|
+
btn.__tcSync = function () {
|
|
40842
|
+
if (enabled) btn.classList.add("rte-ui-active");
|
|
40843
|
+
else btn.classList.remove("rte-ui-active");
|
|
40844
|
+
btn.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
40845
|
+
// WCAG 2.5.3 Label in Name: the visible label (this tooltip) must be
|
|
40846
|
+
// contained in the accessible name. "Turn on suggesting mode" was not
|
|
40847
|
+
// — the accessible name is "Suggesting mode" — so a speech-input user
|
|
40848
|
+
// saying what they see did not match the control. State belongs in
|
|
40849
|
+
// aria-pressed and the active styling, not in the visible label.
|
|
40850
|
+
btn.setAttribute("title", config.text_trackchanges || "Suggesting mode");
|
|
40851
|
+
};
|
|
40852
|
+
btn.__tcSync();
|
|
40853
|
+
return btn;
|
|
40854
|
+
};
|
|
40855
|
+
|
|
40856
|
+
editor.attachEvent("exec_command_trackchanges", function (state) {
|
|
40857
|
+
state.returnValue = true;
|
|
40858
|
+
state.stopBubble = true;
|
|
40859
|
+
if (enabled) disable();
|
|
40860
|
+
else enable();
|
|
40861
|
+
});
|
|
40862
|
+
|
|
40863
|
+
editor.getEditable().addEventListener("beforeinput", onBeforeInput, true);
|
|
40864
|
+
};
|
|
40865
|
+
|
|
40866
|
+
function appendToolbarCommand(toolbar, item) {
|
|
40867
|
+
if (!config[toolbar]) return;
|
|
40868
|
+
if (config[toolbar].indexOf(item) !== -1) return;
|
|
40869
|
+
config[toolbar] = config[toolbar] + item;
|
|
40870
|
+
}
|
|
40871
|
+
|
|
40872
|
+
function enable(user) {
|
|
40873
|
+
if (user) config.currentUser = user;
|
|
40874
|
+
if (!config.currentUser) {
|
|
40875
|
+
config.currentUser = { id: "user", name: "User", color: "#2563eb" };
|
|
40876
|
+
console.warn("trackedchanges: no config.currentUser configured; defaulting to generic user.");
|
|
40877
|
+
}
|
|
40878
|
+
enabled = true;
|
|
40879
|
+
syncToolbarButtons();
|
|
40880
|
+
}
|
|
40881
|
+
|
|
40882
|
+
function disable() {
|
|
40883
|
+
enabled = false;
|
|
40884
|
+
syncToolbarButtons();
|
|
40885
|
+
}
|
|
40886
|
+
|
|
40887
|
+
function syncToolbarButtons() {
|
|
40888
|
+
var doc = editor.iframe && editor.iframe.ownerDocument || document;
|
|
40889
|
+
var btns = doc.querySelectorAll(".rte_command_trackchanges");
|
|
40890
|
+
for (var i = 0; i < btns.length; i++) {
|
|
40891
|
+
if (btns[i].__tcSync) btns[i].__tcSync();
|
|
40892
|
+
}
|
|
40893
|
+
}
|
|
40894
|
+
|
|
40895
|
+
// --- beforeinput intercept ---
|
|
40896
|
+
|
|
40897
|
+
function onBeforeInput(e) {
|
|
40898
|
+
if (!enabled) return;
|
|
40899
|
+
if (!editor.reviewLedger) return; // No ledger => AI toolkit not loaded; do nothing.
|
|
40900
|
+
|
|
40901
|
+
var type = e.inputType;
|
|
40902
|
+
if (!type) return;
|
|
40903
|
+
|
|
40904
|
+
if (type === "insertText" || type === "insertReplacementText") {
|
|
40905
|
+
e.preventDefault();
|
|
40906
|
+
handleInsertText(e.data || "");
|
|
40907
|
+
return;
|
|
40908
|
+
}
|
|
40909
|
+
if (type === "deleteContentBackward" || type === "deleteContentForward" || type === "deleteByCut") {
|
|
40910
|
+
e.preventDefault();
|
|
40911
|
+
handleDelete(type);
|
|
40912
|
+
return;
|
|
40913
|
+
}
|
|
40914
|
+
// insertParagraph, insertCompositionText, insertFromPaste, historyUndo/Redo: pass through for v1.
|
|
40915
|
+
}
|
|
40916
|
+
|
|
40917
|
+
// --- insert tracking ---
|
|
40918
|
+
|
|
40919
|
+
function handleInsertText(text) {
|
|
40920
|
+
if (!text) return;
|
|
40921
|
+
var sel = editor.getSelection();
|
|
40922
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
40923
|
+
var range = sel.getRangeAt(0);
|
|
40924
|
+
if (!range.collapsed) {
|
|
40925
|
+
// Selection replace: wrap selection as delete, then insert new text.
|
|
40926
|
+
if (!wrapRangeAsDelete(range)) return;
|
|
40927
|
+
// After wrapping, caret should be past the delete span; re-fetch selection.
|
|
40928
|
+
range = sel.getRangeAt(0);
|
|
40929
|
+
createInsertSpan(text, range);
|
|
40930
|
+
return;
|
|
40931
|
+
}
|
|
40932
|
+
|
|
40933
|
+
var mergeTarget = adjacentInsertSpan(range);
|
|
40934
|
+
if (mergeTarget) {
|
|
40935
|
+
appendToInsertSpan(mergeTarget, text);
|
|
40936
|
+
} else {
|
|
40937
|
+
createInsertSpan(text, range);
|
|
40938
|
+
}
|
|
40939
|
+
}
|
|
40940
|
+
|
|
40941
|
+
function adjacentInsertSpan(range) {
|
|
40942
|
+
// If caret sits inside an insert span by the current user, or immediately after one, merge.
|
|
40943
|
+
var node = range.startContainer;
|
|
40944
|
+
if (node.nodeType === 3 && node.parentNode && isMyInsert(node.parentNode)) {
|
|
40945
|
+
return node.parentNode;
|
|
40946
|
+
}
|
|
40947
|
+
// Caret just after an insert span (at an element boundary).
|
|
40948
|
+
if (node.nodeType === 1 && range.startOffset > 0) {
|
|
40949
|
+
var prev = node.childNodes[range.startOffset - 1];
|
|
40950
|
+
if (prev && prev.nodeType === 1 && isMyInsert(prev)) return prev;
|
|
40951
|
+
}
|
|
40952
|
+
// Caret at the very beginning just before our span.
|
|
40953
|
+
if (node.nodeType === 3 && node.previousSibling && isMyInsert(node.previousSibling) && range.startOffset === 0) {
|
|
40954
|
+
return node.previousSibling;
|
|
40955
|
+
}
|
|
40956
|
+
return null;
|
|
40957
|
+
}
|
|
40958
|
+
|
|
40959
|
+
function isMyInsert(el) {
|
|
40960
|
+
if (!el || el.nodeType !== 1) return false;
|
|
40961
|
+
if (!el.classList || !el.classList.contains("rte-tc-insert")) return false;
|
|
40962
|
+
return el.getAttribute("data-tc-author") === config.currentUser.id;
|
|
40963
|
+
}
|
|
40964
|
+
|
|
40965
|
+
// We preventDefault() the browser's own text insertion, which means we also
|
|
40966
|
+
// lose the whitespace handling it does for free. A plain second space is
|
|
40967
|
+
// collapsed by HTML rendering, so pressing space a second time moved nothing
|
|
40968
|
+
// on screen and suggesting mode looked like it had stopped accepting input.
|
|
40969
|
+
// Reproduce what contenteditable does: any space followed by another space,
|
|
40970
|
+
// and any trailing space, becomes a non-breaking space. Recomputed from
|
|
40971
|
+
// scratch each time so an NBSP reverts to a normal space once real text
|
|
40972
|
+
// follows it (keeping word wrap and copied text clean). Only typed text
|
|
40973
|
+
// reaches this path -- paste arrives as insertFromPaste and is passed
|
|
40974
|
+
// through -- so this never rewrites an NBSP the user inserted deliberately.
|
|
40975
|
+
function normalizeInsertWhitespace(node) {
|
|
40976
|
+
if (!node || node.nodeType !== 3) return;
|
|
40977
|
+
var v = node.nodeValue;
|
|
40978
|
+
if (v.indexOf(" ") === -1 && v.indexOf("\u00a0") === -1) return;
|
|
40979
|
+
v = v.replace(/\u00a0/g, " ");
|
|
40980
|
+
v = v.replace(/ (?= )/g, "\u00a0");
|
|
40981
|
+
v = v.replace(/ $/, "\u00a0");
|
|
40982
|
+
if (v !== node.nodeValue) node.nodeValue = v;
|
|
40983
|
+
}
|
|
40984
|
+
|
|
40985
|
+
function appendToInsertSpan(span, text) {
|
|
40986
|
+
var editdoc = editor.getDocument();
|
|
40987
|
+
// Append to the span's last text node (or create one).
|
|
40988
|
+
var last = span.lastChild;
|
|
40989
|
+
var node;
|
|
40990
|
+
if (last && last.nodeType === 3) {
|
|
40991
|
+
last.nodeValue = last.nodeValue + text;
|
|
40992
|
+
node = last;
|
|
40993
|
+
} else {
|
|
40994
|
+
node = editdoc.createTextNode(text);
|
|
40995
|
+
span.appendChild(node);
|
|
40996
|
+
}
|
|
40997
|
+
normalizeInsertWhitespace(node);
|
|
40998
|
+
placeCaretAtEnd(node);
|
|
40999
|
+
updateEntryForSpan(span);
|
|
41000
|
+
}
|
|
41001
|
+
|
|
41002
|
+
function createInsertSpan(text, range) {
|
|
41003
|
+
var editdoc = editor.getDocument();
|
|
41004
|
+
var span = editdoc.createElement("span");
|
|
41005
|
+
span.className = config.trackChangesInsertClass;
|
|
41006
|
+
var user = config.currentUser;
|
|
41007
|
+
var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
41008
|
+
span.setAttribute("data-tc-id", id);
|
|
41009
|
+
span.setAttribute("data-tc-author", user.id);
|
|
41010
|
+
span.setAttribute("data-tc-at", new Date().toISOString());
|
|
41011
|
+
span.style.color = user.color;
|
|
41012
|
+
span.style.textDecoration = "underline";
|
|
41013
|
+
span.appendChild(editdoc.createTextNode(text));
|
|
41014
|
+
range.insertNode(span);
|
|
41015
|
+
normalizeInsertWhitespace(span.firstChild);
|
|
41016
|
+
placeCaretAtEnd(span.firstChild);
|
|
41017
|
+
|
|
41018
|
+
editor.reviewLedger.add({
|
|
41019
|
+
id: id,
|
|
41020
|
+
changeType: "insert",
|
|
41021
|
+
author: user,
|
|
41022
|
+
text: text,
|
|
41023
|
+
status: "pending",
|
|
41024
|
+
sourceLabel: "Suggested insert",
|
|
41025
|
+
createdAt: Date.now()
|
|
41026
|
+
});
|
|
41027
|
+
}
|
|
41028
|
+
|
|
41029
|
+
function updateEntryForSpan(span) {
|
|
41030
|
+
var id = span.getAttribute("data-tc-id");
|
|
41031
|
+
if (!id) return;
|
|
41032
|
+
editor.reviewLedger.update(id, { text: span.textContent });
|
|
41033
|
+
}
|
|
41034
|
+
|
|
41035
|
+
// --- delete tracking ---
|
|
41036
|
+
|
|
41037
|
+
function handleDelete(mode) {
|
|
41038
|
+
var sel = editor.getSelection();
|
|
41039
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
41040
|
+
var range = sel.getRangeAt(0);
|
|
41041
|
+
|
|
41042
|
+
if (!range.collapsed) {
|
|
41043
|
+
wrapRangeAsDelete(range);
|
|
41044
|
+
return;
|
|
41045
|
+
}
|
|
41046
|
+
|
|
41047
|
+
// Collapsed: extend by one char in the appropriate direction, then wrap.
|
|
41048
|
+
// The extended range covers only that one character — any delete spans between
|
|
41049
|
+
// it and the caret should be SKIPPED, not re-wrapped.
|
|
41050
|
+
var extended = editor.getDocument().createRange();
|
|
41051
|
+
if (mode === "deleteContentBackward") {
|
|
41052
|
+
var anchor = prevCharPoint(range.startContainer, range.startOffset);
|
|
41053
|
+
if (!anchor) return;
|
|
41054
|
+
extended.setStart(anchor.node, anchor.offset);
|
|
41055
|
+
extended.setEnd(anchor.node, anchor.offset + 1);
|
|
41056
|
+
} else {
|
|
41057
|
+
var forward = nextCharPoint(range.startContainer, range.startOffset);
|
|
41058
|
+
if (!forward) return;
|
|
41059
|
+
extended.setStart(forward.node, forward.offset - 1);
|
|
41060
|
+
extended.setEnd(forward.node, forward.offset);
|
|
41061
|
+
}
|
|
41062
|
+
|
|
41063
|
+
// If that extended range sits entirely inside one of the current user's own
|
|
41064
|
+
// insert spans, treat this as undoing their own typing — actually delete.
|
|
41065
|
+
if (rangeInsideMyInsert(extended)) {
|
|
41066
|
+
extended.deleteContents();
|
|
41067
|
+
var sel2 = editor.getSelection();
|
|
41068
|
+
sel2.removeAllRanges();
|
|
41069
|
+
sel2.addRange(extended);
|
|
41070
|
+
// If the insert span is now empty, remove it and its ledger entry.
|
|
41071
|
+
cleanupEmptyInsertSpans();
|
|
41072
|
+
return;
|
|
41073
|
+
}
|
|
41074
|
+
|
|
41075
|
+
wrapRangeAsDelete(extended);
|
|
41076
|
+
}
|
|
41077
|
+
|
|
41078
|
+
function isInsideDeleteSpan(node) {
|
|
41079
|
+
var walk = node.nodeType === 1 ? node : node.parentNode;
|
|
41080
|
+
while (walk && walk !== editor.getEditable()) {
|
|
41081
|
+
if (walk.classList && walk.classList.contains("rte-tc-delete")) return true;
|
|
41082
|
+
walk = walk.parentNode;
|
|
41083
|
+
}
|
|
41084
|
+
return false;
|
|
41085
|
+
}
|
|
41086
|
+
|
|
41087
|
+
// Walk text leaves in the editable, skipping any that live inside an existing delete
|
|
41088
|
+
// span (those characters have already been visually "deleted" — we shouldn't re-wrap them).
|
|
41089
|
+
function liveTextLeaves() {
|
|
41090
|
+
var editable = editor.getEditable();
|
|
41091
|
+
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT, {
|
|
41092
|
+
acceptNode: function (n) { return isInsideDeleteSpan(n) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
|
|
41093
|
+
});
|
|
41094
|
+
var out = [], n;
|
|
41095
|
+
while ((n = walker.nextNode())) out.push(n);
|
|
41096
|
+
return out;
|
|
41097
|
+
}
|
|
41098
|
+
|
|
41099
|
+
function prevCharPoint(node, offset) {
|
|
41100
|
+
var leaves = liveTextLeaves();
|
|
41101
|
+
if (!leaves.length) return null;
|
|
41102
|
+
var idx = caretIndex(node, offset, leaves);
|
|
41103
|
+
if (idx <= 0) return null;
|
|
41104
|
+
return leafOffsetAt(idx - 1, leaves);
|
|
41105
|
+
}
|
|
41106
|
+
|
|
41107
|
+
function nextCharPoint(node, offset) {
|
|
41108
|
+
var leaves = liveTextLeaves();
|
|
41109
|
+
if (!leaves.length) return null;
|
|
41110
|
+
var total = 0;
|
|
41111
|
+
for (var i = 0; i < leaves.length; i++) total += leaves[i].nodeValue.length;
|
|
41112
|
+
var idx = caretIndex(node, offset, leaves);
|
|
41113
|
+
if (idx >= total) return null;
|
|
41114
|
+
return leafOffsetAt(idx + 1, leaves);
|
|
41115
|
+
}
|
|
41116
|
+
|
|
41117
|
+
// How many live characters appear before the given caret position?
|
|
41118
|
+
function caretIndex(node, offset, leaves) {
|
|
41119
|
+
var r = editor.getDocument().createRange();
|
|
41120
|
+
r.setStart(node, offset);
|
|
41121
|
+
r.collapse(true);
|
|
41122
|
+
var count = 0;
|
|
41123
|
+
for (var i = 0; i < leaves.length; i++) {
|
|
41124
|
+
var leaf = leaves[i];
|
|
41125
|
+
// If the entire leaf is before the caret, add its length.
|
|
41126
|
+
var cmpEnd = r.comparePoint(leaf, leaf.nodeValue.length);
|
|
41127
|
+
if (cmpEnd <= 0) { count += leaf.nodeValue.length; continue; }
|
|
41128
|
+
var cmpStart = r.comparePoint(leaf, 0);
|
|
41129
|
+
if (cmpStart >= 0) break; // leaf starts at or after caret
|
|
41130
|
+
// Caret falls inside this leaf — binary search the split point.
|
|
41131
|
+
var lo = 0, hi = leaf.nodeValue.length;
|
|
41132
|
+
while (lo < hi) {
|
|
41133
|
+
var mid = (lo + hi) >> 1;
|
|
41134
|
+
if (r.comparePoint(leaf, mid) < 0) lo = mid + 1;
|
|
41135
|
+
else hi = mid;
|
|
41136
|
+
}
|
|
41137
|
+
count += lo;
|
|
41138
|
+
break;
|
|
41139
|
+
}
|
|
41140
|
+
return count;
|
|
41141
|
+
}
|
|
41142
|
+
|
|
41143
|
+
function leafOffsetAt(index, leaves) {
|
|
41144
|
+
var remaining = index;
|
|
41145
|
+
for (var i = 0; i < leaves.length; i++) {
|
|
41146
|
+
if (remaining <= leaves[i].nodeValue.length) return { node: leaves[i], offset: remaining };
|
|
41147
|
+
remaining -= leaves[i].nodeValue.length;
|
|
41148
|
+
}
|
|
41149
|
+
return null;
|
|
41150
|
+
}
|
|
41151
|
+
|
|
41152
|
+
function firstTextLeaf(node) {
|
|
41153
|
+
if (!node) return null;
|
|
41154
|
+
if (node.nodeType === 3) return node;
|
|
41155
|
+
var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
41156
|
+
return walker.nextNode();
|
|
41157
|
+
}
|
|
41158
|
+
|
|
41159
|
+
function lastTextLeaf(node) {
|
|
41160
|
+
if (!node) return null;
|
|
41161
|
+
if (node.nodeType === 3) return node;
|
|
41162
|
+
var walker = editor.getDocument().createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
41163
|
+
var last = null, n;
|
|
41164
|
+
while ((n = walker.nextNode())) last = n;
|
|
41165
|
+
return last;
|
|
41166
|
+
}
|
|
41167
|
+
|
|
41168
|
+
function previousTextLeaf(node) {
|
|
41169
|
+
var editable = editor.getEditable();
|
|
41170
|
+
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
|
|
41171
|
+
var prev = null, cur;
|
|
41172
|
+
while ((cur = walker.nextNode())) {
|
|
41173
|
+
if (cur === node) return prev;
|
|
41174
|
+
prev = cur;
|
|
41175
|
+
}
|
|
41176
|
+
return null;
|
|
41177
|
+
}
|
|
41178
|
+
|
|
41179
|
+
function nextTextLeaf(node) {
|
|
41180
|
+
var editable = editor.getEditable();
|
|
41181
|
+
var walker = editor.getDocument().createTreeWalker(editable, NodeFilter.SHOW_TEXT);
|
|
41182
|
+
var seen = false, cur;
|
|
41183
|
+
while ((cur = walker.nextNode())) {
|
|
41184
|
+
if (seen) return cur;
|
|
41185
|
+
if (cur === node) seen = true;
|
|
41186
|
+
}
|
|
41187
|
+
return null;
|
|
41188
|
+
}
|
|
41189
|
+
|
|
41190
|
+
function rangeInsideMyInsert(range) {
|
|
41191
|
+
var span = range.startContainer.parentNode;
|
|
41192
|
+
while (span && span !== editor.getEditable()) {
|
|
41193
|
+
if (isMyInsert(span)) return range.endContainer.parentNode === span || span.contains(range.endContainer);
|
|
41194
|
+
span = span.parentNode;
|
|
41195
|
+
}
|
|
41196
|
+
return false;
|
|
41197
|
+
}
|
|
41198
|
+
|
|
41199
|
+
function wrapRangeAsDelete(range) {
|
|
41200
|
+
if (range.collapsed) return false;
|
|
41201
|
+
var editdoc = editor.getDocument();
|
|
41202
|
+
|
|
41203
|
+
var fragment = range.cloneContents();
|
|
41204
|
+
if (!fragmentHasText(fragment)) return false;
|
|
41205
|
+
|
|
41206
|
+
// Find existing same-author delete spans adjacent to the range so we can merge
|
|
41207
|
+
// sequential backspaces / forward deletes into one contiguous span.
|
|
41208
|
+
var user = config.currentUser;
|
|
41209
|
+
var mergeBefore = adjacentDeleteSpan(range, "before", user);
|
|
41210
|
+
var mergeAfter = adjacentDeleteSpan(range, "after", user);
|
|
41211
|
+
|
|
41212
|
+
range.deleteContents();
|
|
41213
|
+
|
|
41214
|
+
if (mergeBefore) {
|
|
41215
|
+
// Append the fragment's content to the existing "before" span.
|
|
41216
|
+
appendFragmentToSpan(mergeBefore, fragment);
|
|
41217
|
+
if (mergeAfter && mergeAfter !== mergeBefore) {
|
|
41218
|
+
// Collapse both sides: move "after" span's children into "before" and drop it.
|
|
41219
|
+
while (mergeAfter.firstChild) mergeBefore.appendChild(mergeAfter.firstChild);
|
|
41220
|
+
var afterId = mergeAfter.getAttribute("data-tc-id");
|
|
41221
|
+
mergeAfter.remove();
|
|
41222
|
+
if (afterId && editor.reviewLedger) editor.reviewLedger.remove(afterId);
|
|
41223
|
+
}
|
|
41224
|
+
var rangeAfter = editdoc.createRange();
|
|
41225
|
+
rangeAfter.setStartAfter(mergeBefore);
|
|
41226
|
+
rangeAfter.collapse(true);
|
|
41227
|
+
var sel = editor.getSelection();
|
|
41228
|
+
sel.removeAllRanges();
|
|
41229
|
+
sel.addRange(rangeAfter);
|
|
41230
|
+
// 2026-05-28 Defense: if a customer pasted a delete-class span from
|
|
41231
|
+
// outside this plugin, it may lack data-tc-id. Skip the ledger
|
|
41232
|
+
// update instead of writing under a `null` key (would silently
|
|
41233
|
+
// corrupt the ledger via the update("null", ...) coercion).
|
|
41234
|
+
var beforeId = mergeBefore.getAttribute("data-tc-id");
|
|
41235
|
+
if (beforeId && editor.reviewLedger) {
|
|
41236
|
+
editor.reviewLedger.update(beforeId, { text: mergeBefore.textContent });
|
|
41237
|
+
}
|
|
41238
|
+
return true;
|
|
41239
|
+
}
|
|
41240
|
+
|
|
41241
|
+
if (mergeAfter) {
|
|
41242
|
+
prependFragmentToSpan(mergeAfter, fragment);
|
|
41243
|
+
var rangeBefore = editdoc.createRange();
|
|
41244
|
+
rangeBefore.setStartBefore(mergeAfter);
|
|
41245
|
+
rangeBefore.collapse(true);
|
|
41246
|
+
var sel2 = editor.getSelection();
|
|
41247
|
+
sel2.removeAllRanges();
|
|
41248
|
+
sel2.addRange(rangeBefore);
|
|
41249
|
+
// Same defense as above (mergeAfter may lack data-tc-id).
|
|
41250
|
+
var afterMergeId = mergeAfter.getAttribute("data-tc-id");
|
|
41251
|
+
if (afterMergeId && editor.reviewLedger) {
|
|
41252
|
+
editor.reviewLedger.update(afterMergeId, { text: mergeAfter.textContent });
|
|
41253
|
+
}
|
|
41254
|
+
return true;
|
|
41255
|
+
}
|
|
41256
|
+
|
|
41257
|
+
var span = editdoc.createElement("span");
|
|
41258
|
+
span.className = config.trackChangesDeleteClass;
|
|
41259
|
+
var id = "tc-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
41260
|
+
span.setAttribute("data-tc-id", id);
|
|
41261
|
+
span.setAttribute("data-tc-author", user.id);
|
|
41262
|
+
span.setAttribute("data-tc-at", new Date().toISOString());
|
|
41263
|
+
span.style.color = user.color;
|
|
41264
|
+
span.style.textDecoration = "line-through";
|
|
41265
|
+
span.appendChild(fragment);
|
|
41266
|
+
range.insertNode(span);
|
|
41267
|
+
|
|
41268
|
+
var after = editdoc.createRange();
|
|
41269
|
+
after.setStartAfter(span);
|
|
41270
|
+
after.collapse(true);
|
|
41271
|
+
var sel3 = editor.getSelection();
|
|
41272
|
+
sel3.removeAllRanges();
|
|
41273
|
+
sel3.addRange(after);
|
|
41274
|
+
|
|
41275
|
+
editor.reviewLedger.add({
|
|
41276
|
+
id: id,
|
|
41277
|
+
changeType: "delete",
|
|
41278
|
+
author: user,
|
|
41279
|
+
text: span.textContent,
|
|
41280
|
+
originalHtml: span.innerHTML,
|
|
41281
|
+
status: "pending",
|
|
41282
|
+
sourceLabel: "Suggested delete",
|
|
41283
|
+
createdAt: Date.now()
|
|
41284
|
+
});
|
|
41285
|
+
return true;
|
|
41286
|
+
}
|
|
41287
|
+
|
|
41288
|
+
function adjacentDeleteSpan(range, side, user) {
|
|
41289
|
+
// Find element immediately before or after the range boundaries at the container level.
|
|
41290
|
+
var node = side === "before" ? range.startContainer : range.endContainer;
|
|
41291
|
+
var offset = side === "before" ? range.startOffset : range.endOffset;
|
|
41292
|
+
if (node.nodeType === 3) {
|
|
41293
|
+
// Look at the sibling of the text node if we're at the text node's boundary.
|
|
41294
|
+
if (side === "before" && offset === 0) {
|
|
41295
|
+
var prev = node.previousSibling;
|
|
41296
|
+
if (prev && isDeleteSpanByAuthor(prev, user)) return prev;
|
|
41297
|
+
}
|
|
41298
|
+
if (side === "after" && offset === node.nodeValue.length) {
|
|
41299
|
+
var next = node.nextSibling;
|
|
41300
|
+
if (next && isDeleteSpanByAuthor(next, user)) return next;
|
|
41301
|
+
}
|
|
41302
|
+
return null;
|
|
41303
|
+
}
|
|
41304
|
+
// Element container: look at children on either side of the offset.
|
|
41305
|
+
if (side === "before" && offset > 0) {
|
|
41306
|
+
var childBefore = node.childNodes[offset - 1];
|
|
41307
|
+
if (childBefore && isDeleteSpanByAuthor(childBefore, user)) return childBefore;
|
|
41308
|
+
}
|
|
41309
|
+
if (side === "after" && offset < node.childNodes.length) {
|
|
41310
|
+
var childAfter = node.childNodes[offset];
|
|
41311
|
+
if (childAfter && isDeleteSpanByAuthor(childAfter, user)) return childAfter;
|
|
41312
|
+
}
|
|
41313
|
+
return null;
|
|
41314
|
+
}
|
|
41315
|
+
|
|
41316
|
+
function isDeleteSpanByAuthor(el, user) {
|
|
41317
|
+
if (!el || el.nodeType !== 1) return false;
|
|
41318
|
+
if (!el.classList || !el.classList.contains("rte-tc-delete")) return false;
|
|
41319
|
+
return el.getAttribute("data-tc-author") === user.id;
|
|
41320
|
+
}
|
|
41321
|
+
|
|
41322
|
+
function appendFragmentToSpan(span, fragment) {
|
|
41323
|
+
while (fragment.firstChild) span.appendChild(fragment.firstChild);
|
|
41324
|
+
}
|
|
41325
|
+
|
|
41326
|
+
function prependFragmentToSpan(span, fragment) {
|
|
41327
|
+
var first = span.firstChild;
|
|
41328
|
+
while (fragment.firstChild) span.insertBefore(fragment.firstChild, first);
|
|
41329
|
+
}
|
|
41330
|
+
|
|
41331
|
+
function fragmentHasText(fragment) {
|
|
41332
|
+
var t = (fragment.textContent || "").replace(/\s+/g, "");
|
|
41333
|
+
return !!t;
|
|
41334
|
+
}
|
|
41335
|
+
|
|
41336
|
+
function cleanupEmptyInsertSpans() {
|
|
41337
|
+
var spans = editor.getEditable().querySelectorAll("." + "rte-tc-insert");
|
|
41338
|
+
for (var i = 0; i < spans.length; i++) {
|
|
41339
|
+
var s = spans[i];
|
|
41340
|
+
if (!s.textContent || !s.textContent.length) {
|
|
41341
|
+
var id = s.getAttribute("data-tc-id");
|
|
41342
|
+
if (id && editor.reviewLedger) editor.reviewLedger.remove(id);
|
|
41343
|
+
s.remove();
|
|
41344
|
+
} else {
|
|
41345
|
+
updateEntryForSpan(s);
|
|
41346
|
+
}
|
|
41347
|
+
}
|
|
41348
|
+
}
|
|
41349
|
+
|
|
41350
|
+
function placeCaretAtEnd(node) {
|
|
41351
|
+
if (!node) return;
|
|
41352
|
+
var editdoc = editor.getDocument();
|
|
41353
|
+
var range = editdoc.createRange();
|
|
41354
|
+
if (node.nodeType === 3) {
|
|
41355
|
+
range.setStart(node, node.nodeValue.length);
|
|
41356
|
+
} else {
|
|
41357
|
+
range.setStart(node, node.childNodes.length);
|
|
41358
|
+
}
|
|
41359
|
+
range.collapse(true);
|
|
41360
|
+
var sel = editor.getSelection();
|
|
41361
|
+
sel.removeAllRanges();
|
|
41362
|
+
sel.addRange(range);
|
|
41363
|
+
}
|
|
41364
|
+
|
|
41365
|
+
// --- accept / reject ---
|
|
41366
|
+
|
|
41367
|
+
function decisionUser() {
|
|
41368
|
+
return config.currentUser || { id: "user", name: "User", color: "#2563eb" };
|
|
41369
|
+
}
|
|
41370
|
+
|
|
41371
|
+
function canDecide(entry, action) {
|
|
41372
|
+
if (typeof config.reviewCanDecide !== "function") return true;
|
|
41373
|
+
var allowed = false;
|
|
41374
|
+
var user = decisionUser();
|
|
41375
|
+
try { allowed = config.reviewCanDecide(entry, action, user, editor) !== false; }
|
|
41376
|
+
catch (err) {
|
|
41377
|
+
console.warn("trackedchanges: reviewCanDecide threw; denying decision.", err);
|
|
41378
|
+
}
|
|
41379
|
+
if (allowed) return true;
|
|
41380
|
+
var detail = { entry: entry, action: action, user: user };
|
|
41381
|
+
try { typeof config.onReviewDecisionDenied === "function" && config.onReviewDecisionDenied(detail); } catch (ignore) { }
|
|
41382
|
+
try { typeof editor.fireEvent === "function" && editor.fireEvent("review_decision_denied", detail); } catch (ignore2) { }
|
|
41383
|
+
return false;
|
|
41384
|
+
}
|
|
41385
|
+
|
|
41386
|
+
function acceptEntry(id) {
|
|
41387
|
+
var entry = editor.reviewLedger.get(id);
|
|
41388
|
+
if (!entry) return false;
|
|
41389
|
+
if (!canDecide(entry, "accept")) return false;
|
|
41390
|
+
var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
|
|
41391
|
+
if (entry.changeType === "insert") {
|
|
41392
|
+
if (span) unwrapKeepChildren(span);
|
|
41393
|
+
} else if (entry.changeType === "delete") {
|
|
41394
|
+
if (span) span.remove();
|
|
41395
|
+
}
|
|
41396
|
+
editor.reviewLedger.update(id, { status: "accepted", decidedAt: Date.now(), decidedBy: decisionUser() });
|
|
41397
|
+
return true;
|
|
41398
|
+
}
|
|
41399
|
+
|
|
41400
|
+
function rejectEntry(id) {
|
|
41401
|
+
var entry = editor.reviewLedger.get(id);
|
|
41402
|
+
if (!entry) return false;
|
|
41403
|
+
if (!canDecide(entry, "reject")) return false;
|
|
41404
|
+
var span = editor.getEditable().querySelector('[data-tc-id="' + cssEscape(id) + '"]');
|
|
41405
|
+
if (entry.changeType === "insert") {
|
|
41406
|
+
if (span) span.remove();
|
|
41407
|
+
} else if (entry.changeType === "delete") {
|
|
41408
|
+
if (span) unwrapKeepChildren(span);
|
|
41409
|
+
}
|
|
41410
|
+
editor.reviewLedger.update(id, { status: "rejected", decidedAt: Date.now(), decidedBy: decisionUser() });
|
|
41411
|
+
return true;
|
|
41412
|
+
}
|
|
41413
|
+
|
|
41414
|
+
function acceptAll(filter) {
|
|
41415
|
+
var entries = editor.reviewLedger.list(filter).filter(function (e) {
|
|
41416
|
+
return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
|
|
41417
|
+
});
|
|
41418
|
+
var accepted = 0;
|
|
41419
|
+
for (var i = 0; i < entries.length; i++) if (acceptEntry(entries[i].id)) accepted++;
|
|
41420
|
+
return accepted;
|
|
41421
|
+
}
|
|
41422
|
+
|
|
41423
|
+
function rejectAll(filter) {
|
|
41424
|
+
var entries = editor.reviewLedger.list(filter).filter(function (e) {
|
|
41425
|
+
return (e.changeType === "insert" || e.changeType === "delete") && e.status === "pending";
|
|
41426
|
+
});
|
|
41427
|
+
var rejected = 0;
|
|
41428
|
+
for (var i = 0; i < entries.length; i++) if (rejectEntry(entries[i].id)) rejected++;
|
|
41429
|
+
return rejected;
|
|
41430
|
+
}
|
|
41431
|
+
|
|
41432
|
+
function unwrapKeepChildren(el) {
|
|
41433
|
+
var parent = el.parentNode;
|
|
41434
|
+
if (!parent) return;
|
|
41435
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
41436
|
+
parent.removeChild(el);
|
|
41437
|
+
}
|
|
41438
|
+
|
|
41439
|
+
// 2026-05-28 Prefer native CSS.escape — same hardening as comments.js.
|
|
41440
|
+
// Defense-in-depth for customers who wire reviewLedger.add() with custom IDs
|
|
41441
|
+
// containing punctuation (e.g. UUIDs with dashes, OAuth subject claims with
|
|
41442
|
+
// colons, base64 IDs with `=` padding).
|
|
41443
|
+
function cssEscape(s) {
|
|
41444
|
+
var str = String(s);
|
|
41445
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
41446
|
+
try { return CSS.escape(str); } catch (e) { /* fall through */ }
|
|
41447
|
+
}
|
|
41448
|
+
return str.replace(/["\\\]\[\(\)\{\}'#:;,<>=`~!@$%^&*+/?\s\x00-\x1F]/g, function (c) {
|
|
41449
|
+
return "\\" + c;
|
|
41450
|
+
});
|
|
41451
|
+
}
|
|
41452
|
+
|
|
41453
|
+
// --- styles ---
|
|
41454
|
+
|
|
41455
|
+
function injectStyles() {
|
|
41456
|
+
var host = (editor && editor.iframe && editor.iframe.ownerDocument) || document;
|
|
41457
|
+
if (!host.querySelector("style[data-rte-trackchanges]")) {
|
|
41458
|
+
var style = host.createElement("style");
|
|
41459
|
+
style.setAttribute("data-rte-trackchanges", "1");
|
|
41460
|
+
style.textContent = [
|
|
41461
|
+
".richtexteditor rte-toolbar-button.rte_command_trackchanges{position:relative;border-radius:8px;transition:background .14s ease,color .14s ease,box-shadow .14s ease}",
|
|
41462
|
+
".richtexteditor rte-toolbar-button.rte_command_trackchanges.rte-ui-active{background:linear-gradient(135deg,#e9f7ef,#eef5ff);color:#17633f;box-shadow:inset 0 0 0 1px rgba(34,132,82,.28),0 6px 14px rgba(34,132,82,.12)}",
|
|
41463
|
+
".richtexteditor rte-toolbar-button.rte_command_trackchanges.rte-ui-active:after{content:'';position:absolute;right:5px;top:5px;width:6px;height:6px;border-radius:50%;background:#22a66a;box-shadow:0 0 0 2px #fff}",
|
|
41464
|
+
".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
|
|
41465
|
+
".rte-tc-insert{background:linear-gradient(180deg,rgba(232,248,239,.78),rgba(232,248,239,.38));color:#125b39;text-decoration:underline;text-decoration-color:#22a66a;text-decoration-thickness:2px;text-underline-offset:3px}",
|
|
41466
|
+
".rte-tc-delete{background:linear-gradient(180deg,rgba(255,240,240,.82),rgba(255,240,240,.42));color:#9f1d1d;text-decoration:line-through;text-decoration-color:#dc4c4c;text-decoration-thickness:2px;opacity:.92}"
|
|
41467
|
+
].join("\n");
|
|
41468
|
+
host.head.appendChild(style);
|
|
41469
|
+
}
|
|
41470
|
+
|
|
41471
|
+
var editdoc = editor.getDocument();
|
|
41472
|
+
if (editdoc && editdoc.head && !editdoc.querySelector("style[data-rte-trackchanges-inline]")) {
|
|
41473
|
+
var iStyle = editdoc.createElement("style");
|
|
41474
|
+
iStyle.setAttribute("data-rte-trackchanges-inline", "1");
|
|
41475
|
+
iStyle.textContent = [
|
|
41476
|
+
".rte-tc{border-radius:4px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}",
|
|
41477
|
+
".rte-tc-insert{background:linear-gradient(180deg,rgba(232,248,239,.78),rgba(232,248,239,.38));color:#125b39;text-decoration:underline;text-decoration-color:#22a66a;text-decoration-thickness:2px;text-underline-offset:3px}",
|
|
41478
|
+
".rte-tc-delete{background:linear-gradient(180deg,rgba(255,240,240,.82),rgba(255,240,240,.42));color:#9f1d1d;text-decoration:line-through;text-decoration-color:#dc4c4c;text-decoration-thickness:2px;opacity:.92}"
|
|
41479
|
+
].join("\n");
|
|
41480
|
+
editdoc.head.appendChild(iStyle);
|
|
41481
|
+
}
|
|
41482
|
+
}
|
|
41483
|
+
}
|
|
39582
41484
|
|
|
39583
41485
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
39584
41486
|
|
|
@@ -40175,7 +42077,7 @@ function RTE_Plugin_WordExport() {
|
|
|
40175
42077
|
if (!name) return "";
|
|
40176
42078
|
// Strip control chars + filesystem-reserved characters, collapse spaces.
|
|
40177
42079
|
return String(name)
|
|
40178
|
-
.replace(/[\\/:*?"
|
|
42080
|
+
.replace(/[\\/:*?"<>|\x00-\x1f]+/g, " ")
|
|
40179
42081
|
.replace(/\s+/g, " ")
|
|
40180
42082
|
.trim()
|
|
40181
42083
|
.slice(0, 80);
|