@richscripts/richtexteditor 2.7.2 → 2.9.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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/richtexteditor/all_plugins.js +2583 -719
  3. package/richtexteditor/all_plugins.min.js +59 -58
  4. package/richtexteditor/plugins/accessibilitychecker.js +7 -0
  5. package/richtexteditor/plugins/all_plugins.js +2583 -719
  6. package/richtexteditor/plugins/all_plugins.min.js +59 -58
  7. package/richtexteditor/plugins/autoembed.js +7 -0
  8. package/richtexteditor/plugins/blocktypes.js +7 -0
  9. package/richtexteditor/plugins/bookmarkcard.js +7 -0
  10. package/richtexteditor/plugins/charlimit.js +7 -0
  11. package/richtexteditor/plugins/comments.js +14 -0
  12. package/richtexteditor/plugins/contentminimap.js +7 -0
  13. package/richtexteditor/plugins/crossreference.js +7 -0
  14. package/richtexteditor/plugins/dictation.js +7 -0
  15. package/richtexteditor/plugins/documentoutline.js +9 -3
  16. package/richtexteditor/plugins/draghandle.js +7 -0
  17. package/richtexteditor/plugins/foldheadings.js +7 -0
  18. package/richtexteditor/plugins/footnotes.js +7 -0
  19. package/richtexteditor/plugins/formatpainter.js +7 -0
  20. package/richtexteditor/plugins/formattingmarks.js +7 -0
  21. package/richtexteditor/plugins/gapcursor.js +7 -0
  22. package/richtexteditor/plugins/ghostcomplete.js +7 -0
  23. package/richtexteditor/plugins/layouttable.js +7 -0
  24. package/richtexteditor/plugins/linenumbers.js +7 -0
  25. package/richtexteditor/plugins/linkchecker.js +7 -0
  26. package/richtexteditor/plugins/mentionplugin.js +14 -0
  27. package/richtexteditor/plugins/mergefields.js +7 -0
  28. package/richtexteditor/plugins/mermaiddiagram.js +7 -0
  29. package/richtexteditor/plugins/multilevellist.js +7 -0
  30. package/richtexteditor/plugins/nodetypes.js +439 -0
  31. package/richtexteditor/plugins/pagination.js +7 -0
  32. package/richtexteditor/plugins/restrictedediting.js +7 -0
  33. package/richtexteditor/plugins/revisionhistory.js +7 -0
  34. package/richtexteditor/plugins/rtlui.js +7 -0
  35. package/richtexteditor/plugins/rubyannotation.js +255 -0
  36. package/richtexteditor/plugins/slashcommand.js +7 -0
  37. package/richtexteditor/plugins/smartchips.js +7 -0
  38. package/richtexteditor/plugins/spellcheck.js +7 -0
  39. package/richtexteditor/plugins/stateinspector.js +866 -0
  40. package/richtexteditor/plugins/tableofcontents.js +7 -0
  41. package/richtexteditor/plugins/textdirection.js +7 -0
  42. package/richtexteditor/plugins/textpartlanguage.js +7 -0
  43. package/richtexteditor/plugins/todolist.js +7 -0
  44. package/richtexteditor/plugins/trackedchanges.js +730 -716
  45. package/richtexteditor/plugins/typewriter.js +7 -0
  46. package/richtexteditor/plugins/watermark.js +15 -0
  47. package/richtexteditor/plugins/yjscollab.js +7 -0
  48. package/richtexteditor/rte.js +2 -2
