@richscripts/richtexteditor 2.7.0 → 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 -22
- package/package.json +146 -142
- package/richtexteditor/all_plugins.js +513 -14
- package/richtexteditor/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/accessibilitychecker.js +283 -0
- package/richtexteditor/plugins/all_plugins.js +513 -14
- package/richtexteditor/plugins/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/dialoga11y.js +50 -6
- package/richtexteditor/plugins/documentimport.js +34 -0
- package/richtexteditor/plugins/docxexport.js +26 -3
- package/richtexteditor/plugins/keyboarda11y.js +108 -0
- package/richtexteditor/plugins/pdfexport.js +12 -5
- package/richtexteditor/rte.js +41 -3
|
@@ -82,9 +82,24 @@ function RTE_Plugin_DialogA11y() {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// 2026-09-05 Widened. The original selector only matched
|
|
86
|
+
// <rte-dropdown-panel>/.rte-panel-general, but Find&Replace, Insert Link and
|
|
87
|
+
// their siblings render as <rte-dialog-float class="rte-panel-find"> wrapping
|
|
88
|
+
// an <rte-dialog-inner role="dialog">. Those carry EXACTLY the markup this
|
|
89
|
+
// plugin consumes - an <rte-dialog-header> and <rte-dialog-line-X> field
|
|
90
|
+
// wrappers - and were simply never visited, so the plugin reported nothing
|
|
91
|
+
// and covered nothing. Measured before the change: calling
|
|
92
|
+
// editor.applyDialogAccessibility() with the find dialog open labelled 0 of
|
|
93
|
+
// its 4 inputs, leaving "Find" and "Replace" announced as bare "edit"
|
|
94
|
+
// (WCAG 1.3.1 / 3.3.2 / 4.1.2) while the two checkboxes were fine because
|
|
95
|
+
// the core happens to wrap those in <label>.
|
|
96
|
+
//
|
|
97
|
+
// Widening is safe because enhancePanel() still requires an
|
|
98
|
+
// <rte-dialog-header>, which is what keeps colour/font dropdowns out.
|
|
85
99
|
function isPanel(el) {
|
|
86
100
|
return el && el.nodeType === 1 &&
|
|
87
|
-
(el.tagName === "RTE-DROPDOWN-PANEL" ||
|
|
101
|
+
(el.tagName === "RTE-DROPDOWN-PANEL" || el.tagName === "RTE-DIALOG-FLOAT" ||
|
|
102
|
+
/rte-panel-general/.test(el.className || ""));
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
function schedule(panel) {
|
|
@@ -104,7 +119,7 @@ function RTE_Plugin_DialogA11y() {
|
|
|
104
119
|
|
|
105
120
|
function enhanceAll(host) {
|
|
106
121
|
if (!host || !host.querySelectorAll) return;
|
|
107
|
-
var ps = host.querySelectorAll("rte-dropdown-panel,[class*='rte-panel-general']");
|
|
122
|
+
var ps = host.querySelectorAll("rte-dropdown-panel,rte-dialog-float,[class*='rte-panel-general']");
|
|
108
123
|
for (var i = 0; i < ps.length; i++) enhancePanel(ps[i]);
|
|
109
124
|
}
|
|
110
125
|
|
|
@@ -116,12 +131,22 @@ function RTE_Plugin_DialogA11y() {
|
|
|
116
131
|
var name = (headerEl.textContent || "").trim()
|
|
117
132
|
.replace(/\s*\((?:Ctrl|Cmd|Alt|Shift|⌘|⇧|⌥)[^)]*\)\s*$/i, "").trim();
|
|
118
133
|
|
|
134
|
+
// Name the element that IS the dialog. The float-style panels already
|
|
135
|
+
// carry role="dialog" on an inner <rte-dialog-inner>; adding a second
|
|
136
|
+
// role="dialog" to the wrapper would announce two nested dialogs, so
|
|
137
|
+
// when an inner one exists it is the target and the wrapper is left
|
|
138
|
+
// alone. When there is none (the original dropdown-panel shape) this
|
|
139
|
+
// resolves to the panel itself and behaves exactly as before.
|
|
140
|
+
var dialogEl = panel.getAttribute("role") === "dialog"
|
|
141
|
+
? panel
|
|
142
|
+
: (panel.querySelector('[role="dialog"]') || panel);
|
|
143
|
+
|
|
119
144
|
// A titled, form-bearing popup is a dialog, not a menu — override the
|
|
120
145
|
// core's default role="menu" (a menu must not contain text fields).
|
|
121
|
-
var role =
|
|
122
|
-
if (!role || role === "menu")
|
|
123
|
-
if (name && !
|
|
124
|
-
|
|
146
|
+
var role = dialogEl.getAttribute("role");
|
|
147
|
+
if (!role || role === "menu") dialogEl.setAttribute("role", "dialog");
|
|
148
|
+
if (name && !dialogEl.getAttribute("aria-label") && !dialogEl.getAttribute("aria-labelledby")) {
|
|
149
|
+
dialogEl.setAttribute("aria-label", name);
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
var fields = panel.querySelectorAll("input,select,textarea");
|
|
@@ -136,6 +161,25 @@ function RTE_Plugin_DialogA11y() {
|
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
function fieldLabel(f, dialogName) {
|
|
164
|
+
// 0) THE VISIBLE LABEL WINS. 2026-09-05: deriving the name from the
|
|
165
|
+
// <rte-dialog-line-X> suffix alone produced "Keyword" for a field whose
|
|
166
|
+
// visible label reads "Find". That fails WCAG 2.5.3 Label in Name, and
|
|
167
|
+
// it breaks speech input concretely: a user says "Find" and nothing
|
|
168
|
+
// matches, because the accessible name does not contain the words they
|
|
169
|
+
// can see. The markup already carries the right text in a sibling
|
|
170
|
+
// <rte-dialog-input-label>, so prefer it and keep the suffix as the
|
|
171
|
+
// fallback for fields that have no visible label.
|
|
172
|
+
var lineEl = f.parentElement;
|
|
173
|
+
for (var d = 0; d < 8 && lineEl; d++) {
|
|
174
|
+
if (/^RTE-DIALOG-LINE-/.test(lineEl.tagName || "")) break;
|
|
175
|
+
lineEl = lineEl.parentElement;
|
|
176
|
+
}
|
|
177
|
+
if (lineEl && lineEl.querySelector) {
|
|
178
|
+
var visEl = lineEl.querySelector("rte-dialog-input-label");
|
|
179
|
+
var vis = visEl ? (visEl.textContent || "").replace(/^\s+|\s+$/g, "").replace(/[::]\s*$/, "") : "";
|
|
180
|
+
if (vis) return vis;
|
|
181
|
+
}
|
|
182
|
+
|
|
139
183
|
// 1) nearest <rte-dialog-line-X> ancestor — the suffix is the field meaning
|
|
140
184
|
var n = f.parentElement;
|
|
141
185
|
for (var i = 0; i < 8 && n; i++) {
|
|
@@ -951,6 +951,25 @@ function RTE_Plugin_DocumentImport() {
|
|
|
951
951
|
var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
|
|
952
952
|
if (hv && hv !== "none") css += "background-color:" + hv + ";";
|
|
953
953
|
}
|
|
954
|
+
// w:shd is the OTHER way Word carries a background, and it is the
|
|
955
|
+
// one our own exporter writes (w:highlight only accepts a fixed
|
|
956
|
+
// set of colour NAMES, shd takes any hex). Reading only
|
|
957
|
+
// w:highlight meant every highlight we exported came back
|
|
958
|
+
// unhighlighted - the two sides used different vocabulary for the
|
|
959
|
+
// same thing.
|
|
960
|
+
var shd = firstChildEl(rpr, "shd");
|
|
961
|
+
if (shd) {
|
|
962
|
+
var fill = shd.getAttributeNS(W, "fill") || shd.getAttribute("w:fill") || "";
|
|
963
|
+
if (fill && /^[0-9A-Fa-f]{6}$/.test(fill)) css += "background-color:#" + fill + ";";
|
|
964
|
+
}
|
|
965
|
+
// Monospace run -> inline code. Our exporter marks <code>/<kbd>/
|
|
966
|
+
// <samp>/<pre> with w:rFonts Consolas; nothing read it back.
|
|
967
|
+
var rf = firstChildEl(rpr, "rFonts");
|
|
968
|
+
if (rf) {
|
|
969
|
+
var asc = rf.getAttributeNS(W, "ascii") || rf.getAttribute("w:ascii") || "";
|
|
970
|
+
if (/consolas|courier|monospace|mono$/i.test(asc)) txt = "<code>" + txt + "</code>";
|
|
971
|
+
else if (asc) css += "font-family:" + asc.replace(/[<>"]/g, "") + ";";
|
|
972
|
+
}
|
|
954
973
|
var sz = firstChildEl(rpr, "sz");
|
|
955
974
|
if (sz) {
|
|
956
975
|
var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
|
|
@@ -1123,7 +1142,16 @@ function RTE_Plugin_DocumentImport() {
|
|
|
1123
1142
|
// without this they came back as plain <p> and the round-trip lost
|
|
1124
1143
|
// every blockquote. Match Quote / IntenseQuote / BlockQuote.
|
|
1125
1144
|
else if (/^(intense)?quote$|blockquote/i.test(v.replace(/\s+/g, ""))) info.quote = true;
|
|
1145
|
+
// docxexport.js writes <w:pStyle w:val="Code"/> for <pre>; without
|
|
1146
|
+
// this a code block came back as an ordinary paragraph and lost
|
|
1147
|
+
// both its monospace and its whitespace.
|
|
1148
|
+
else if (/^(code|sourcecode|htmlpre|preformatted)$/i.test(v.replace(/\s+/g, ""))) info.pre = true;
|
|
1126
1149
|
}
|
|
1150
|
+
// A horizontal rule is exported as an EMPTY paragraph carrying a
|
|
1151
|
+
// bottom border - Word has no <hr>. Nothing read it back, so every
|
|
1152
|
+
// rule became a stray blank paragraph on import.
|
|
1153
|
+
var pbdr = firstChildEl(ppr, "pBdr");
|
|
1154
|
+
if (pbdr && firstChildEl(pbdr, "bottom")) info.rule = true;
|
|
1127
1155
|
var numPr = firstChildEl(ppr, "numPr");
|
|
1128
1156
|
if (numPr) {
|
|
1129
1157
|
info.list = true;
|
|
@@ -1217,6 +1245,12 @@ function RTE_Plugin_DocumentImport() {
|
|
|
1217
1245
|
continue;
|
|
1218
1246
|
}
|
|
1219
1247
|
closeListsTo(0);
|
|
1248
|
+
// Only a rule when the paragraph carries no text of its own -
|
|
1249
|
+
// a bordered paragraph WITH content is a real bordered paragraph.
|
|
1250
|
+
if (st.rule && !(inner || "").replace(/<[^>]*>/g, "").trim()) {
|
|
1251
|
+
closeListsTo(0); out.push("<hr>"); continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (st.pre) { out.push("<pre>" + (inner || "<br>") + "</pre>"); continue; }
|
|
1220
1254
|
if (st.heading) {
|
|
1221
1255
|
// Headings carry w:ind too; without this an indented
|
|
1222
1256
|
// heading imported flush left.
|
|
@@ -194,7 +194,7 @@ function RTE_Plugin_DocxExport() {
|
|
|
194
194
|
bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
|
|
195
195
|
strike: inherited.strike, sizeHalfPoints: inherited.sizeHalfPoints,
|
|
196
196
|
color: inherited.color, highlight: inherited.highlight, vertAlign: inherited.vertAlign,
|
|
197
|
-
mono: inherited.mono
|
|
197
|
+
mono: inherited.mono, font: inherited.font
|
|
198
198
|
};
|
|
199
199
|
var tag = node.tagName ? node.tagName.toLowerCase() : "";
|
|
200
200
|
if (tag === "b" || tag === "strong") s.bold = true;
|
|
@@ -204,6 +204,10 @@ function RTE_Plugin_DocxExport() {
|
|
|
204
204
|
if (tag === "sup") s.vertAlign = "superscript";
|
|
205
205
|
if (tag === "sub") s.vertAlign = "subscript";
|
|
206
206
|
if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "pre") s.mono = true;
|
|
207
|
+
// <mark> carries no inline style, so the CSS branch below never sees it
|
|
208
|
+
// and highlighted text was dropped entirely on export. Word's own
|
|
209
|
+
// default highlight is yellow.
|
|
210
|
+
if (tag === "mark") s.highlight = s.highlight || "FFFF00";
|
|
207
211
|
|
|
208
212
|
var css = node.style;
|
|
209
213
|
if (css) {
|
|
@@ -214,6 +218,12 @@ function RTE_Plugin_DocxExport() {
|
|
|
214
218
|
if (deco.indexOf("line-through") >= 0) s.strike = true;
|
|
215
219
|
if (css.color) { var c = hexColor(css.color); if (c) s.color = c; }
|
|
216
220
|
if (css.backgroundColor) { var h = hexColor(css.backgroundColor); if (h) s.highlight = h; }
|
|
221
|
+
// Font family was read by nothing, so an exported document lost every
|
|
222
|
+
// typeface choice. Word wants ONE family name, not a CSS stack.
|
|
223
|
+
if (css.fontFamily) {
|
|
224
|
+
var fam = String(css.fontFamily).split(",")[0].replace(/^\s*['"]?|['"]?\s*$/g, "");
|
|
225
|
+
if (fam) s.font = fam;
|
|
226
|
+
}
|
|
217
227
|
if (css.fontSize) {
|
|
218
228
|
var pt = cssToPoints(css.fontSize);
|
|
219
229
|
// w:sz is in HALF-POINTS. Writing points here makes every
|
|
@@ -250,7 +260,10 @@ function RTE_Plugin_DocxExport() {
|
|
|
250
260
|
|
|
251
261
|
function runProps(s) {
|
|
252
262
|
var p = "";
|
|
253
|
-
|
|
263
|
+
// One rFonts element only: mono wins, because <code> inside a styled span
|
|
264
|
+
// should still read as code.
|
|
265
|
+
var fontName = s.mono ? "Consolas" : (s.font || "");
|
|
266
|
+
if (fontName) p += '<w:rFonts w:ascii="' + esc(fontName) + '" w:hAnsi="' + esc(fontName) + '"/>';
|
|
254
267
|
if (s.bold) p += "<w:b/>";
|
|
255
268
|
if (s.italic) p += "<w:i/>";
|
|
256
269
|
if (s.underline) p += '<w:u w:val="single"/>';
|
|
@@ -668,7 +681,17 @@ function RTE_Plugin_DocxExport() {
|
|
|
668
681
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
669
682
|
var li = node.childNodes[i];
|
|
670
683
|
if (li.nodeType !== 1 || li.tagName.toLowerCase() !== "li") continue;
|
|
671
|
-
|
|
684
|
+
// Serialise the item's OWN content only. para() walks every
|
|
685
|
+
// descendant, so passing the whole <li> emitted the nested list's
|
|
686
|
+
// text here AND again as its own items below - a round trip turned
|
|
687
|
+
// "xx" with a child "yy" into "xxyy". Content duplication, not loss.
|
|
688
|
+
var own = li.ownerDocument.createElement("li");
|
|
689
|
+
for (var k = 0; k < li.childNodes.length; k++) {
|
|
690
|
+
var ch = li.childNodes[k];
|
|
691
|
+
if (ch.nodeType === 1 && /^(ul|ol)$/.test(ch.tagName.toLowerCase())) continue;
|
|
692
|
+
own.appendChild(ch.cloneNode(true));
|
|
693
|
+
}
|
|
694
|
+
xml += para(own, styleFrom(li, style), { numId: numId, level: level, pageBreakBefore: pendingBreak });
|
|
672
695
|
pendingBreak = false;
|
|
673
696
|
for (var j = 0; j < li.childNodes.length; j++) {
|
|
674
697
|
var sub = li.childNodes[j];
|
|
@@ -55,6 +55,21 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
55
55
|
if (config.keyboardA11y === false) return;
|
|
56
56
|
|
|
57
57
|
editor.focusToolbar = function () { return focusFirstToolbarButton(); };
|
|
58
|
+
// Public so plugins can report their own outcomes -- find/replace match
|
|
59
|
+
// counts, what the paste filter removed, export completion.
|
|
60
|
+
editor.announce = function (text, opts) { return announce(text, opts); };
|
|
61
|
+
// setReadOnly() lives in the core and cannot call into a plugin, so wrap
|
|
62
|
+
// it here rather than leaving the two states to drift apart.
|
|
63
|
+
if (typeof editor.setReadOnly === "function" && !editor.setReadOnly.__a11yWrapped) {
|
|
64
|
+
var origSetReadOnly = editor.setReadOnly;
|
|
65
|
+
var wrapped = function (v) {
|
|
66
|
+
var r = origSetReadOnly.apply(editor, arguments);
|
|
67
|
+
try { syncReadOnlyState(); } catch (e) { }
|
|
68
|
+
return r;
|
|
69
|
+
};
|
|
70
|
+
wrapped.__a11yWrapped = true;
|
|
71
|
+
editor.setReadOnly = wrapped;
|
|
72
|
+
}
|
|
58
73
|
|
|
59
74
|
setup();
|
|
60
75
|
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
@@ -78,6 +93,8 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
78
93
|
function setup() {
|
|
79
94
|
var root = shell();
|
|
80
95
|
if (!root) return;
|
|
96
|
+
ensureLiveRegions();
|
|
97
|
+
syncReadOnlyState();
|
|
81
98
|
bindEscapeHatch();
|
|
82
99
|
applyRovingTabindex(root);
|
|
83
100
|
syncToggleStates(root);
|
|
@@ -85,6 +102,97 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
85
102
|
watch(root);
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
|
|
106
|
+
// ------------------------------------------------- 4. status messages
|
|
107
|
+
//
|
|
108
|
+
// WCAG 4.1.3 Status Messages (Level AA): a change of state that is NOT
|
|
109
|
+
// given focus still has to reach assistive technology. Before this the
|
|
110
|
+
// editor had no live region at all -- not one aria-live node, in the page
|
|
111
|
+
// or in the iframe -- so a screen reader user got silence for every
|
|
112
|
+
// outcome the sighted user reads off the chrome: how many matches Find
|
|
113
|
+
// found, what the paste filter stripped, that the length limit was hit,
|
|
114
|
+
// that the document went read-only.
|
|
115
|
+
//
|
|
116
|
+
// Two regions, because politeness is not a detail: 'polite' waits for a
|
|
117
|
+
// pause in speech (counts, confirmations), 'assertive' interrupts (errors,
|
|
118
|
+
// refusals). aria-atomic="true" on both so the whole message is re-read
|
|
119
|
+
// rather than only the words that changed -- reading a diff aloud produces
|
|
120
|
+
// sentences that were never written.
|
|
121
|
+
//
|
|
122
|
+
// The region lives in the HOST document, not the editing iframe: a message
|
|
123
|
+
// announced from inside the document the user is editing would also become
|
|
124
|
+
// part of what they are editing.
|
|
125
|
+
var liveNodes = null;
|
|
126
|
+
function ensureLiveRegions() {
|
|
127
|
+
if (liveNodes && liveNodes.polite && liveNodes.polite.isConnected) return liveNodes;
|
|
128
|
+
var root = shell();
|
|
129
|
+
if (!root) return null;
|
|
130
|
+
function mk(politeness) {
|
|
131
|
+
var el = root.querySelector('[data-rte-live="' + politeness + '"]');
|
|
132
|
+
if (el) return el;
|
|
133
|
+
el = document.createElement("div");
|
|
134
|
+
el.setAttribute("data-rte-live", politeness);
|
|
135
|
+
el.setAttribute("aria-live", politeness);
|
|
136
|
+
el.setAttribute("aria-atomic", "true");
|
|
137
|
+
el.setAttribute("role", politeness === "assertive" ? "alert" : "status");
|
|
138
|
+
// Visually hidden but still rendered: display:none and
|
|
139
|
+
// visibility:hidden remove the node from the accessibility tree,
|
|
140
|
+
// which silences it. Clip is the technique that does not.
|
|
141
|
+
el.style.cssText = "position:absolute;width:1px;height:1px;margin:-1px;" +
|
|
142
|
+
"padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;";
|
|
143
|
+
root.appendChild(el);
|
|
144
|
+
return el;
|
|
145
|
+
}
|
|
146
|
+
liveNodes = { polite: mk("polite"), assertive: mk("assertive") };
|
|
147
|
+
return liveNodes;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// editor.announce(text, {assertive}) -- also used by plugins.
|
|
151
|
+
function announce(text, opts) {
|
|
152
|
+
var msg = String(text == null ? "" : text).trim();
|
|
153
|
+
if (!msg) return false;
|
|
154
|
+
var nodes = ensureLiveRegions();
|
|
155
|
+
if (!nodes) return false;
|
|
156
|
+
var el = (opts && opts.assertive) ? nodes.assertive : nodes.polite;
|
|
157
|
+
// Re-announcing the SAME string is a no-op for most screen readers
|
|
158
|
+
// unless the node is cleared first, so "3 results" after "3 results"
|
|
159
|
+
// would be silent. Clear, then set on the next frame.
|
|
160
|
+
el.textContent = "";
|
|
161
|
+
setTimeout(function () { el.textContent = msg; }, 30);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ------------------------------------------------- 5. read-only state
|
|
166
|
+
//
|
|
167
|
+
// setReadOnly() flips designMode, which genuinely blocks editing -- but
|
|
168
|
+
// designMode is invisible to assistive technology. The toolbar already
|
|
169
|
+
// marks its buttons aria-disabled; the editing region itself said nothing,
|
|
170
|
+
// so a screen reader user entered a field described as a rich text editor,
|
|
171
|
+
// typed, and got no response and no explanation. aria-readonly is the
|
|
172
|
+
// attribute that carries this, and the accessible name says it too because
|
|
173
|
+
// a name is what gets announced on entry.
|
|
174
|
+
var lastReadOnly = null;
|
|
175
|
+
function syncReadOnlyState() {
|
|
176
|
+
var ed;
|
|
177
|
+
try { ed = editor.getEditable(); } catch (e) { return; }
|
|
178
|
+
if (!ed) return;
|
|
179
|
+
var ro = false;
|
|
180
|
+
try { ro = !!(editor.getReadOnly ? editor.getReadOnly() : editor.isReadOnly && editor.isReadOnly()); } catch (e) { }
|
|
181
|
+
ed.setAttribute("aria-readonly", ro ? "true" : "false");
|
|
182
|
+
var suffix = config.a11yReadOnlySuffix || "Read only.";
|
|
183
|
+
// Plain string trimming rather than a built regex: the suffix is
|
|
184
|
+
// config-supplied and would otherwise need escaping to be safe.
|
|
185
|
+
var label = (ed.getAttribute("aria-label") || "");
|
|
186
|
+
if (label.length >= suffix.length && label.slice(-suffix.length) === suffix) {
|
|
187
|
+
label = label.slice(0, -suffix.length).replace(/\s+$/, "");
|
|
188
|
+
}
|
|
189
|
+
ed.setAttribute("aria-label", ro ? (label + " " + suffix) : label);
|
|
190
|
+
if (lastReadOnly !== null && lastReadOnly !== ro) {
|
|
191
|
+
announce(ro ? suffix : (config.a11yEditableAgain || "Editing enabled."));
|
|
192
|
+
}
|
|
193
|
+
lastReadOnly = ro;
|
|
194
|
+
}
|
|
195
|
+
|
|
88
196
|
// ---------------------------------------------------- 1. keyboard trap
|
|
89
197
|
//
|
|
90
198
|
// Tab keeps its editing meaning (indent, next table cell) because that is
|
|
@@ -415,7 +415,7 @@ function RTE_Plugin_PdfExport() {
|
|
|
415
415
|
return px * 0.75;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
function block(node, style, indent, listMarker, listId, listOrdered) {
|
|
418
|
+
function block(node, style, indent, listMarker, listId, listOrdered, roleOverride) {
|
|
419
419
|
var tag = node.tagName.toLowerCase();
|
|
420
420
|
var s = styleOf(node, style);
|
|
421
421
|
var align = "left";
|
|
@@ -441,12 +441,18 @@ function RTE_Plugin_PdfExport() {
|
|
|
441
441
|
// untagged still fails an accessibility audit: assistive
|
|
442
442
|
// technology gets a flat stream with no headings, no list
|
|
443
443
|
// structure and no reliable reading order.
|
|
444
|
-
|
|
444
|
+
// roleOverride carries the quotation down to the paragraphs INSIDE a
|
|
445
|
+
// <blockquote>. The role was only ever emitted for a blockquote
|
|
446
|
+
// holding inline text directly - and the editor writes
|
|
447
|
+
// <blockquote><p>...</p></blockquote>, so in practice every
|
|
448
|
+
// blockquote we produce was tagged /P and the quotation was absent
|
|
449
|
+
// from the accessibility tree we publish claims about.
|
|
450
|
+
role: roleOverride || (HEADING_SCALE[tag] ? "H" + tag.charAt(1) : (tag === "blockquote" ? "BlockQuote" : "P")),
|
|
445
451
|
listId: listId || null, listOrdered: !!listOrdered
|
|
446
452
|
});
|
|
447
453
|
}
|
|
448
454
|
|
|
449
|
-
function walk(node, style, indent) {
|
|
455
|
+
function walk(node, style, indent, roleOverride) {
|
|
450
456
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
451
457
|
var c = node.childNodes[i];
|
|
452
458
|
if (c.nodeType === 3) {
|
|
@@ -488,8 +494,9 @@ function RTE_Plugin_PdfExport() {
|
|
|
488
494
|
}
|
|
489
495
|
if (isBlockTag(tag)) {
|
|
490
496
|
var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
|
|
491
|
-
|
|
492
|
-
|
|
497
|
+
var childRole = (tag === "blockquote") ? "BlockQuote" : roleOverride;
|
|
498
|
+
if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own, childRole); continue; }
|
|
499
|
+
block(c, style, indent + own, null, null, false, childRole);
|
|
493
500
|
continue;
|
|
494
501
|
}
|
|
495
502
|
// Inline content sitting directly under a container.
|