@richscripts/richtexteditor 2.5.3 → 2.6.0
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/README.md +449 -360
- package/package.json +142 -142
- package/richtexteditor/CHANGELOG.md +2714 -2201
- package/richtexteditor/all_plugins.js +1064 -310
- package/richtexteditor/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/accessibilitychecker.js +67 -4
- package/richtexteditor/plugins/aitoolkit.js +24 -6
- package/richtexteditor/plugins/aitoolkit.min.css +1 -0
- package/richtexteditor/plugins/all_plugins.js +1064 -310
- package/richtexteditor/plugins/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/keyboarda11y.js +370 -342
- package/richtexteditor/plugins/layouttable.js +383 -0
- package/richtexteditor/plugins/shadowdom.js +252 -0
- package/richtexteditor/plugins/slashcommand.js +8 -0
- package/richtexteditor/rte-watchdog.js +303 -0
- package/richtexteditor/rte-webcomponent.js +351 -0
- package/richtexteditor/rte_mobile.min.css +1 -0
- package/richtexteditor/rte_theme_default.css +1 -1
- package/richtexteditor/rte_theme_default.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_content.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_preview.min.css +1 -0
|
@@ -499,7 +499,13 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
499
499
|
if (headingText) previousHeadingLevel = level;
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
-
|
|
502
|
+
// querySelectorAll only returns DESCENDANTS, so an image that is
|
|
503
|
+
// itself a top-level block — which is exactly where a pasted or
|
|
504
|
+
// dropped image lands in an empty editor — was never checked, and
|
|
505
|
+
// missing alt text went unreported. The table rule below already
|
|
506
|
+
// guards this case; the image rule did not. Measured 2026-08-27:
|
|
507
|
+
// bare top-level img => 0 issues, the same img inside a <p> => 1.
|
|
508
|
+
var images = tag === "img" ? [node] : (node.querySelectorAll ? node.querySelectorAll("img") : []);
|
|
503
509
|
for (var imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
|
504
510
|
var image = images[imageIndex];
|
|
505
511
|
if (!String(image.getAttribute("alt") || "").replace(/^\s+|\s+$/g, "")) {
|
|
@@ -516,11 +522,33 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
516
522
|
var tables = tag === "table" ? [node] : (node.querySelectorAll ? node.querySelectorAll("table") : []);
|
|
517
523
|
for (var tableIndex = 0; tableIndex < tables.length; tableIndex++) {
|
|
518
524
|
var table = tables[tableIndex];
|
|
519
|
-
|
|
525
|
+
var tableRole = String(table.getAttribute("role") || "").toLowerCase();
|
|
526
|
+
var isLayoutTable = (tableRole === "presentation" || tableRole === "none");
|
|
527
|
+
|
|
528
|
+
// 2026-08-30 A layout table MUST NOT have headers, so demanding
|
|
529
|
+
// them here produced an error the author could never clear —
|
|
530
|
+
// which is how a checker teaches people to ignore it. Presentation
|
|
531
|
+
// tables are held to the opposite rule instead: they must not
|
|
532
|
+
// carry data-table semantics, because <th>/scope/<caption> make a
|
|
533
|
+
// screen reader announce relationships that do not exist
|
|
534
|
+
// (WCAG 1.3.1). See layouttable.js.
|
|
535
|
+
if (isLayoutTable) {
|
|
536
|
+
var semantic = table.querySelector("th, [scope], caption") || table.getAttribute("summary");
|
|
537
|
+
if (semantic) {
|
|
538
|
+
issues.push({
|
|
539
|
+
code: "layout-table-semantics",
|
|
540
|
+
severity: "error",
|
|
541
|
+
message: "Layout table still carries data-table markup (header cells, scope or a caption). A screen reader will announce relationships this table does not have.",
|
|
542
|
+
path: path + ".table[" + tableIndex + "]",
|
|
543
|
+
_target: table
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (!table.querySelector("th")) {
|
|
520
548
|
issues.push({
|
|
521
549
|
code: "table-missing-header",
|
|
522
550
|
severity: "warning",
|
|
523
|
-
message: "Table has no header cells. Promote the first row to headers when it
|
|
551
|
+
message: "Table has no header cells. Promote the first row to headers, or mark it as a layout table when it only positions content.",
|
|
524
552
|
path: path + ".table[" + tableIndex + "]",
|
|
525
553
|
_target: table
|
|
526
554
|
});
|
|
@@ -551,7 +579,13 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
551
579
|
target.parentNode.replaceChild(replacement, target);
|
|
552
580
|
}
|
|
553
581
|
else if (issue.code === "table-missing-header") {
|
|
554
|
-
|
|
582
|
+
// Two valid outcomes, and only the author knows which: the table
|
|
583
|
+
// describes data (give it headers) or only positions it (say so).
|
|
584
|
+
if (options && options.markLayout) markTableAsLayout(target);
|
|
585
|
+
else promoteFirstTableRow(target);
|
|
586
|
+
}
|
|
587
|
+
else if (issue.code === "layout-table-semantics") {
|
|
588
|
+
markTableAsLayout(target);
|
|
555
589
|
}
|
|
556
590
|
else if (issue.code === "language-of-parts") {
|
|
557
591
|
markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
|
|
@@ -636,6 +670,35 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
636
670
|
}
|
|
637
671
|
}
|
|
638
672
|
|
|
673
|
+
// Delegates to layouttable.js when it is loaded so there is ONE definition of
|
|
674
|
+
// what "layout table" means; the fallback keeps the repair working in builds
|
|
675
|
+
// where that plugin was left out of the bundle.
|
|
676
|
+
function markTableAsLayout(table) {
|
|
677
|
+
if (!table) return;
|
|
678
|
+
if (editor && typeof editor.setTableLayoutMode === "function") {
|
|
679
|
+
try { editor.setTableLayoutMode("layout", table); return; } catch (e) {}
|
|
680
|
+
}
|
|
681
|
+
table.setAttribute("role", "presentation");
|
|
682
|
+
table.removeAttribute("summary");
|
|
683
|
+
var caption = table.querySelector("caption");
|
|
684
|
+
if (caption && caption.parentNode === table) caption.parentNode.removeChild(caption);
|
|
685
|
+
var cells = table.querySelectorAll("th,[scope]");
|
|
686
|
+
for (var i = 0; i < cells.length; i++) {
|
|
687
|
+
var cell = cells[i];
|
|
688
|
+
// Tables nest — do not strip the headers of a data table that
|
|
689
|
+
// happens to sit inside a cell of this one.
|
|
690
|
+
if (cell.closest && cell.closest("table") !== table) continue;
|
|
691
|
+
cell.removeAttribute("scope");
|
|
692
|
+
cell.removeAttribute("headers");
|
|
693
|
+
if (String(cell.nodeName || "").toLowerCase() !== "th") continue;
|
|
694
|
+
var td = table.ownerDocument.createElement("td");
|
|
695
|
+
while (cell.firstChild) td.appendChild(cell.firstChild);
|
|
696
|
+
copyAttributes(cell, td);
|
|
697
|
+
td.removeAttribute("scope");
|
|
698
|
+
cell.parentNode.replaceChild(td, cell);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
639
702
|
function copyAttributes(source, target) {
|
|
640
703
|
if (!source || !target || !source.attributes) return;
|
|
641
704
|
for (var index = 0; index < source.attributes.length; index++) {
|
|
@@ -869,7 +932,9 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
|
869
932
|
var existing = document.querySelectorAll("link[rel=stylesheet]");
|
|
870
933
|
for (var i = 0; i < existing.length; i++) {
|
|
871
934
|
var href = existing[i].getAttribute("href") || "";
|
|
872
|
-
|
|
935
|
+
// Matches the minified variant too - a host page that links either one
|
|
936
|
+
// must not get a second copy injected.
|
|
937
|
+
if (/(?:^|\/)aitoolkit(?:\.min)?\.css(?:\?.*)?$/i.test(href)) return;
|
|
873
938
|
}
|
|
874
939
|
// Locate our own script tag (aitoolkit.js or all_plugins.js) so we
|
|
875
940
|
// can resolve aitoolkit.css next to it. Matches a custom mount path.
|
|
@@ -883,14 +948,30 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
|
883
948
|
}
|
|
884
949
|
}
|
|
885
950
|
var srcAttr = ourScript ? ourScript.getAttribute("src") : "";
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
951
|
+
// Prefer the minified stylesheet. aitoolkit.css is 537 KB unminified - larger
|
|
952
|
+
// than the obfuscated rte.js - and every page that loads the AI toolkit was
|
|
953
|
+
// paying for it. The readable .css is still shipped and still the one to edit
|
|
954
|
+
// or override; this only changes what gets fetched by default.
|
|
955
|
+
function resolve(name) {
|
|
956
|
+
return srcAttr
|
|
957
|
+
? srcAttr.replace(/[^/]+$/, name)
|
|
958
|
+
: (((window.RTE_DefaultConfig && window.RTE_DefaultConfig.url_base) || "/richtexteditor") + "/plugins/" + name);
|
|
959
|
+
}
|
|
960
|
+
function bust(href) { return href.indexOf("?") < 0 ? href + "?v=20260830a" : href; }
|
|
961
|
+
|
|
890
962
|
var link = document.createElement("link");
|
|
891
963
|
link.rel = "stylesheet";
|
|
892
|
-
link.href =
|
|
964
|
+
link.href = bust(resolve("aitoolkit.min.css"));
|
|
893
965
|
link.setAttribute("data-rte-auto-injected", "aitoolkit");
|
|
966
|
+
// .min.css is newer than .css. An install that predates it - an unpacked older
|
|
967
|
+
// zip, a tier the mirror missed - would otherwise lose ALL AI toolkit styling
|
|
968
|
+
// silently, which is a far worse failure than shipping the larger file. Fall
|
|
969
|
+
// back to the readable stylesheet if the minified one is not there.
|
|
970
|
+
link.onerror = function () {
|
|
971
|
+
if (link.getAttribute("data-rte-css-fallback")) return;
|
|
972
|
+
link.setAttribute("data-rte-css-fallback", "1");
|
|
973
|
+
link.href = bust(resolve("aitoolkit.css"));
|
|
974
|
+
};
|
|
894
975
|
(document.head || document.documentElement).appendChild(link);
|
|
895
976
|
})();
|
|
896
977
|
|
|
@@ -30895,346 +30976,758 @@ function RTE_Plugin_InsertTemplate() {
|
|
|
30895
30976
|
};
|
|
30896
30977
|
}
|
|
30897
30978
|
|
|
30979
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
30980
|
+
|
|
30981
|
+
// 2026-08-02 Keyboard and state accessibility for the editor CHROME.
|
|
30982
|
+
//
|
|
30983
|
+
// a11yenhance.js names the editing region and dialoga11y.js names the dialogs.
|
|
30984
|
+
// This closes what an audit of the running editor found still open — four
|
|
30985
|
+
// findings, three of them WCAG Level A:
|
|
30986
|
+
//
|
|
30987
|
+
// 1. KEYBOARD TRAP (2.1.2, Level A). Tab inside the editing area is
|
|
30988
|
+
// preventDefault-ed and inserts spaces, so a keyboard-only or screen
|
|
30989
|
+
// reader user who enters the editor can never leave it. Reloading the page
|
|
30990
|
+
// is the only way out. This is the most serious kind of accessibility
|
|
30991
|
+
// defect: it does not degrade the experience, it ends it.
|
|
30992
|
+
//
|
|
30993
|
+
// 2. TOGGLE STATE NOT EXPOSED (4.1.2, Level A). Bold, italic, underline and
|
|
30994
|
+
// the alignment buttons carry their state in a CSS class
|
|
30995
|
+
// (rte-command-active / rte-command-deactive) and nowhere else. Sighted
|
|
30996
|
+
// users see a highlighted button; assistive technology is told nothing.
|
|
30997
|
+
// Measured: 0 of 6 toggle buttons exposed aria-pressed.
|
|
30998
|
+
//
|
|
30999
|
+
// 3. POPUP STATE NOT EXPOSED (4.1.2, Level A). Buttons carry
|
|
31000
|
+
// aria-haspopup but never aria-expanded, so there is no way to know
|
|
31001
|
+
// whether a menu is open. Measured: 0 of 7.
|
|
31002
|
+
//
|
|
31003
|
+
// 4. EVERY TOOLBAR BUTTON IS A TAB STOP (2.4.3, and plain usability).
|
|
31004
|
+
// Measured 53. The ARIA Authoring Practices toolbar pattern is a single
|
|
31005
|
+
// tab stop per toolbar with arrow keys moving between buttons. 53 presses
|
|
31006
|
+
// of Tab to reach the text you came to write is not operable in any
|
|
31007
|
+
// meaningful sense.
|
|
31008
|
+
//
|
|
31009
|
+
// All four are fixed here rather than in the core, so they ship without a
|
|
31010
|
+
// re-obfuscation cycle.
|
|
31011
|
+
//
|
|
31012
|
+
// Config:
|
|
31013
|
+
// config.keyboardA11y = false // opt out entirely
|
|
31014
|
+
// config.a11yEscapeHint = "..." // wording appended to the editing area's name
|
|
31015
|
+
// config.a11yEditorLabel = "..." // accessible NAME of the editing area (default "Rich text editor")
|
|
31016
|
+
// config.a11yRovingToolbar = false // keep every button as a tab stop
|
|
31017
|
+
RTE_DefaultConfig.plugin_keyboarda11y = RTE_Plugin_KeyboardA11y;
|
|
31018
|
+
if (typeof RTE_DefaultConfig.keyboardA11y === "undefined") RTE_DefaultConfig.keyboardA11y = true;
|
|
31019
|
+
|
|
31020
|
+
function RTE_Plugin_KeyboardA11y() {
|
|
31021
|
+
var obj = this;
|
|
31022
|
+
var config, editor;
|
|
31023
|
+
var observers = [];
|
|
31024
|
+
var popupOwner = null; // last activated [aria-haspopup]
|
|
31025
|
+
var openPanels = []; // [{ panel, owner }]
|
|
31026
|
+
|
|
31027
|
+
obj.PluginName = "KeyboardA11y";
|
|
31028
|
+
|
|
31029
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
31030
|
+
|
|
31031
|
+
obj.InitEditor = function (argeditor) {
|
|
31032
|
+
editor = argeditor;
|
|
31033
|
+
if (config.keyboardA11y === false) return;
|
|
31034
|
+
|
|
31035
|
+
editor.focusToolbar = function () { return focusFirstToolbarButton(); };
|
|
31036
|
+
|
|
31037
|
+
setup();
|
|
31038
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
31039
|
+
// The toolbar is built asynchronously; a deferred pass catches the case
|
|
31040
|
+
// where InitEditor runs before it exists.
|
|
31041
|
+
setTimeout(setup, 0);
|
|
31042
|
+
setTimeout(setup, 400);
|
|
31043
|
+
};
|
|
31044
|
+
|
|
31045
|
+
function shell() {
|
|
31046
|
+
try {
|
|
31047
|
+
var ed = editor.getEditable();
|
|
31048
|
+
if (!ed) return null;
|
|
31049
|
+
var win = ed.ownerDocument.defaultView;
|
|
31050
|
+
var node = (win && win.frameElement) ? win.frameElement : ed;
|
|
31051
|
+
while (node && node.classList && !node.classList.contains("richtexteditor")) node = node.parentNode;
|
|
31052
|
+
return (node && node.classList) ? node : null;
|
|
31053
|
+
} catch (e) { return null; }
|
|
31054
|
+
}
|
|
31055
|
+
|
|
31056
|
+
function setup() {
|
|
31057
|
+
var root = shell();
|
|
31058
|
+
if (!root) return;
|
|
31059
|
+
bindEscapeHatch();
|
|
31060
|
+
applyRovingTabindex(root);
|
|
31061
|
+
syncToggleStates(root);
|
|
31062
|
+
trackPopups(root);
|
|
31063
|
+
watch(root);
|
|
31064
|
+
}
|
|
31065
|
+
|
|
31066
|
+
// ---------------------------------------------------- 1. keyboard trap
|
|
31067
|
+
//
|
|
31068
|
+
// Tab keeps its editing meaning (indent, next table cell) because that is
|
|
31069
|
+
// what writers expect and what every other editor does. The escape is a
|
|
31070
|
+
// separate, documented key: Escape leaves the editing area and puts focus
|
|
31071
|
+
// on the toolbar, from which Tab continues through the page normally.
|
|
31072
|
+
//
|
|
31073
|
+
// Escape is only intercepted when nothing is open — a dialog or dropdown
|
|
31074
|
+
// must still get its own Escape first, or closing a colour picker would
|
|
31075
|
+
// throw the user out of the editor.
|
|
31076
|
+
// Bound on the DOCUMENT rather than on the body element. Escape bubbles, so
|
|
31077
|
+
// one listener at document level covers the editing area no matter how the
|
|
31078
|
+
// surface is re-created, and it does not depend on a marker flag pinned to a
|
|
31079
|
+
// particular element surviving. Defensive only: no shipped operation is known
|
|
31080
|
+
// to replace the editing body — setHTML, htmlview, fullscreen, readingmode,
|
|
31081
|
+
// preview and toggleborder all preserve its identity (measured 2026-08-27).
|
|
31082
|
+
// Keeping the binding here costs nothing and removes an assumption, but it
|
|
31083
|
+
// fixes no known live defect; do not describe it as a bug fix.
|
|
31084
|
+
function bindEscapeHatch() {
|
|
31085
|
+
var ed, doc;
|
|
31086
|
+
try { ed = editor.getEditable(); doc = ed && ed.ownerDocument; } catch (e) { return; }
|
|
31087
|
+
if (!ed || !doc) return;
|
|
31088
|
+
|
|
31089
|
+
if (!doc.__rteEscapeHatch) {
|
|
31090
|
+
doc.__rteEscapeHatch = true;
|
|
31091
|
+
doc.addEventListener("keydown", function (e) {
|
|
31092
|
+
if (e.key !== "Escape" && e.keyCode !== 27) return;
|
|
31093
|
+
if (anythingOpen()) return; // let the panel close itself
|
|
31094
|
+
e.preventDefault();
|
|
31095
|
+
e.stopPropagation();
|
|
31096
|
+
if (!focusFirstToolbarButton()) focusAfterEditor();
|
|
31097
|
+
}, false);
|
|
31098
|
+
|
|
31099
|
+
// Deliberately not re-asserting the body's aria-label here. An
|
|
31100
|
+
// earlier version did, on the theory that the editing body could be
|
|
31101
|
+
// replaced underneath us; that mechanism was retracted 2026-08-27
|
|
31102
|
+
// after it turned out to be an instrumentation artefact, and
|
|
31103
|
+
// editor.getEditable() is verified to return the live, connected
|
|
31104
|
+
// body on every shipped path. announceEscape() at the end of this
|
|
31105
|
+
// function is sufficient.
|
|
31106
|
+
}
|
|
31107
|
+
|
|
31108
|
+
announceEscape(ed);
|
|
31109
|
+
}
|
|
31110
|
+
|
|
31111
|
+
function anythingOpen() {
|
|
31112
|
+
try {
|
|
31113
|
+
var panels = document.querySelectorAll("rte-dropdown-panel, rte-dialog-float, rte-floatpanel");
|
|
31114
|
+
for (var i = 0; i < panels.length; i++) {
|
|
31115
|
+
var s = getComputedStyle(panels[i]);
|
|
31116
|
+
if (s.display !== "none" && s.visibility !== "hidden") return true;
|
|
31117
|
+
}
|
|
31118
|
+
} catch (e) {}
|
|
31119
|
+
return false;
|
|
31120
|
+
}
|
|
31121
|
+
|
|
31122
|
+
// The way out has to be discoverable, or it may as well not exist. The
|
|
31123
|
+
// editing region's accessible name is the one thing a screen reader always
|
|
31124
|
+
// announces on entry, so the hint goes there rather than into the content.
|
|
31125
|
+
//
|
|
31126
|
+
// But an instruction is not a NAME. When the host page had not labelled the
|
|
31127
|
+
// editing area, appending the hint to an empty label left the region's
|
|
31128
|
+
// accessible name as nothing but "Press Escape to leave the editing area." —
|
|
31129
|
+
// a screen reader then told the user how to LEAVE a field without ever
|
|
31130
|
+
// saying what it was, and two editors on one page were indistinguishable.
|
|
31131
|
+
// So: a host-supplied label still wins as the name; otherwise we supply a
|
|
31132
|
+
// real one and the hint follows it.
|
|
31133
|
+
function announceEscape(ed) {
|
|
31134
|
+
var hint = config.a11yEscapeHint ||
|
|
31135
|
+
"Press Escape to leave the editing area.";
|
|
31136
|
+
var label = ed.getAttribute("aria-label") || "";
|
|
31137
|
+
if (label.indexOf(hint) >= 0) return;
|
|
31138
|
+
var name = label || config.a11yEditorLabel || "Rich text editor";
|
|
31139
|
+
ed.setAttribute("aria-label", name.replace(/\s*$/, "") + " " + hint);
|
|
31140
|
+
}
|
|
31141
|
+
|
|
31142
|
+
function focusAfterEditor() {
|
|
31143
|
+
var root = shell();
|
|
31144
|
+
if (!root) return false;
|
|
31145
|
+
var all = [].slice.call(document.querySelectorAll(
|
|
31146
|
+
'a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
|
|
31147
|
+
)).filter(function (el) {
|
|
31148
|
+
return !root.contains(el) && el.offsetParent !== null;
|
|
31149
|
+
});
|
|
31150
|
+
// The first focusable that follows the editor in document order.
|
|
31151
|
+
for (var i = 0; i < all.length; i++) {
|
|
31152
|
+
if (root.compareDocumentPosition(all[i]) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
31153
|
+
all[i].focus();
|
|
31154
|
+
return true;
|
|
31155
|
+
}
|
|
31156
|
+
}
|
|
31157
|
+
return false;
|
|
31158
|
+
}
|
|
31159
|
+
|
|
31160
|
+
// ------------------------------------------------- 4. roving tabindex
|
|
31161
|
+
function toolbarsIn(root) {
|
|
31162
|
+
return [].slice.call(root.querySelectorAll('[role="toolbar"]'));
|
|
31163
|
+
}
|
|
31164
|
+
// Toolbars nest — an overflow or ribbon toolbar sits inside the main one.
|
|
31165
|
+
// A plain descendant query therefore makes the outer toolbar claim the
|
|
31166
|
+
// inner one's buttons, and the two passes fight: the first run measured two
|
|
31167
|
+
// tab stops in one toolbar and NONE in the other two, which left those
|
|
31168
|
+
// buttons unreachable by keyboard altogether. Each button belongs to its
|
|
31169
|
+
// NEAREST toolbar and to no other.
|
|
31170
|
+
function buttonsIn(bar) {
|
|
31171
|
+
return [].slice.call(bar.querySelectorAll('[role="button"]')).filter(function (b) {
|
|
31172
|
+
if (b.getAttribute("aria-disabled") === "true") return false;
|
|
31173
|
+
if (b.offsetParent === null) return false; // hidden: not reachable anyway
|
|
31174
|
+
return (b.closest && b.closest('[role="toolbar"]')) === bar;
|
|
31175
|
+
});
|
|
31176
|
+
}
|
|
31177
|
+
|
|
31178
|
+
function applyRovingTabindex(root) {
|
|
31179
|
+
if (config.a11yRovingToolbar === false) return;
|
|
31180
|
+
var bars = toolbarsIn(root);
|
|
31181
|
+
for (var i = 0; i < bars.length; i++) {
|
|
31182
|
+
(function (bar) {
|
|
31183
|
+
var btns = buttonsIn(bar);
|
|
31184
|
+
if (!btns.length) return;
|
|
31185
|
+
// Exactly one tab stop per toolbar, which is the ARIA pattern.
|
|
31186
|
+
var current = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
|
|
31187
|
+
for (var j = 0; j < btns.length; j++) btns[j].setAttribute("tabindex", btns[j] === current ? "0" : "-1");
|
|
31188
|
+
|
|
31189
|
+
if (bar.__rteRoving) return;
|
|
31190
|
+
bar.__rteRoving = true;
|
|
31191
|
+
bar.addEventListener("keydown", function (e) {
|
|
31192
|
+
var list = buttonsIn(bar);
|
|
31193
|
+
var at = list.indexOf(document.activeElement);
|
|
31194
|
+
if (at < 0) return;
|
|
31195
|
+
var rtl = (bar.closest && bar.closest('[dir="rtl"]')) ? true : false;
|
|
31196
|
+
var next = null;
|
|
31197
|
+
if (e.key === "ArrowRight") next = list[(at + (rtl ? -1 : 1) + list.length) % list.length];
|
|
31198
|
+
else if (e.key === "ArrowLeft") next = list[(at + (rtl ? 1 : -1) + list.length) % list.length];
|
|
31199
|
+
else if (e.key === "Home") next = list[0];
|
|
31200
|
+
else if (e.key === "End") next = list[list.length - 1];
|
|
31201
|
+
else return;
|
|
31202
|
+
e.preventDefault();
|
|
31203
|
+
for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === next ? "0" : "-1");
|
|
31204
|
+
next.focus();
|
|
31205
|
+
}, false);
|
|
31206
|
+
// Clicking a button makes it the new tab stop, so returning by
|
|
31207
|
+
// Tab lands where the user last was.
|
|
31208
|
+
bar.addEventListener("focusin", function (e) {
|
|
31209
|
+
var list = buttonsIn(bar);
|
|
31210
|
+
if (list.indexOf(e.target) < 0) return;
|
|
31211
|
+
for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === e.target ? "0" : "-1");
|
|
31212
|
+
}, false);
|
|
31213
|
+
})(bars[i]);
|
|
31214
|
+
}
|
|
31215
|
+
}
|
|
31216
|
+
|
|
31217
|
+
function focusFirstToolbarButton() {
|
|
31218
|
+
var root = shell();
|
|
31219
|
+
if (!root) return false;
|
|
31220
|
+
var bars = toolbarsIn(root);
|
|
31221
|
+
for (var i = 0; i < bars.length; i++) {
|
|
31222
|
+
var btns = buttonsIn(bars[i]);
|
|
31223
|
+
if (!btns.length) continue;
|
|
31224
|
+
var target = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
|
|
31225
|
+
target.focus();
|
|
31226
|
+
return true;
|
|
31227
|
+
}
|
|
31228
|
+
return false;
|
|
31229
|
+
}
|
|
31230
|
+
|
|
31231
|
+
// ------------------------------------------------- 2. aria-pressed
|
|
31232
|
+
//
|
|
31233
|
+
// The editor already tracks active state — it just keeps it in a class.
|
|
31234
|
+
// Mirroring rather than recomputing means the announced state can never
|
|
31235
|
+
// disagree with the highlighted button.
|
|
31236
|
+
// Command names here must match the TOOLBAR command exactly — the regex is
|
|
31237
|
+
// anchored, so a near-miss is a silent no-op that looks like coverage.
|
|
31238
|
+
// "blockquote" is one: the command is `insertblockquote`, so the entry
|
|
31239
|
+
// written specifically for it never matched and that button ships with no
|
|
31240
|
+
// aria-pressed. Found by checking every genuine toggle on the full toolbar
|
|
31241
|
+
// rather than the three the published verify page samples.
|
|
31242
|
+
//
|
|
31243
|
+
// This mirror only copies the editor's own active state, so a command may
|
|
31244
|
+
// only be listed here once core actually tracks it. Emitting the attribute
|
|
31245
|
+
// for an untracked command yields a permanent aria-pressed="false" —
|
|
31246
|
+
// confidently announcing "not pressed" while the formatting IS applied,
|
|
31247
|
+
// which makes announced state DISAGREE with the highlight. That is the exact
|
|
31248
|
+
// failure this mirror exists to prevent, so silence beats a wrong answer.
|
|
31249
|
+
// - `toggleborder` was already tracked (core checks the editable's
|
|
31250
|
+
// rte-toggleborder class); it reads inactive simply when borders are off.
|
|
31251
|
+
// - `insertblockquote` was NOT tracked and now is, via a core case added
|
|
31252
|
+
// alongside `indent`, which resolves the same way.
|
|
31253
|
+
// Both verified on the running editor rather than inferred.
|
|
31254
|
+
//
|
|
31255
|
+
// `inlinecode`, `pagination`, `typewriter` and `focusmode` match nothing in
|
|
31256
|
+
// this build, and now the reason is known rather than assumed: pagination.js
|
|
31257
|
+
// and typewriter.js are API-only (togglePageView / toggleTypewriterMode …)
|
|
31258
|
+
// and register no toolbar command, and insertcode.js registers `insertcode`,
|
|
31259
|
+
// which inserts a block rather than toggling the selection. They are kept —
|
|
31260
|
+
// harmless, and correct the moment a toolbar button is added — but they
|
|
31261
|
+
// cover nothing today, so do not read this list as evidence of coverage.
|
|
31262
|
+
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)$/;
|
|
31263
|
+
|
|
31264
|
+
function syncToggleStates(root) {
|
|
31265
|
+
var btns = [].slice.call(root.querySelectorAll('[role="button"][rte-cmd-name]'));
|
|
31266
|
+
for (var i = 0; i < btns.length; i++) {
|
|
31267
|
+
var cmd = (btns[i].getAttribute("rte-cmd-name") || "").toLowerCase();
|
|
31268
|
+
if (!TOGGLE_CMD.test(cmd)) continue;
|
|
31269
|
+
var active = btns[i].classList.contains("rte-command-active");
|
|
31270
|
+
var pressed = active ? "true" : "false";
|
|
31271
|
+
if (btns[i].getAttribute("aria-pressed") !== pressed) btns[i].setAttribute("aria-pressed", pressed);
|
|
31272
|
+
}
|
|
31273
|
+
}
|
|
31274
|
+
|
|
31275
|
+
// ------------------------------------------------- 3. aria-expanded
|
|
31276
|
+
//
|
|
31277
|
+
// Opening a menu inserts an <rte-dropdown-panel> elsewhere in the document
|
|
31278
|
+
// and leaves the button untouched, so the button and its panel have to be
|
|
31279
|
+
// correlated: remember which popup button was activated, then pair it with
|
|
31280
|
+
// the panel that appears.
|
|
31281
|
+
function trackPopups(root) {
|
|
31282
|
+
var pops = [].slice.call(root.querySelectorAll("[aria-haspopup]"));
|
|
31283
|
+
for (var i = 0; i < pops.length; i++) {
|
|
31284
|
+
if (!pops[i].hasAttribute("aria-expanded")) pops[i].setAttribute("aria-expanded", "false");
|
|
31285
|
+
if (pops[i].__rtePopupBound) continue;
|
|
31286
|
+
pops[i].__rtePopupBound = true;
|
|
31287
|
+
(function (el) {
|
|
31288
|
+
function remember() { popupOwner = el; }
|
|
31289
|
+
el.addEventListener("mousedown", remember, true);
|
|
31290
|
+
el.addEventListener("keydown", function (e) {
|
|
31291
|
+
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") remember();
|
|
31292
|
+
}, true);
|
|
31293
|
+
})(pops[i]);
|
|
31294
|
+
}
|
|
31295
|
+
|
|
31296
|
+
if (trackPopups.bound) return;
|
|
31297
|
+
trackPopups.bound = true;
|
|
31298
|
+
var mo = new MutationObserver(function (records) {
|
|
31299
|
+
for (var r = 0; r < records.length; r++) {
|
|
31300
|
+
var rec = records[r];
|
|
31301
|
+
for (var a = 0; a < rec.addedNodes.length; a++) {
|
|
31302
|
+
var n = rec.addedNodes[a];
|
|
31303
|
+
if (n.nodeType !== 1 || !isPanel(n)) continue;
|
|
31304
|
+
if (popupOwner) {
|
|
31305
|
+
popupOwner.setAttribute("aria-expanded", "true");
|
|
31306
|
+
openPanels.push({ panel: n, owner: popupOwner });
|
|
31307
|
+
popupOwner = null;
|
|
31308
|
+
}
|
|
31309
|
+
}
|
|
31310
|
+
for (var d = 0; d < rec.removedNodes.length; d++) {
|
|
31311
|
+
var m = rec.removedNodes[d];
|
|
31312
|
+
if (m.nodeType !== 1 || !isPanel(m)) continue;
|
|
31313
|
+
for (var k = openPanels.length - 1; k >= 0; k--) {
|
|
31314
|
+
if (openPanels[k].panel === m) {
|
|
31315
|
+
openPanels[k].owner.setAttribute("aria-expanded", "false");
|
|
31316
|
+
openPanels.splice(k, 1);
|
|
31317
|
+
}
|
|
31318
|
+
}
|
|
31319
|
+
}
|
|
31320
|
+
}
|
|
31321
|
+
});
|
|
31322
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
31323
|
+
observers.push(mo);
|
|
31324
|
+
}
|
|
31325
|
+
function isPanel(el) {
|
|
31326
|
+
var t = (el.tagName || "").toLowerCase();
|
|
31327
|
+
return t === "rte-dropdown-panel" || t === "rte-floatpanel" || t === "rte-dialog-float";
|
|
31328
|
+
}
|
|
31329
|
+
|
|
31330
|
+
// Toolbar buttons are rebuilt and re-classed as the selection moves, so the
|
|
31331
|
+
// mirrored state has to follow rather than be set once.
|
|
31332
|
+
function watch(root) {
|
|
31333
|
+
if (root.__rteA11yWatch) return;
|
|
31334
|
+
root.__rteA11yWatch = true;
|
|
31335
|
+
var pending = null;
|
|
31336
|
+
var mo = new MutationObserver(function () {
|
|
31337
|
+
if (pending) return;
|
|
31338
|
+
pending = setTimeout(function () {
|
|
31339
|
+
pending = null;
|
|
31340
|
+
syncToggleStates(root);
|
|
31341
|
+
applyRovingTabindex(root);
|
|
31342
|
+
trackPopups(root);
|
|
31343
|
+
}, 60);
|
|
31344
|
+
});
|
|
31345
|
+
mo.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "aria-disabled"] });
|
|
31346
|
+
observers.push(mo);
|
|
31347
|
+
}
|
|
31348
|
+
}
|
|
31349
|
+
|
|
30898
31350
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
30899
31351
|
|
|
30900
|
-
// 2026-08-
|
|
30901
|
-
//
|
|
30902
|
-
// a11yenhance.js names the editing region and dialoga11y.js names the dialogs.
|
|
30903
|
-
// This closes what an audit of the running editor found still open — four
|
|
30904
|
-
// findings, three of them WCAG Level A:
|
|
31352
|
+
// 2026-08-30 Layout tables — say whether a table carries data or only geometry.
|
|
30905
31353
|
//
|
|
30906
|
-
//
|
|
30907
|
-
//
|
|
30908
|
-
//
|
|
30909
|
-
// is the only way out. This is the most serious kind of accessibility
|
|
30910
|
-
// defect: it does not degrade the experience, it ends it.
|
|
31354
|
+
// Found by re-sweeping CKEditor 5's feature pages (they ship `layout-tables`);
|
|
31355
|
+
// TinyMCE has no equivalent. Every other editor treats <table> as one thing.
|
|
31356
|
+
// It is two:
|
|
30911
31357
|
//
|
|
30912
|
-
//
|
|
30913
|
-
//
|
|
30914
|
-
//
|
|
30915
|
-
//
|
|
30916
|
-
//
|
|
31358
|
+
// * a DATA table states relationships — this cell is described by that header —
|
|
31359
|
+
// and a screen reader announces them on every cell;
|
|
31360
|
+
// * a LAYOUT table states nothing. Newsletters, signatures and pre-CSS page
|
|
31361
|
+
// furniture use it purely for geometry, and the correct markup is
|
|
31362
|
+
// role="presentation", which tells assistive technology to read the cells as
|
|
31363
|
+
// plain flow content instead of narrating a fake grid.
|
|
30917
31364
|
//
|
|
30918
|
-
//
|
|
30919
|
-
//
|
|
30920
|
-
//
|
|
31365
|
+
// Using one where the other is meant is a real WCAG 1.3.1 failure, not a style
|
|
31366
|
+
// preference: a layout table with <th>/scope/<caption> makes a screen reader
|
|
31367
|
+
// announce column headers that describe nothing.
|
|
30921
31368
|
//
|
|
30922
|
-
//
|
|
30923
|
-
//
|
|
30924
|
-
//
|
|
30925
|
-
//
|
|
30926
|
-
//
|
|
31369
|
+
// It also fixes a false alarm we were shipping. `accessibilitychecker.js` raised
|
|
31370
|
+
// `table-missing-header` on EVERY table without a <th>, so an email layout — which
|
|
31371
|
+
// must not have one — produced a permanent error the author could never clear. A
|
|
31372
|
+
// checker that cannot be satisfied teaches people to ignore it, so the rule now
|
|
31373
|
+
// skips presentation tables and instead flags the genuine fault: a layout table
|
|
31374
|
+
// that still carries data-table semantics.
|
|
30927
31375
|
//
|
|
30928
|
-
//
|
|
30929
|
-
//
|
|
30930
|
-
//
|
|
30931
|
-
//
|
|
30932
|
-
//
|
|
30933
|
-
//
|
|
30934
|
-
//
|
|
30935
|
-
|
|
30936
|
-
|
|
30937
|
-
|
|
30938
|
-
|
|
31376
|
+
// Design notes:
|
|
31377
|
+
// - role="presentation" is the whole marker. No vendor class is added, so the
|
|
31378
|
+
// exported HTML gains nothing this editor has to own; `role=none` is the ARIA
|
|
31379
|
+
// synonym and is accepted on read, though we always write "presentation"
|
|
31380
|
+
// because Outlook-era mail clients are more likely to have heard of it.
|
|
31381
|
+
// - The dashed outline is injected into the EDITING document only (a <style> in
|
|
31382
|
+
// the iframe head, like formattingmarks.js), never into the content. A layout
|
|
31383
|
+
// table is invisible by design; without an affordance the author cannot see
|
|
31384
|
+
// the thing they are editing.
|
|
31385
|
+
// - Tables nest. Marking the outer table layout must not strip the headers of a
|
|
31386
|
+
// data table sitting inside one of its cells, so every walk is restricted to
|
|
31387
|
+
// cells whose nearest ancestor table is this one.
|
|
31388
|
+
// - Converting <th> to <td> loses information. Which cells were headers is
|
|
31389
|
+
// remembered per table in a WeakMap so an accidental toggle is reversible in
|
|
31390
|
+
// the session, and that memory is deliberately NOT written into the markup —
|
|
31391
|
+
// restoring it is a convenience, not a document property. Reloaded documents
|
|
31392
|
+
// therefore restore nothing; `promoteFirstRow` is offered for that case.
|
|
31393
|
+
RTE_DefaultConfig.plugin_layouttable = RTE_Plugin_LayoutTable;
|
|
31394
|
+
|
|
31395
|
+
// Show a dashed outline around layout tables while editing.
|
|
31396
|
+
if (typeof RTE_DefaultConfig.layoutTableOutline === "undefined") RTE_DefaultConfig.layoutTableOutline = true;
|
|
31397
|
+
|
|
31398
|
+
function RTE_Plugin_LayoutTable() {
|
|
30939
31399
|
var obj = this;
|
|
30940
31400
|
var config, editor;
|
|
30941
|
-
var
|
|
30942
|
-
var popupOwner = null; // last activated [aria-haspopup]
|
|
30943
|
-
var openPanels = []; // [{ panel, owner }]
|
|
31401
|
+
var headerMemory = (typeof WeakMap === "function") ? new WeakMap() : null;
|
|
30944
31402
|
|
|
30945
|
-
obj.PluginName = "
|
|
31403
|
+
obj.PluginName = "LayoutTable";
|
|
30946
31404
|
|
|
30947
31405
|
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
30948
31406
|
|
|
30949
31407
|
obj.InitEditor = function (argeditor) {
|
|
30950
31408
|
editor = argeditor;
|
|
30951
|
-
if (config.keyboardA11y === false) return;
|
|
30952
31409
|
|
|
30953
|
-
editor.
|
|
31410
|
+
editor.attachEvent("exec_command_tablelayoutmode", function (state) {
|
|
31411
|
+
state.returnValue = true;
|
|
31412
|
+
obj.SetMode(tableAtCaret(), "layout");
|
|
31413
|
+
});
|
|
31414
|
+
editor.attachEvent("exec_command_tabledatamode", function (state) {
|
|
31415
|
+
state.returnValue = true;
|
|
31416
|
+
obj.SetMode(tableAtCaret(), "data");
|
|
31417
|
+
});
|
|
31418
|
+
editor.attachEvent("exec_command_toggletablelayout", function (state) {
|
|
31419
|
+
state.returnValue = true;
|
|
31420
|
+
obj.Toggle();
|
|
31421
|
+
});
|
|
31422
|
+
editor.attachEvent("exec_command_insertlayouttable", function (state) {
|
|
31423
|
+
state.returnValue = true;
|
|
31424
|
+
obj.InsertLayoutTable();
|
|
31425
|
+
});
|
|
30954
31426
|
|
|
30955
|
-
|
|
30956
|
-
|
|
30957
|
-
|
|
30958
|
-
|
|
30959
|
-
|
|
30960
|
-
|
|
30961
|
-
|
|
31427
|
+
// Public API.
|
|
31428
|
+
editor.setTableLayoutMode = function (mode, table, options) {
|
|
31429
|
+
return obj.SetMode(table || tableAtCaret(), mode, options);
|
|
31430
|
+
};
|
|
31431
|
+
editor.getTableLayoutMode = function (table) {
|
|
31432
|
+
var t = table || tableAtCaret();
|
|
31433
|
+
return t ? (isLayout(t) ? "layout" : "data") : null;
|
|
31434
|
+
};
|
|
31435
|
+
editor.isLayoutTable = function (table) { return isLayout(table || tableAtCaret()); };
|
|
31436
|
+
editor.toggleTableLayoutMode = function () { return obj.Toggle(); };
|
|
31437
|
+
editor.insertLayoutTable = function (rows, cols, options) {
|
|
31438
|
+
return obj.InsertLayoutTable(rows, cols, options);
|
|
31439
|
+
};
|
|
31440
|
+
editor.listLayoutTables = function () {
|
|
31441
|
+
var ed = getEditable();
|
|
31442
|
+
if (!ed) return [];
|
|
31443
|
+
var out = [], all = ed.getElementsByTagName("table");
|
|
31444
|
+
for (var i = 0; i < all.length; i++) if (isLayout(all[i])) out.push(all[i]);
|
|
31445
|
+
return out;
|
|
31446
|
+
};
|
|
31447
|
+
editor.showLayoutTableOutlines = function (on) {
|
|
31448
|
+
outlineOn = (on !== false);
|
|
31449
|
+
injectStyles(editableDoc());
|
|
31450
|
+
return outlineOn;
|
|
31451
|
+
};
|
|
30962
31452
|
|
|
30963
|
-
function shell() {
|
|
30964
31453
|
try {
|
|
30965
|
-
|
|
30966
|
-
|
|
30967
|
-
|
|
30968
|
-
|
|
30969
|
-
|
|
30970
|
-
|
|
30971
|
-
} catch (e) {
|
|
31454
|
+
if (config && typeof config.layoutTableOutline !== "undefined") outlineOn = !!config.layoutTableOutline;
|
|
31455
|
+
} catch (e) {}
|
|
31456
|
+
injectStyles(editableDoc());
|
|
31457
|
+
try {
|
|
31458
|
+
editor.attachEvent("editor_ready", function () { injectStyles(editableDoc()); });
|
|
31459
|
+
editor.attachEvent("update_design", function () { injectStyles(editableDoc()); });
|
|
31460
|
+
} catch (e) {}
|
|
31461
|
+
};
|
|
31462
|
+
|
|
31463
|
+
var outlineOn = true;
|
|
31464
|
+
|
|
31465
|
+
// ---------------------------------------------------------------- commands
|
|
31466
|
+
|
|
31467
|
+
obj.SetMode = function (table, mode, options) {
|
|
31468
|
+
if (!table) return null;
|
|
31469
|
+
if (String(mode).toLowerCase() === "layout") return toLayout(table);
|
|
31470
|
+
return toData(table, options);
|
|
31471
|
+
};
|
|
31472
|
+
|
|
31473
|
+
obj.Toggle = function () {
|
|
31474
|
+
var t = tableAtCaret();
|
|
31475
|
+
if (!t) return null;
|
|
31476
|
+
return isLayout(t) ? toData(t) : toLayout(t);
|
|
31477
|
+
};
|
|
31478
|
+
|
|
31479
|
+
obj.InsertLayoutTable = function (rows, cols, options) {
|
|
31480
|
+
var doc = editableDoc();
|
|
31481
|
+
if (!doc) return null;
|
|
31482
|
+
rows = Math.max(1, parseInt(rows, 10) || 2);
|
|
31483
|
+
cols = Math.max(1, parseInt(cols, 10) || 2);
|
|
31484
|
+
options = options || {};
|
|
31485
|
+
|
|
31486
|
+
var table = doc.createElement("table");
|
|
31487
|
+
table.setAttribute("role", "presentation");
|
|
31488
|
+
// The three attributes every email layout table needs; without
|
|
31489
|
+
// border-collapse the cells show a 2px gap in Outlook.
|
|
31490
|
+
table.setAttribute("cellpadding", "0");
|
|
31491
|
+
table.setAttribute("cellspacing", "0");
|
|
31492
|
+
table.style.width = options.width || "100%";
|
|
31493
|
+
table.style.borderCollapse = "collapse";
|
|
31494
|
+
|
|
31495
|
+
var tbody = doc.createElement("tbody");
|
|
31496
|
+
for (var r = 0; r < rows; r++) {
|
|
31497
|
+
var tr = doc.createElement("tr");
|
|
31498
|
+
for (var c = 0; c < cols; c++) {
|
|
31499
|
+
var td = doc.createElement("td");
|
|
31500
|
+
td.style.verticalAlign = "top";
|
|
31501
|
+
td.appendChild(doc.createElement("br"));
|
|
31502
|
+
tr.appendChild(td);
|
|
31503
|
+
}
|
|
31504
|
+
tbody.appendChild(tr);
|
|
31505
|
+
}
|
|
31506
|
+
table.appendChild(tbody);
|
|
31507
|
+
|
|
31508
|
+
insertAtCaret(table, doc);
|
|
31509
|
+
injectStyles(doc);
|
|
31510
|
+
fireChange();
|
|
31511
|
+
return table;
|
|
31512
|
+
};
|
|
31513
|
+
|
|
31514
|
+
// ------------------------------------------------------------- conversions
|
|
31515
|
+
|
|
31516
|
+
function toLayout(table) {
|
|
31517
|
+
table.setAttribute("role", "presentation");
|
|
31518
|
+
|
|
31519
|
+
// A caption is announced as the table's name; a layout table has no name
|
|
31520
|
+
// to announce. Keep the text — it is content the author typed — by moving
|
|
31521
|
+
// it out into a paragraph before the table rather than deleting it.
|
|
31522
|
+
var caption = firstOwnChild(table, "caption");
|
|
31523
|
+
if (caption) {
|
|
31524
|
+
var text = String(caption.textContent || "").replace(/^\s+|\s+$/g, "");
|
|
31525
|
+
if (text) {
|
|
31526
|
+
var p = table.ownerDocument.createElement("p");
|
|
31527
|
+
while (caption.firstChild) p.appendChild(caption.firstChild);
|
|
31528
|
+
if (table.parentNode) table.parentNode.insertBefore(p, table);
|
|
31529
|
+
}
|
|
31530
|
+
caption.parentNode.removeChild(caption);
|
|
31531
|
+
}
|
|
31532
|
+
|
|
31533
|
+
table.removeAttribute("summary");
|
|
31534
|
+
|
|
31535
|
+
var cells = ownCells(table), converted = [];
|
|
31536
|
+
for (var i = 0; i < cells.length; i++) {
|
|
31537
|
+
var cell = cells[i];
|
|
31538
|
+
cell.removeAttribute("scope");
|
|
31539
|
+
cell.removeAttribute("headers");
|
|
31540
|
+
cell.removeAttribute("abbr");
|
|
31541
|
+
if (cell.nodeName === "TH") {
|
|
31542
|
+
var td = rename(cell, "td");
|
|
31543
|
+
converted.push(td);
|
|
31544
|
+
}
|
|
31545
|
+
}
|
|
31546
|
+
if (headerMemory && converted.length) headerMemory.set(table, converted);
|
|
31547
|
+
|
|
31548
|
+
// <thead> on a layout table is harmless to AT once role=presentation is
|
|
31549
|
+
// set, but it makes the markup lie to the next human who reads it.
|
|
31550
|
+
var thead = firstOwnChild(table, "thead");
|
|
31551
|
+
if (thead) unwrapSection(table, thead);
|
|
31552
|
+
|
|
31553
|
+
fireChange();
|
|
31554
|
+
return table;
|
|
30972
31555
|
}
|
|
30973
31556
|
|
|
30974
|
-
function
|
|
30975
|
-
|
|
30976
|
-
|
|
30977
|
-
|
|
30978
|
-
|
|
30979
|
-
|
|
30980
|
-
|
|
30981
|
-
|
|
31557
|
+
function toData(table, options) {
|
|
31558
|
+
table.removeAttribute("role");
|
|
31559
|
+
|
|
31560
|
+
var restored = headerMemory ? headerMemory.get(table) : null;
|
|
31561
|
+
var promoted = false;
|
|
31562
|
+
if (restored && restored.length) {
|
|
31563
|
+
for (var i = 0; i < restored.length; i++) {
|
|
31564
|
+
var cell = restored[i];
|
|
31565
|
+
// The cell may have been deleted, or the whole table replaced,
|
|
31566
|
+
// since it was remembered.
|
|
31567
|
+
if (cell && cell.parentNode && contains(table, cell)) {
|
|
31568
|
+
rename(cell, "th");
|
|
31569
|
+
promoted = true;
|
|
31570
|
+
}
|
|
31571
|
+
}
|
|
31572
|
+
headerMemory["delete"](table);
|
|
31573
|
+
}
|
|
31574
|
+
if (!promoted && options && options.promoteFirstRow) promoteFirstRow(table);
|
|
31575
|
+
|
|
31576
|
+
fireChange();
|
|
31577
|
+
return table;
|
|
30982
31578
|
}
|
|
30983
31579
|
|
|
30984
|
-
|
|
30985
|
-
|
|
30986
|
-
|
|
30987
|
-
|
|
30988
|
-
|
|
30989
|
-
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
// throw the user out of the editor.
|
|
30994
|
-
function bindEscapeHatch() {
|
|
30995
|
-
var ed;
|
|
30996
|
-
try { ed = editor.getEditable(); } catch (e) { return; }
|
|
30997
|
-
if (!ed || ed.__rteEscapeHatch) return;
|
|
30998
|
-
ed.__rteEscapeHatch = true;
|
|
31580
|
+
function promoteFirstRow(table) {
|
|
31581
|
+
var rows = ownRows(table);
|
|
31582
|
+
if (!rows.length) return;
|
|
31583
|
+
var cells = rowCells(rows[0]);
|
|
31584
|
+
for (var i = 0; i < cells.length; i++) {
|
|
31585
|
+
var th = rename(cells[i], "th");
|
|
31586
|
+
th.setAttribute("scope", "col");
|
|
31587
|
+
}
|
|
31588
|
+
}
|
|
30999
31589
|
|
|
31000
|
-
|
|
31001
|
-
if (e.key !== "Escape" && e.keyCode !== 27) return;
|
|
31002
|
-
if (anythingOpen()) return; // let the panel close itself
|
|
31003
|
-
e.preventDefault();
|
|
31004
|
-
e.stopPropagation();
|
|
31005
|
-
if (!focusFirstToolbarButton()) focusAfterEditor();
|
|
31006
|
-
}, false);
|
|
31590
|
+
// ------------------------------------------------------------------ helpers
|
|
31007
31591
|
|
|
31008
|
-
|
|
31592
|
+
function isLayout(table) {
|
|
31593
|
+
if (!table || table.nodeName !== "TABLE") return false;
|
|
31594
|
+
var role = String(table.getAttribute("role") || "").toLowerCase();
|
|
31595
|
+
return role === "presentation" || role === "none";
|
|
31009
31596
|
}
|
|
31010
31597
|
|
|
31011
|
-
|
|
31012
|
-
|
|
31013
|
-
|
|
31014
|
-
|
|
31015
|
-
|
|
31016
|
-
|
|
31017
|
-
|
|
31018
|
-
|
|
31019
|
-
return false;
|
|
31598
|
+
// Tables nest: only touch nodes whose nearest ancestor table is this one.
|
|
31599
|
+
function ownsNode(table, node) {
|
|
31600
|
+
var n = node.parentNode;
|
|
31601
|
+
while (n && n !== table) {
|
|
31602
|
+
if (n.nodeName === "TABLE") return false;
|
|
31603
|
+
n = n.parentNode;
|
|
31604
|
+
}
|
|
31605
|
+
return n === table;
|
|
31020
31606
|
}
|
|
31021
31607
|
|
|
31022
|
-
|
|
31023
|
-
|
|
31024
|
-
|
|
31025
|
-
|
|
31026
|
-
|
|
31027
|
-
|
|
31028
|
-
|
|
31029
|
-
|
|
31030
|
-
|
|
31031
|
-
|
|
31032
|
-
|
|
31033
|
-
|
|
31034
|
-
|
|
31035
|
-
|
|
31036
|
-
var
|
|
31037
|
-
|
|
31038
|
-
)).filter(function (el) {
|
|
31039
|
-
return !root.contains(el) && el.offsetParent !== null;
|
|
31040
|
-
});
|
|
31041
|
-
// The first focusable that follows the editor in document order.
|
|
31042
|
-
for (var i = 0; i < all.length; i++) {
|
|
31043
|
-
if (root.compareDocumentPosition(all[i]) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
31044
|
-
all[i].focus();
|
|
31045
|
-
return true;
|
|
31046
|
-
}
|
|
31608
|
+
function ownCells(table) {
|
|
31609
|
+
var out = [], all = table.querySelectorAll ? table.querySelectorAll("th,td") : [];
|
|
31610
|
+
for (var i = 0; i < all.length; i++) if (ownsNode(table, all[i])) out.push(all[i]);
|
|
31611
|
+
return out;
|
|
31612
|
+
}
|
|
31613
|
+
|
|
31614
|
+
function ownRows(table) {
|
|
31615
|
+
var out = [], all = table.querySelectorAll ? table.querySelectorAll("tr") : [];
|
|
31616
|
+
for (var i = 0; i < all.length; i++) if (ownsNode(table, all[i])) out.push(all[i]);
|
|
31617
|
+
return out;
|
|
31618
|
+
}
|
|
31619
|
+
|
|
31620
|
+
function rowCells(row) {
|
|
31621
|
+
var out = [];
|
|
31622
|
+
for (var n = row.firstChild; n; n = n.nextSibling) {
|
|
31623
|
+
if (n.nodeName === "TH" || n.nodeName === "TD") out.push(n);
|
|
31047
31624
|
}
|
|
31625
|
+
return out;
|
|
31626
|
+
}
|
|
31627
|
+
|
|
31628
|
+
function firstOwnChild(table, name) {
|
|
31629
|
+
name = name.toUpperCase();
|
|
31630
|
+
for (var n = table.firstChild; n; n = n.nextSibling) if (n.nodeName === name) return n;
|
|
31631
|
+
return null;
|
|
31632
|
+
}
|
|
31633
|
+
|
|
31634
|
+
function contains(root, node) {
|
|
31635
|
+
var n = node;
|
|
31636
|
+
while (n) { if (n === root) return true; n = n.parentNode; }
|
|
31048
31637
|
return false;
|
|
31049
31638
|
}
|
|
31050
31639
|
|
|
31051
|
-
//
|
|
31052
|
-
function
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
|
|
31058
|
-
|
|
31059
|
-
|
|
31060
|
-
// NEAREST toolbar and to no other.
|
|
31061
|
-
function buttonsIn(bar) {
|
|
31062
|
-
return [].slice.call(bar.querySelectorAll('[role="button"]')).filter(function (b) {
|
|
31063
|
-
if (b.getAttribute("aria-disabled") === "true") return false;
|
|
31064
|
-
if (b.offsetParent === null) return false; // hidden: not reachable anyway
|
|
31065
|
-
return (b.closest && b.closest('[role="toolbar"]')) === bar;
|
|
31066
|
-
});
|
|
31640
|
+
// Move a <thead>'s rows into the table body and drop the section.
|
|
31641
|
+
function unwrapSection(table, section) {
|
|
31642
|
+
var body = firstOwnChild(table, "tbody");
|
|
31643
|
+
if (!body) {
|
|
31644
|
+
body = table.ownerDocument.createElement("tbody");
|
|
31645
|
+
table.insertBefore(body, section.nextSibling);
|
|
31646
|
+
}
|
|
31647
|
+
while (section.firstChild) body.insertBefore(section.firstChild, body.firstChild);
|
|
31648
|
+
section.parentNode.removeChild(section);
|
|
31067
31649
|
}
|
|
31068
31650
|
|
|
31069
|
-
|
|
31070
|
-
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
|
|
31075
|
-
|
|
31076
|
-
|
|
31077
|
-
var current = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
|
|
31078
|
-
for (var j = 0; j < btns.length; j++) btns[j].setAttribute("tabindex", btns[j] === current ? "0" : "-1");
|
|
31079
|
-
|
|
31080
|
-
if (bar.__rteRoving) return;
|
|
31081
|
-
bar.__rteRoving = true;
|
|
31082
|
-
bar.addEventListener("keydown", function (e) {
|
|
31083
|
-
var list = buttonsIn(bar);
|
|
31084
|
-
var at = list.indexOf(document.activeElement);
|
|
31085
|
-
if (at < 0) return;
|
|
31086
|
-
var rtl = (bar.closest && bar.closest('[dir="rtl"]')) ? true : false;
|
|
31087
|
-
var next = null;
|
|
31088
|
-
if (e.key === "ArrowRight") next = list[(at + (rtl ? -1 : 1) + list.length) % list.length];
|
|
31089
|
-
else if (e.key === "ArrowLeft") next = list[(at + (rtl ? 1 : -1) + list.length) % list.length];
|
|
31090
|
-
else if (e.key === "Home") next = list[0];
|
|
31091
|
-
else if (e.key === "End") next = list[list.length - 1];
|
|
31092
|
-
else return;
|
|
31093
|
-
e.preventDefault();
|
|
31094
|
-
for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === next ? "0" : "-1");
|
|
31095
|
-
next.focus();
|
|
31096
|
-
}, false);
|
|
31097
|
-
// Clicking a button makes it the new tab stop, so returning by
|
|
31098
|
-
// Tab lands where the user last was.
|
|
31099
|
-
bar.addEventListener("focusin", function (e) {
|
|
31100
|
-
var list = buttonsIn(bar);
|
|
31101
|
-
if (list.indexOf(e.target) < 0) return;
|
|
31102
|
-
for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === e.target ? "0" : "-1");
|
|
31103
|
-
}, false);
|
|
31104
|
-
})(bars[i]);
|
|
31105
|
-
}
|
|
31106
|
-
}
|
|
31107
|
-
|
|
31108
|
-
function focusFirstToolbarButton() {
|
|
31109
|
-
var root = shell();
|
|
31110
|
-
if (!root) return false;
|
|
31111
|
-
var bars = toolbarsIn(root);
|
|
31112
|
-
for (var i = 0; i < bars.length; i++) {
|
|
31113
|
-
var btns = buttonsIn(bars[i]);
|
|
31114
|
-
if (!btns.length) continue;
|
|
31115
|
-
var target = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
|
|
31116
|
-
target.focus();
|
|
31117
|
-
return true;
|
|
31651
|
+
// Replace an element with the same content and attributes under a new tag.
|
|
31652
|
+
// The caret may be inside it, so the range is restored afterwards.
|
|
31653
|
+
function rename(el, tagName) {
|
|
31654
|
+
var doc = el.ownerDocument;
|
|
31655
|
+
var replacement = doc.createElement(tagName);
|
|
31656
|
+
for (var i = 0; i < el.attributes.length; i++) {
|
|
31657
|
+
var a = el.attributes[i];
|
|
31658
|
+
try { replacement.setAttribute(a.name, a.value); } catch (e) {}
|
|
31118
31659
|
}
|
|
31119
|
-
|
|
31660
|
+
while (el.firstChild) replacement.appendChild(el.firstChild);
|
|
31661
|
+
el.parentNode.replaceChild(replacement, el);
|
|
31662
|
+
return replacement;
|
|
31120
31663
|
}
|
|
31121
31664
|
|
|
31122
|
-
|
|
31123
|
-
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
31127
|
-
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31131
|
-
|
|
31132
|
-
|
|
31133
|
-
|
|
31134
|
-
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
|
|
31140
|
-
|
|
31141
|
-
|
|
31142
|
-
|
|
31143
|
-
|
|
31144
|
-
|
|
31145
|
-
|
|
31146
|
-
|
|
31147
|
-
|
|
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)$/;
|
|
31154
|
-
|
|
31155
|
-
function syncToggleStates(root) {
|
|
31156
|
-
var btns = [].slice.call(root.querySelectorAll('[role="button"][rte-cmd-name]'));
|
|
31157
|
-
for (var i = 0; i < btns.length; i++) {
|
|
31158
|
-
var cmd = (btns[i].getAttribute("rte-cmd-name") || "").toLowerCase();
|
|
31159
|
-
if (!TOGGLE_CMD.test(cmd)) continue;
|
|
31160
|
-
var active = btns[i].classList.contains("rte-command-active");
|
|
31161
|
-
var pressed = active ? "true" : "false";
|
|
31162
|
-
if (btns[i].getAttribute("aria-pressed") !== pressed) btns[i].setAttribute("aria-pressed", pressed);
|
|
31163
|
-
}
|
|
31164
|
-
}
|
|
31165
|
-
|
|
31166
|
-
// ------------------------------------------------- 3. aria-expanded
|
|
31167
|
-
//
|
|
31168
|
-
// Opening a menu inserts an <rte-dropdown-panel> elsewhere in the document
|
|
31169
|
-
// and leaves the button untouched, so the button and its panel have to be
|
|
31170
|
-
// correlated: remember which popup button was activated, then pair it with
|
|
31171
|
-
// the panel that appears.
|
|
31172
|
-
function trackPopups(root) {
|
|
31173
|
-
var pops = [].slice.call(root.querySelectorAll("[aria-haspopup]"));
|
|
31174
|
-
for (var i = 0; i < pops.length; i++) {
|
|
31175
|
-
if (!pops[i].hasAttribute("aria-expanded")) pops[i].setAttribute("aria-expanded", "false");
|
|
31176
|
-
if (pops[i].__rtePopupBound) continue;
|
|
31177
|
-
pops[i].__rtePopupBound = true;
|
|
31178
|
-
(function (el) {
|
|
31179
|
-
function remember() { popupOwner = el; }
|
|
31180
|
-
el.addEventListener("mousedown", remember, true);
|
|
31181
|
-
el.addEventListener("keydown", function (e) {
|
|
31182
|
-
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") remember();
|
|
31183
|
-
}, true);
|
|
31184
|
-
})(pops[i]);
|
|
31185
|
-
}
|
|
31186
|
-
|
|
31187
|
-
if (trackPopups.bound) return;
|
|
31188
|
-
trackPopups.bound = true;
|
|
31189
|
-
var mo = new MutationObserver(function (records) {
|
|
31190
|
-
for (var r = 0; r < records.length; r++) {
|
|
31191
|
-
var rec = records[r];
|
|
31192
|
-
for (var a = 0; a < rec.addedNodes.length; a++) {
|
|
31193
|
-
var n = rec.addedNodes[a];
|
|
31194
|
-
if (n.nodeType !== 1 || !isPanel(n)) continue;
|
|
31195
|
-
if (popupOwner) {
|
|
31196
|
-
popupOwner.setAttribute("aria-expanded", "true");
|
|
31197
|
-
openPanels.push({ panel: n, owner: popupOwner });
|
|
31198
|
-
popupOwner = null;
|
|
31199
|
-
}
|
|
31200
|
-
}
|
|
31201
|
-
for (var d = 0; d < rec.removedNodes.length; d++) {
|
|
31202
|
-
var m = rec.removedNodes[d];
|
|
31203
|
-
if (m.nodeType !== 1 || !isPanel(m)) continue;
|
|
31204
|
-
for (var k = openPanels.length - 1; k >= 0; k--) {
|
|
31205
|
-
if (openPanels[k].panel === m) {
|
|
31206
|
-
openPanels[k].owner.setAttribute("aria-expanded", "false");
|
|
31207
|
-
openPanels.splice(k, 1);
|
|
31208
|
-
}
|
|
31209
|
-
}
|
|
31210
|
-
}
|
|
31665
|
+
function tableAtCaret() {
|
|
31666
|
+
var ed = getEditable();
|
|
31667
|
+
if (!ed) return null;
|
|
31668
|
+
var node = null;
|
|
31669
|
+
try {
|
|
31670
|
+
var sel = editor.getSelection();
|
|
31671
|
+
if (sel && sel.rangeCount) node = sel.getRangeAt(0).startContainer;
|
|
31672
|
+
} catch (e) {}
|
|
31673
|
+
if (!node) return null;
|
|
31674
|
+
if (node.nodeType === 3) node = node.parentNode;
|
|
31675
|
+
while (node && node !== ed) {
|
|
31676
|
+
if (node.nodeName === "TABLE") return node;
|
|
31677
|
+
node = node.parentNode;
|
|
31678
|
+
}
|
|
31679
|
+
return null;
|
|
31680
|
+
}
|
|
31681
|
+
|
|
31682
|
+
function insertAtCaret(node, doc) {
|
|
31683
|
+
var inserted = false;
|
|
31684
|
+
try {
|
|
31685
|
+
var sel = editor.getSelection();
|
|
31686
|
+
if (sel && sel.rangeCount) {
|
|
31687
|
+
var range = sel.getRangeAt(0);
|
|
31688
|
+
range.deleteContents();
|
|
31689
|
+
range.insertNode(node);
|
|
31690
|
+
inserted = true;
|
|
31211
31691
|
}
|
|
31212
|
-
})
|
|
31213
|
-
|
|
31214
|
-
|
|
31692
|
+
} catch (e) {}
|
|
31693
|
+
if (!inserted) {
|
|
31694
|
+
var ed = getEditable();
|
|
31695
|
+
if (ed) { ed.appendChild(node); inserted = true; }
|
|
31696
|
+
}
|
|
31697
|
+
// A table at the very end of the document is a caret trap; gapcursor.js
|
|
31698
|
+
// handles escaping it, so nothing is appended here.
|
|
31699
|
+
return inserted;
|
|
31215
31700
|
}
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
var
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
31701
|
+
|
|
31702
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
31703
|
+
|
|
31704
|
+
function editableDoc() {
|
|
31705
|
+
var ed = getEditable();
|
|
31706
|
+
return ed ? ed.ownerDocument : null;
|
|
31707
|
+
}
|
|
31708
|
+
|
|
31709
|
+
function injectStyles(doc) {
|
|
31710
|
+
if (!doc) return;
|
|
31711
|
+
var text = outlineOn
|
|
31712
|
+
? "table[role=presentation]{outline:1px dashed rgba(120,120,140,.55);outline-offset:1px;}" +
|
|
31713
|
+
"table[role=presentation] td{outline:1px dashed rgba(120,120,140,.28);}"
|
|
31714
|
+
: "";
|
|
31715
|
+
var existing = doc.getElementById("rte-layout-table-styles");
|
|
31716
|
+
if (existing) {
|
|
31717
|
+
if (existing.getAttribute("data-css") === text) return;
|
|
31718
|
+
existing.parentNode && existing.parentNode.removeChild(existing);
|
|
31719
|
+
}
|
|
31720
|
+
if (!text) return;
|
|
31721
|
+
var st = doc.createElement("style");
|
|
31722
|
+
st.id = "rte-layout-table-styles";
|
|
31723
|
+
st.setAttribute("data-css", text);
|
|
31724
|
+
st.appendChild(doc.createTextNode(text));
|
|
31725
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
31726
|
+
}
|
|
31727
|
+
|
|
31728
|
+
function fireChange() {
|
|
31729
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
31730
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
31238
31731
|
}
|
|
31239
31732
|
}
|
|
31240
31733
|
|
|
@@ -37846,6 +38339,259 @@ function RTE_Plugin_Sanitizer() {
|
|
|
37846
38339
|
|
|
37847
38340
|
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
37848
38341
|
|
|
38342
|
+
// 2026-08-30 Shadow DOM support — make the editor usable inside a shadow root.
|
|
38343
|
+
//
|
|
38344
|
+
// TinyMCE documents shadow-DOM support as a supported integration; we had none
|
|
38345
|
+
// under any name. Design systems, micro-frontends and anything built as a custom
|
|
38346
|
+
// element put their markup inside a shadow root, and that is where this editor
|
|
38347
|
+
// was quietly broken.
|
|
38348
|
+
//
|
|
38349
|
+
// Measured before writing a line of it, on a real instance in Chromium:
|
|
38350
|
+
//
|
|
38351
|
+
// | mounted in | toolbar height | font |
|
|
38352
|
+
// |-----------------------|----------------|-------------------|
|
|
38353
|
+
// | the page (light DOM) | 82 px | system-ui |
|
|
38354
|
+
// | a shadow root | 45,468 px | Times New Roman |
|
|
38355
|
+
//
|
|
38356
|
+
// So it is not "mostly fine". The editor CONSTRUCTS, the iframe appears, and
|
|
38357
|
+
// setHTMLCode/getHTMLCode round-trip correctly — which is exactly why this was
|
|
38358
|
+
// never noticed — but a style boundary is a wall: `<link rel=stylesheet>` in the
|
|
38359
|
+
// document head does not cross into a shadow root, so every rule in
|
|
38360
|
+
// rte_theme_default.css misses, and the toolbar renders as a 45,000-pixel column
|
|
38361
|
+
// of unstyled buttons.
|
|
38362
|
+
//
|
|
38363
|
+
// The fix is to put the editor's own stylesheets on the inside of the wall too.
|
|
38364
|
+
//
|
|
38365
|
+
// Design notes:
|
|
38366
|
+
// - <link> clones, not fetch + CSSStyleSheet. Constructing a stylesheet means
|
|
38367
|
+
// fetching the CSS with JS, which needs CORS on a CDN-hosted theme and fails
|
|
38368
|
+
// closed — a cloned <link> hits the HTTP cache, needs no CORS, and works on
|
|
38369
|
+
// every browser that has shadow DOM at all. adoptedStyleSheets is used only
|
|
38370
|
+
// for sheets already adopted by the document, which have no URL to clone.
|
|
38371
|
+
// - Only the EDITOR's stylesheets are copied. Cloning the whole head into a
|
|
38372
|
+
// shadow root would undo the encapsulation the integrator chose it for. A
|
|
38373
|
+
// sheet counts as ours by origin — served from the folder rte.js was loaded
|
|
38374
|
+
// from — plus an `rte-`/`__rte` element id, the same origin-based test that
|
|
38375
|
+
// importstyles.js had to arrive at after prefix matching failed.
|
|
38376
|
+
// - A MutationObserver keeps watching the head. Plugins inject their styles
|
|
38377
|
+
// lazily, when their panel first opens — the accessibility checker and the AI
|
|
38378
|
+
// toolkit both do — so a one-shot copy at init produces a styled toolbar and
|
|
38379
|
+
// an unstyled dialog half an hour later, which is a worse bug to diagnose
|
|
38380
|
+
// than a wholly unstyled editor.
|
|
38381
|
+
// - Nothing is copied when the editor is NOT in a shadow root, and the
|
|
38382
|
+
// observer is not even created. This plugin must cost nothing for the 99%.
|
|
38383
|
+
RTE_DefaultConfig.plugin_shadowdom = RTE_Plugin_ShadowDom;
|
|
38384
|
+
|
|
38385
|
+
// Copy the editor's stylesheets into the shadow root that contains it.
|
|
38386
|
+
if (typeof RTE_DefaultConfig.shadowStyleAdoption === "undefined") RTE_DefaultConfig.shadowStyleAdoption = true;
|
|
38387
|
+
// Extra stylesheet URLs to copy in as well (an integrator's own skin overrides).
|
|
38388
|
+
if (typeof RTE_DefaultConfig.shadowStyleSources === "undefined") RTE_DefaultConfig.shadowStyleSources = null;
|
|
38389
|
+
|
|
38390
|
+
function RTE_Plugin_ShadowDom() {
|
|
38391
|
+
var obj = this;
|
|
38392
|
+
var config, editor;
|
|
38393
|
+
var root = null;
|
|
38394
|
+
var observer = null;
|
|
38395
|
+
var copied = null;
|
|
38396
|
+
|
|
38397
|
+
obj.PluginName = "ShadowDom";
|
|
38398
|
+
|
|
38399
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
38400
|
+
|
|
38401
|
+
obj.InitEditor = function (argeditor) {
|
|
38402
|
+
editor = argeditor;
|
|
38403
|
+
|
|
38404
|
+
// Public API — useful even outside a shadow root, for a host page that
|
|
38405
|
+
// wants to style a second root (a dialog portal, a preview pane).
|
|
38406
|
+
editor.getShadowRoot = function () { return findRoot(); };
|
|
38407
|
+
editor.isInShadowDom = function () { return !!findRoot(); };
|
|
38408
|
+
editor.adoptEditorStyles = function (target) { return adopt(target || findRoot(), true); };
|
|
38409
|
+
editor.getAdoptedStyleCount = function () { return copied ? copied.size : 0; };
|
|
38410
|
+
|
|
38411
|
+
if (config && config.shadowStyleAdoption === false) return;
|
|
38412
|
+
|
|
38413
|
+
// The chrome is built asynchronously; the container may not exist yet.
|
|
38414
|
+
whenReady(function () {
|
|
38415
|
+
root = findRoot();
|
|
38416
|
+
if (!root) return;
|
|
38417
|
+
copied = new (window.Set || Array)();
|
|
38418
|
+
adopt(root, false);
|
|
38419
|
+
watchHead(root);
|
|
38420
|
+
});
|
|
38421
|
+
};
|
|
38422
|
+
|
|
38423
|
+
obj.Destroy = function () {
|
|
38424
|
+
if (observer) { try { observer.disconnect(); } catch (e) {} observer = null; }
|
|
38425
|
+
};
|
|
38426
|
+
|
|
38427
|
+
// ------------------------------------------------------------------ lookup
|
|
38428
|
+
|
|
38429
|
+
// The editor's container is the element the integrator passed in; from the
|
|
38430
|
+
// editing iframe it is the nearest ancestor carrying the editor's own class.
|
|
38431
|
+
function container() {
|
|
38432
|
+
try {
|
|
38433
|
+
var editable = editor.getEditable();
|
|
38434
|
+
var frame = editable && editable.ownerDocument && editable.ownerDocument.defaultView
|
|
38435
|
+
? editable.ownerDocument.defaultView.frameElement : null;
|
|
38436
|
+
if (!frame) return null;
|
|
38437
|
+
var node = frame;
|
|
38438
|
+
while (node) {
|
|
38439
|
+
if (node.classList && node.classList.contains("richtexteditor")) return node;
|
|
38440
|
+
node = node.parentNode || node.host;
|
|
38441
|
+
}
|
|
38442
|
+
return frame;
|
|
38443
|
+
}
|
|
38444
|
+
catch (e) { return null; }
|
|
38445
|
+
}
|
|
38446
|
+
|
|
38447
|
+
function findRoot() {
|
|
38448
|
+
var el = container();
|
|
38449
|
+
if (!el || typeof el.getRootNode !== "function") return null;
|
|
38450
|
+
var r = el.getRootNode();
|
|
38451
|
+
// A DocumentFragment with a `host` is a shadow root; a plain document is not.
|
|
38452
|
+
return (r && r.host && r !== document) ? r : null;
|
|
38453
|
+
}
|
|
38454
|
+
|
|
38455
|
+
function whenReady(callback) {
|
|
38456
|
+
if (container()) { callback(); return; }
|
|
38457
|
+
var tries = 0;
|
|
38458
|
+
var timer = window.setInterval(function () {
|
|
38459
|
+
if (container() || ++tries > 100) { window.clearInterval(timer); callback(); }
|
|
38460
|
+
}, 50);
|
|
38461
|
+
}
|
|
38462
|
+
|
|
38463
|
+
// ------------------------------------------------------------------ adopt
|
|
38464
|
+
|
|
38465
|
+
// Where rte.js itself was served from. Everything under that folder is ours.
|
|
38466
|
+
function assetBase() {
|
|
38467
|
+
try {
|
|
38468
|
+
var scripts = document.getElementsByTagName("script");
|
|
38469
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
38470
|
+
var src = scripts[i].src || "";
|
|
38471
|
+
if (/(^|\/)rte(\.min)?\.js(\?|$)/.test(src)) return src.replace(/[^\/]*$/, "");
|
|
38472
|
+
}
|
|
38473
|
+
}
|
|
38474
|
+
catch (e) {}
|
|
38475
|
+
return null;
|
|
38476
|
+
}
|
|
38477
|
+
|
|
38478
|
+
function isOurs(node) {
|
|
38479
|
+
var id = String(node.id || "");
|
|
38480
|
+
if (/^rte[-_]|^__rte/.test(id)) return true;
|
|
38481
|
+
var href = node.href || "";
|
|
38482
|
+
if (!href) return false;
|
|
38483
|
+
var base = assetBase();
|
|
38484
|
+
if (base && href.indexOf(base) === 0) return true;
|
|
38485
|
+
// Fall back to the file names this product actually ships, so an
|
|
38486
|
+
// integrator who renamed or re-hosted the folder still gets styled
|
|
38487
|
+
// chrome rather than a silent 45,000-pixel toolbar.
|
|
38488
|
+
return /rte_theme|rte_mobile|richtexteditor_content|richtexteditor_preview|aitoolkit/.test(href);
|
|
38489
|
+
}
|
|
38490
|
+
|
|
38491
|
+
function extraSources() {
|
|
38492
|
+
var list = (config && config.shadowStyleSources) || null;
|
|
38493
|
+
if (!list) return [];
|
|
38494
|
+
return typeof list === "string" ? [list] : list;
|
|
38495
|
+
}
|
|
38496
|
+
|
|
38497
|
+
function key(node) {
|
|
38498
|
+
return (node.href || "") + "|" + (node.id || "") + "|" + (node.textContent || "").length;
|
|
38499
|
+
}
|
|
38500
|
+
|
|
38501
|
+
function has(k) {
|
|
38502
|
+
return copied && (copied.has ? copied.has(k) : copied.indexOf(k) >= 0);
|
|
38503
|
+
}
|
|
38504
|
+
|
|
38505
|
+
function remember(k) {
|
|
38506
|
+
if (!copied) return;
|
|
38507
|
+
if (copied.add) copied.add(k); else copied.push(k);
|
|
38508
|
+
}
|
|
38509
|
+
|
|
38510
|
+
function adopt(target, force) {
|
|
38511
|
+
if (!target) return 0;
|
|
38512
|
+
if (!copied) copied = new (window.Set || Array)();
|
|
38513
|
+
var added = 0;
|
|
38514
|
+
var nodes = document.querySelectorAll("link[rel~=stylesheet],style");
|
|
38515
|
+
|
|
38516
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
38517
|
+
var node = nodes[i];
|
|
38518
|
+
if (!isOurs(node)) continue;
|
|
38519
|
+
var k = key(node);
|
|
38520
|
+
if (!force && has(k)) continue;
|
|
38521
|
+
if (has(k)) continue;
|
|
38522
|
+
target.appendChild(cloneSheet(node, target));
|
|
38523
|
+
remember(k);
|
|
38524
|
+
added++;
|
|
38525
|
+
}
|
|
38526
|
+
|
|
38527
|
+
var extras = extraSources();
|
|
38528
|
+
for (var e = 0; e < extras.length; e++) {
|
|
38529
|
+
var ek = "extra|" + extras[e];
|
|
38530
|
+
if (has(ek)) continue;
|
|
38531
|
+
var link = document.createElement("link");
|
|
38532
|
+
link.rel = "stylesheet";
|
|
38533
|
+
link.href = extras[e];
|
|
38534
|
+
link.setAttribute("data-rte-adopted", "1");
|
|
38535
|
+
target.appendChild(link);
|
|
38536
|
+
remember(ek);
|
|
38537
|
+
added++;
|
|
38538
|
+
}
|
|
38539
|
+
|
|
38540
|
+
// Sheets the page adopted programmatically have no URL to clone, so they
|
|
38541
|
+
// are re-adopted by reference where the browser supports it.
|
|
38542
|
+
try {
|
|
38543
|
+
if (target.adoptedStyleSheets && document.adoptedStyleSheets && document.adoptedStyleSheets.length) {
|
|
38544
|
+
var merged = target.adoptedStyleSheets.slice();
|
|
38545
|
+
for (var a = 0; a < document.adoptedStyleSheets.length; a++) {
|
|
38546
|
+
if (merged.indexOf(document.adoptedStyleSheets[a]) < 0) merged.push(document.adoptedStyleSheets[a]);
|
|
38547
|
+
}
|
|
38548
|
+
target.adoptedStyleSheets = merged;
|
|
38549
|
+
}
|
|
38550
|
+
}
|
|
38551
|
+
catch (e2) {}
|
|
38552
|
+
|
|
38553
|
+
return added;
|
|
38554
|
+
}
|
|
38555
|
+
|
|
38556
|
+
function cloneSheet(node, target) {
|
|
38557
|
+
if (node.tagName === "LINK") {
|
|
38558
|
+
var link = document.createElement("link");
|
|
38559
|
+
link.rel = "stylesheet";
|
|
38560
|
+
// Resolved (absolute) href — a relative one would resolve against the
|
|
38561
|
+
// document anyway, but being explicit survives a <base> element.
|
|
38562
|
+
link.href = node.href;
|
|
38563
|
+
if (node.media) link.media = node.media;
|
|
38564
|
+
link.setAttribute("data-rte-adopted", "1");
|
|
38565
|
+
return link;
|
|
38566
|
+
}
|
|
38567
|
+
var style = document.createElement("style");
|
|
38568
|
+
style.textContent = node.textContent;
|
|
38569
|
+
if (node.id) style.setAttribute("data-rte-source-id", node.id);
|
|
38570
|
+
style.setAttribute("data-rte-adopted", "1");
|
|
38571
|
+
return style;
|
|
38572
|
+
}
|
|
38573
|
+
|
|
38574
|
+
// A plugin's <style> arrives when its panel first opens, long after init.
|
|
38575
|
+
function watchHead(target) {
|
|
38576
|
+
if (!window.MutationObserver) return;
|
|
38577
|
+
observer = new MutationObserver(function (records) {
|
|
38578
|
+
var interesting = false;
|
|
38579
|
+
for (var i = 0; i < records.length && !interesting; i++) {
|
|
38580
|
+
var added = records[i].addedNodes || [];
|
|
38581
|
+
for (var j = 0; j < added.length; j++) {
|
|
38582
|
+
var n = added[j];
|
|
38583
|
+
if (n.nodeType !== 1) continue;
|
|
38584
|
+
if ((n.tagName === "STYLE" || n.tagName === "LINK") && isOurs(n)) { interesting = true; break; }
|
|
38585
|
+
}
|
|
38586
|
+
}
|
|
38587
|
+
if (interesting) adopt(target, false);
|
|
38588
|
+
});
|
|
38589
|
+
observer.observe(document.head || document.documentElement, { childList: true, subtree: true });
|
|
38590
|
+
}
|
|
38591
|
+
}
|
|
38592
|
+
|
|
38593
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
38594
|
+
|
|
37849
38595
|
RTE_DefaultConfig.plugin_slashcommand = RTE_Plugin_SlashCommand;
|
|
37850
38596
|
|
|
37851
38597
|
function RTE_Plugin_SlashCommand() {
|
|
@@ -38091,6 +38837,14 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38091
38837
|
push("Tools", "tablerownumbers", "Number table rows", "Add or remove an automatic row-numbering column", ["number", "rows", "table", "count", "index"], iconSort(),
|
|
38092
38838
|
function () { editor.execCommand("tablerownumbers"); });
|
|
38093
38839
|
}
|
|
38840
|
+
if (typeof editor.setTableLayoutMode === "function") {
|
|
38841
|
+
push("Insert", "layouttable", "Layout table", "Insert a borderless table for positioning content (role=presentation)", ["layout", "table", "grid", "email", "columns", "presentation"], iconTable(),
|
|
38842
|
+
function () { editor.insertLayoutTable(2, 2); });
|
|
38843
|
+
push("Tools", "tablelayoutmode", "Mark table as layout", "This table only positions content — stop screen readers announcing it as data", ["layout", "table", "presentation", "accessibility", "screen reader"], iconTable(),
|
|
38844
|
+
function () { editor.setTableLayoutMode("layout"); });
|
|
38845
|
+
push("Tools", "tabledatamode", "Mark table as data", "This table states relationships — restore its header cells", ["data", "table", "header", "accessibility"], iconTable(),
|
|
38846
|
+
function () { editor.setTableLayoutMode("data", null, { promoteFirstRow: true }); });
|
|
38847
|
+
}
|
|
38094
38848
|
if (typeof editor.moveBlockUp === "function") {
|
|
38095
38849
|
push("Tools", "moveblockup", "Move block up", "Move this paragraph or block above the previous one (Alt+Shift+Up)", ["move", "block", "up", "reorder", "drag"], iconMoveBlock(),
|
|
38096
38850
|
function () { editor.moveBlockUp(); });
|