@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
|
@@ -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
|
|
@@ -393,7 +393,29 @@ function RTE_Plugin_PdfExport() {
|
|
|
393
393
|
var self = this;
|
|
394
394
|
var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
|
|
395
395
|
|
|
396
|
-
|
|
396
|
+
// A block indented in the editor carries margin-left (or padding-left, per
|
|
397
|
+
// indentUseMargin). Before 2026-09-04 the PDF honoured an indent only when
|
|
398
|
+
// it was spelled <blockquote>, because that was the only way the editor
|
|
399
|
+
// could express one. Points, since that is the PDF's unit: 1px = 0.75pt.
|
|
400
|
+
function cssIndentPt(node) {
|
|
401
|
+
if (!node || !node.style) return 0;
|
|
402
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
403
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
404
|
+
if (!m) return 0;
|
|
405
|
+
var n = parseFloat(m[1]);
|
|
406
|
+
if (!n || n <= 0) return 0;
|
|
407
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
408
|
+
var px = unit === "px" ? n
|
|
409
|
+
: unit === "pt" ? n * (4 / 3)
|
|
410
|
+
: unit === "in" ? n * 96
|
|
411
|
+
: unit === "cm" ? n * 37.795
|
|
412
|
+
: unit === "mm" ? n * 3.7795
|
|
413
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
414
|
+
: 0;
|
|
415
|
+
return px * 0.75;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function block(node, style, indent, listMarker, listId, listOrdered, roleOverride) {
|
|
397
419
|
var tag = node.tagName.toLowerCase();
|
|
398
420
|
var s = styleOf(node, style);
|
|
399
421
|
var align = "left";
|
|
@@ -419,12 +441,18 @@ function RTE_Plugin_PdfExport() {
|
|
|
419
441
|
// untagged still fails an accessibility audit: assistive
|
|
420
442
|
// technology gets a flat stream with no headings, no list
|
|
421
443
|
// structure and no reliable reading order.
|
|
422
|
-
|
|
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")),
|
|
423
451
|
listId: listId || null, listOrdered: !!listOrdered
|
|
424
452
|
});
|
|
425
453
|
}
|
|
426
454
|
|
|
427
|
-
function walk(node, style, indent) {
|
|
455
|
+
function walk(node, style, indent, roleOverride) {
|
|
428
456
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
429
457
|
var c = node.childNodes[i];
|
|
430
458
|
if (c.nodeType === 3) {
|
|
@@ -465,8 +493,10 @@ function RTE_Plugin_PdfExport() {
|
|
|
465
493
|
continue;
|
|
466
494
|
}
|
|
467
495
|
if (isBlockTag(tag)) {
|
|
468
|
-
|
|
469
|
-
|
|
496
|
+
var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
|
|
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);
|
|
470
500
|
continue;
|
|
471
501
|
}
|
|
472
502
|
// Inline content sitting directly under a container.
|
|
@@ -146,7 +146,9 @@ function RTE_Plugin_WordExport() {
|
|
|
146
146
|
"table { border-collapse: collapse; }\r\n" +
|
|
147
147
|
"td, th { border: 1px solid #999; padding: 4px 8px; }\r\n" +
|
|
148
148
|
"img { max-width: 100%; height: auto; }\r\n" +
|
|
149
|
-
|
|
149
|
+
// margin-left was zeroed here, which also flattened any indentation
|
|
150
|
+
// expressed as a margin. Keep the quote bar, keep the offset.
|
|
151
|
+
"blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #444; }\r\n" +
|
|
150
152
|
"pre { font-family: Consolas, 'Courier New', monospace; background: #f4f4f4; padding: 8px; }\r\n" +
|
|
151
153
|
"</style>\r\n" +
|
|
152
154
|
"</head>\r\n" +
|