@@ -0,0 +1,439 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ RTE_DefaultConfig.plugin_nodetypes = RTE_Plugin_NodeTypes;
4
+
5
+ // 2026-09-04 Custom node type registry.
6
+ //
7
+ // WHAT THE GAP ACTUALLY IS. Measured before this was designed, rather than
8
+ // assumed. A custom element with data-* attributes already survives more of this
9
+ // editor than expected:
10
+ //
11
+ // setHTML -> getHTML round trip ......... survives, attributes intact
12
+ // contenteditable="false" atomicity ..... preserved
13
+ // undo of an editor-driven delete ....... restored, attributes intact
14
+ // sanitizer ............................. already extensible via
15
+ // config.sanitizerAllowTags / -Attributes
16
+ // markdown export ....................... DEGRADES to the element's text
17
+ // .docx export .......................... DEGRADES to the element's text
18
+ // an API to declare any of this ......... DID NOT EXIST
19
+ //
20
+ // So the gap is not "you cannot put your own element in the document" - you can.
21
+ // The gap is that nothing lets you DECLARE that the element is a node type, and
22
+ // therefore nothing downstream can treat it deliberately. Degrading to text in
23
+ // Markdown and .docx is a defensible default; having no way to say what should
24
+ // happen instead is not.
25
+ //
26
+ // This registry is that declaration. Register a type once and it gets:
27
+ // - its tags and attributes allowed through the sanitizer automatically
28
+ // - atomicity enforced and repaired (chips stop being atomic the moment
29
+ // contenteditable is lost, and then the caret edits their internals)
30
+ // - JSON serialization of every instance, for saving state outside the HTML
31
+ // - a Markdown representation you control instead of raw text content
32
+ //
33
+ // ORDERING NOTE: plugins initialise in alphabetical bundle order. "nodetypes"
34
+ // sorts BEFORE "sanitizer" and BEFORE "slashcommand", so this file must NOT call
35
+ // editor.slashCommands.register() from init - it would silently no-op. Feeding
36
+ // the sanitizer works precisely because we run first: we write config before
37
+ // sanitizer.js reads it.
38
+ function RTE_Plugin_NodeTypes() {
39
+ var obj = this;
40
+ var config;
41
+ var editor;
42
+ var types = []; // registration order
43
+ var byName = {};
44
+ var enforceTimer = 0;
45
+ var originalToMarkdown = null;
46
+
47
+ obj.PluginName = "NodeTypes";
48
+
49
+ obj.InitConfig = function (argconfig) {
50
+ config = argconfig;
51
+ if (config.nodeTypesEnabled === false) return;
52
+
53
+ // Types can be declared in config so a host does not have to wait for an
54
+ // editor instance to exist.
55
+ if (Array.isArray(config.nodeTypes)) {
56
+ for (var i = 0; i < config.nodeTypes.length; i++) registerType(config.nodeTypes[i]);
57
+ }
58
+ };
59
+
60
+ obj.InitEditor = function (argeditor) {
61
+ editor = argeditor;
62
+ if (config.nodeTypesEnabled === false) return;
63
+
64
+ editor.nodeTypes = {
65
+ find: function (name) { return findInstances(name); },
66
+ get: function (name) { return byName[name] || null; },
67
+ insert: function (name, data) { return insertNode(name, data); },
68
+ list: function () { return types.map(describe); },
69
+ register: function (def) { return registerType(def); },
70
+ serialize: function () { return serializeAll(); },
71
+ typeOf: function (el) { var t = typeFor(el); return t ? t.name : null; },
72
+ unregister: function (name) { return unregisterType(name); }
73
+ };
74
+
75
+ // Atomicity is a live invariant, not a one-off: a paste, an undo or a
76
+ // competing plugin can strip contenteditable and the node silently stops
77
+ // behaving like one unit.
78
+ //
79
+ // Observed rather than event-driven, deliberately. setHTML() does not
80
+ // fire "change", so an invariant hung off editor events is not enforced
81
+ // on loaded content at all - which is the most common way a node enters
82
+ // the document. Measured: with only the "change" handler, a node loaded
83
+ // via setHTML never gained contenteditable="false".
84
+ editor.attachEvent("change", function () { scheduleEnforce(); });
85
+ observeEditable();
86
+
87
+ wrapMarkdown();
88
+
89
+ // Deferred deliberately. Plugins initialise in alphabetical bundle order:
90
+ // docxexport (19) runs BEFORE nodetypes (49), but pdfexport (51) and
91
+ // wordexport (72) run AFTER, so editor.getPdfBytes does not exist yet at
92
+ // this point. Wrapping now would silently miss PDF entirely - the same
93
+ // ordering trap that makes slashCommands.register() a no-op from here.
94
+ // A zero-delay timeout lands after every plugin has initialised.
95
+ setTimeout(installExportHooks, 0);
96
+
97
+ enforceAll();
98
+ };
99
+
100
+ // ------------------------------------------------------------ registration
101
+
102
+ function registerType(def) {
103
+ if (!def || !def.name) return false;
104
+ if (typeof def.match !== "string" && typeof def.match !== "function") return false;
105
+ if (byName[def.name]) unregisterType(def.name);
106
+
107
+ var type = {
108
+ name: def.name,
109
+ match: def.match,
110
+ atomic: def.atomic !== false, // atomic by default
111
+ inline: !!def.inline,
112
+ toJSON: typeof def.toJSON === "function" ? def.toJSON : null,
113
+ fromJSON: typeof def.fromJSON === "function" ? def.fromJSON : null,
114
+ toMarkdown: typeof def.toMarkdown === "function" ? def.toMarkdown : null,
115
+ // Export representations. toExportHTML covers .docx and PDF at once;
116
+ // toDocx / toPdf override it per format when they differ.
117
+ toExportHTML: typeof def.toExportHTML === "function" ? def.toExportHTML : null,
118
+ toDocx: typeof def.toDocx === "function" ? def.toDocx : null,
119
+ toPdf: typeof def.toPdf === "function" ? def.toPdf : null,
120
+ toWord: typeof def.toWord === "function" ? def.toWord : null,
121
+ allowTags: def.allowTags || [],
122
+ allowAttributes: def.allowAttributes || []
123
+ };
124
+ types.push(type);
125
+ byName[type.name] = type;
126
+ feedSanitizer(type);
127
+ if (editor) { enforceAll(); }
128
+ return true;
129
+ }
130
+
131
+ function unregisterType(name) {
132
+ if (!byName[name]) return false;
133
+ types = types.filter(function (t) { return t.name !== name; });
134
+ delete byName[name];
135
+ return true;
136
+ }
137
+
138
+ function describe(t) {
139
+ return {
140
+ name: t.name, atomic: t.atomic, inline: t.inline,
141
+ hasToJSON: !!t.toJSON, hasFromJSON: !!t.fromJSON, hasToMarkdown: !!t.toMarkdown,
142
+ hasExportHTML: !!(t.toExportHTML || t.toDocx || t.toPdf),
143
+ instances: findInstances(t.name).length
144
+ };
145
+ }
146
+
147
+ // The sanitizer is an allowlist: an unknown tag is dropped on input, on
148
+ // output and from the live DOM. A registered type that did not feed the
149
+ // allowlist would be deleted by the editor's own security layer, so this is
150
+ // wired automatically rather than left to the host to remember.
151
+ function feedSanitizer(type) {
152
+ if (!config) return;
153
+ mergeInto("sanitizerAllowTags", type.allowTags);
154
+ mergeInto("sanitizerAllowAttributes", type.allowAttributes);
155
+ }
156
+
157
+ function mergeInto(key, values) {
158
+ if (!values || !values.length) return;
159
+ var list = config[key];
160
+ if (!Array.isArray(list)) list = list ? [list] : [];
161
+ for (var i = 0; i < values.length; i++) {
162
+ if (list.indexOf(values[i]) === -1) list.push(values[i]);
163
+ }
164
+ config[key] = list;
165
+ }
166
+
167
+ // ------------------------------------------------------------- instances
168
+
169
+ function matches(type, el) {
170
+ if (!el || el.nodeType !== 1) return false;
171
+ if (typeof type.match === "string") {
172
+ try { return el.matches(type.match); } catch (e) { return false; }
173
+ }
174
+ try { return !!type.match(el); } catch (e) { return false; }
175
+ }
176
+
177
+ function typeFor(el) {
178
+ for (var i = 0; i < types.length; i++) {
179
+ if (matches(types[i], el)) return types[i];
180
+ }
181
+ return null;
182
+ }
183
+
184
+ function editable() {
185
+ return editor && editor.getEditable ? editor.getEditable() : null;
186
+ }
187
+
188
+ function findInstances(name) {
189
+ var root = editable();
190
+ var type = byName[name];
191
+ var out = [];
192
+ if (!root || !type) return out;
193
+ // Include the root's own children: querySelectorAll finds descendants
194
+ // only, and a node pasted at the top level is a child of the root. That
195
+ // exact blind spot is why the accessibility checker used to miss
196
+ // top-level images.
197
+ var all = root.querySelectorAll("*");
198
+ for (var i = 0; i < all.length; i++) {
199
+ if (matches(type, all[i])) out.push(all[i]);
200
+ }
201
+ return out;
202
+ }
203
+
204
+ function allInstances() {
205
+ var root = editable();
206
+ var out = [];
207
+ if (!root) return out;
208
+ var all = root.querySelectorAll("*");
209
+ for (var i = 0; i < all.length; i++) {
210
+ var t = typeFor(all[i]);
211
+ if (t) out.push({ type: t, el: all[i] });
212
+ }
213
+ return out;
214
+ }
215
+
216
+ function insertNode(name, data) {
217
+ var type = byName[name];
218
+ if (!type || !type.fromJSON || !editor) return null;
219
+ var produced = type.fromJSON(data || {});
220
+ var html = typeof produced === "string" ? produced : (produced && produced.outerHTML);
221
+ if (!html) return null;
222
+ editor.insertHTML(html);
223
+ scheduleEnforce();
224
+ return html;
225
+ }
226
+
227
+ function serializeAll() {
228
+ var out = [];
229
+ var found = allInstances();
230
+ for (var i = 0; i < found.length; i++) {
231
+ var t = found[i].type, el = found[i].el;
232
+ out.push({
233
+ name: t.name,
234
+ data: t.toJSON ? safe(t.toJSON, el, {}) : readDataAttributes(el)
235
+ });
236
+ }
237
+ return out;
238
+ }
239
+
240
+ function readDataAttributes(el) {
241
+ var data = {};
242
+ var attrs = el.attributes;
243
+ for (var i = 0; attrs && i < attrs.length; i++) {
244
+ if (attrs[i].name.indexOf("data-") === 0) data[attrs[i].name] = attrs[i].value;
245
+ }
246
+ return data;
247
+ }
248
+
249
+ function safe(fn, arg, fallback) {
250
+ try { return fn(arg); } catch (e) { return fallback; }
251
+ }
252
+
253
+ // -------------------------------------------------------------- atomicity
254
+
255
+ function observeEditable() {
256
+ var root = editable();
257
+ if (!root || typeof MutationObserver === "undefined") return;
258
+ var observer = new MutationObserver(function () { scheduleEnforce(); });
259
+ observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["contenteditable"] });
260
+ }
261
+
262
+ function scheduleEnforce() {
263
+ clearTimeout(enforceTimer);
264
+ enforceTimer = setTimeout(enforceAll, 60);
265
+ }
266
+
267
+ function enforceAll() {
268
+ var found = allInstances();
269
+ for (var i = 0; i < found.length; i++) {
270
+ if (found[i].type.atomic && found[i].el.getAttribute("contenteditable") !== "false") {
271
+ found[i].el.setAttribute("contenteditable", "false");
272
+ }
273
+ }
274
+ }
275
+
276
+ // ----------------------------------------------------------------- export
277
+
278
+ // .docx and PDF both build from the live editable, so the same swap the
279
+ // Markdown path uses works here: replace each registered node with the HTML
280
+ // the type says represents it, export, then put the originals back.
281
+ //
282
+ // Unlike Markdown these are partly ASYNCHRONOUS - getDocxBlob returns a
283
+ // Promise - so the restore has to wait for the result to settle. Restoring
284
+ // synchronously would put the originals back while the exporter was still
285
+ // reading the DOM.
286
+ function installExportHooks() {
287
+ wrapExport("getDocxBlob", "toDocx");
288
+ wrapExport("exportToDocx", "toDocx");
289
+ wrapExport("getPdfBytes", "toPdf");
290
+ wrapExport("exportToPdf", "toPdf");
291
+ wrapExport("downloadPdf", "toPdf");
292
+ // wordexport.js sorts at 72 - after this file at 49 - which is exactly
293
+ // why the install is deferred. It builds from editor.getHTMLCode(), and
294
+ // getHTMLCode() serializes the live DOM, so the same swap reaches it.
295
+ wrapExport("getWordDocument", "toWord");
296
+ wrapExport("exportToWord", "toWord");
297
+ wrapExport("downloadWord", "toWord");
298
+ }
299
+
300
+ function wrapExport(apiName, hookName) {
301
+ if (!editor || typeof editor[apiName] !== "function") return;
302
+ var original = editor[apiName];
303
+ editor[apiName] = function () {
304
+ var args = arguments;
305
+ var found = allInstances().filter(function (f) {
306
+ return f.type[hookName] || f.type.toExportHTML;
307
+ });
308
+ if (!found.length) return original.apply(editor, args);
309
+
310
+ var restore = swapNodes(found, hookName);
311
+ var result;
312
+ try {
313
+ result = original.apply(editor, args);
314
+ } catch (e) {
315
+ restore();
316
+ throw e;
317
+ }
318
+ if (result && typeof result.then === "function") {
319
+ return result.then(function (v) { restore(); return v; },
320
+ function (e) { restore(); throw e; });
321
+ }
322
+ restore();
323
+ return result;
324
+ };
325
+ }
326
+
327
+ // Replaces each instance with its declared export HTML and returns an
328
+ // idempotent restore function.
329
+ function swapNodes(found, hookName) {
330
+ var doc = editor.getDocument();
331
+ var swaps = [];
332
+ var savedRange = null;
333
+ try {
334
+ var sel = doc.getSelection();
335
+ if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
336
+ } catch (e) { savedRange = null; }
337
+
338
+ for (var i = 0; i < found.length; i++) {
339
+ var el = found[i].el;
340
+ var fn = found[i].type[hookName] || found[i].type.toExportHTML;
341
+ var html = fn ? safe(fn, el, null) : null;
342
+ if (html === null || html === undefined) continue;
343
+ var holder = doc.createElement(found[i].type.inline ? "span" : "div");
344
+ holder.innerHTML = String(html);
345
+ el.parentNode.insertBefore(holder, el);
346
+ el.parentNode.removeChild(el);
347
+ swaps.push({ holder: holder, original: el });
348
+ }
349
+
350
+ var done = false;
351
+ return function () {
352
+ if (done) return;
353
+ done = true;
354
+ for (var j = swaps.length - 1; j >= 0; j--) {
355
+ var s = swaps[j];
356
+ if (s.holder.parentNode) {
357
+ s.holder.parentNode.insertBefore(s.original, s.holder);
358
+ s.holder.parentNode.removeChild(s.holder);
359
+ }
360
+ }
361
+ if (savedRange) {
362
+ try {
363
+ var sel2 = doc.getSelection();
364
+ sel2.removeAllRanges();
365
+ sel2.addRange(savedRange);
366
+ } catch (e) { /* selection may no longer be placeable */ }
367
+ }
368
+ };
369
+ }
370
+
371
+ // --------------------------------------------------------------- markdown
372
+
373
+ // The core's markdown engine takes a root element, but the public
374
+ // editor.toMarkdown() closes over the live editable and accepts no argument,
375
+ // so there is no way to hand it a modified clone. The only seam is to swap
376
+ // registered nodes for their markdown text in the live DOM, convert, and put
377
+ // the originals back.
378
+ //
379
+ // That mutates the document the user is editing, so it is wrapped in
380
+ // try/finally: if a host's toMarkdown throws, the originals are restored
381
+ // anyway. Selection is saved and restored for the same reason.
382
+ function wrapMarkdown() {
383
+ if (!editor || typeof editor.toMarkdown !== "function") return;
384
+ originalToMarkdown = editor.toMarkdown;
385
+ editor.toMarkdown = function () {
386
+ var found = allInstances().filter(function (f) { return f.type.toMarkdown; });
387
+ if (!found.length) return originalToMarkdown.call(editor);
388
+
389
+ var doc = editor.getDocument();
390
+ var swaps = [];
391
+ var tokens = [];
392
+ var savedRange = null;
393
+ try {
394
+ var sel = doc.getSelection();
395
+ if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
396
+ } catch (e) { savedRange = null; }
397
+
398
+ try {
399
+ for (var i = 0; i < found.length; i++) {
400
+ var el = found[i].el;
401
+ var md = safe(found[i].type.toMarkdown, el, null);
402
+ if (md === null || md === undefined) continue;
403
+ // An ALPHANUMERIC sentinel, not the markdown itself. The core
404
+ // engine escapes markdown punctuation in text content, so a
405
+ // hook returning "**Approval**" would emit "\*\*Approval\*\*".
406
+ // The sentinel has nothing to escape; the real markdown is
407
+ // substituted into the finished string instead.
408
+ var token = "RTENODETYPETOKEN" + i + "ENDRTENODETYPETOKEN";
409
+ var holder = doc.createElement(found[i].type.inline ? "span" : "p");
410
+ holder.textContent = token;
411
+ el.parentNode.insertBefore(holder, el);
412
+ el.parentNode.removeChild(el);
413
+ swaps.push({ holder: holder, original: el });
414
+ tokens.push({ token: token, markdown: String(md) });
415
+ }
416
+ var out = originalToMarkdown.call(editor);
417
+ for (var k = 0; k < tokens.length; k++) {
418
+ out = out.split(tokens[k].token).join(tokens[k].markdown);
419
+ }
420
+ return out;
421
+ } finally {
422
+ for (var j = swaps.length - 1; j >= 0; j--) {
423
+ var s = swaps[j];
424
+ if (s.holder.parentNode) {
425
+ s.holder.parentNode.insertBefore(s.original, s.holder);
426
+ s.holder.parentNode.removeChild(s.holder);
427
+ }
428
+ }
429
+ if (savedRange) {
430
+ try {
431
+ var sel2 = doc.getSelection();
432
+ sel2.removeAllRanges();
433
+ sel2.addRange(savedRange);
434
+ } catch (e) { /* selection may no longer be placeable */ }
435
+ }
436
+ }
437
+ };
438
+ }
439
+ }
@@ -458,6 +458,13 @@ function RTE_Plugin_Pagination() {
458
458
  st.id = "rte-pagination-styles";
459
459
  st.appendChild(doc.createTextNode(css));
460
460
  (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
461
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
462
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
463
+ // the path everyone else takes; this re-injects through the CSSOM only when
464
+ // the policy actually emptied it.
465
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
466
+ editor.ensureStyleSheetLive(doc, st, "rte-pagination-styles", st.textContent);
467
+ }
461
468
  }
