@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
|
@@ -231,6 +231,15 @@ function RTE_Plugin_InsertGallery() {
|
|
|
231
231
|
|
|
232
232
|
var browser = append(dialoginner, "div", "", "rte-gallery-browser");
|
|
233
233
|
|
|
234
|
+
// Exactly what the upload endpoint accepts, and nothing more. The file
|
|
235
|
+
// picker used to advertise "image/*,...,.svg", so it offered files the
|
|
236
|
+
// server then rejected with "Invalid file extension" and the user had no
|
|
237
|
+
// way to know the real list until after the failure. Declared here
|
|
238
|
+
// because both the header hint and the file input read it.
|
|
239
|
+
// SVG is excluded on purpose: it can carry script, so it is not a safe
|
|
240
|
+
// thing to store and serve back from our own origin.
|
|
241
|
+
var GALLERY_ACCEPT = [".jpg", ".jpeg", ".jfif", ".png", ".gif", ".webp", ".bmp", ".avif"];
|
|
242
|
+
|
|
234
243
|
// The dialog frame already draws a title bar and close button, so this is
|
|
235
244
|
// a one-line subtitle, not a second heading.
|
|
236
245
|
var header = append(browser, "div", "", "rte-dialog-browser-header");
|
|
@@ -239,6 +248,12 @@ function RTE_Plugin_InsertGallery() {
|
|
|
239
248
|
? "Browse folders, upload new files, and insert the selected image into the editor."
|
|
240
249
|
: "Browse the available images, filter by name, and insert the selected image into the editor.";
|
|
241
250
|
|
|
251
|
+
// State the accepted formats up front. Previously the only way to learn
|
|
252
|
+
// them was to pick an unsupported file and read "Invalid file extension".
|
|
253
|
+
var acceptHint = append(header, "div", "", "rte-dialog-browser-accept");
|
|
254
|
+
acceptHint.innerText = "Accepted formats: " +
|
|
255
|
+
GALLERY_ACCEPT.map(function (ext) { return ext.replace(".", "").toUpperCase(); }).join(", ");
|
|
256
|
+
|
|
242
257
|
var toolbar = append(browser, "div", "", "rte-gallery-browser-toolbar");
|
|
243
258
|
var toolbarLeft = append(toolbar, "div", "", "rte-gallery-browser-toolbar-group");
|
|
244
259
|
var toolbarRight = append(toolbar, "div", "", "rte-gallery-browser-toolbar-group rte-gallery-browser-toolbar-group-right");
|
|
@@ -273,7 +288,7 @@ function RTE_Plugin_InsertGallery() {
|
|
|
273
288
|
|
|
274
289
|
var fileInput = append(toolbarRight, "input", "display:none;");
|
|
275
290
|
fileInput.type = "file";
|
|
276
|
-
fileInput.accept = "
|
|
291
|
+
fileInput.accept = GALLERY_ACCEPT.join(",");
|
|
277
292
|
fileInput.multiple = true;
|
|
278
293
|
|
|
279
294
|
// Without a server there is no folder tree to walk and nothing to create
|
|
@@ -299,6 +314,12 @@ function RTE_Plugin_InsertGallery() {
|
|
|
299
314
|
grid.setAttribute("role", "listbox");
|
|
300
315
|
grid.setAttribute("aria-label", "Available images");
|
|
301
316
|
|
|
317
|
+
// Non-blocking failure notice. Sits above the grid instead of replacing
|
|
318
|
+
// it, so a rejected upload never hides the images you can still pick.
|
|
319
|
+
var errorBar = append(surface, "div", "", "rte-gallery-browser-error");
|
|
320
|
+
errorBar.setAttribute("role", "status");
|
|
321
|
+
show(errorBar, false);
|
|
322
|
+
|
|
302
323
|
var empty = append(surface, "div", "", "rte-gallery-browser-empty");
|
|
303
324
|
|
|
304
325
|
var footer = append(browser, "div", "", "rte-gallery-browser-footer");
|
|
@@ -341,6 +362,9 @@ function RTE_Plugin_InsertGallery() {
|
|
|
341
362
|
|
|
342
363
|
function setSelected(url) {
|
|
343
364
|
state.selectedUrl = url || "";
|
|
365
|
+
// Picking an image is a fresh action, so a stale failure notice from
|
|
366
|
+
// an earlier upload must not linger next to it.
|
|
367
|
+
state.error = "";
|
|
344
368
|
render();
|
|
345
369
|
}
|
|
346
370
|
|
|
@@ -494,12 +518,16 @@ function RTE_Plugin_InsertGallery() {
|
|
|
494
518
|
return;
|
|
495
519
|
}
|
|
496
520
|
|
|
521
|
+
// An upload failure must not wipe the browser. It used to replace the
|
|
522
|
+
// whole grid, so one rejected file left the dialog reading "0 items"
|
|
523
|
+
// with an error where the images should be - while the footer still
|
|
524
|
+
// said the previously selected image was ready to insert. Two panels
|
|
525
|
+
// contradicting each other. Show it as a banner and keep browsing.
|
|
497
526
|
if (state.error) {
|
|
498
|
-
|
|
499
|
-
show(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return;
|
|
527
|
+
errorBar.innerText = state.error;
|
|
528
|
+
show(errorBar, true);
|
|
529
|
+
} else {
|
|
530
|
+
show(errorBar, false);
|
|
503
531
|
}
|
|
504
532
|
|
|
505
533
|
for (var folderIndex = 0; folderIndex < visibleFolders.length; folderIndex++) {
|
|
@@ -227,7 +227,33 @@ function RTE_Plugin_KeyboardA11y() {
|
|
|
227
227
|
// The editor already tracks active state — it just keeps it in a class.
|
|
228
228
|
// Mirroring rather than recomputing means the announced state can never
|
|
229
229
|
// disagree with the highlighted button.
|
|
230
|
-
|
|
230
|
+
// Command names here must match the TOOLBAR command exactly — the regex is
|
|
231
|
+
// anchored, so a near-miss is a silent no-op that looks like coverage.
|
|
232
|
+
// "blockquote" is one: the command is `insertblockquote`, so the entry
|
|
233
|
+
// written specifically for it never matched and that button ships with no
|
|
234
|
+
// aria-pressed. Found by checking every genuine toggle on the full toolbar
|
|
235
|
+
// rather than the three the published verify page samples.
|
|
236
|
+
//
|
|
237
|
+
// This mirror only copies the editor's own active state, so a command may
|
|
238
|
+
// only be listed here once core actually tracks it. Emitting the attribute
|
|
239
|
+
// for an untracked command yields a permanent aria-pressed="false" —
|
|
240
|
+
// confidently announcing "not pressed" while the formatting IS applied,
|
|
241
|
+
// which makes announced state DISAGREE with the highlight. That is the exact
|
|
242
|
+
// failure this mirror exists to prevent, so silence beats a wrong answer.
|
|
243
|
+
// - `toggleborder` was already tracked (core checks the editable's
|
|
244
|
+
// rte-toggleborder class); it reads inactive simply when borders are off.
|
|
245
|
+
// - `insertblockquote` was NOT tracked and now is, via a core case added
|
|
246
|
+
// alongside `indent`, which resolves the same way.
|
|
247
|
+
// Both verified on the running editor rather than inferred.
|
|
248
|
+
//
|
|
249
|
+
// `inlinecode`, `pagination`, `typewriter` and `focusmode` match nothing in
|
|
250
|
+
// this build, and now the reason is known rather than assumed: pagination.js
|
|
251
|
+
// and typewriter.js are API-only (togglePageView / toggleTypewriterMode …)
|
|
252
|
+
// and register no toolbar command, and insertcode.js registers `insertcode`,
|
|
253
|
+
// which inserts a block rather than toggling the selection. They are kept —
|
|
254
|
+
// harmless, and correct the moment a toolbar button is added — but they
|
|
255
|
+
// cover nothing today, so do not read this list as evidence of coverage.
|
|
256
|
+
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)$/;
|
|
231
257
|
|
|
232
258
|
function syncToggleStates(root) {
|
|
233
259
|
var btns = [].slice.call(root.querySelectorAll('[role="button"][rte-cmd-name]'));
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-08-15 Multi-root editing — one toolbar, several editable regions.
|
|
4
|
+
//
|
|
5
|
+
// The CMS / page-builder case: a page has a title, a summary and a body, and the
|
|
6
|
+
// author wants one toolbar that acts on whichever region they are in, not three
|
|
7
|
+
// stacked toolbars eating the viewport.
|
|
8
|
+
//
|
|
9
|
+
// SCOPE — read this before assuming parity with CKEditor's multi-root:
|
|
10
|
+
// This editor edits <body> inside an IFRAME and has no div/inline mode. One
|
|
11
|
+
// instance therefore owns exactly one editable document, and every plugin,
|
|
12
|
+
// the selection model and the CRDT binding are written against that
|
|
13
|
+
// assumption. Making a single instance own N editables is a core rewrite, not
|
|
14
|
+
// a plugin.
|
|
15
|
+
// What this does instead is COORDINATE several ordinary instances: they share
|
|
16
|
+
// one visible toolbar and one group-level API. That covers the layout and the
|
|
17
|
+
// workflow, which is what the feature is actually for.
|
|
18
|
+
// What it does NOT do, and cannot without core work:
|
|
19
|
+
// - a shared undo stack (Ctrl+Z undoes within the focused region only),
|
|
20
|
+
// - a selection spanning two regions,
|
|
21
|
+
// - one collaboration session across regions (each root syncs separately).
|
|
22
|
+
// Those are stated in getCapabilities() as well as here, so an integrator
|
|
23
|
+
// finds out from the API rather than from a bug report.
|
|
24
|
+
//
|
|
25
|
+
// Design notes:
|
|
26
|
+
// - Toolbars are SHOWN/HIDDEN, not rebuilt or re-pointed. Each instance's
|
|
27
|
+
// toolbar already drives its own editor correctly; re-routing commands
|
|
28
|
+
// across instances would mean reimplementing every command's notion of
|
|
29
|
+
// "current editor" and would break the moment a plugin cached one.
|
|
30
|
+
// - Moving the active toolbar into a shared host is optional. DOM moves
|
|
31
|
+
// preserve listeners, so the toolbar keeps working, but a host that is
|
|
32
|
+
// positioned or scrolled differently can change the float-panel anchoring —
|
|
33
|
+
// hence opt-in rather than default.
|
|
34
|
+
// - Registration is by GROUP NAME in a module-level registry, because plugins
|
|
35
|
+
// are constructed per instance and have no other way to see each other.
|
|
36
|
+
RTE_DefaultConfig.plugin_multiroot = RTE_Plugin_MultiRoot;
|
|
37
|
+
|
|
38
|
+
// Instances sharing a group name act as one multi-root editor. null = off.
|
|
39
|
+
if (typeof RTE_DefaultConfig.multiRootGroup === "undefined") RTE_DefaultConfig.multiRootGroup = null;
|
|
40
|
+
// A name for this root in the group API. Defaults to the container's id.
|
|
41
|
+
if (typeof RTE_DefaultConfig.multiRootName === "undefined") RTE_DefaultConfig.multiRootName = null;
|
|
42
|
+
// Element (or its id) to move the active toolbar into. null = leave in place.
|
|
43
|
+
if (typeof RTE_DefaultConfig.multiRootToolbarHost === "undefined") RTE_DefaultConfig.multiRootToolbarHost = null;
|
|
44
|
+
|
|
45
|
+
// group name -> { roots: [entry], active: entry }
|
|
46
|
+
if (!window.__RTE_MultiRootGroups) window.__RTE_MultiRootGroups = {};
|
|
47
|
+
|
|
48
|
+
function RTE_Plugin_MultiRoot() {
|
|
49
|
+
var obj = this;
|
|
50
|
+
var config, editor;
|
|
51
|
+
var entry = null;
|
|
52
|
+
|
|
53
|
+
obj.PluginName = "MultiRoot";
|
|
54
|
+
|
|
55
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
56
|
+
|
|
57
|
+
obj.InitEditor = function (argeditor) {
|
|
58
|
+
editor = argeditor;
|
|
59
|
+
|
|
60
|
+
var group = config.multiRootGroup;
|
|
61
|
+
if (!group) return; // opt-in
|
|
62
|
+
|
|
63
|
+
entry = {
|
|
64
|
+
name: config.multiRootName || containerId() || ("root" + (groupOf(group).roots.length + 1)),
|
|
65
|
+
editor: editor,
|
|
66
|
+
container: container(),
|
|
67
|
+
group: group
|
|
68
|
+
};
|
|
69
|
+
groupOf(group).roots.push(entry);
|
|
70
|
+
|
|
71
|
+
editor.multiRoot = api(group);
|
|
72
|
+
|
|
73
|
+
// Wait for the toolbar to exist before hiding anything.
|
|
74
|
+
setTimeout(function () { bindFocus(); settle(group); }, 0);
|
|
75
|
+
try { editor.attachEvent("ready", function () { bindFocus(); settle(group); }); } catch (e) {}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function groupOf(name) {
|
|
79
|
+
// `listeners` lives on the GROUP, not in this closure. Every root builds
|
|
80
|
+
// its own copy of this plugin, so an instance-local list means a callback
|
|
81
|
+
// registered through one root's API never hears an activation fired by a
|
|
82
|
+
// different root — the subscriber silently lags by one event, or misses
|
|
83
|
+
// everything, depending on which root the user clicks.
|
|
84
|
+
if (!window.__RTE_MultiRootGroups[name]) {
|
|
85
|
+
window.__RTE_MultiRootGroups[name] = { roots: [], active: null, listeners: [] };
|
|
86
|
+
}
|
|
87
|
+
var g = window.__RTE_MultiRootGroups[name];
|
|
88
|
+
if (!g.listeners) g.listeners = [];
|
|
89
|
+
return g;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The editor exposes its host element as `container` (a property, not a
|
|
93
|
+
// getter — there is no getContainer()). Falling back through the iframe
|
|
94
|
+
// covers any build where that property is absent, rather than silently
|
|
95
|
+
// returning null and leaving every toolbar visible.
|
|
96
|
+
function container() {
|
|
97
|
+
try {
|
|
98
|
+
if (editor.container) return editor.container;
|
|
99
|
+
if (editor.iframe) {
|
|
100
|
+
var n = editor.iframe.parentNode;
|
|
101
|
+
while (n && !(n.classList && n.classList.contains("richtexteditor"))) n = n.parentNode;
|
|
102
|
+
return n || null;
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function containerId() {
|
|
109
|
+
var c = container();
|
|
110
|
+
return c && c.id ? c.id : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- toolbar visibility ----------------------------------------------
|
|
114
|
+
|
|
115
|
+
function toolbarsOf(root) {
|
|
116
|
+
var c = root.container;
|
|
117
|
+
if (!c) return [];
|
|
118
|
+
return [].slice.call(c.querySelectorAll("rte-toolbar"));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setVisible(root, visible) {
|
|
122
|
+
var bars = toolbarsOf(root);
|
|
123
|
+
for (var i = 0; i < bars.length; i++) {
|
|
124
|
+
var bar = bars[i];
|
|
125
|
+
if (visible) {
|
|
126
|
+
// Restore whatever the editor's own responsive logic had chosen
|
|
127
|
+
// (desktop vs mobile bar), rather than forcing display:flex on
|
|
128
|
+
// both and showing two toolbars at once.
|
|
129
|
+
if (bar.__rteMultiRootPrevDisplay !== undefined) {
|
|
130
|
+
bar.style.display = bar.__rteMultiRootPrevDisplay;
|
|
131
|
+
delete bar.__rteMultiRootPrevDisplay;
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
if (bar.__rteMultiRootPrevDisplay === undefined) {
|
|
135
|
+
bar.__rteMultiRootPrevDisplay = bar.style.display;
|
|
136
|
+
}
|
|
137
|
+
bar.style.display = "none";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function settle(groupName) {
|
|
143
|
+
var g = groupOf(groupName);
|
|
144
|
+
if (!g.roots.length) return;
|
|
145
|
+
if (!g.active) g.active = g.roots[0];
|
|
146
|
+
for (var i = 0; i < g.roots.length; i++) {
|
|
147
|
+
setVisible(g.roots[i], g.roots[i] === g.active);
|
|
148
|
+
}
|
|
149
|
+
hostToolbar(g.active);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hostToolbar(root) {
|
|
153
|
+
var hostRef = config.multiRootToolbarHost;
|
|
154
|
+
if (!hostRef || !root) return;
|
|
155
|
+
var host = (typeof hostRef === "string") ? document.getElementById(hostRef) : hostRef;
|
|
156
|
+
if (!host) return;
|
|
157
|
+
var bars = toolbarsOf(root);
|
|
158
|
+
for (var i = 0; i < bars.length; i++) {
|
|
159
|
+
// Moving a node keeps its listeners, so the toolbar stays live.
|
|
160
|
+
if (bars[i].parentNode !== host) host.appendChild(bars[i]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---- focus tracking ---------------------------------------------------
|
|
165
|
+
|
|
166
|
+
function bindFocus() {
|
|
167
|
+
var doc;
|
|
168
|
+
try { doc = editor.getDocument(); } catch (e) { return; }
|
|
169
|
+
if (!doc || doc.__rteMultiRootBound) return;
|
|
170
|
+
doc.__rteMultiRootBound = true;
|
|
171
|
+
|
|
172
|
+
var activate = function () {
|
|
173
|
+
if (!entry) return;
|
|
174
|
+
var g = groupOf(entry.group);
|
|
175
|
+
if (g.active === entry) return;
|
|
176
|
+
g.active = entry;
|
|
177
|
+
settle(entry.group);
|
|
178
|
+
fire(entry.group, entry);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// focusin bubbles; focus does not.
|
|
182
|
+
doc.addEventListener("focusin", activate, true);
|
|
183
|
+
doc.addEventListener("mousedown", activate, true);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function fire(groupName, root) {
|
|
187
|
+
var list = groupOf(groupName).listeners;
|
|
188
|
+
for (var i = 0; i < list.length; i++) {
|
|
189
|
+
try { list[i]({ active: root.name, editor: root.editor }); } catch (e) {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- group API --------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
function api(groupName) {
|
|
196
|
+
var g = groupOf(groupName);
|
|
197
|
+
return {
|
|
198
|
+
group: groupName,
|
|
199
|
+
|
|
200
|
+
getRoots: function () {
|
|
201
|
+
return g.roots.map(function (r) { return r.name; });
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
getActive: function () {
|
|
205
|
+
return g.active ? g.active.name : null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
getEditor: function (name) {
|
|
209
|
+
for (var i = 0; i < g.roots.length; i++) if (g.roots[i].name === name) return g.roots[i].editor;
|
|
210
|
+
return null;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
focusRoot: function (name) {
|
|
214
|
+
var target = null;
|
|
215
|
+
for (var i = 0; i < g.roots.length; i++) if (g.roots[i].name === name) target = g.roots[i];
|
|
216
|
+
if (!target) return false;
|
|
217
|
+
g.active = target;
|
|
218
|
+
settle(groupName);
|
|
219
|
+
try { target.editor.focus(); } catch (e) {}
|
|
220
|
+
fire(groupName, target);
|
|
221
|
+
return true;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// The whole document, one entry per root — what a CMS saves.
|
|
225
|
+
getHTML: function () {
|
|
226
|
+
var out = {};
|
|
227
|
+
for (var i = 0; i < g.roots.length; i++) {
|
|
228
|
+
try { out[g.roots[i].name] = g.roots[i].editor.getHTMLCode(); } catch (e) { out[g.roots[i].name] = null; }
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
setHTML: function (map) {
|
|
234
|
+
if (!map) return false;
|
|
235
|
+
for (var i = 0; i < g.roots.length; i++) {
|
|
236
|
+
var r = g.roots[i];
|
|
237
|
+
if (!(r.name in map)) continue;
|
|
238
|
+
try { r.editor.setHTMLCode(map[r.name]); } catch (e) {}
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
onActiveChange: function (fn) {
|
|
244
|
+
if (typeof fn !== "function") return function () {};
|
|
245
|
+
g.listeners.push(fn);
|
|
246
|
+
return function () {
|
|
247
|
+
var at = g.listeners.indexOf(fn);
|
|
248
|
+
if (at >= 0) g.listeners.splice(at, 1);
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Say plainly what this does and does not do, so an integrator does
|
|
253
|
+
// not discover the limits from a support ticket.
|
|
254
|
+
getCapabilities: function () {
|
|
255
|
+
return {
|
|
256
|
+
sharedToolbar: true,
|
|
257
|
+
sharedGroupApi: true,
|
|
258
|
+
perRootUndo: true,
|
|
259
|
+
sharedUndoStack: false,
|
|
260
|
+
selectionAcrossRoots: false,
|
|
261
|
+
sharedCollaborationSession: false,
|
|
262
|
+
note: "Each root is a separate editor instance sharing one visible toolbar. " +
|
|
263
|
+
"Undo, selection and collaboration are per-root; the editor edits an " +
|
|
264
|
+
"iframe body, so a single instance cannot own several editables without core changes."
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -270,6 +270,11 @@ function RTE_Plugin_RevisionHistory() {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
function restore(id, opts) {
|
|
273
|
+
// The dialog's own Restore button calls restore(id) with no options, so
|
|
274
|
+
// reading opts.skipConfirm threw and the button did nothing at all. The
|
|
275
|
+
// public API wrapper defaulted this, the internal caller did not - so
|
|
276
|
+
// default it here, where every caller is covered.
|
|
277
|
+
opts = opts || {};
|
|
273
278
|
var entry = findById(id);
|
|
274
279
|
if (!entry) return false;
|
|
275
280
|
if (!opts.skipConfirm) {
|
|
@@ -141,6 +141,17 @@ function RTE_Plugin_Sanitizer() {
|
|
|
141
141
|
var v = String(value == null ? "" : value).replace(/[\u0000-\u0020\u007F-\u00A0]+/g, "").trim();
|
|
142
142
|
if (!v) return false;
|
|
143
143
|
|
|
144
|
+
// <use> resolves its reference by FETCHING it. The only legitimate form
|
|
145
|
+
// in editor content is a same-document fragment (#icon-id) pointing at a
|
|
146
|
+
// <symbol>/<defs> in the same markup — nobody authoring a document means
|
|
147
|
+
// to reference a remote SVG. Left unrestricted it is a beacon that fires
|
|
148
|
+
// in every reader's browser for content one author saved, which is the
|
|
149
|
+
// same class of problem as an embedded remote logo or analytics pixel
|
|
150
|
+
// (see the no-external-calls hardening). Browsers additionally refuse
|
|
151
|
+
// cross-origin <use>, but relying on that is relying on someone else's
|
|
152
|
+
// policy to enforce ours.
|
|
153
|
+
if (tag === "use") return v.charAt(0) === "#";
|
|
154
|
+
|
|
144
155
|
// Fragments, absolute paths and query-only URLs carry no scheme.
|
|
145
156
|
if (/^[#?/]/.test(v)) return true;
|
|
146
157
|
|
|
@@ -243,6 +254,19 @@ function RTE_Plugin_Sanitizer() {
|
|
|
243
254
|
// This is the construct that actually executed downstream.
|
|
244
255
|
if (el.hasAttribute("srcdoc")) { note(report.removedAttributes, "iframe[srcdoc]"); el.removeAttribute("srcdoc"); }
|
|
245
256
|
if (!iframeAllowed(el)) { note(report.removedTags, "iframe"); el.parentNode.removeChild(el); continue; }
|
|
257
|
+
} else if (tag === "title" || tag === "desc") {
|
|
258
|
+
// Legal inside <svg> (they supply its accessible name), which is
|
|
259
|
+
// why they are in ALLOWED_TAGS. But the allowance was
|
|
260
|
+
// unqualified, so a document-level <title> from a pasted or
|
|
261
|
+
// set full HTML document survived into SAVED content while
|
|
262
|
+
// <style>/<meta>/<link> beside it were stripped. Head-level
|
|
263
|
+
// markup has no business in body content; keep these only where
|
|
264
|
+
// they are actually valid.
|
|
265
|
+
if (!el.closest || !el.closest("svg")) {
|
|
266
|
+
note(report.removedTags, tag);
|
|
267
|
+
el.parentNode.removeChild(el);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
246
270
|
} else if (DROP_WHOLE[tag]) {
|
|
247
271
|
note(report.removedTags, tag);
|
|
248
272
|
el.parentNode.removeChild(el);
|