@richscripts/richtexteditor 2.6.4 → 2.7.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/LICENSE.md +66 -0
- package/README.md +22 -2
- package/package.json +146 -142
- package/richtexteditor/all_plugins.js +866 -17
- package/richtexteditor/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/accessibilitychecker.js +283 -0
- package/richtexteditor/plugins/all_plugins.js +866 -17
- package/richtexteditor/plugins/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/dialoga11y.js +50 -6
- package/richtexteditor/plugins/documentimport.js +59 -2
- package/richtexteditor/plugins/docxexport.js +57 -3
- package/richtexteditor/plugins/inlinecode.js +270 -0
- package/richtexteditor/plugins/keyboarda11y.js +108 -0
- package/richtexteditor/plugins/pdfexport.js +35 -5
- package/richtexteditor/plugins/wordexport.js +3 -1
- package/richtexteditor/rte.js +54 -4
|
@@ -519,6 +519,29 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
// 2026-09-04 A link with no accessible name is announced as just
|
|
523
|
+
// "link", giving a keyboard or screen-reader user nothing to decide
|
|
524
|
+
// on (WCAG 2.4.4 / 4.1.2). Reached the editor easily: an image-only
|
|
525
|
+
// link whose <img> has empty alt, or a link left empty after its
|
|
526
|
+
// text was deleted.
|
|
527
|
+
//
|
|
528
|
+
// Only elements with href are links. A bare <a name="x"> or an <a>
|
|
529
|
+
// with no href is an anchor TARGET and has no name to announce, so
|
|
530
|
+
// flagging it would be an error with no valid fix.
|
|
531
|
+
var links = tag === "a" ? [node] : (node.querySelectorAll ? node.querySelectorAll("a") : []);
|
|
532
|
+
for (var linkIndex = 0; linkIndex < links.length; linkIndex++) {
|
|
533
|
+
var link = links[linkIndex];
|
|
534
|
+
if (!link.getAttribute || link.getAttribute("href") === null) continue;
|
|
535
|
+
if (accessibleNameOf(link)) continue;
|
|
536
|
+
issues.push({
|
|
537
|
+
code: "link-empty",
|
|
538
|
+
severity: "error",
|
|
539
|
+
message: "Link has no text, so it is announced only as \"link\". Add link text, an aria-label, or alt text on the image inside it.",
|
|
540
|
+
path: path + ".link[" + linkIndex + "]",
|
|
541
|
+
_target: link
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
522
545
|
var tables = tag === "table" ? [node] : (node.querySelectorAll ? node.querySelectorAll("table") : []);
|
|
523
546
|
for (var tableIndex = 0; tableIndex < tables.length; tableIndex++) {
|
|
524
547
|
var table = tables[tableIndex];
|
|
@@ -553,11 +576,168 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
553
576
|
_target: table
|
|
554
577
|
});
|
|
555
578
|
}
|
|
579
|
+
else {
|
|
580
|
+
// 2026-09-04 A table can HAVE headers and still be unusable if
|
|
581
|
+
// one is blank: the screen reader announces the column's name as
|
|
582
|
+
// an empty string, which is worse than no header at all because
|
|
583
|
+
// the relationship exists and says nothing (WCAG 1.3.1).
|
|
584
|
+
// `table-missing-header` above only fires when there is not a
|
|
585
|
+
// single <th>, so this case reported valid. Found by testing
|
|
586
|
+
// CKEditor issue #19204 against this checker.
|
|
587
|
+
//
|
|
588
|
+
// THE CORNER CELL IS EXEMPT. In a cross-tab - column headers
|
|
589
|
+
// across the top, row headers down the side - the top-left cell
|
|
590
|
+
// is conventionally and correctly empty; it names neither axis.
|
|
591
|
+
// Flagging it would be an error the author can only clear by
|
|
592
|
+
// inventing a word, which is how a checker teaches people to
|
|
593
|
+
// ignore it. Same reasoning that exempts layout tables above.
|
|
594
|
+
var headerCells = table.querySelectorAll("th");
|
|
595
|
+
var hasRowHeaders = false;
|
|
596
|
+
for (var probeIndex = 0; probeIndex < headerCells.length; probeIndex++) {
|
|
597
|
+
var probeRow = headerCells[probeIndex].parentNode;
|
|
598
|
+
if (probeRow && probeRow.rowIndex > 0) { hasRowHeaders = true; break; }
|
|
599
|
+
}
|
|
600
|
+
for (var headerIndex = 0; headerIndex < headerCells.length; headerIndex++) {
|
|
601
|
+
var headerCell = headerCells[headerIndex];
|
|
602
|
+
if (accessibleNameOf(headerCell)) continue;
|
|
603
|
+
var ownerRow = headerCell.parentNode;
|
|
604
|
+
if (hasRowHeaders && ownerRow && ownerRow.rowIndex === 0 && headerCell.cellIndex === 0) continue;
|
|
605
|
+
issues.push({
|
|
606
|
+
code: "table-header-empty",
|
|
607
|
+
severity: "error",
|
|
608
|
+
message: "Header cell is empty, so the column or row it labels is announced with no name. Give it text, or make it a normal cell if it labels nothing.",
|
|
609
|
+
path: path + ".table[" + tableIndex + "].th[" + headerIndex + "]",
|
|
610
|
+
_target: headerCell
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
// 2026-09-07 COLOUR CONTRAST (WCAG 1.4.3). The most-cited criterion in
|
|
618
|
+
// any accessibility audit and the checker had no rule for it -- and
|
|
619
|
+
// our own comparison page once claimed one, which had to be retracted.
|
|
620
|
+
//
|
|
621
|
+
// Only INLINE colour is judged. Contrast against a stylesheet the
|
|
622
|
+
// editor cannot see would be a guess, and a checker that guesses
|
|
623
|
+
// teaches people to ignore it. Where no background is declared on the
|
|
624
|
+
// element or an ancestor, white is assumed and stated in the message,
|
|
625
|
+
// because that is the editing surface's own ground.
|
|
626
|
+
var coloured = node.querySelectorAll ? node.querySelectorAll("[style*='color']") : [];
|
|
627
|
+
var colouredList = (node.getAttribute && /(^|;)\s*color\s*:/i.test(node.getAttribute("style") || ""))
|
|
628
|
+
? [node].concat(Array.prototype.slice.call(coloured))
|
|
629
|
+
: Array.prototype.slice.call(coloured);
|
|
630
|
+
for (var cIndex = 0; cIndex < colouredList.length; cIndex++) {
|
|
631
|
+
var cEl = colouredList[cIndex];
|
|
632
|
+
var fg = parseCssColour(cEl.style && cEl.style.color);
|
|
633
|
+
if (!fg) continue;
|
|
634
|
+
var bg = nearestBackground(cEl);
|
|
635
|
+
var ratio = contrastRatio(fg, bg.rgb);
|
|
636
|
+
// 4.5:1 is the AA threshold for body text; 3:1 for large text
|
|
637
|
+
// (>=18pt, or >=14pt bold). Using the body number on a heading
|
|
638
|
+
// would report a failure that is not one.
|
|
639
|
+
var large = isLargeText(cEl);
|
|
640
|
+
var need = large ? 3 : 4.5;
|
|
641
|
+
// Epsilon absorbs float noise ONLY. 0.05 was 1% of the threshold and
|
|
642
|
+
// swallowed real near-misses: #777777 on white is 4.48:1, a genuine
|
|
643
|
+
// failure, and went unreported. It must also not fire on a colour that
|
|
644
|
+
// passes at exactly 4.5, so the comparison errs toward silence.
|
|
645
|
+
if (ratio < need - 0.001) {
|
|
646
|
+
issues.push({
|
|
647
|
+
code: "contrast-insufficient",
|
|
648
|
+
severity: "error",
|
|
649
|
+
message: "Text contrast is " + ratio.toFixed(2) + ":1 against " +
|
|
650
|
+
(bg.assumed ? "an assumed white background" : "its background") +
|
|
651
|
+
", below the " + need + ":1 needed for " +
|
|
652
|
+
(large ? "large text" : "body text") + " (WCAG 1.4.3).",
|
|
653
|
+
path: path + ".contrast[" + cIndex + "]",
|
|
654
|
+
_target: cEl
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// 2026-09-07 BLOCKQUOTE USED AS INDENTATION (WCAG 1.3.1). A quotation
|
|
660
|
+
// element used for visual offset is announced as a quotation by screen
|
|
661
|
+
// readers and exported to Word as the Quote style.
|
|
662
|
+
//
|
|
663
|
+
// This is OUR OWN historical output: until 2026-09-04 the indent button
|
|
664
|
+
// fell through to the browser's execCommand, which wraps the block in
|
|
665
|
+
// <blockquote style="margin:0 0 0 40px;border:none;padding:0px">. Every
|
|
666
|
+
// document indented in an older build carries these, so the rule
|
|
667
|
+
// matters most for content we produced ourselves.
|
|
668
|
+
//
|
|
669
|
+
// Recognise only that SHAPE -- an offset with the quote decoration
|
|
670
|
+
// explicitly switched off. A blockquote that keeps its border is a real
|
|
671
|
+
// quotation and must not be flagged.
|
|
672
|
+
var quotes = tag === "blockquote" ? [node] : (node.querySelectorAll ? node.querySelectorAll("blockquote") : []);
|
|
673
|
+
for (var qIndex = 0; qIndex < quotes.length; qIndex++) {
|
|
674
|
+
var q = quotes[qIndex];
|
|
675
|
+
var qs = q.style;
|
|
676
|
+
if (!qs) continue;
|
|
677
|
+
var hasOffset = /^\s*[\d.]+\s*[a-z%]+\s*$/i.test(qs.marginLeft || "") && parseFloat(qs.marginLeft) > 0;
|
|
678
|
+
var borderOff = /^(none|0|0px)$/i.test(String(qs.borderStyle || qs.border || "").trim());
|
|
679
|
+
if (hasOffset && borderOff) {
|
|
680
|
+
issues.push({
|
|
681
|
+
code: "blockquote-as-indent",
|
|
682
|
+
severity: "warning",
|
|
683
|
+
message: "This looks like indentation, not a quotation: a <blockquote> with a left margin and no quote styling. Screen readers announce it as a quotation. Use the indent button instead.",
|
|
684
|
+
path: path + ".blockquote[" + qIndex + "]",
|
|
685
|
+
_target: q
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// 2026-09-07 AMBIGUOUS LINK TEXT (WCAG 2.4.4). "Click here" read out of
|
|
691
|
+
// context -- which is how a screen-reader user listing links hears it --
|
|
692
|
+
// conveys nothing about the destination.
|
|
693
|
+
var ambiguous = /^(click here|here|read more|more|link|this|this link|learn more|details|go)$/i;
|
|
694
|
+
var namedLinks = tag === "a" ? [node] : (node.querySelectorAll ? node.querySelectorAll("a") : []);
|
|
695
|
+
for (var aIndex = 0; aIndex < namedLinks.length; aIndex++) {
|
|
696
|
+
var aEl = namedLinks[aIndex];
|
|
697
|
+
if (!aEl.getAttribute || aEl.getAttribute("href") === null) continue;
|
|
698
|
+
var aName = accessibleNameOf(aEl);
|
|
699
|
+
if (aName && ambiguous.test(aName.replace(/[\s.,!:;]+$/g, "").replace(/^\s+/, ""))) {
|
|
700
|
+
issues.push({
|
|
701
|
+
code: "link-ambiguous-text",
|
|
702
|
+
severity: "warning",
|
|
703
|
+
message: "Link text \"" + aName + "\" does not say where it goes. Out of context a screen reader announces only this text.",
|
|
704
|
+
path: path + ".link[" + aIndex + "]",
|
|
705
|
+
_target: aEl
|
|
706
|
+
});
|
|
707
|
+
}
|
|
556
708
|
}
|
|
557
709
|
|
|
558
710
|
collectUnmarkedLanguageRuns(node, editable, path, issues);
|
|
559
711
|
}
|
|
560
712
|
|
|
713
|
+
// 2026-09-07 DUPLICATE id (WCAG 4.1.1). Two elements sharing an id break
|
|
714
|
+
// every aria-labelledby / aria-describedby / label that points at it --
|
|
715
|
+
// the reference silently resolves to the first one. Reached easily by
|
|
716
|
+
// copying and pasting a block that carries an id.
|
|
717
|
+
//
|
|
718
|
+
// Runs ONCE over the whole editable, deliberately NOT inside the per-node
|
|
719
|
+
// walk: querySelectorAll returns DESCENDANTS only, so a walk visiting each
|
|
720
|
+
// sibling separately sees one id at a time and can never detect a pair.
|
|
721
|
+
// The same trap already cost this file its top-level image check.
|
|
722
|
+
if (editable && editable.querySelectorAll) {
|
|
723
|
+
var allWithIds = editable.querySelectorAll("[id]");
|
|
724
|
+
var idSeen = {};
|
|
725
|
+
for (var dupIndex = 0; dupIndex < allWithIds.length; dupIndex++) {
|
|
726
|
+
var dupId = allWithIds[dupIndex].getAttribute("id");
|
|
727
|
+
if (!dupId) continue;
|
|
728
|
+
if (idSeen[dupId]) {
|
|
729
|
+
issues.push({
|
|
730
|
+
code: "duplicate-id",
|
|
731
|
+
severity: "error",
|
|
732
|
+
message: "id \"" + dupId + "\" is used more than once. Any aria-labelledby, aria-describedby or label pointing at it resolves to only the first element.",
|
|
733
|
+
path: "document.id[" + dupIndex + "]",
|
|
734
|
+
_target: allWithIds[dupIndex]
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
idSeen[dupId] = true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
561
741
|
return { document: null, issues: issues, valid: !issues.length, source: "dom" };
|
|
562
742
|
}
|
|
563
743
|
|
|
@@ -587,6 +767,29 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
587
767
|
else if (issue.code === "layout-table-semantics") {
|
|
588
768
|
markTableAsLayout(target);
|
|
589
769
|
}
|
|
770
|
+
else if (issue.code === "table-header-empty") {
|
|
771
|
+
// Two valid outcomes and only the author knows which: the cell labels
|
|
772
|
+
// something (give it text) or it labels nothing (it is not a header).
|
|
773
|
+
var headerText = String((options && options.headerText) || "").replace(/^\s+|\s+$/g, "");
|
|
774
|
+
if (headerText) target.textContent = headerText;
|
|
775
|
+
else if (options && options.demoteToCell) {
|
|
776
|
+
var cell = target.ownerDocument.createElement("td");
|
|
777
|
+
while (target.firstChild) cell.appendChild(target.firstChild);
|
|
778
|
+
copyAttributes(target, cell);
|
|
779
|
+
cell.removeAttribute("scope");
|
|
780
|
+
target.parentNode.replaceChild(cell, target);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
else if (issue.code === "link-empty") {
|
|
784
|
+
// Prefer real text: an aria-label is invisible, so a sighted editor
|
|
785
|
+
// cannot see what the link says and it drifts out of date.
|
|
786
|
+
var linkText = String((options && options.linkText) || "").replace(/^\s+|\s+$/g, "");
|
|
787
|
+
if (linkText) target.textContent = linkText;
|
|
788
|
+
else {
|
|
789
|
+
var linkLabel = String((options && options.linkLabel) || "").replace(/^\s+|\s+$/g, "");
|
|
790
|
+
if (linkLabel) target.setAttribute("aria-label", linkLabel);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
590
793
|
else if (issue.code === "language-of-parts") {
|
|
591
794
|
markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
|
|
592
795
|
}
|
|
@@ -673,6 +876,86 @@ function RTE_Plugin_AccessibilityChecker() {
|
|
|
673
876
|
// Delegates to layouttable.js when it is loaded so there is ONE definition of
|
|
674
877
|
// what "layout table" means; the fallback keeps the repair working in builds
|
|
675
878
|
// where that plugin was left out of the bundle.
|
|
879
|
+
// The accessible name of an element, as a screen reader would compute it:
|
|
880
|
+
// aria-label wins, then visible text, then title, then the alt text of an
|
|
881
|
+
// image standing in for the text. Empty string means "announced as nothing".
|
|
882
|
+
// Deliberately NOT aria-labelledby - resolving it needs the whole document
|
|
883
|
+
// and a wrong answer here would produce an error the author cannot clear.
|
|
884
|
+
|
|
885
|
+
// --- contrast helpers (WCAG 1.4.3) ---------------------------------------
|
|
886
|
+
function parseCssColour(v) {
|
|
887
|
+
if (!v) return null;
|
|
888
|
+
var str = String(v).trim();
|
|
889
|
+
var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(str);
|
|
890
|
+
if (m) {
|
|
891
|
+
var hex = m[1];
|
|
892
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
893
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
894
|
+
}
|
|
895
|
+
var rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(str);
|
|
896
|
+
if (rgb) return [Math.round(+rgb[1]), Math.round(+rgb[2]), Math.round(+rgb[3])];
|
|
897
|
+
return null; // named colours are not resolved: a guess is worse than silence
|
|
898
|
+
}
|
|
899
|
+
// Nearest ancestor that DECLARES a background. Reports whether it had to
|
|
900
|
+
// fall back, so the message can say "assumed white" rather than asserting a
|
|
901
|
+
// background the document never set.
|
|
902
|
+
function nearestBackground(el) {
|
|
903
|
+
var n = el;
|
|
904
|
+
while (n && n.style) {
|
|
905
|
+
var c = parseCssColour(n.style.backgroundColor);
|
|
906
|
+
if (c) return { rgb: c, assumed: false };
|
|
907
|
+
n = n.parentElement;
|
|
908
|
+
}
|
|
909
|
+
return { rgb: [255, 255, 255], assumed: true };
|
|
910
|
+
}
|
|
911
|
+
function relativeLuminance(rgb) {
|
|
912
|
+
var a = [rgb[0], rgb[1], rgb[2]].map(function (v) {
|
|
913
|
+
v = v / 255;
|
|
914
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
915
|
+
});
|
|
916
|
+
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
|
917
|
+
}
|
|
918
|
+
function contrastRatio(fg, bg) {
|
|
919
|
+
var l1 = relativeLuminance(fg), l2 = relativeLuminance(bg);
|
|
920
|
+
var hi = Math.max(l1, l2), lo = Math.min(l1, l2);
|
|
921
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
922
|
+
}
|
|
923
|
+
// WCAG "large text": >=18pt, or >=14pt bold. A heading judged against the
|
|
924
|
+
// body threshold reports a failure that is not one.
|
|
925
|
+
function isLargeText(el) {
|
|
926
|
+
var st = el.style || {};
|
|
927
|
+
var size = String(st.fontSize || "");
|
|
928
|
+
var pt = null;
|
|
929
|
+
var m = /^([\d.]+)(px|pt)$/i.exec(size.trim());
|
|
930
|
+
if (m) pt = m[2].toLowerCase() === "pt" ? parseFloat(m[1]) : parseFloat(m[1]) * 0.75;
|
|
931
|
+
if (pt === null) {
|
|
932
|
+
var tagName = (el.tagName || "").toLowerCase();
|
|
933
|
+
if (/^h[1-3]$/.test(tagName)) return true;
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
var bold = /bold|^[6-9]00$/.test(String(st.fontWeight || ""));
|
|
937
|
+
return pt >= 18 || (pt >= 14 && bold);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function accessibleNameOf(element) {
|
|
941
|
+
if (!element) return "";
|
|
942
|
+
var trim = function (v) { return String(v || "").replace(/^\s+|\s+$/g, ""); };
|
|
943
|
+
var label = trim(element.getAttribute && element.getAttribute("aria-label"));
|
|
944
|
+
if (label) return label;
|
|
945
|
+
var text = trim(element.textContent);
|
|
946
|
+
if (text) return text;
|
|
947
|
+
var title = trim(element.getAttribute && element.getAttribute("title"));
|
|
948
|
+
if (title) return title;
|
|
949
|
+
if (element.querySelectorAll) {
|
|
950
|
+
var imgs = element.querySelectorAll("img");
|
|
951
|
+
for (var i = 0; i < imgs.length; i++) {
|
|
952
|
+
var alt = trim(imgs[i].getAttribute("alt"));
|
|
953
|
+
if (alt) return alt;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return "";
|
|
957
|
+
}
|
|
958
|
+
|
|
676
959
|
function markTableAsLayout(table) {
|
|
677
960
|
if (!table) return;
|
|
678
961
|
if (editor && typeof editor.setTableLayoutMode === "function") {
|
|
@@ -23020,9 +23303,24 @@ function RTE_Plugin_DialogA11y() {
|
|
|
23020
23303
|
}
|
|
23021
23304
|
}
|
|
23022
23305
|
|
|
23306
|
+
// 2026-09-05 Widened. The original selector only matched
|
|
23307
|
+
// <rte-dropdown-panel>/.rte-panel-general, but Find&Replace, Insert Link and
|
|
23308
|
+
// their siblings render as <rte-dialog-float class="rte-panel-find"> wrapping
|
|
23309
|
+
// an <rte-dialog-inner role="dialog">. Those carry EXACTLY the markup this
|
|
23310
|
+
// plugin consumes - an <rte-dialog-header> and <rte-dialog-line-X> field
|
|
23311
|
+
// wrappers - and were simply never visited, so the plugin reported nothing
|
|
23312
|
+
// and covered nothing. Measured before the change: calling
|
|
23313
|
+
// editor.applyDialogAccessibility() with the find dialog open labelled 0 of
|
|
23314
|
+
// its 4 inputs, leaving "Find" and "Replace" announced as bare "edit"
|
|
23315
|
+
// (WCAG 1.3.1 / 3.3.2 / 4.1.2) while the two checkboxes were fine because
|
|
23316
|
+
// the core happens to wrap those in <label>.
|
|
23317
|
+
//
|
|
23318
|
+
// Widening is safe because enhancePanel() still requires an
|
|
23319
|
+
// <rte-dialog-header>, which is what keeps colour/font dropdowns out.
|
|
23023
23320
|
function isPanel(el) {
|
|
23024
23321
|
return el && el.nodeType === 1 &&
|
|
23025
|
-
(el.tagName === "RTE-DROPDOWN-PANEL" ||
|
|
23322
|
+
(el.tagName === "RTE-DROPDOWN-PANEL" || el.tagName === "RTE-DIALOG-FLOAT" ||
|
|
23323
|
+
/rte-panel-general/.test(el.className || ""));
|
|
23026
23324
|
}
|
|
23027
23325
|
|
|
23028
23326
|
function schedule(panel) {
|
|
@@ -23042,7 +23340,7 @@ function RTE_Plugin_DialogA11y() {
|
|
|
23042
23340
|
|
|
23043
23341
|
function enhanceAll(host) {
|
|
23044
23342
|
if (!host || !host.querySelectorAll) return;
|
|
23045
|
-
var ps = host.querySelectorAll("rte-dropdown-panel,[class*='rte-panel-general']");
|
|
23343
|
+
var ps = host.querySelectorAll("rte-dropdown-panel,rte-dialog-float,[class*='rte-panel-general']");
|
|
23046
23344
|
for (var i = 0; i < ps.length; i++) enhancePanel(ps[i]);
|
|
23047
23345
|
}
|
|
23048
23346
|
|
|
@@ -23054,12 +23352,22 @@ function RTE_Plugin_DialogA11y() {
|
|
|
23054
23352
|
var name = (headerEl.textContent || "").trim()
|
|
23055
23353
|
.replace(/\s*\((?:Ctrl|Cmd|Alt|Shift|⌘|⇧|⌥)[^)]*\)\s*$/i, "").trim();
|
|
23056
23354
|
|
|
23355
|
+
// Name the element that IS the dialog. The float-style panels already
|
|
23356
|
+
// carry role="dialog" on an inner <rte-dialog-inner>; adding a second
|
|
23357
|
+
// role="dialog" to the wrapper would announce two nested dialogs, so
|
|
23358
|
+
// when an inner one exists it is the target and the wrapper is left
|
|
23359
|
+
// alone. When there is none (the original dropdown-panel shape) this
|
|
23360
|
+
// resolves to the panel itself and behaves exactly as before.
|
|
23361
|
+
var dialogEl = panel.getAttribute("role") === "dialog"
|
|
23362
|
+
? panel
|
|
23363
|
+
: (panel.querySelector('[role="dialog"]') || panel);
|
|
23364
|
+
|
|
23057
23365
|
// A titled, form-bearing popup is a dialog, not a menu — override the
|
|
23058
23366
|
// core's default role="menu" (a menu must not contain text fields).
|
|
23059
|
-
var role =
|
|
23060
|
-
if (!role || role === "menu")
|
|
23061
|
-
if (name && !
|
|
23062
|
-
|
|
23367
|
+
var role = dialogEl.getAttribute("role");
|
|
23368
|
+
if (!role || role === "menu") dialogEl.setAttribute("role", "dialog");
|
|
23369
|
+
if (name && !dialogEl.getAttribute("aria-label") && !dialogEl.getAttribute("aria-labelledby")) {
|
|
23370
|
+
dialogEl.setAttribute("aria-label", name);
|
|
23063
23371
|
}
|
|
23064
23372
|
|
|
23065
23373
|
var fields = panel.querySelectorAll("input,select,textarea");
|
|
@@ -23074,6 +23382,25 @@ function RTE_Plugin_DialogA11y() {
|
|
|
23074
23382
|
}
|
|
23075
23383
|
|
|
23076
23384
|
function fieldLabel(f, dialogName) {
|
|
23385
|
+
// 0) THE VISIBLE LABEL WINS. 2026-09-05: deriving the name from the
|
|
23386
|
+
// <rte-dialog-line-X> suffix alone produced "Keyword" for a field whose
|
|
23387
|
+
// visible label reads "Find". That fails WCAG 2.5.3 Label in Name, and
|
|
23388
|
+
// it breaks speech input concretely: a user says "Find" and nothing
|
|
23389
|
+
// matches, because the accessible name does not contain the words they
|
|
23390
|
+
// can see. The markup already carries the right text in a sibling
|
|
23391
|
+
// <rte-dialog-input-label>, so prefer it and keep the suffix as the
|
|
23392
|
+
// fallback for fields that have no visible label.
|
|
23393
|
+
var lineEl = f.parentElement;
|
|
23394
|
+
for (var d = 0; d < 8 && lineEl; d++) {
|
|
23395
|
+
if (/^RTE-DIALOG-LINE-/.test(lineEl.tagName || "")) break;
|
|
23396
|
+
lineEl = lineEl.parentElement;
|
|
23397
|
+
}
|
|
23398
|
+
if (lineEl && lineEl.querySelector) {
|
|
23399
|
+
var visEl = lineEl.querySelector("rte-dialog-input-label");
|
|
23400
|
+
var vis = visEl ? (visEl.textContent || "").replace(/^\s+|\s+$/g, "").replace(/[::]\s*$/, "") : "";
|
|
23401
|
+
if (vis) return vis;
|
|
23402
|
+
}
|
|
23403
|
+
|
|
23077
23404
|
// 1) nearest <rte-dialog-line-X> ancestor — the suffix is the field meaning
|
|
23078
23405
|
var n = f.parentElement;
|
|
23079
23406
|
for (var i = 0; i < 8 && n; i++) {
|
|
@@ -24333,6 +24660,25 @@ function RTE_Plugin_DocumentImport() {
|
|
|
24333
24660
|
var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
|
|
24334
24661
|
if (hv && hv !== "none") css += "background-color:" + hv + ";";
|
|
24335
24662
|
}
|
|
24663
|
+
// w:shd is the OTHER way Word carries a background, and it is the
|
|
24664
|
+
// one our own exporter writes (w:highlight only accepts a fixed
|
|
24665
|
+
// set of colour NAMES, shd takes any hex). Reading only
|
|
24666
|
+
// w:highlight meant every highlight we exported came back
|
|
24667
|
+
// unhighlighted - the two sides used different vocabulary for the
|
|
24668
|
+
// same thing.
|
|
24669
|
+
var shd = firstChildEl(rpr, "shd");
|
|
24670
|
+
if (shd) {
|
|
24671
|
+
var fill = shd.getAttributeNS(W, "fill") || shd.getAttribute("w:fill") || "";
|
|
24672
|
+
if (fill && /^[0-9A-Fa-f]{6}$/.test(fill)) css += "background-color:#" + fill + ";";
|
|
24673
|
+
}
|
|
24674
|
+
// Monospace run -> inline code. Our exporter marks <code>/<kbd>/
|
|
24675
|
+
// <samp>/<pre> with w:rFonts Consolas; nothing read it back.
|
|
24676
|
+
var rf = firstChildEl(rpr, "rFonts");
|
|
24677
|
+
if (rf) {
|
|
24678
|
+
var asc = rf.getAttributeNS(W, "ascii") || rf.getAttribute("w:ascii") || "";
|
|
24679
|
+
if (/consolas|courier|monospace|mono$/i.test(asc)) txt = "<code>" + txt + "</code>";
|
|
24680
|
+
else if (asc) css += "font-family:" + asc.replace(/[<>"]/g, "") + ";";
|
|
24681
|
+
}
|
|
24336
24682
|
var sz = firstChildEl(rpr, "sz");
|
|
24337
24683
|
if (sz) {
|
|
24338
24684
|
var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
|
|
@@ -24505,7 +24851,16 @@ function RTE_Plugin_DocumentImport() {
|
|
|
24505
24851
|
// without this they came back as plain <p> and the round-trip lost
|
|
24506
24852
|
// every blockquote. Match Quote / IntenseQuote / BlockQuote.
|
|
24507
24853
|
else if (/^(intense)?quote$|blockquote/i.test(v.replace(/\s+/g, ""))) info.quote = true;
|
|
24854
|
+
// docxexport.js writes <w:pStyle w:val="Code"/> for <pre>; without
|
|
24855
|
+
// this a code block came back as an ordinary paragraph and lost
|
|
24856
|
+
// both its monospace and its whitespace.
|
|
24857
|
+
else if (/^(code|sourcecode|htmlpre|preformatted)$/i.test(v.replace(/\s+/g, ""))) info.pre = true;
|
|
24508
24858
|
}
|
|
24859
|
+
// A horizontal rule is exported as an EMPTY paragraph carrying a
|
|
24860
|
+
// bottom border - Word has no <hr>. Nothing read it back, so every
|
|
24861
|
+
// rule became a stray blank paragraph on import.
|
|
24862
|
+
var pbdr = firstChildEl(ppr, "pBdr");
|
|
24863
|
+
if (pbdr && firstChildEl(pbdr, "bottom")) info.rule = true;
|
|
24509
24864
|
var numPr = firstChildEl(ppr, "numPr");
|
|
24510
24865
|
if (numPr) {
|
|
24511
24866
|
info.list = true;
|
|
@@ -24520,6 +24875,19 @@ function RTE_Plugin_DocumentImport() {
|
|
|
24520
24875
|
// matches the old behaviour rather than inventing <ol>s.
|
|
24521
24876
|
info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
|
|
24522
24877
|
}
|
|
24878
|
+
// Word paragraph indentation. Nothing read this before 2026-09-04,
|
|
24879
|
+
// so every indented paragraph in every imported .docx arrived
|
|
24880
|
+
// flush left and the indentation was lost silently.
|
|
24881
|
+
// w:left is in twips; 15 twips = 1px.
|
|
24882
|
+
var ind = firstChildEl(ppr, "ind");
|
|
24883
|
+
if (ind) {
|
|
24884
|
+
var lft = ind.getAttributeNS(W, "left") || ind.getAttribute("w:left") || "";
|
|
24885
|
+
var tw = parseInt(lft, 10);
|
|
24886
|
+
// 720 twips alongside the Quote style is the blockquote's own
|
|
24887
|
+
// indent, already carried by the blockquote element itself.
|
|
24888
|
+
if (tw > 0 && !(info.quote && tw === 720))
|
|
24889
|
+
info.indentPx = Math.round(tw / 15);
|
|
24890
|
+
}
|
|
24523
24891
|
var jc = firstChildEl(ppr, "jc");
|
|
24524
24892
|
if (jc) {
|
|
24525
24893
|
var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
|
|
@@ -24586,10 +24954,26 @@ function RTE_Plugin_DocumentImport() {
|
|
|
24586
24954
|
continue;
|
|
24587
24955
|
}
|
|
24588
24956
|
closeListsTo(0);
|
|
24589
|
-
|
|
24957
|
+
// Only a rule when the paragraph carries no text of its own -
|
|
24958
|
+
// a bordered paragraph WITH content is a real bordered paragraph.
|
|
24959
|
+
if (st.rule && !(inner || "").replace(/<[^>]*>/g, "").trim()) {
|
|
24960
|
+
closeListsTo(0); out.push("<hr>"); continue;
|
|
24961
|
+
}
|
|
24962
|
+
if (st.pre) { out.push("<pre>" + (inner || "<br>") + "</pre>"); continue; }
|
|
24963
|
+
if (st.heading) {
|
|
24964
|
+
// Headings carry w:ind too; without this an indented
|
|
24965
|
+
// heading imported flush left.
|
|
24966
|
+
var hs = st.indentPx ? ' style="margin-left:' + st.indentPx + 'px"' : "";
|
|
24967
|
+
out.push("<h" + st.heading + hs + ">" + (inner || "") + "</h" + st.heading + ">");
|
|
24968
|
+
}
|
|
24590
24969
|
else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
|
|
24591
24970
|
else {
|
|
24592
|
-
var
|
|
24971
|
+
var decl = [];
|
|
24972
|
+
if (st.align) decl.push("text-align:" + st.align);
|
|
24973
|
+
// indentUseMargin defaults to margin, and margin is what
|
|
24974
|
+
// w:ind means, so import writes margin-left to match.
|
|
24975
|
+
if (st.indentPx) decl.push("margin-left:" + st.indentPx + "px");
|
|
24976
|
+
var style = decl.length ? ' style="' + decl.join(";") + '"' : "";
|
|
24593
24977
|
// A paragraph that contained ONLY a text box has no text of
|
|
24594
24978
|
// its own; emitting an empty <p> before the box adds a blank
|
|
24595
24979
|
// line that was never in the document.
|
|
@@ -25194,7 +25578,7 @@ function RTE_Plugin_DocxExport() {
|
|
|
25194
25578
|
bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
|
|
25195
25579
|
strike: inherited.strike, sizeHalfPoints: inherited.sizeHalfPoints,
|
|
25196
25580
|
color: inherited.color, highlight: inherited.highlight, vertAlign: inherited.vertAlign,
|
|
25197
|
-
mono: inherited.mono
|
|
25581
|
+
mono: inherited.mono, font: inherited.font
|
|
25198
25582
|
};
|
|
25199
25583
|
var tag = node.tagName ? node.tagName.toLowerCase() : "";
|
|
25200
25584
|
if (tag === "b" || tag === "strong") s.bold = true;
|
|
@@ -25204,6 +25588,10 @@ function RTE_Plugin_DocxExport() {
|
|
|
25204
25588
|
if (tag === "sup") s.vertAlign = "superscript";
|
|
25205
25589
|
if (tag === "sub") s.vertAlign = "subscript";
|
|
25206
25590
|
if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "pre") s.mono = true;
|
|
25591
|
+
// <mark> carries no inline style, so the CSS branch below never sees it
|
|
25592
|
+
// and highlighted text was dropped entirely on export. Word's own
|
|
25593
|
+
// default highlight is yellow.
|
|
25594
|
+
if (tag === "mark") s.highlight = s.highlight || "FFFF00";
|
|
25207
25595
|
|
|
25208
25596
|
var css = node.style;
|
|
25209
25597
|
if (css) {
|
|
@@ -25214,6 +25602,12 @@ function RTE_Plugin_DocxExport() {
|
|
|
25214
25602
|
if (deco.indexOf("line-through") >= 0) s.strike = true;
|
|
25215
25603
|
if (css.color) { var c = hexColor(css.color); if (c) s.color = c; }
|
|
25216
25604
|
if (css.backgroundColor) { var h = hexColor(css.backgroundColor); if (h) s.highlight = h; }
|
|
25605
|
+
// Font family was read by nothing, so an exported document lost every
|
|
25606
|
+
// typeface choice. Word wants ONE family name, not a CSS stack.
|
|
25607
|
+
if (css.fontFamily) {
|
|
25608
|
+
var fam = String(css.fontFamily).split(",")[0].replace(/^\s*['"]?|['"]?\s*$/g, "");
|
|
25609
|
+
if (fam) s.font = fam;
|
|
25610
|
+
}
|
|
25217
25611
|
if (css.fontSize) {
|
|
25218
25612
|
var pt = cssToPoints(css.fontSize);
|
|
25219
25613
|
// w:sz is in HALF-POINTS. Writing points here makes every
|
|
@@ -25250,7 +25644,10 @@ function RTE_Plugin_DocxExport() {
|
|
|
25250
25644
|
|
|
25251
25645
|
function runProps(s) {
|
|
25252
25646
|
var p = "";
|
|
25253
|
-
|
|
25647
|
+
// One rFonts element only: mono wins, because <code> inside a styled span
|
|
25648
|
+
// should still read as code.
|
|
25649
|
+
var fontName = s.mono ? "Consolas" : (s.font || "");
|
|
25650
|
+
if (fontName) p += '<w:rFonts w:ascii="' + esc(fontName) + '" w:hAnsi="' + esc(fontName) + '"/>';
|
|
25254
25651
|
if (s.bold) p += "<w:b/>";
|
|
25255
25652
|
if (s.italic) p += "<w:i/>";
|
|
25256
25653
|
if (s.underline) p += '<w:u w:val="single"/>';
|
|
@@ -25612,6 +26009,31 @@ function RTE_Plugin_DocxExport() {
|
|
|
25612
26009
|
var revSeq = { n: 1 };
|
|
25613
26010
|
var base = { bold: false, italic: false, underline: false, strike: false, sizeHalfPoints: 0, color: null, highlight: null, vertAlign: null, mono: false };
|
|
25614
26011
|
|
|
26012
|
+
|
|
26013
|
+
// A block indented in the editor carries margin-left (or padding-left,
|
|
26014
|
+
// per indentUseMargin). Word expresses the same thing as w:ind, in
|
|
26015
|
+
// TWIPS. Before 2026-09-04 indentation reached Word only when it was
|
|
26016
|
+
// spelled <blockquote>, which dragged the Quote style along with it -
|
|
26017
|
+
// so indented body text arrived italic and accent-coloured.
|
|
26018
|
+
// 1px = 0.75pt and 1pt = 20 twips, hence px * 15.
|
|
26019
|
+
function indentTwips(node) {
|
|
26020
|
+
if (!node || !node.style) return 0;
|
|
26021
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
26022
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
26023
|
+
if (!m) return 0;
|
|
26024
|
+
var n = parseFloat(m[1]);
|
|
26025
|
+
if (!n || n <= 0) return 0;
|
|
26026
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
26027
|
+
var px = unit === "px" ? n
|
|
26028
|
+
: unit === "pt" ? n * (4 / 3)
|
|
26029
|
+
: unit === "in" ? n * 96
|
|
26030
|
+
: unit === "cm" ? n * 37.795
|
|
26031
|
+
: unit === "mm" ? n * 3.7795
|
|
26032
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
26033
|
+
: 0;
|
|
26034
|
+
return Math.round(px * 15);
|
|
26035
|
+
}
|
|
26036
|
+
|
|
25615
26037
|
function para(node, style, opts) {
|
|
25616
26038
|
opts = opts || {};
|
|
25617
26039
|
var pr = "";
|
|
@@ -25620,6 +26042,12 @@ function RTE_Plugin_DocxExport() {
|
|
|
25620
26042
|
if (opts.pre) pr += '<w:pStyle w:val="Code"/>';
|
|
25621
26043
|
if (opts.numId) pr += "<w:numPr><w:ilvl w:val=\"" + (opts.level || 0) + "\"/><w:numId w:val=\"" + opts.numId + "\"/></w:numPr>";
|
|
25622
26044
|
if (opts.pageBreakBefore) pr += "<w:pageBreakBefore/>";
|
|
26045
|
+
// Independent of the Quote style: a real blockquote already got
|
|
26046
|
+
// its w:ind above, so only emit this for everything else.
|
|
26047
|
+
if (!opts.quote) {
|
|
26048
|
+
var indTw = indentTwips(node);
|
|
26049
|
+
if (indTw > 0) pr += '<w:ind w:left="' + indTw + '"/>';
|
|
26050
|
+
}
|
|
25623
26051
|
var align = node && node.style ? node.style.textAlign : "";
|
|
25624
26052
|
if (align === "center" || align === "right") pr += '<w:jc w:val="' + align + '"/>';
|
|
25625
26053
|
else if (align === "justify") pr += '<w:jc w:val="both"/>';
|
|
@@ -25637,7 +26065,17 @@ function RTE_Plugin_DocxExport() {
|
|
|
25637
26065
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
25638
26066
|
var li = node.childNodes[i];
|
|
25639
26067
|
if (li.nodeType !== 1 || li.tagName.toLowerCase() !== "li") continue;
|
|
25640
|
-
|
|
26068
|
+
// Serialise the item's OWN content only. para() walks every
|
|
26069
|
+
// descendant, so passing the whole <li> emitted the nested list's
|
|
26070
|
+
// text here AND again as its own items below - a round trip turned
|
|
26071
|
+
// "xx" with a child "yy" into "xxyy". Content duplication, not loss.
|
|
26072
|
+
var own = li.ownerDocument.createElement("li");
|
|
26073
|
+
for (var k = 0; k < li.childNodes.length; k++) {
|
|
26074
|
+
var ch = li.childNodes[k];
|
|
26075
|
+
if (ch.nodeType === 1 && /^(ul|ol)$/.test(ch.tagName.toLowerCase())) continue;
|
|
26076
|
+
own.appendChild(ch.cloneNode(true));
|
|
26077
|
+
}
|
|
26078
|
+
xml += para(own, styleFrom(li, style), { numId: numId, level: level, pageBreakBefore: pendingBreak });
|
|
25641
26079
|
pendingBreak = false;
|
|
25642
26080
|
for (var j = 0; j < li.childNodes.length; j++) {
|
|
25643
26081
|
var sub = li.childNodes[j];
|
|
@@ -29261,6 +29699,277 @@ function RTE_Plugin_ImportStyles() {
|
|
|
29261
29699
|
}
|
|
29262
29700
|
}
|
|
29263
29701
|
|
|
29702
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
29703
|
+
|
|
29704
|
+
// 2026-09-04 Inline code — CKEditor 5's "Code" feature (`<code>`), the one
|
|
29705
|
+
// basic-style it ships that this editor had no command for. Markdown `x` was
|
|
29706
|
+
// the only way to produce inline code here, which meant there was no way to
|
|
29707
|
+
// REMOVE it and no toolbar affordance at all.
|
|
29708
|
+
//
|
|
29709
|
+
// The command is "inlinecode", NOT "code". "code" is already taken by the
|
|
29710
|
+
// HTML-source-view toggle in the core (__Is_Cmd_Active case "code" ->
|
|
29711
|
+
// ___Is_CodeMode()), so registering "code" here would have collided with source
|
|
29712
|
+
// view. keyboarda11y.js has carried "inlinecode" in its TOGGLE_CMD list all
|
|
29713
|
+
// along, with a comment that it matched nothing in this build; a core
|
|
29714
|
+
// __Is_Cmd_Active case added alongside this plugin is what makes it match, so
|
|
29715
|
+
// the button reports aria-pressed and not just a highlight.
|
|
29716
|
+
//
|
|
29717
|
+
// Design notes:
|
|
29718
|
+
// - Toggling is done by SPLITTING the <code> element around the selection
|
|
29719
|
+
// rather than by lifting text nodes out of it. Splitting preserves nested
|
|
29720
|
+
// inline markup: un-coding the middle of <code>a <em>b</em> c</code> keeps
|
|
29721
|
+
// the <em>. Lifting text nodes would have dropped it.
|
|
29722
|
+
// - A collapsed selection acts on the word under the caret. That is the same
|
|
29723
|
+
// choice changecase.js makes, and for the same reason: it makes the command
|
|
29724
|
+
// usable from a keyboard shortcut without selecting first. The alternative -
|
|
29725
|
+
// inserting an empty <code> and putting the caret inside - is the caret trap
|
|
29726
|
+
// documented in the footnotes work, so it is deliberately not done.
|
|
29727
|
+
// - Never nests inside <pre> or an existing <code>: both already mean
|
|
29728
|
+
// "preformatted", and nesting produces markup no serializer round-trips.
|
|
29729
|
+
// - Emits a bare <code> element and no rte-* class. That keeps the styling in
|
|
29730
|
+
// the content stylesheet where authors can override it, and keeps the plugin
|
|
29731
|
+
// out of check-css-contract.mjs, which only scans the four theme sheets.
|
|
29732
|
+
RTE_DefaultConfig.plugin_inlinecode = RTE_Plugin_InlineCode;
|
|
29733
|
+
|
|
29734
|
+
if (!RTE_DefaultConfig.text_inlinecode) RTE_DefaultConfig.text_inlinecode = "Inline code";
|
|
29735
|
+
|
|
29736
|
+
if (!RTE_DefaultConfig.svgCode_inlinecode) {
|
|
29737
|
+
RTE_DefaultConfig.svgCode_inlinecode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 8 3 12 7 16"/><polyline points="17 8 21 12 17 16"/></svg>';
|
|
29738
|
+
}
|
|
29739
|
+
|
|
29740
|
+
function RTE_Plugin_InlineCode() {
|
|
29741
|
+
var obj = this;
|
|
29742
|
+
var config, editor;
|
|
29743
|
+
|
|
29744
|
+
obj.PluginName = "InlineCode";
|
|
29745
|
+
|
|
29746
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
29747
|
+
|
|
29748
|
+
obj.InitEditor = function (argeditor) {
|
|
29749
|
+
editor = argeditor;
|
|
29750
|
+
|
|
29751
|
+
editor.attachEvent("exec_command_inlinecode", function (state) {
|
|
29752
|
+
state.returnValue = true;
|
|
29753
|
+
obj.Toggle();
|
|
29754
|
+
});
|
|
29755
|
+
|
|
29756
|
+
// Public API, matching the shape the other toggle plugins expose.
|
|
29757
|
+
editor.toggleInlineCode = function () { return obj.Toggle(); };
|
|
29758
|
+
editor.isInlineCode = function () { return obj.IsActive(); };
|
|
29759
|
+
};
|
|
29760
|
+
|
|
29761
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
29762
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
29763
|
+
|
|
29764
|
+
function getRange(doc) {
|
|
29765
|
+
var sel = doc && doc.getSelection ? doc.getSelection() : null;
|
|
29766
|
+
if (!sel || !sel.rangeCount) return null;
|
|
29767
|
+
return sel.getRangeAt(0);
|
|
29768
|
+
}
|
|
29769
|
+
|
|
29770
|
+
// Nearest <code> ancestor that is still inside the editable area.
|
|
29771
|
+
function codeAncestor(node, editable) {
|
|
29772
|
+
while (node && node !== editable) {
|
|
29773
|
+
if (node.nodeType === 1 && node.nodeName === "CODE") return node;
|
|
29774
|
+
node = node.parentNode;
|
|
29775
|
+
}
|
|
29776
|
+
return null;
|
|
29777
|
+
}
|
|
29778
|
+
|
|
29779
|
+
function inPre(node, editable) {
|
|
29780
|
+
while (node && node !== editable) {
|
|
29781
|
+
if (node.nodeType === 1 && node.nodeName === "PRE") return true;
|
|
29782
|
+
node = node.parentNode;
|
|
29783
|
+
}
|
|
29784
|
+
return false;
|
|
29785
|
+
}
|
|
29786
|
+
|
|
29787
|
+
obj.IsActive = function () {
|
|
29788
|
+
var doc = getDoc(), editable = getEditable();
|
|
29789
|
+
if (!doc || !editable) return false;
|
|
29790
|
+
var range = getRange(doc);
|
|
29791
|
+
if (!range) return false;
|
|
29792
|
+
return !!codeAncestor(range.startContainer, editable);
|
|
29793
|
+
};
|
|
29794
|
+
|
|
29795
|
+
// ---- selection helpers -----------------------------------------------
|
|
29796
|
+
|
|
29797
|
+
// Expand a collapsed caret to the word under it. Returns false when the
|
|
29798
|
+
// caret is not sitting in a word, in which case the command does nothing
|
|
29799
|
+
// rather than creating an empty element.
|
|
29800
|
+
function expandToWord(range) {
|
|
29801
|
+
var node = range.startContainer;
|
|
29802
|
+
if (!node || node.nodeType !== 3) return false;
|
|
29803
|
+
var text = node.data || "";
|
|
29804
|
+
var offset = range.startOffset;
|
|
29805
|
+
var start = offset, end = offset;
|
|
29806
|
+
while (start > 0 && /\S/.test(text.charAt(start - 1))) start--;
|
|
29807
|
+
while (end < text.length && /\S/.test(text.charAt(end))) end++;
|
|
29808
|
+
if (start === end) return false;
|
|
29809
|
+
range.setStart(node, start);
|
|
29810
|
+
range.setEnd(node, end);
|
|
29811
|
+
return true;
|
|
29812
|
+
}
|
|
29813
|
+
|
|
29814
|
+
// Every text node the range actually covers. Boundary text nodes are split
|
|
29815
|
+
// first so a partially-selected node is not treated as fully selected.
|
|
29816
|
+
function selectedTextNodes(doc, range, editable) {
|
|
29817
|
+
if (range.startContainer.nodeType === 3 && range.startOffset > 0) {
|
|
29818
|
+
var s = range.startContainer;
|
|
29819
|
+
if (range.startOffset < s.data.length) {
|
|
29820
|
+
var rest = s.splitText(range.startOffset);
|
|
29821
|
+
var endSame = (range.endContainer === s);
|
|
29822
|
+
var endOff = range.endOffset;
|
|
29823
|
+
range.setStart(rest, 0);
|
|
29824
|
+
if (endSame) range.setEnd(rest, endOff - s.data.length);
|
|
29825
|
+
}
|
|
29826
|
+
}
|
|
29827
|
+
if (range.endContainer.nodeType === 3 && range.endOffset < range.endContainer.data.length && range.endOffset > 0) {
|
|
29828
|
+
range.endContainer.splitText(range.endOffset);
|
|
29829
|
+
}
|
|
29830
|
+
|
|
29831
|
+
var out = [];
|
|
29832
|
+
var walker = doc.createTreeWalker(editable, 4 /* SHOW_TEXT */, null, false);
|
|
29833
|
+
var n;
|
|
29834
|
+
while ((n = walker.nextNode())) {
|
|
29835
|
+
if (!n.data) continue;
|
|
29836
|
+
if (range.intersectsNode ? range.intersectsNode(n) : true) {
|
|
29837
|
+
// intersectsNode is true for merely-touching nodes, so confirm
|
|
29838
|
+
// the node is genuinely within the boundaries.
|
|
29839
|
+
var r = doc.createRange();
|
|
29840
|
+
r.selectNodeContents(n);
|
|
29841
|
+
if (range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
|
|
29842
|
+
range.compareBoundaryPoints(Range.START_TO_END, r) > 0) {
|
|
29843
|
+
out.push(n);
|
|
29844
|
+
}
|
|
29845
|
+
}
|
|
29846
|
+
}
|
|
29847
|
+
return out;
|
|
29848
|
+
}
|
|
29849
|
+
|
|
29850
|
+
// ---- apply / remove ---------------------------------------------------
|
|
29851
|
+
|
|
29852
|
+
// Split `code` around the selection and unwrap only the overlapping part,
|
|
29853
|
+
// so partial removal keeps the untouched halves coded.
|
|
29854
|
+
function unwrapPart(doc, code, range) {
|
|
29855
|
+
var parent = code.parentNode;
|
|
29856
|
+
if (!parent) return;
|
|
29857
|
+
|
|
29858
|
+
var startsInside = code.contains(range.startContainer);
|
|
29859
|
+
var endsInside = code.contains(range.endContainer);
|
|
29860
|
+
|
|
29861
|
+
// Tail first: extracting it cannot disturb the head's boundaries.
|
|
29862
|
+
var after = doc.createRange();
|
|
29863
|
+
if (endsInside) after.setStart(range.endContainer, range.endOffset);
|
|
29864
|
+
else after.setStart(code, code.childNodes.length);
|
|
29865
|
+
after.setEnd(code, code.childNodes.length);
|
|
29866
|
+
var afterFrag = after.extractContents();
|
|
29867
|
+
|
|
29868
|
+
var before = doc.createRange();
|
|
29869
|
+
before.setStart(code, 0);
|
|
29870
|
+
if (startsInside) before.setEnd(range.startContainer, range.startOffset);
|
|
29871
|
+
else before.setEnd(code, 0);
|
|
29872
|
+
var beforeFrag = before.extractContents();
|
|
29873
|
+
|
|
29874
|
+
if (beforeFrag && beforeFrag.textContent !== "") {
|
|
29875
|
+
var head = doc.createElement("code");
|
|
29876
|
+
head.appendChild(beforeFrag);
|
|
29877
|
+
parent.insertBefore(head, code);
|
|
29878
|
+
}
|
|
29879
|
+
|
|
29880
|
+
// What is left in `code` is exactly the selected part: unwrap it.
|
|
29881
|
+
while (code.firstChild) parent.insertBefore(code.firstChild, code);
|
|
29882
|
+
|
|
29883
|
+
if (afterFrag && afterFrag.textContent !== "") {
|
|
29884
|
+
var tail = doc.createElement("code");
|
|
29885
|
+
tail.appendChild(afterFrag);
|
|
29886
|
+
parent.insertBefore(tail, code);
|
|
29887
|
+
}
|
|
29888
|
+
|
|
29889
|
+
parent.removeChild(code);
|
|
29890
|
+
}
|
|
29891
|
+
|
|
29892
|
+
function wrapNodes(doc, nodes, editable) {
|
|
29893
|
+
// Group contiguous siblings so a run of text under one parent becomes a
|
|
29894
|
+
// single <code>, not one per text node.
|
|
29895
|
+
var group = [], groups = [];
|
|
29896
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
29897
|
+
var n = nodes[i];
|
|
29898
|
+
if (inPre(n, editable) || codeAncestor(n, editable)) continue;
|
|
29899
|
+
if (group.length && group[group.length - 1].parentNode === n.parentNode &&
|
|
29900
|
+
group[group.length - 1].nextSibling === n) {
|
|
29901
|
+
group.push(n);
|
|
29902
|
+
} else {
|
|
29903
|
+
if (group.length) groups.push(group);
|
|
29904
|
+
group = [n];
|
|
29905
|
+
}
|
|
29906
|
+
}
|
|
29907
|
+
if (group.length) groups.push(group);
|
|
29908
|
+
|
|
29909
|
+
for (var g = 0; g < groups.length; g++) {
|
|
29910
|
+
var run = groups[g];
|
|
29911
|
+
// Whitespace-only runs would produce a <code> containing nothing but
|
|
29912
|
+
// a space, which is invisible formatting debris.
|
|
29913
|
+
var joined = "";
|
|
29914
|
+
for (var k = 0; k < run.length; k++) joined += run[k].data;
|
|
29915
|
+
if (!/\S/.test(joined)) continue;
|
|
29916
|
+
|
|
29917
|
+
var code = doc.createElement("code");
|
|
29918
|
+
run[0].parentNode.insertBefore(code, run[0]);
|
|
29919
|
+
for (var j = 0; j < run.length; j++) code.appendChild(run[j]);
|
|
29920
|
+
}
|
|
29921
|
+
}
|
|
29922
|
+
|
|
29923
|
+
// Adjacent <code><code> pairs read as one to the user but serialize as two.
|
|
29924
|
+
function mergeAdjacent(editable) {
|
|
29925
|
+
var all = editable.querySelectorAll("code");
|
|
29926
|
+
for (var i = 0; i < all.length; i++) {
|
|
29927
|
+
var code = all[i];
|
|
29928
|
+
var next = code.nextSibling;
|
|
29929
|
+
while (next && next.nodeType === 1 && next.nodeName === "CODE") {
|
|
29930
|
+
while (next.firstChild) code.appendChild(next.firstChild);
|
|
29931
|
+
next.parentNode.removeChild(next);
|
|
29932
|
+
next = code.nextSibling;
|
|
29933
|
+
}
|
|
29934
|
+
}
|
|
29935
|
+
}
|
|
29936
|
+
|
|
29937
|
+
obj.Toggle = function () {
|
|
29938
|
+
var doc = getDoc(), editable = getEditable();
|
|
29939
|
+
if (!doc || !editable) return false;
|
|
29940
|
+
var range = getRange(doc);
|
|
29941
|
+
if (!range) return false;
|
|
29942
|
+
if (!editable.contains(range.startContainer)) return false;
|
|
29943
|
+
|
|
29944
|
+
if (range.collapsed && !expandToWord(range)) return false;
|
|
29945
|
+
|
|
29946
|
+
var nodes = selectedTextNodes(doc, range, editable);
|
|
29947
|
+
if (!nodes.length) return false;
|
|
29948
|
+
|
|
29949
|
+
// Remove when every selected text node is already coded; otherwise apply.
|
|
29950
|
+
var allCoded = true;
|
|
29951
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
29952
|
+
if (!/\S/.test(nodes[i].data)) continue;
|
|
29953
|
+
if (!codeAncestor(nodes[i], editable)) { allCoded = false; break; }
|
|
29954
|
+
}
|
|
29955
|
+
|
|
29956
|
+
if (allCoded) {
|
|
29957
|
+
var codes = [], seen = [];
|
|
29958
|
+
for (var n = 0; n < nodes.length; n++) {
|
|
29959
|
+
var c = codeAncestor(nodes[n], editable);
|
|
29960
|
+
if (c && seen.indexOf(c) < 0) { seen.push(c); codes.push(c); }
|
|
29961
|
+
}
|
|
29962
|
+
for (var x = 0; x < codes.length; x++) unwrapPart(doc, codes[x], range);
|
|
29963
|
+
} else {
|
|
29964
|
+
wrapNodes(doc, nodes, editable);
|
|
29965
|
+
mergeAdjacent(editable);
|
|
29966
|
+
}
|
|
29967
|
+
|
|
29968
|
+
try { editor.updateToolbarStatus && editor.updateToolbarStatus(); } catch (e) { }
|
|
29969
|
+
return true;
|
|
29970
|
+
};
|
|
29971
|
+
}
|
|
29972
|
+
|
|
29264
29973
|
|
|
29265
29974
|
if (!RTE_DefaultConfig.svgCode_insertcode) {
|
|
29266
29975
|
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>';
|
|
@@ -31054,6 +31763,21 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
31054
31763
|
if (config.keyboardA11y === false) return;
|
|
31055
31764
|
|
|
31056
31765
|
editor.focusToolbar = function () { return focusFirstToolbarButton(); };
|
|
31766
|
+
// Public so plugins can report their own outcomes -- find/replace match
|
|
31767
|
+
// counts, what the paste filter removed, export completion.
|
|
31768
|
+
editor.announce = function (text, opts) { return announce(text, opts); };
|
|
31769
|
+
// setReadOnly() lives in the core and cannot call into a plugin, so wrap
|
|
31770
|
+
// it here rather than leaving the two states to drift apart.
|
|
31771
|
+
if (typeof editor.setReadOnly === "function" && !editor.setReadOnly.__a11yWrapped) {
|
|
31772
|
+
var origSetReadOnly = editor.setReadOnly;
|
|
31773
|
+
var wrapped = function (v) {
|
|
31774
|
+
var r = origSetReadOnly.apply(editor, arguments);
|
|
31775
|
+
try { syncReadOnlyState(); } catch (e) { }
|
|
31776
|
+
return r;
|
|
31777
|
+
};
|
|
31778
|
+
wrapped.__a11yWrapped = true;
|
|
31779
|
+
editor.setReadOnly = wrapped;
|
|
31780
|
+
}
|
|
31057
31781
|
|
|
31058
31782
|
setup();
|
|
31059
31783
|
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
@@ -31077,6 +31801,8 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
31077
31801
|
function setup() {
|
|
31078
31802
|
var root = shell();
|
|
31079
31803
|
if (!root) return;
|
|
31804
|
+
ensureLiveRegions();
|
|
31805
|
+
syncReadOnlyState();
|
|
31080
31806
|
bindEscapeHatch();
|
|
31081
31807
|
applyRovingTabindex(root);
|
|
31082
31808
|
syncToggleStates(root);
|
|
@@ -31084,6 +31810,97 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
31084
31810
|
watch(root);
|
|
31085
31811
|
}
|
|
31086
31812
|
|
|
31813
|
+
|
|
31814
|
+
// ------------------------------------------------- 4. status messages
|
|
31815
|
+
//
|
|
31816
|
+
// WCAG 4.1.3 Status Messages (Level AA): a change of state that is NOT
|
|
31817
|
+
// given focus still has to reach assistive technology. Before this the
|
|
31818
|
+
// editor had no live region at all -- not one aria-live node, in the page
|
|
31819
|
+
// or in the iframe -- so a screen reader user got silence for every
|
|
31820
|
+
// outcome the sighted user reads off the chrome: how many matches Find
|
|
31821
|
+
// found, what the paste filter stripped, that the length limit was hit,
|
|
31822
|
+
// that the document went read-only.
|
|
31823
|
+
//
|
|
31824
|
+
// Two regions, because politeness is not a detail: 'polite' waits for a
|
|
31825
|
+
// pause in speech (counts, confirmations), 'assertive' interrupts (errors,
|
|
31826
|
+
// refusals). aria-atomic="true" on both so the whole message is re-read
|
|
31827
|
+
// rather than only the words that changed -- reading a diff aloud produces
|
|
31828
|
+
// sentences that were never written.
|
|
31829
|
+
//
|
|
31830
|
+
// The region lives in the HOST document, not the editing iframe: a message
|
|
31831
|
+
// announced from inside the document the user is editing would also become
|
|
31832
|
+
// part of what they are editing.
|
|
31833
|
+
var liveNodes = null;
|
|
31834
|
+
function ensureLiveRegions() {
|
|
31835
|
+
if (liveNodes && liveNodes.polite && liveNodes.polite.isConnected) return liveNodes;
|
|
31836
|
+
var root = shell();
|
|
31837
|
+
if (!root) return null;
|
|
31838
|
+
function mk(politeness) {
|
|
31839
|
+
var el = root.querySelector('[data-rte-live="' + politeness + '"]');
|
|
31840
|
+
if (el) return el;
|
|
31841
|
+
el = document.createElement("div");
|
|
31842
|
+
el.setAttribute("data-rte-live", politeness);
|
|
31843
|
+
el.setAttribute("aria-live", politeness);
|
|
31844
|
+
el.setAttribute("aria-atomic", "true");
|
|
31845
|
+
el.setAttribute("role", politeness === "assertive" ? "alert" : "status");
|
|
31846
|
+
// Visually hidden but still rendered: display:none and
|
|
31847
|
+
// visibility:hidden remove the node from the accessibility tree,
|
|
31848
|
+
// which silences it. Clip is the technique that does not.
|
|
31849
|
+
el.style.cssText = "position:absolute;width:1px;height:1px;margin:-1px;" +
|
|
31850
|
+
"padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;";
|
|
31851
|
+
root.appendChild(el);
|
|
31852
|
+
return el;
|
|
31853
|
+
}
|
|
31854
|
+
liveNodes = { polite: mk("polite"), assertive: mk("assertive") };
|
|
31855
|
+
return liveNodes;
|
|
31856
|
+
}
|
|
31857
|
+
|
|
31858
|
+
// editor.announce(text, {assertive}) -- also used by plugins.
|
|
31859
|
+
function announce(text, opts) {
|
|
31860
|
+
var msg = String(text == null ? "" : text).trim();
|
|
31861
|
+
if (!msg) return false;
|
|
31862
|
+
var nodes = ensureLiveRegions();
|
|
31863
|
+
if (!nodes) return false;
|
|
31864
|
+
var el = (opts && opts.assertive) ? nodes.assertive : nodes.polite;
|
|
31865
|
+
// Re-announcing the SAME string is a no-op for most screen readers
|
|
31866
|
+
// unless the node is cleared first, so "3 results" after "3 results"
|
|
31867
|
+
// would be silent. Clear, then set on the next frame.
|
|
31868
|
+
el.textContent = "";
|
|
31869
|
+
setTimeout(function () { el.textContent = msg; }, 30);
|
|
31870
|
+
return true;
|
|
31871
|
+
}
|
|
31872
|
+
|
|
31873
|
+
// ------------------------------------------------- 5. read-only state
|
|
31874
|
+
//
|
|
31875
|
+
// setReadOnly() flips designMode, which genuinely blocks editing -- but
|
|
31876
|
+
// designMode is invisible to assistive technology. The toolbar already
|
|
31877
|
+
// marks its buttons aria-disabled; the editing region itself said nothing,
|
|
31878
|
+
// so a screen reader user entered a field described as a rich text editor,
|
|
31879
|
+
// typed, and got no response and no explanation. aria-readonly is the
|
|
31880
|
+
// attribute that carries this, and the accessible name says it too because
|
|
31881
|
+
// a name is what gets announced on entry.
|
|
31882
|
+
var lastReadOnly = null;
|
|
31883
|
+
function syncReadOnlyState() {
|
|
31884
|
+
var ed;
|
|
31885
|
+
try { ed = editor.getEditable(); } catch (e) { return; }
|
|
31886
|
+
if (!ed) return;
|
|
31887
|
+
var ro = false;
|
|
31888
|
+
try { ro = !!(editor.getReadOnly ? editor.getReadOnly() : editor.isReadOnly && editor.isReadOnly()); } catch (e) { }
|
|
31889
|
+
ed.setAttribute("aria-readonly", ro ? "true" : "false");
|
|
31890
|
+
var suffix = config.a11yReadOnlySuffix || "Read only.";
|
|
31891
|
+
// Plain string trimming rather than a built regex: the suffix is
|
|
31892
|
+
// config-supplied and would otherwise need escaping to be safe.
|
|
31893
|
+
var label = (ed.getAttribute("aria-label") || "");
|
|
31894
|
+
if (label.length >= suffix.length && label.slice(-suffix.length) === suffix) {
|
|
31895
|
+
label = label.slice(0, -suffix.length).replace(/\s+$/, "");
|
|
31896
|
+
}
|
|
31897
|
+
ed.setAttribute("aria-label", ro ? (label + " " + suffix) : label);
|
|
31898
|
+
if (lastReadOnly !== null && lastReadOnly !== ro) {
|
|
31899
|
+
announce(ro ? suffix : (config.a11yEditableAgain || "Editing enabled."));
|
|
31900
|
+
}
|
|
31901
|
+
lastReadOnly = ro;
|
|
31902
|
+
}
|
|
31903
|
+
|
|
31087
31904
|
// ---------------------------------------------------- 1. keyboard trap
|
|
31088
31905
|
//
|
|
31089
31906
|
// Tab keeps its editing meaning (indent, next table cell) because that is
|
|
@@ -35450,7 +36267,29 @@ function RTE_Plugin_PdfExport() {
|
|
|
35450
36267
|
var self = this;
|
|
35451
36268
|
var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
|
|
35452
36269
|
|
|
35453
|
-
|
|
36270
|
+
// A block indented in the editor carries margin-left (or padding-left, per
|
|
36271
|
+
// indentUseMargin). Before 2026-09-04 the PDF honoured an indent only when
|
|
36272
|
+
// it was spelled <blockquote>, because that was the only way the editor
|
|
36273
|
+
// could express one. Points, since that is the PDF's unit: 1px = 0.75pt.
|
|
36274
|
+
function cssIndentPt(node) {
|
|
36275
|
+
if (!node || !node.style) return 0;
|
|
36276
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
36277
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
36278
|
+
if (!m) return 0;
|
|
36279
|
+
var n = parseFloat(m[1]);
|
|
36280
|
+
if (!n || n <= 0) return 0;
|
|
36281
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
36282
|
+
var px = unit === "px" ? n
|
|
36283
|
+
: unit === "pt" ? n * (4 / 3)
|
|
36284
|
+
: unit === "in" ? n * 96
|
|
36285
|
+
: unit === "cm" ? n * 37.795
|
|
36286
|
+
: unit === "mm" ? n * 3.7795
|
|
36287
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
36288
|
+
: 0;
|
|
36289
|
+
return px * 0.75;
|
|
36290
|
+
}
|
|
36291
|
+
|
|
36292
|
+
function block(node, style, indent, listMarker, listId, listOrdered, roleOverride) {
|
|
35454
36293
|
var tag = node.tagName.toLowerCase();
|
|
35455
36294
|
var s = styleOf(node, style);
|
|
35456
36295
|
var align = "left";
|
|
@@ -35476,12 +36315,18 @@ function RTE_Plugin_PdfExport() {
|
|
|
35476
36315
|
// untagged still fails an accessibility audit: assistive
|
|
35477
36316
|
// technology gets a flat stream with no headings, no list
|
|
35478
36317
|
// structure and no reliable reading order.
|
|
35479
|
-
|
|
36318
|
+
// roleOverride carries the quotation down to the paragraphs INSIDE a
|
|
36319
|
+
// <blockquote>. The role was only ever emitted for a blockquote
|
|
36320
|
+
// holding inline text directly - and the editor writes
|
|
36321
|
+
// <blockquote><p>...</p></blockquote>, so in practice every
|
|
36322
|
+
// blockquote we produce was tagged /P and the quotation was absent
|
|
36323
|
+
// from the accessibility tree we publish claims about.
|
|
36324
|
+
role: roleOverride || (HEADING_SCALE[tag] ? "H" + tag.charAt(1) : (tag === "blockquote" ? "BlockQuote" : "P")),
|
|
35480
36325
|
listId: listId || null, listOrdered: !!listOrdered
|
|
35481
36326
|
});
|
|
35482
36327
|
}
|
|
35483
36328
|
|
|
35484
|
-
function walk(node, style, indent) {
|
|
36329
|
+
function walk(node, style, indent, roleOverride) {
|
|
35485
36330
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
35486
36331
|
var c = node.childNodes[i];
|
|
35487
36332
|
if (c.nodeType === 3) {
|
|
@@ -35522,8 +36367,10 @@ function RTE_Plugin_PdfExport() {
|
|
|
35522
36367
|
continue;
|
|
35523
36368
|
}
|
|
35524
36369
|
if (isBlockTag(tag)) {
|
|
35525
|
-
|
|
35526
|
-
|
|
36370
|
+
var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
|
|
36371
|
+
var childRole = (tag === "blockquote") ? "BlockQuote" : roleOverride;
|
|
36372
|
+
if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own, childRole); continue; }
|
|
36373
|
+
block(c, style, indent + own, null, null, false, childRole);
|
|
35527
36374
|
continue;
|
|
35528
36375
|
}
|
|
35529
36376
|
// Inline content sitting directly under a container.
|
|
@@ -43026,7 +43873,9 @@ function RTE_Plugin_WordExport() {
|
|
|
43026
43873
|
"table { border-collapse: collapse; }\r\n" +
|
|
43027
43874
|
"td, th { border: 1px solid #999; padding: 4px 8px; }\r\n" +
|
|
43028
43875
|
"img { max-width: 100%; height: auto; }\r\n" +
|
|
43029
|
-
|
|
43876
|
+
// margin-left was zeroed here, which also flattened any indentation
|
|
43877
|
+
// expressed as a margin. Keep the quote bar, keep the offset.
|
|
43878
|
+
"blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #444; }\r\n" +
|
|
43030
43879
|
"pre { font-family: Consolas, 'Courier New', monospace; background: #f4f4f4; padding: 8px; }\r\n" +
|
|
43031
43880
|
"</style>\r\n" +
|
|
43032
43881
|
"</head>\r\n" +
|