462
469
 
463
470
  // ---- serialization guard --------------------------------------------
@@ -83,6 +83,13 @@ function RTE_Plugin_RestrictedEditing() {
83
83
  "body.rte-restricted [" + editableAttr + "='true']{background:rgba(34,197,94,.16)}"
84
84
  ].join("\n");
85
85
  (doc.head || doc.getElementsByTagName("head")[0]).appendChild(s);
86
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
87
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
88
+ // the path everyone else takes; this re-injects through the CSSOM only when
89
+ // the policy actually emptied it.
90
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
91
+ editor.ensureStyleSheetLive(doc, s, "__rte_restrictedediting_styles", s.textContent);
92
+ }
86
93
  }
87
94
 
88
95
  function enable() {
@@ -684,5 +684,12 @@ function RTE_Plugin_RevisionHistory() {
684
684
  ".rte-rev-dark .rte-rev-diff-del{background:rgba(244,63,94,.16);color:#fda4af}"
685
685
  ].join("\n");
686
686
  host.head.appendChild(style);
687
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
688
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
689
+ // the path everyone else takes; this re-injects through the CSSOM only when
690
+ // the policy actually emptied it.
691
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
692
+ editor.ensureStyleSheetLive(host, style, "rte-revisionhistory-style", style.textContent);
693
+ }
687
694
  }
688
695
  }
@@ -146,5 +146,12 @@ function RTE_Plugin_RtlUi() {
146
146
  st.id = "rte-rtl-ui-styles";
147
147
  st.appendChild(doc.createTextNode(css()));
148
148
  (doc.head || doc.documentElement).appendChild(st);
149
+ // A <style> ELEMENT is governed by style-src: under a strict policy the browser
150
+ // drops its rules, leaving this plugin's UI unstyled. The element above stays
151
+ // the path everyone else takes; this re-injects through the CSSOM only when
152
+ // the policy actually emptied it.
153
+ if (editor && typeof editor.ensureStyleSheetLive === "function") {
154
+ editor.ensureStyleSheetLive(doc, st, "rte-rtl-ui-styles", st.textContent);
155
+ }
149
156
  }
150
157
  }