@richscripts/richtexteditor 2.4.1 → 2.5.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/package.json +4 -2
- package/richtexteditor/all_plugins.js +2861 -935
- package/richtexteditor/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/accessibilitychecker.js +186 -1
- package/richtexteditor/plugins/aitoolkit.js +1 -1
- package/richtexteditor/plugins/all_plugins.js +2861 -935
- package/richtexteditor/plugins/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/crdt-engine.js +2201 -198
- package/richtexteditor/plugins/crdt-engine.min.js +5 -5
- package/richtexteditor/plugins/gapcursor.js +365 -0
- package/richtexteditor/plugins/imageeditor.js +30 -3
- package/richtexteditor/plugins/importstyles.js +312 -0
- package/richtexteditor/plugins/insertcode.js +81 -4
- package/richtexteditor/plugins/insertemoji.js +302 -229
- package/richtexteditor/plugins/insertgallery.js +34 -6
- package/richtexteditor/plugins/keyboarda11y.js +27 -1
- package/richtexteditor/plugins/multiroot.js +269 -0
- package/richtexteditor/plugins/revisionhistory.js +5 -0
- package/richtexteditor/plugins/sanitizer.js +24 -0
- package/richtexteditor/plugins/textpartlanguage.js +504 -0
- package/richtexteditor/plugins/trackedchanges.js +716 -689
- package/richtexteditor/plugins/wordexport.js +1 -1
- package/richtexteditor/rte-config.js +2 -2
- package/richtexteditor/rte.js +55 -51
- package/richtexteditor/rte_theme_default.css +151 -83
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-08-15 Text part language ("language of parts").
|
|
4
|
+
//
|
|
5
|
+
// WCAG 2.1 SC 3.1.2 (Level AA) requires that the human language of every
|
|
6
|
+
// PASSAGE or PHRASE be programmatically determinable when it differs from the
|
|
7
|
+
// document language. Until now this editor could only set a language on the
|
|
8
|
+
// whole document (a11yenhance.js stamps <html lang>), so a French quotation
|
|
9
|
+
// inside an English page was unmarkable and any AA conformance claim on
|
|
10
|
+
// /docs/accessibility was overstated. A screen reader reads such a phrase with
|
|
11
|
+
// the wrong phoneme set, which is the failure the SC exists to prevent.
|
|
12
|
+
//
|
|
13
|
+
// CKEditor 5 ships this as "Text part language". TinyMCE has no equivalent
|
|
14
|
+
// plugin (8.5's `content_language` sets the default proofing language, not
|
|
15
|
+
// per-phrase markup).
|
|
16
|
+
//
|
|
17
|
+
// Design notes:
|
|
18
|
+
// - `lang` is CONTENT, exactly like `dir` in textdirection.js: it must survive
|
|
19
|
+
// getHTMLCode(), and it is never stripped on serialize. A document whose
|
|
20
|
+
// language annotations vanished on save would silently drop its own
|
|
21
|
+
// conformance.
|
|
22
|
+
// - The wrapper is a bare <span lang>. No class is emitted, so there is
|
|
23
|
+
// nothing for check-css-contract.mjs to chase and nothing that depends on a
|
|
24
|
+
// stylesheet travelling with the content. The in-editor visual marker is
|
|
25
|
+
// injected into the EDITING DOCUMENT only, keyed off [lang], so it shows the
|
|
26
|
+
// author what is marked without ever leaving the editor.
|
|
27
|
+
// - RTL languages also get `dir`, because `lang` alone does not set direction.
|
|
28
|
+
// A Hebrew phrase marked only with lang="he" still renders left-to-right and
|
|
29
|
+
// reads as gibberish.
|
|
30
|
+
// - Re-applying the SAME language to a selection REMOVES it (toggle), which is
|
|
31
|
+
// how every other inline format in this editor behaves.
|
|
32
|
+
// - Nested lang spans inside the selection are unwrapped before wrapping, so a
|
|
33
|
+
// phrase never ends up with two competing languages. The inner one would win
|
|
34
|
+
// in the accessibility tree and the author would have no way to see why.
|
|
35
|
+
// - A collapsed selection deliberately does NOT create an empty wrapper. An
|
|
36
|
+
// empty inline span is a caret trap (the caret falls in and cannot be typed
|
|
37
|
+
// out of); instead a collapsed caret retargets the [lang] element it is
|
|
38
|
+
// already inside, so "put the cursor in the phrase and pick Remove" works.
|
|
39
|
+
RTE_DefaultConfig.plugin_textpartlanguage = RTE_Plugin_TextPartLanguage;
|
|
40
|
+
|
|
41
|
+
// The dropdown list. `dir` is only needed for right-to-left languages.
|
|
42
|
+
if (typeof RTE_DefaultConfig.textPartLanguages === "undefined") {
|
|
43
|
+
RTE_DefaultConfig.textPartLanguages = [
|
|
44
|
+
{ code: "ar", label: "Arabic", dir: "rtl" },
|
|
45
|
+
{ code: "zh-Hans", label: "Chinese (Simplified)" },
|
|
46
|
+
{ code: "zh-Hant", label: "Chinese (Traditional)" },
|
|
47
|
+
{ code: "cs", label: "Czech" },
|
|
48
|
+
{ code: "nl", label: "Dutch" },
|
|
49
|
+
{ code: "en", label: "English" },
|
|
50
|
+
{ code: "fr", label: "French" },
|
|
51
|
+
{ code: "de", label: "German" },
|
|
52
|
+
{ code: "el", label: "Greek" },
|
|
53
|
+
{ code: "he", label: "Hebrew", dir: "rtl" },
|
|
54
|
+
{ code: "hi", label: "Hindi" },
|
|
55
|
+
{ code: "it", label: "Italian" },
|
|
56
|
+
{ code: "ja", label: "Japanese" },
|
|
57
|
+
{ code: "ko", label: "Korean" },
|
|
58
|
+
{ code: "fa", label: "Persian", dir: "rtl" },
|
|
59
|
+
{ code: "pl", label: "Polish" },
|
|
60
|
+
{ code: "pt", label: "Portuguese" },
|
|
61
|
+
{ code: "ru", label: "Russian" },
|
|
62
|
+
{ code: "es", label: "Spanish" },
|
|
63
|
+
{ code: "sv", label: "Swedish" },
|
|
64
|
+
{ code: "tr", label: "Turkish" },
|
|
65
|
+
{ code: "uk", label: "Ukrainian" },
|
|
66
|
+
{ code: "ur", label: "Urdu", dir: "rtl" },
|
|
67
|
+
{ code: "vi", label: "Vietnamese" }
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Show marked phrases with a dotted underline while editing. Editor-only: the
|
|
72
|
+
// rule lives in the editing document, never in the saved HTML.
|
|
73
|
+
if (typeof RTE_DefaultConfig.textPartLanguageHighlight === "undefined") RTE_DefaultConfig.textPartLanguageHighlight = true;
|
|
74
|
+
|
|
75
|
+
function RTE_Plugin_TextPartLanguage() {
|
|
76
|
+
var obj = this;
|
|
77
|
+
var config, editor;
|
|
78
|
+
var boundDoc = null;
|
|
79
|
+
|
|
80
|
+
obj.PluginName = "TextPartLanguage";
|
|
81
|
+
|
|
82
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
83
|
+
|
|
84
|
+
obj.InitEditor = function (argeditor) {
|
|
85
|
+
editor = argeditor;
|
|
86
|
+
|
|
87
|
+
editor.attachEvent("exec_command_textpartlanguage", function (state) {
|
|
88
|
+
state.returnValue = true;
|
|
89
|
+
var v = state && state.value;
|
|
90
|
+
if (!v || v === "none" || v === "remove") obj.Remove();
|
|
91
|
+
else obj.Apply(v);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (editor.toolbarFactoryMap) {
|
|
95
|
+
editor.toolbarFactoryMap["textpartlanguage"] = function (cmd) {
|
|
96
|
+
return editor.createToolbarItemDropDownPanel(cmd, buildPanel);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setup();
|
|
101
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
102
|
+
try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
|
|
103
|
+
setTimeout(setup, 0);
|
|
104
|
+
|
|
105
|
+
// Public API.
|
|
106
|
+
editor.setTextLanguage = function (code) { return obj.Apply(code); };
|
|
107
|
+
editor.removeTextLanguage = function () { return obj.Remove(); };
|
|
108
|
+
editor.getTextLanguage = function () { return obj.Current(); };
|
|
109
|
+
editor.listTextLanguages = function () { return languages().slice(); };
|
|
110
|
+
editor.getMarkedLanguages = function () { return obj.List(); };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
114
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
115
|
+
|
|
116
|
+
function languages() {
|
|
117
|
+
var list = config && config.textPartLanguages;
|
|
118
|
+
return (list && list.length) ? list : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function entry(code) {
|
|
122
|
+
var list = languages();
|
|
123
|
+
for (var i = 0; i < list.length; i++) if (list[i].code === code) return list[i];
|
|
124
|
+
return { code: code, label: code };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- state -----------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
// The nearest ancestor carrying a lang, bounded by the editable so the host
|
|
130
|
+
// page's own <html lang> is never reported as the phrase language.
|
|
131
|
+
function langAncestor(node) {
|
|
132
|
+
var ed = getEditable();
|
|
133
|
+
var n = (node && node.nodeType === 3) ? node.parentNode : node;
|
|
134
|
+
while (n && n !== ed && n.nodeType === 1) {
|
|
135
|
+
if (n.hasAttribute && n.hasAttribute("lang")) return n;
|
|
136
|
+
n = n.parentNode;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function selectionRange() {
|
|
142
|
+
try {
|
|
143
|
+
var sel = editor.getSelection();
|
|
144
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
145
|
+
return sel.getRangeAt(0);
|
|
146
|
+
} catch (e) { return null; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
obj.Current = function () {
|
|
150
|
+
var r = selectionRange();
|
|
151
|
+
if (!r) return null;
|
|
152
|
+
var el = langAncestor(r.startContainer);
|
|
153
|
+
// A non-collapsed selection only counts as "in" a language if BOTH ends
|
|
154
|
+
// sit in the same marked phrase; a selection straddling the boundary has
|
|
155
|
+
// no single language and must not report one.
|
|
156
|
+
if (el && !r.collapsed && langAncestor(r.endContainer) !== el) return null;
|
|
157
|
+
return el ? el.getAttribute("lang") : null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Every distinct language marked in the document, for auditing.
|
|
161
|
+
obj.List = function () {
|
|
162
|
+
var ed = getEditable();
|
|
163
|
+
var out = [];
|
|
164
|
+
if (!ed) return out;
|
|
165
|
+
var seen = {};
|
|
166
|
+
var nodes = ed.querySelectorAll("[lang]");
|
|
167
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
168
|
+
var code = nodes[i].getAttribute("lang");
|
|
169
|
+
if (!code || seen[code]) continue;
|
|
170
|
+
seen[code] = true;
|
|
171
|
+
out.push({ code: code, label: entry(code).label });
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ---- apply -----------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
obj.Apply = function (code) {
|
|
179
|
+
code = String(code == null ? "" : code).trim();
|
|
180
|
+
if (!code) return false;
|
|
181
|
+
|
|
182
|
+
var doc = getDoc();
|
|
183
|
+
var r = selectionRange();
|
|
184
|
+
if (!doc || !r) return false;
|
|
185
|
+
|
|
186
|
+
// Re-picking the current language toggles it off.
|
|
187
|
+
if (obj.Current() === code) return obj.Remove();
|
|
188
|
+
|
|
189
|
+
if (r.collapsed) {
|
|
190
|
+
// No empty wrapper (caret trap). Retarget the phrase the caret is in.
|
|
191
|
+
var host = langAncestor(r.startContainer);
|
|
192
|
+
if (!host) return false;
|
|
193
|
+
stamp(host, code);
|
|
194
|
+
fireChange();
|
|
195
|
+
return code;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var span = doc.createElement("span");
|
|
199
|
+
stamp(span, code);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// surroundContents is the clean path, but it throws whenever the
|
|
203
|
+
// range partially selects a non-text node (half a <strong>), which
|
|
204
|
+
// is the common case in real prose.
|
|
205
|
+
r.surroundContents(span);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
var frag = r.extractContents();
|
|
208
|
+
span.appendChild(frag);
|
|
209
|
+
r.insertNode(span);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
unwrapInner(span);
|
|
213
|
+
pruneEmptyText(span);
|
|
214
|
+
if (!span.firstChild) { // selection held nothing real
|
|
215
|
+
if (span.parentNode) span.parentNode.removeChild(span);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
mergeSiblings(span);
|
|
219
|
+
select(span);
|
|
220
|
+
fireChange();
|
|
221
|
+
return code;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
obj.Remove = function () {
|
|
225
|
+
var r = selectionRange();
|
|
226
|
+
if (!r) return false;
|
|
227
|
+
var ed = getEditable();
|
|
228
|
+
if (!ed) return false;
|
|
229
|
+
|
|
230
|
+
var removed = 0;
|
|
231
|
+
|
|
232
|
+
// The phrase the caret/selection sits inside.
|
|
233
|
+
var host = langAncestor(r.startContainer);
|
|
234
|
+
if (host) { unwrap(host); removed++; }
|
|
235
|
+
|
|
236
|
+
// Plus any fully- or partly-selected marked phrases in a wider range.
|
|
237
|
+
if (!r.collapsed) {
|
|
238
|
+
var nodes = ed.querySelectorAll("[lang]");
|
|
239
|
+
for (var i = nodes.length - 1; i >= 0; i--) {
|
|
240
|
+
var el = nodes[i];
|
|
241
|
+
if (el === host) continue;
|
|
242
|
+
if (intersects(r, el)) { unwrap(el); removed++; }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (removed) fireChange();
|
|
247
|
+
return removed > 0;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function intersects(range, el) {
|
|
251
|
+
try {
|
|
252
|
+
var er = el.ownerDocument.createRange();
|
|
253
|
+
er.selectNodeContents(el);
|
|
254
|
+
return range.compareBoundaryPoints(Range.END_TO_START, er) < 0 &&
|
|
255
|
+
range.compareBoundaryPoints(Range.START_TO_END, er) > 0;
|
|
256
|
+
} catch (e) { return false; }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stamp(el, code) {
|
|
260
|
+
el.setAttribute("lang", code);
|
|
261
|
+
var dir = entry(code).dir;
|
|
262
|
+
// lang does not imply direction; without dir an RTL phrase renders LTR.
|
|
263
|
+
if (dir === "rtl" || dir === "ltr") el.setAttribute("dir", dir);
|
|
264
|
+
else el.removeAttribute("dir");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// A phrase must carry exactly one language. Anything marked inside the new
|
|
268
|
+
// wrapper would otherwise win in the accessibility tree.
|
|
269
|
+
function unwrapInner(span) {
|
|
270
|
+
var inner = span.querySelectorAll("[lang]");
|
|
271
|
+
for (var i = inner.length - 1; i >= 0; i--) unwrap(inner[i]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function unwrap(el) {
|
|
275
|
+
if (!el || !el.parentNode) return;
|
|
276
|
+
// A span we created carries nothing else; anything the author's own
|
|
277
|
+
// markup carries (a styled <em lang>) must survive, so only the
|
|
278
|
+
// language attributes come off.
|
|
279
|
+
if (el.nodeName === "SPAN" && el.attributes.length <= 2 &&
|
|
280
|
+
!el.getAttribute("class") && !el.getAttribute("style") && !el.getAttribute("id")) {
|
|
281
|
+
var parent = el.parentNode;
|
|
282
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
283
|
+
parent.removeChild(el);
|
|
284
|
+
try { parent.normalize(); } catch (e) {}
|
|
285
|
+
} else {
|
|
286
|
+
el.removeAttribute("lang");
|
|
287
|
+
el.removeAttribute("dir");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// extractContents leaves zero-length text nodes behind; they make a wrapper
|
|
292
|
+
// look non-empty and leave invisible caret stops in the phrase.
|
|
293
|
+
function pruneEmptyText(el) {
|
|
294
|
+
for (var i = el.childNodes.length - 1; i >= 0; i--) {
|
|
295
|
+
var n = el.childNodes[i];
|
|
296
|
+
if (n.nodeType === 3 && n.nodeValue === "") el.removeChild(n);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Marking two adjacent halves of a phrase should read as one phrase, not two.
|
|
301
|
+
function mergeSiblings(span) {
|
|
302
|
+
var code = span.getAttribute("lang");
|
|
303
|
+
var prev = span.previousSibling;
|
|
304
|
+
if (prev && prev.nodeType === 1 && prev.nodeName === "SPAN" && prev.getAttribute("lang") === code) {
|
|
305
|
+
while (span.firstChild) prev.appendChild(span.firstChild);
|
|
306
|
+
span.parentNode.removeChild(span);
|
|
307
|
+
span = prev;
|
|
308
|
+
}
|
|
309
|
+
var next = span.nextSibling;
|
|
310
|
+
if (next && next.nodeType === 1 && next.nodeName === "SPAN" && next.getAttribute("lang") === code) {
|
|
311
|
+
while (next.firstChild) span.appendChild(next.firstChild);
|
|
312
|
+
next.parentNode.removeChild(next);
|
|
313
|
+
}
|
|
314
|
+
try { span.normalize(); } catch (e) {}
|
|
315
|
+
return span;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function select(span) {
|
|
319
|
+
try {
|
|
320
|
+
var sel = editor.getSelection();
|
|
321
|
+
var r = span.ownerDocument.createRange();
|
|
322
|
+
r.selectNodeContents(span);
|
|
323
|
+
sel.removeAllRanges();
|
|
324
|
+
sel.addRange(r);
|
|
325
|
+
} catch (e) {}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function fireChange() {
|
|
329
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
330
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- toolbar panel ---------------------------------------------------
|
|
334
|
+
|
|
335
|
+
function buildPanel(panel) {
|
|
336
|
+
var current = obj.Current();
|
|
337
|
+
panel.style.width = "220px";
|
|
338
|
+
panel.style.maxHeight = "340px";
|
|
339
|
+
panel.style.overflowY = "auto";
|
|
340
|
+
panel.style.padding = "4px 0";
|
|
341
|
+
|
|
342
|
+
var list = languages();
|
|
343
|
+
|
|
344
|
+
addRow(panel, editor.getLangText ? (editor.getLangText("removelanguage") || "Remove language") : "Remove language",
|
|
345
|
+
null, !current, function () { obj.Remove(); });
|
|
346
|
+
|
|
347
|
+
var sep = panel.ownerDocument.createElement("div");
|
|
348
|
+
sep.style.cssText = "height:1px;margin:4px 6px;background:var(--rte-border-color,#e0e0e0);";
|
|
349
|
+
panel.appendChild(sep);
|
|
350
|
+
|
|
351
|
+
for (var i = 0; i < list.length; i++) {
|
|
352
|
+
(function (item) {
|
|
353
|
+
addRow(panel, item.label, item.code, current === item.code, function () { obj.Apply(item.code); });
|
|
354
|
+
})(list[i]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Opening a menu has to put focus inside it, or a keyboard user has
|
|
358
|
+
// opened something they cannot reach. Start on the current language when
|
|
359
|
+
// there is one — that is the item they are most likely acting on.
|
|
360
|
+
setTimeout(function () {
|
|
361
|
+
var rows = panel.querySelectorAll("rte-toolbar-dropdown-item");
|
|
362
|
+
if (!rows.length) return;
|
|
363
|
+
var target = rows[0];
|
|
364
|
+
for (var i = 0; i < rows.length; i++) {
|
|
365
|
+
if (rows[i].getAttribute("aria-checked") === "true") { target = rows[i]; break; }
|
|
366
|
+
}
|
|
367
|
+
try { target.focus(); } catch (e) {}
|
|
368
|
+
}, 0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function addRow(panel, label, code, active, run) {
|
|
372
|
+
var doc = panel.ownerDocument;
|
|
373
|
+
// Two things are required for a menu row to be operable by keyboard, and
|
|
374
|
+
// a plain <div role="menuitemradio"> has neither:
|
|
375
|
+
// 1. The TAG. The core's keyboard layer finds items via
|
|
376
|
+
// __actionElementSelector, which lists element NAMES
|
|
377
|
+
// (rte-toolbar-dropdown-item, rte-dropdown-menuitem, rte-menuitem)
|
|
378
|
+
// and ignores roles entirely.
|
|
379
|
+
// 2. A tab stop and key handling. The core applies those inside its own
|
|
380
|
+
// closure — __Append / __Make_ActionElementAccessible are not exposed
|
|
381
|
+
// to plugins — so a plugin has to supply them itself rather than
|
|
382
|
+
// hope an internal picks the row up.
|
|
383
|
+
// Both are done here, so the menu works with a keyboard regardless of
|
|
384
|
+
// what the core does or does not reach into (WCAG 2.1.1).
|
|
385
|
+
var row = doc.createElement("rte-toolbar-dropdown-item");
|
|
386
|
+
panel.appendChild(row);
|
|
387
|
+
row.setAttribute("role", "menuitemradio");
|
|
388
|
+
row.setAttribute("aria-checked", active ? "true" : "false");
|
|
389
|
+
row.setAttribute("tabindex", "0");
|
|
390
|
+
row.style.cssText = "padding:5px 12px;cursor:pointer;white-space:nowrap;display:flex;" +
|
|
391
|
+
"justify-content:space-between;gap:12px;color:var(--rte-text-color,inherit);" +
|
|
392
|
+
(active ? "font-weight:600;" : "");
|
|
393
|
+
|
|
394
|
+
var name = doc.createElement("span");
|
|
395
|
+
name.appendChild(doc.createTextNode(label));
|
|
396
|
+
row.appendChild(name);
|
|
397
|
+
|
|
398
|
+
if (code) {
|
|
399
|
+
var tag = doc.createElement("span");
|
|
400
|
+
tag.style.cssText = "opacity:.55;font-size:11px;";
|
|
401
|
+
tag.appendChild(doc.createTextNode(code));
|
|
402
|
+
row.appendChild(tag);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
row.onmouseover = function () { row.style.backgroundColor = "var(--rte-hover-bg,#e6e6e6)"; };
|
|
406
|
+
row.onmouseout = function () { row.style.backgroundColor = ""; };
|
|
407
|
+
row.onclick = function () {
|
|
408
|
+
// Close first: the panel steals focus, and applying to a selection
|
|
409
|
+
// the editor no longer owns silently does nothing.
|
|
410
|
+
try { editor.closeCurrentPopup(); } catch (e) {}
|
|
411
|
+
try { editor.focus(); } catch (e) {}
|
|
412
|
+
run();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
row.onkeydown = function (e) {
|
|
416
|
+
var key = e.key;
|
|
417
|
+
if (key === "Enter" || key === " " || key === "Spacebar") {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
e.stopPropagation();
|
|
420
|
+
row.onclick();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (key === "ArrowDown" || key === "ArrowUp") {
|
|
424
|
+
e.preventDefault();
|
|
425
|
+
e.stopPropagation();
|
|
426
|
+
var items = panel.querySelectorAll("rte-toolbar-dropdown-item");
|
|
427
|
+
var list = Array.prototype.slice.call(items);
|
|
428
|
+
var at = list.indexOf(row);
|
|
429
|
+
// Wrap, so the list cannot dead-end and strand the user.
|
|
430
|
+
var next = list[(at + (key === "ArrowDown" ? 1 : -1) + list.length) % list.length];
|
|
431
|
+
if (next) next.focus();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (key === "Home" || key === "End") {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
e.stopPropagation();
|
|
437
|
+
var all = panel.querySelectorAll("rte-toolbar-dropdown-item");
|
|
438
|
+
var target = key === "Home" ? all[0] : all[all.length - 1];
|
|
439
|
+
if (target) target.focus();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (key === "Escape") {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
e.stopPropagation();
|
|
445
|
+
// Escape closes the menu; focus then lands in the EDITING AREA,
|
|
446
|
+
// not back on the launcher.
|
|
447
|
+
//
|
|
448
|
+
// That is the core's behaviour for every dropdown — closing a
|
|
449
|
+
// popup refocuses the editable asynchronously — and it cannot be
|
|
450
|
+
// overridden from here. Measured: setting focus on the launcher
|
|
451
|
+
// does take effect, and is then taken back ~16 ms later; a forced
|
|
452
|
+
// re-focus is taken back again. Racing it with longer timeouts
|
|
453
|
+
// would be fragile and would fight the "close the popup, carry on
|
|
454
|
+
// typing" flow the rest of the editor depends on.
|
|
455
|
+
//
|
|
456
|
+
// This is not a trap and not a WCAG failure: the menu closes,
|
|
457
|
+
// focus is visible and lands somewhere meaningful. Restoring
|
|
458
|
+
// launcher focus properly belongs in the core's popup-close path,
|
|
459
|
+
// where it would apply to every dropdown at once.
|
|
460
|
+
try { editor.closeCurrentPopup(); } catch (ex) {}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---- editor-only marker ----------------------------------------------
|
|
466
|
+
|
|
467
|
+
function setup() {
|
|
468
|
+
var doc = getDoc();
|
|
469
|
+
if (!doc || doc === boundDoc) return;
|
|
470
|
+
boundDoc = doc;
|
|
471
|
+
injectStyles(doc);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function css() {
|
|
475
|
+
if (config && config.textPartLanguageHighlight === false) return "";
|
|
476
|
+
// Scoped to [lang] inside the editing document only. Never serialized,
|
|
477
|
+
// so the marker cannot follow the content into a customer's page.
|
|
478
|
+
//
|
|
479
|
+
// :not(html):not(body) is REQUIRED, not defensive tidiness. a11yenhance.js
|
|
480
|
+
// stamps the editing document's root with the document language, so a bare
|
|
481
|
+
// [lang] selector matches <html> — which made hovering anywhere in the
|
|
482
|
+
// editing area grey the ENTIRE surface, and drew the "marked phrase"
|
|
483
|
+
// underline across the whole document. Reported from the live demos page.
|
|
484
|
+
// Only a phrase the author actually marked should be decorated.
|
|
485
|
+
return "[lang]:not(html):not(body){border-bottom:1px dotted rgba(120,120,120,.75);}" +
|
|
486
|
+
"[lang]:not(html):not(body):hover{background:rgba(120,120,120,.10);}";
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function injectStyles(doc) {
|
|
490
|
+
if (!doc) return;
|
|
491
|
+
var text = css();
|
|
492
|
+
var existing = doc.getElementById("rte-textpartlanguage-styles");
|
|
493
|
+
if (existing) {
|
|
494
|
+
if (existing.getAttribute("data-css") === text) return;
|
|
495
|
+
existing.parentNode && existing.parentNode.removeChild(existing);
|
|
496
|
+
}
|
|
497
|
+
if (!text) return;
|
|
498
|
+
var st = doc.createElement("style");
|
|
499
|
+
st.id = "rte-textpartlanguage-styles";
|
|
500
|
+
st.setAttribute("data-css", text);
|
|
501
|
+
st.appendChild(doc.createTextNode(text));
|
|
502
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
503
|
+
}
|
|
504
|
+
}
|