@richscripts/richtexteditor 2.5.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +449 -360
- package/package.json +142 -142
- package/richtexteditor/CHANGELOG.md +2714 -2201
- package/richtexteditor/all_plugins.js +1064 -310
- package/richtexteditor/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/accessibilitychecker.js +67 -4
- package/richtexteditor/plugins/aitoolkit.js +24 -6
- package/richtexteditor/plugins/aitoolkit.min.css +1 -0
- package/richtexteditor/plugins/all_plugins.js +1064 -310
- package/richtexteditor/plugins/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/keyboarda11y.js +370 -342
- package/richtexteditor/plugins/layouttable.js +383 -0
- package/richtexteditor/plugins/shadowdom.js +252 -0
- package/richtexteditor/plugins/slashcommand.js +8 -0
- package/richtexteditor/rte-watchdog.js +303 -0
- package/richtexteditor/rte-webcomponent.js +351 -0
- package/richtexteditor/rte_mobile.min.css +1 -0
- package/richtexteditor/rte_theme_default.css +1 -1
- package/richtexteditor/rte_theme_default.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_content.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_preview.min.css +1 -0
|
@@ -1,342 +1,370 @@
|
|
|
1
|
-
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
-
|
|
3
|
-
// 2026-08-02 Keyboard and state accessibility for the editor CHROME.
|
|
4
|
-
//
|
|
5
|
-
// a11yenhance.js names the editing region and dialoga11y.js names the dialogs.
|
|
6
|
-
// This closes what an audit of the running editor found still open — four
|
|
7
|
-
// findings, three of them WCAG Level A:
|
|
8
|
-
//
|
|
9
|
-
// 1. KEYBOARD TRAP (2.1.2, Level A). Tab inside the editing area is
|
|
10
|
-
// preventDefault-ed and inserts spaces, so a keyboard-only or screen
|
|
11
|
-
// reader user who enters the editor can never leave it. Reloading the page
|
|
12
|
-
// is the only way out. This is the most serious kind of accessibility
|
|
13
|
-
// defect: it does not degrade the experience, it ends it.
|
|
14
|
-
//
|
|
15
|
-
// 2. TOGGLE STATE NOT EXPOSED (4.1.2, Level A). Bold, italic, underline and
|
|
16
|
-
// the alignment buttons carry their state in a CSS class
|
|
17
|
-
// (rte-command-active / rte-command-deactive) and nowhere else. Sighted
|
|
18
|
-
// users see a highlighted button; assistive technology is told nothing.
|
|
19
|
-
// Measured: 0 of 6 toggle buttons exposed aria-pressed.
|
|
20
|
-
//
|
|
21
|
-
// 3. POPUP STATE NOT EXPOSED (4.1.2, Level A). Buttons carry
|
|
22
|
-
// aria-haspopup but never aria-expanded, so there is no way to know
|
|
23
|
-
// whether a menu is open. Measured: 0 of 7.
|
|
24
|
-
//
|
|
25
|
-
// 4. EVERY TOOLBAR BUTTON IS A TAB STOP (2.4.3, and plain usability).
|
|
26
|
-
// Measured 53. The ARIA Authoring Practices toolbar pattern is a single
|
|
27
|
-
// tab stop per toolbar with arrow keys moving between buttons. 53 presses
|
|
28
|
-
// of Tab to reach the text you came to write is not operable in any
|
|
29
|
-
// meaningful sense.
|
|
30
|
-
//
|
|
31
|
-
// All four are fixed here rather than in the core, so they ship without a
|
|
32
|
-
// re-obfuscation cycle.
|
|
33
|
-
//
|
|
34
|
-
// Config:
|
|
35
|
-
// config.keyboardA11y = false // opt out entirely
|
|
36
|
-
// config.a11yEscapeHint = "..." // wording appended to the editing area's name
|
|
37
|
-
// config.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
setTimeout(setup,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
var
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// the
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
var
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-08-02 Keyboard and state accessibility for the editor CHROME.
|
|
4
|
+
//
|
|
5
|
+
// a11yenhance.js names the editing region and dialoga11y.js names the dialogs.
|
|
6
|
+
// This closes what an audit of the running editor found still open — four
|
|
7
|
+
// findings, three of them WCAG Level A:
|
|
8
|
+
//
|
|
9
|
+
// 1. KEYBOARD TRAP (2.1.2, Level A). Tab inside the editing area is
|
|
10
|
+
// preventDefault-ed and inserts spaces, so a keyboard-only or screen
|
|
11
|
+
// reader user who enters the editor can never leave it. Reloading the page
|
|
12
|
+
// is the only way out. This is the most serious kind of accessibility
|
|
13
|
+
// defect: it does not degrade the experience, it ends it.
|
|
14
|
+
//
|
|
15
|
+
// 2. TOGGLE STATE NOT EXPOSED (4.1.2, Level A). Bold, italic, underline and
|
|
16
|
+
// the alignment buttons carry their state in a CSS class
|
|
17
|
+
// (rte-command-active / rte-command-deactive) and nowhere else. Sighted
|
|
18
|
+
// users see a highlighted button; assistive technology is told nothing.
|
|
19
|
+
// Measured: 0 of 6 toggle buttons exposed aria-pressed.
|
|
20
|
+
//
|
|
21
|
+
// 3. POPUP STATE NOT EXPOSED (4.1.2, Level A). Buttons carry
|
|
22
|
+
// aria-haspopup but never aria-expanded, so there is no way to know
|
|
23
|
+
// whether a menu is open. Measured: 0 of 7.
|
|
24
|
+
//
|
|
25
|
+
// 4. EVERY TOOLBAR BUTTON IS A TAB STOP (2.4.3, and plain usability).
|
|
26
|
+
// Measured 53. The ARIA Authoring Practices toolbar pattern is a single
|
|
27
|
+
// tab stop per toolbar with arrow keys moving between buttons. 53 presses
|
|
28
|
+
// of Tab to reach the text you came to write is not operable in any
|
|
29
|
+
// meaningful sense.
|
|
30
|
+
//
|
|
31
|
+
// All four are fixed here rather than in the core, so they ship without a
|
|
32
|
+
// re-obfuscation cycle.
|
|
33
|
+
//
|
|
34
|
+
// Config:
|
|
35
|
+
// config.keyboardA11y = false // opt out entirely
|
|
36
|
+
// config.a11yEscapeHint = "..." // wording appended to the editing area's name
|
|
37
|
+
// config.a11yEditorLabel = "..." // accessible NAME of the editing area (default "Rich text editor")
|
|
38
|
+
// config.a11yRovingToolbar = false // keep every button as a tab stop
|
|
39
|
+
RTE_DefaultConfig.plugin_keyboarda11y = RTE_Plugin_KeyboardA11y;
|
|
40
|
+
if (typeof RTE_DefaultConfig.keyboardA11y === "undefined") RTE_DefaultConfig.keyboardA11y = true;
|
|
41
|
+
|
|
42
|
+
function RTE_Plugin_KeyboardA11y() {
|
|
43
|
+
var obj = this;
|
|
44
|
+
var config, editor;
|
|
45
|
+
var observers = [];
|
|
46
|
+
var popupOwner = null; // last activated [aria-haspopup]
|
|
47
|
+
var openPanels = []; // [{ panel, owner }]
|
|
48
|
+
|
|
49
|
+
obj.PluginName = "KeyboardA11y";
|
|
50
|
+
|
|
51
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
52
|
+
|
|
53
|
+
obj.InitEditor = function (argeditor) {
|
|
54
|
+
editor = argeditor;
|
|
55
|
+
if (config.keyboardA11y === false) return;
|
|
56
|
+
|
|
57
|
+
editor.focusToolbar = function () { return focusFirstToolbarButton(); };
|
|
58
|
+
|
|
59
|
+
setup();
|
|
60
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
61
|
+
// The toolbar is built asynchronously; a deferred pass catches the case
|
|
62
|
+
// where InitEditor runs before it exists.
|
|
63
|
+
setTimeout(setup, 0);
|
|
64
|
+
setTimeout(setup, 400);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function shell() {
|
|
68
|
+
try {
|
|
69
|
+
var ed = editor.getEditable();
|
|
70
|
+
if (!ed) return null;
|
|
71
|
+
var win = ed.ownerDocument.defaultView;
|
|
72
|
+
var node = (win && win.frameElement) ? win.frameElement : ed;
|
|
73
|
+
while (node && node.classList && !node.classList.contains("richtexteditor")) node = node.parentNode;
|
|
74
|
+
return (node && node.classList) ? node : null;
|
|
75
|
+
} catch (e) { return null; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setup() {
|
|
79
|
+
var root = shell();
|
|
80
|
+
if (!root) return;
|
|
81
|
+
bindEscapeHatch();
|
|
82
|
+
applyRovingTabindex(root);
|
|
83
|
+
syncToggleStates(root);
|
|
84
|
+
trackPopups(root);
|
|
85
|
+
watch(root);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------- 1. keyboard trap
|
|
89
|
+
//
|
|
90
|
+
// Tab keeps its editing meaning (indent, next table cell) because that is
|
|
91
|
+
// what writers expect and what every other editor does. The escape is a
|
|
92
|
+
// separate, documented key: Escape leaves the editing area and puts focus
|
|
93
|
+
// on the toolbar, from which Tab continues through the page normally.
|
|
94
|
+
//
|
|
95
|
+
// Escape is only intercepted when nothing is open — a dialog or dropdown
|
|
96
|
+
// must still get its own Escape first, or closing a colour picker would
|
|
97
|
+
// throw the user out of the editor.
|
|
98
|
+
// Bound on the DOCUMENT rather than on the body element. Escape bubbles, so
|
|
99
|
+
// one listener at document level covers the editing area no matter how the
|
|
100
|
+
// surface is re-created, and it does not depend on a marker flag pinned to a
|
|
101
|
+
// particular element surviving. Defensive only: no shipped operation is known
|
|
102
|
+
// to replace the editing body — setHTML, htmlview, fullscreen, readingmode,
|
|
103
|
+
// preview and toggleborder all preserve its identity (measured 2026-08-27).
|
|
104
|
+
// Keeping the binding here costs nothing and removes an assumption, but it
|
|
105
|
+
// fixes no known live defect; do not describe it as a bug fix.
|
|
106
|
+
function bindEscapeHatch() {
|
|
107
|
+
var ed, doc;
|
|
108
|
+
try { ed = editor.getEditable(); doc = ed && ed.ownerDocument; } catch (e) { return; }
|
|
109
|
+
if (!ed || !doc) return;
|
|
110
|
+
|
|
111
|
+
if (!doc.__rteEscapeHatch) {
|
|
112
|
+
doc.__rteEscapeHatch = true;
|
|
113
|
+
doc.addEventListener("keydown", function (e) {
|
|
114
|
+
if (e.key !== "Escape" && e.keyCode !== 27) return;
|
|
115
|
+
if (anythingOpen()) return; // let the panel close itself
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
if (!focusFirstToolbarButton()) focusAfterEditor();
|
|
119
|
+
}, false);
|
|
120
|
+
|
|
121
|
+
// Deliberately not re-asserting the body's aria-label here. An
|
|
122
|
+
// earlier version did, on the theory that the editing body could be
|
|
123
|
+
// replaced underneath us; that mechanism was retracted 2026-08-27
|
|
124
|
+
// after it turned out to be an instrumentation artefact, and
|
|
125
|
+
// editor.getEditable() is verified to return the live, connected
|
|
126
|
+
// body on every shipped path. announceEscape() at the end of this
|
|
127
|
+
// function is sufficient.
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
announceEscape(ed);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function anythingOpen() {
|
|
134
|
+
try {
|
|
135
|
+
var panels = document.querySelectorAll("rte-dropdown-panel, rte-dialog-float, rte-floatpanel");
|
|
136
|
+
for (var i = 0; i < panels.length; i++) {
|
|
137
|
+
var s = getComputedStyle(panels[i]);
|
|
138
|
+
if (s.display !== "none" && s.visibility !== "hidden") return true;
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The way out has to be discoverable, or it may as well not exist. The
|
|
145
|
+
// editing region's accessible name is the one thing a screen reader always
|
|
146
|
+
// announces on entry, so the hint goes there rather than into the content.
|
|
147
|
+
//
|
|
148
|
+
// But an instruction is not a NAME. When the host page had not labelled the
|
|
149
|
+
// editing area, appending the hint to an empty label left the region's
|
|
150
|
+
// accessible name as nothing but "Press Escape to leave the editing area." —
|
|
151
|
+
// a screen reader then told the user how to LEAVE a field without ever
|
|
152
|
+
// saying what it was, and two editors on one page were indistinguishable.
|
|
153
|
+
// So: a host-supplied label still wins as the name; otherwise we supply a
|
|
154
|
+
// real one and the hint follows it.
|
|
155
|
+
function announceEscape(ed) {
|
|
156
|
+
var hint = config.a11yEscapeHint ||
|
|
157
|
+
"Press Escape to leave the editing area.";
|
|
158
|
+
var label = ed.getAttribute("aria-label") || "";
|
|
159
|
+
if (label.indexOf(hint) >= 0) return;
|
|
160
|
+
var name = label || config.a11yEditorLabel || "Rich text editor";
|
|
161
|
+
ed.setAttribute("aria-label", name.replace(/\s*$/, "") + " " + hint);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function focusAfterEditor() {
|
|
165
|
+
var root = shell();
|
|
166
|
+
if (!root) return false;
|
|
167
|
+
var all = [].slice.call(document.querySelectorAll(
|
|
168
|
+
'a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
|
|
169
|
+
)).filter(function (el) {
|
|
170
|
+
return !root.contains(el) && el.offsetParent !== null;
|
|
171
|
+
});
|
|
172
|
+
// The first focusable that follows the editor in document order.
|
|
173
|
+
for (var i = 0; i < all.length; i++) {
|
|
174
|
+
if (root.compareDocumentPosition(all[i]) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
175
|
+
all[i].focus();
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ------------------------------------------------- 4. roving tabindex
|
|
183
|
+
function toolbarsIn(root) {
|
|
184
|
+
return [].slice.call(root.querySelectorAll('[role="toolbar"]'));
|
|
185
|
+
}
|
|
186
|
+
// Toolbars nest — an overflow or ribbon toolbar sits inside the main one.
|
|
187
|
+
// A plain descendant query therefore makes the outer toolbar claim the
|
|
188
|
+
// inner one's buttons, and the two passes fight: the first run measured two
|
|
189
|
+
// tab stops in one toolbar and NONE in the other two, which left those
|
|
190
|
+
// buttons unreachable by keyboard altogether. Each button belongs to its
|
|
191
|
+
// NEAREST toolbar and to no other.
|
|
192
|
+
function buttonsIn(bar) {
|
|
193
|
+
return [].slice.call(bar.querySelectorAll('[role="button"]')).filter(function (b) {
|
|
194
|
+
if (b.getAttribute("aria-disabled") === "true") return false;
|
|
195
|
+
if (b.offsetParent === null) return false; // hidden: not reachable anyway
|
|
196
|
+
return (b.closest && b.closest('[role="toolbar"]')) === bar;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function applyRovingTabindex(root) {
|
|
201
|
+
if (config.a11yRovingToolbar === false) return;
|
|
202
|
+
var bars = toolbarsIn(root);
|
|
203
|
+
for (var i = 0; i < bars.length; i++) {
|
|
204
|
+
(function (bar) {
|
|
205
|
+
var btns = buttonsIn(bar);
|
|
206
|
+
if (!btns.length) return;
|
|
207
|
+
// Exactly one tab stop per toolbar, which is the ARIA pattern.
|
|
208
|
+
var current = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
|
|
209
|
+
for (var j = 0; j < btns.length; j++) btns[j].setAttribute("tabindex", btns[j] === current ? "0" : "-1");
|
|
210
|
+
|
|
211
|
+
if (bar.__rteRoving) return;
|
|
212
|
+
bar.__rteRoving = true;
|
|
213
|
+
bar.addEventListener("keydown", function (e) {
|
|
214
|
+
var list = buttonsIn(bar);
|
|
215
|
+
var at = list.indexOf(document.activeElement);
|
|
216
|
+
if (at < 0) return;
|
|
217
|
+
var rtl = (bar.closest && bar.closest('[dir="rtl"]')) ? true : false;
|
|
218
|
+
var next = null;
|
|
219
|
+
if (e.key === "ArrowRight") next = list[(at + (rtl ? -1 : 1) + list.length) % list.length];
|
|
220
|
+
else if (e.key === "ArrowLeft") next = list[(at + (rtl ? 1 : -1) + list.length) % list.length];
|
|
221
|
+
else if (e.key === "Home") next = list[0];
|
|
222
|
+
else if (e.key === "End") next = list[list.length - 1];
|
|
223
|
+
else return;
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === next ? "0" : "-1");
|
|
226
|
+
next.focus();
|
|
227
|
+
}, false);
|
|
228
|
+
// Clicking a button makes it the new tab stop, so returning by
|
|
229
|
+
// Tab lands where the user last was.
|
|
230
|
+
bar.addEventListener("focusin", function (e) {
|
|
231
|
+
var list = buttonsIn(bar);
|
|
232
|
+
if (list.indexOf(e.target) < 0) return;
|
|
233
|
+
for (var k = 0; k < list.length; k++) list[k].setAttribute("tabindex", list[k] === e.target ? "0" : "-1");
|
|
234
|
+
}, false);
|
|
235
|
+
})(bars[i]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function focusFirstToolbarButton() {
|
|
240
|
+
var root = shell();
|
|
241
|
+
if (!root) return false;
|
|
242
|
+
var bars = toolbarsIn(root);
|
|
243
|
+
for (var i = 0; i < bars.length; i++) {
|
|
244
|
+
var btns = buttonsIn(bars[i]);
|
|
245
|
+
if (!btns.length) continue;
|
|
246
|
+
var target = btns.filter(function (b) { return b.getAttribute("tabindex") === "0"; })[0] || btns[0];
|
|
247
|
+
target.focus();
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ------------------------------------------------- 2. aria-pressed
|
|
254
|
+
//
|
|
255
|
+
// The editor already tracks active state — it just keeps it in a class.
|
|
256
|
+
// Mirroring rather than recomputing means the announced state can never
|
|
257
|
+
// disagree with the highlighted button.
|
|
258
|
+
// Command names here must match the TOOLBAR command exactly — the regex is
|
|
259
|
+
// anchored, so a near-miss is a silent no-op that looks like coverage.
|
|
260
|
+
// "blockquote" is one: the command is `insertblockquote`, so the entry
|
|
261
|
+
// written specifically for it never matched and that button ships with no
|
|
262
|
+
// aria-pressed. Found by checking every genuine toggle on the full toolbar
|
|
263
|
+
// rather than the three the published verify page samples.
|
|
264
|
+
//
|
|
265
|
+
// This mirror only copies the editor's own active state, so a command may
|
|
266
|
+
// only be listed here once core actually tracks it. Emitting the attribute
|
|
267
|
+
// for an untracked command yields a permanent aria-pressed="false" —
|
|
268
|
+
// confidently announcing "not pressed" while the formatting IS applied,
|
|
269
|
+
// which makes announced state DISAGREE with the highlight. That is the exact
|
|
270
|
+
// failure this mirror exists to prevent, so silence beats a wrong answer.
|
|
271
|
+
// - `toggleborder` was already tracked (core checks the editable's
|
|
272
|
+
// rte-toggleborder class); it reads inactive simply when borders are off.
|
|
273
|
+
// - `insertblockquote` was NOT tracked and now is, via a core case added
|
|
274
|
+
// alongside `indent`, which resolves the same way.
|
|
275
|
+
// Both verified on the running editor rather than inferred.
|
|
276
|
+
//
|
|
277
|
+
// `inlinecode`, `pagination`, `typewriter` and `focusmode` match nothing in
|
|
278
|
+
// this build, and now the reason is known rather than assumed: pagination.js
|
|
279
|
+
// and typewriter.js are API-only (togglePageView / toggleTypewriterMode …)
|
|
280
|
+
// and register no toolbar command, and insertcode.js registers `insertcode`,
|
|
281
|
+
// which inserts a block rather than toggling the selection. They are kept —
|
|
282
|
+
// harmless, and correct the moment a toolbar button is added — but they
|
|
283
|
+
// cover nothing today, so do not read this list as evidence of coverage.
|
|
284
|
+
var TOGGLE_CMD = /^(bold|italic|underline|strikethrough|subscript|superscript|justifyleft|justifycenter|justifyright|justifyfull|insertorderedlist|insertunorderedlist|outdent|indent|insertblockquote|toggleborder|inlinecode|trackchanges|typewriter|focusmode|pagination|formattingmarks|linenumbers|permanentpen|rtlui)$/;
|
|
285
|
+
|
|
286
|
+
function syncToggleStates(root) {
|
|
287
|
+
var btns = [].slice.call(root.querySelectorAll('[role="button"][rte-cmd-name]'));
|
|
288
|
+
for (var i = 0; i < btns.length; i++) {
|
|
289
|
+
var cmd = (btns[i].getAttribute("rte-cmd-name") || "").toLowerCase();
|
|
290
|
+
if (!TOGGLE_CMD.test(cmd)) continue;
|
|
291
|
+
var active = btns[i].classList.contains("rte-command-active");
|
|
292
|
+
var pressed = active ? "true" : "false";
|
|
293
|
+
if (btns[i].getAttribute("aria-pressed") !== pressed) btns[i].setAttribute("aria-pressed", pressed);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ------------------------------------------------- 3. aria-expanded
|
|
298
|
+
//
|
|
299
|
+
// Opening a menu inserts an <rte-dropdown-panel> elsewhere in the document
|
|
300
|
+
// and leaves the button untouched, so the button and its panel have to be
|
|
301
|
+
// correlated: remember which popup button was activated, then pair it with
|
|
302
|
+
// the panel that appears.
|
|
303
|
+
function trackPopups(root) {
|
|
304
|
+
var pops = [].slice.call(root.querySelectorAll("[aria-haspopup]"));
|
|
305
|
+
for (var i = 0; i < pops.length; i++) {
|
|
306
|
+
if (!pops[i].hasAttribute("aria-expanded")) pops[i].setAttribute("aria-expanded", "false");
|
|
307
|
+
if (pops[i].__rtePopupBound) continue;
|
|
308
|
+
pops[i].__rtePopupBound = true;
|
|
309
|
+
(function (el) {
|
|
310
|
+
function remember() { popupOwner = el; }
|
|
311
|
+
el.addEventListener("mousedown", remember, true);
|
|
312
|
+
el.addEventListener("keydown", function (e) {
|
|
313
|
+
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") remember();
|
|
314
|
+
}, true);
|
|
315
|
+
})(pops[i]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (trackPopups.bound) return;
|
|
319
|
+
trackPopups.bound = true;
|
|
320
|
+
var mo = new MutationObserver(function (records) {
|
|
321
|
+
for (var r = 0; r < records.length; r++) {
|
|
322
|
+
var rec = records[r];
|
|
323
|
+
for (var a = 0; a < rec.addedNodes.length; a++) {
|
|
324
|
+
var n = rec.addedNodes[a];
|
|
325
|
+
if (n.nodeType !== 1 || !isPanel(n)) continue;
|
|
326
|
+
if (popupOwner) {
|
|
327
|
+
popupOwner.setAttribute("aria-expanded", "true");
|
|
328
|
+
openPanels.push({ panel: n, owner: popupOwner });
|
|
329
|
+
popupOwner = null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (var d = 0; d < rec.removedNodes.length; d++) {
|
|
333
|
+
var m = rec.removedNodes[d];
|
|
334
|
+
if (m.nodeType !== 1 || !isPanel(m)) continue;
|
|
335
|
+
for (var k = openPanels.length - 1; k >= 0; k--) {
|
|
336
|
+
if (openPanels[k].panel === m) {
|
|
337
|
+
openPanels[k].owner.setAttribute("aria-expanded", "false");
|
|
338
|
+
openPanels.splice(k, 1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
345
|
+
observers.push(mo);
|
|
346
|
+
}
|
|
347
|
+
function isPanel(el) {
|
|
348
|
+
var t = (el.tagName || "").toLowerCase();
|
|
349
|
+
return t === "rte-dropdown-panel" || t === "rte-floatpanel" || t === "rte-dialog-float";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Toolbar buttons are rebuilt and re-classed as the selection moves, so the
|
|
353
|
+
// mirrored state has to follow rather than be set once.
|
|
354
|
+
function watch(root) {
|
|
355
|
+
if (root.__rteA11yWatch) return;
|
|
356
|
+
root.__rteA11yWatch = true;
|
|
357
|
+
var pending = null;
|
|
358
|
+
var mo = new MutationObserver(function () {
|
|
359
|
+
if (pending) return;
|
|
360
|
+
pending = setTimeout(function () {
|
|
361
|
+
pending = null;
|
|
362
|
+
syncToggleStates(root);
|
|
363
|
+
applyRovingTabindex(root);
|
|
364
|
+
trackPopups(root);
|
|
365
|
+
}, 60);
|
|
366
|
+
});
|
|
367
|
+
mo.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "aria-disabled"] });
|
|
368
|
+
observers.push(mo);
|
|
369
|
+
}
|
|
370
|
+
}
|