@richscripts/richtexteditor 2.11.0 → 2.12.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.
@@ -14,10 +14,12 @@
14
14
  //
15
15
  // config.galleryEndpoint URL returning { currentFolder, currentFolderDisplay,
16
16
  // parentFolder, folders: [{folder,name}],
17
- // images: [{url,name,folder,size,source}] }.
17
+ // images: [{url,name,folder,size,source,alt}] }.
18
18
  // Absent -> local mode: presets only, no request.
19
19
  // config.galleryImages Preset images. String, [url, text], or an object
20
- // with url/src/href plus optional thumbnail/name/meta.
20
+ // with url/src/href plus optional thumbnail/name/meta/alt.
21
+ // alt (presets and server images) becomes the inserted
22
+ // image's alt text; without it the attribute stays absent.
21
23
  //
22
24
  // Uploads use window.richTextBoxUploadFile when present (it accepts a target
23
25
  // folder), otherwise the generic window.rte_file_upload_handler.
@@ -148,7 +150,8 @@ function RTE_Plugin_InsertGallery() {
148
150
  url: url,
149
151
  thumbnail: item.thumbnail || item.thumb || item.preview || url,
150
152
  name: item.name || item.text || item.title || getFileName(url),
151
- meta: item.meta || item.description || item.alt || ""
153
+ meta: item.meta || item.description || item.alt || "",
154
+ alt: typeof item.alt === "string" ? item.alt : undefined
152
155
  };
153
156
  }
154
157
 
@@ -188,6 +191,7 @@ function RTE_Plugin_InsertGallery() {
188
191
  folder: item.folder || "",
189
192
  source: item.source || "upload",
190
193
  size: item.size || 0,
194
+ alt: typeof item.alt === "string" ? item.alt : undefined,
191
195
  searchText: (name + " " + item.url).toLowerCase()
192
196
  };
193
197
  }
@@ -381,7 +385,9 @@ function RTE_Plugin_InsertGallery() {
381
385
  return;
382
386
  }
383
387
 
384
- editor.insertImageByUrl(selected.url);
388
+ // Only an alt the host supplied (preset or server listing). The file name is
389
+ // not alt text; leaving it absent lets the accessibility checker flag it.
390
+ editor.insertImageByUrl(selected.url, selected.alt);
385
391
  closeDialog();
386
392
  editor.focus();
387
393
  }
@@ -612,6 +618,10 @@ function RTE_Plugin_InsertGallery() {
612
618
  render();
613
619
  }
614
620
 
621
+ // Upload failures to show once the folder has been re-listed (applyResponse
622
+ // clears the error, so it would otherwise vanish).
623
+ var pendingError = "";
624
+
615
625
  function loadFolder(folder, selectAfterLoad) {
616
626
  if (!serverMode) {
617
627
  state.images = presetImages.slice(0);
@@ -625,6 +635,7 @@ function RTE_Plugin_InsertGallery() {
625
635
 
626
636
  requestJson("GET", withFolder(endpoint, folder), null, function (payload, errorCode) {
627
637
  if (errorCode) {
638
+ pendingError = "";
628
639
  loadFallback(errorCode);
629
640
  return;
630
641
  }
@@ -633,6 +644,8 @@ function RTE_Plugin_InsertGallery() {
633
644
  if (selectAfterLoad) {
634
645
  state.selectedUrl = selectAfterLoad;
635
646
  }
647
+ state.error = pendingError;
648
+ pendingError = "";
636
649
  state.loading = false;
637
650
  render();
638
651
  });
@@ -667,67 +680,120 @@ function RTE_Plugin_InsertGallery() {
667
680
  });
668
681
  }
669
682
 
683
+ // config.maxUploadFileSize is enforced by the core only on its own insert
684
+ // paths. The gallery calls the upload handler directly, so the limit a
685
+ // customer set to protect their database was skipped for every image
686
+ // uploaded through this dialog. The accept list is only a hint to the file
687
+ // picker ("All files" bypasses it), so the format is checked here too.
688
+ function rejectReason(file) {
689
+ var name = String(file.name || "").toLowerCase();
690
+ var dot = name.lastIndexOf(".");
691
+ if (dot < 0 || GALLERY_ACCEPT.indexOf(name.substring(dot)) < 0) {
692
+ return file.name + " is not an accepted image format.";
693
+ }
694
+ var max = parseInt(config.maxUploadFileSize, 10);
695
+ if (max > 0 && file.size > max) {
696
+ return file.name + " is " + humanSize(file.size) + ". The maximum allowed size is " + humanSize(max) + ".";
697
+ }
698
+ return "";
699
+ }
700
+
670
701
  function uploadFiles(fileList) {
671
702
  var files = [];
703
+ var problems = [];
672
704
  var index;
673
705
  for (index = 0; index < fileList.length; index++) {
674
- files.push(fileList[index]);
706
+ var reason = rejectReason(fileList[index]);
707
+ if (reason) {
708
+ problems.push(reason);
709
+ } else {
710
+ files.push(fileList[index]);
711
+ }
675
712
  }
676
713
 
677
- if (!files.length || !canUpload) {
714
+ if (!canUpload) {
715
+ return;
716
+ }
717
+ if (!files.length) {
718
+ if (problems.length) {
719
+ state.error = problems.join(" ");
720
+ render();
721
+ }
678
722
  return;
679
723
  }
680
724
 
681
- var lastUploadedUrl = "";
725
+ // One failed file used to stop the batch, and in local mode only the
726
+ // LAST success was added to the list - uploading three images showed
727
+ // one. Now every file is attempted, every success is kept, and the
728
+ // failures are reported together at the end.
729
+ var uploadedUrls = [];
682
730
 
683
- function uploadNext(nextIndex) {
684
- if (nextIndex >= files.length) {
685
- if (serverMode) {
686
- loadFolder(state.currentFolder, lastUploadedUrl);
687
- return;
688
- }
731
+ function finish() {
732
+ var lastUploadedUrl = uploadedUrls.length ? uploadedUrls[uploadedUrls.length - 1] : "";
733
+
734
+ if (serverMode) {
735
+ pendingError = problems.join(" ");
736
+ loadFolder(state.currentFolder, lastUploadedUrl || state.selectedUrl);
737
+ return;
738
+ }
689
739
 
690
- // No server to re-list from, so fold the upload into the
691
- // preset list directly and keep it selected.
692
- if (lastUploadedUrl) {
693
- var uploaded = normalizePreset(lastUploadedUrl);
694
- if (uploaded) {
695
- uploaded.source = "upload";
696
- presetImages.unshift(uploaded);
697
- config.galleryImages.unshift(lastUploadedUrl);
698
- }
699
- state.selectedUrl = lastUploadedUrl;
740
+ // No server to re-list from, so fold the uploads into the preset
741
+ // list directly, newest first, and select the last one.
742
+ for (var u = 0; u < uploadedUrls.length; u++) {
743
+ var uploaded = normalizePreset(uploadedUrls[u]);
744
+ if (uploaded) {
745
+ uploaded.source = "upload";
746
+ presetImages.unshift(uploaded);
747
+ config.galleryImages.unshift(uploadedUrls[u]);
700
748
  }
701
- state.images = presetImages.slice(0);
702
- state.loading = false;
703
- render();
749
+ }
750
+ if (lastUploadedUrl) {
751
+ state.selectedUrl = lastUploadedUrl;
752
+ }
753
+ state.images = presetImages.slice(0);
754
+ state.loading = false;
755
+ state.error = problems.join(" ");
756
+ render();
757
+ }
758
+
759
+ function uploadNext(nextIndex) {
760
+ if (nextIndex >= files.length) {
761
+ finish();
704
762
  return;
705
763
  }
706
764
 
707
765
  var file = files[nextIndex];
766
+ var settled = false;
708
767
 
709
- function done(url, errorCode) {
710
- if (!url) {
711
- state.error = errorCode || ("Upload failed for " + file.name + ".");
712
- state.loading = false;
713
- render();
768
+ function done(url, errorText) {
769
+ // a handler that calls back twice must not upload the rest twice
770
+ if (settled) {
714
771
  return;
715
772
  }
716
-
717
- lastUploadedUrl = url;
773
+ settled = true;
774
+ if (url) {
775
+ uploadedUrls.push(url);
776
+ } else {
777
+ problems.push(typeof errorText === "string" && errorText ? errorText : ("Upload failed for " + file.name + "."));
778
+ }
718
779
  uploadNext(nextIndex + 1);
719
780
  }
720
781
 
721
- if (uploadToFolder) {
722
- window.richTextBoxUploadFile(file, function (url, errorCode) {
723
- done(errorCode ? "" : url, errorCode ? ("Upload failed for " + file.name + ".") : "");
724
- }, { folder: state.currentFolder }, nextIndex, files);
725
- return;
726
- }
782
+ try {
783
+ if (uploadToFolder) {
784
+ window.richTextBoxUploadFile(file, function (url, errorCode) {
785
+ done(errorCode ? "" : url, errorCode ? ("Upload failed for " + file.name + ".") : "");
786
+ }, { folder: state.currentFolder }, nextIndex, files);
787
+ return;
788
+ }
727
789
 
728
- window.rte_file_upload_handler(file, function (url, error) {
729
- done(url, error);
730
- }, nextIndex, files);
790
+ window.rte_file_upload_handler(file, function (url, error) {
791
+ done(url, error);
792
+ }, nextIndex, files);
793
+ } catch (ex) {
794
+ // a handler that throws must not leave the dialog stuck on "Loading"
795
+ done("", "Upload failed for " + file.name + ".");
796
+ }
731
797
  }
732
798
 
733
799
  state.loading = true;
@@ -203,13 +203,35 @@ function RTE_Plugin_Sanitizer() {
203
203
  // sanitizerAllowedIframeHosts is still the stronger control.
204
204
  var DEFAULT_IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-presentation";
205
205
 
206
+ // 2026-09-21 Two holes in the default sandbox, both closed here:
207
+ // - it was only ADDED when the iframe had none, so injected markup that
208
+ // brought its own sandbox="allow-top-navigation allow-popups allow-forms"
209
+ // kept every permission the comment above says is withheld. An existing
210
+ // sandbox is now narrowed to the default set, never trusted.
211
+ // - allow-scripts + allow-same-origin isolates nothing when the frame is
212
+ // served from the page's OWN origin: the framed page can reach the parent
213
+ // and remove its own sandbox. Same-origin frames lose allow-same-origin.
214
+ function applyDefaultSandbox(el, src) {
215
+ var allowed = DEFAULT_IFRAME_SANDBOX.split(" ");
216
+ var sameOrigin = false;
217
+ try { sameOrigin = new URL(src, location.href).origin === location.origin; } catch (e) {}
218
+ if (sameOrigin) allowed = allowed.filter(function (t) { return t !== "allow-same-origin"; });
219
+ if (el.hasAttribute("sandbox")) {
220
+ // keep only what the frame asked for AND the default permits; an empty
221
+ // sandbox (the strictest) stays empty
222
+ var asked = (el.getAttribute("sandbox") || "").toLowerCase().split(/\s+/);
223
+ allowed = allowed.filter(function (t) { return asked.indexOf(t) >= 0; });
224
+ }
225
+ el.setAttribute("sandbox", allowed.join(" "));
226
+ }
227
+
206
228
  function iframeAllowed(el) {
207
229
  if (config.sanitizerAllowIframes === false) return false;
208
230
  var src = el.getAttribute("src") || "";
209
231
  if (!/^https?:/i.test(src)) return false;
210
232
  var hosts = config.sanitizerAllowedIframeHosts;
211
233
  if (!hosts || !hosts.length) {
212
- if (!el.hasAttribute("sandbox")) el.setAttribute("sandbox", DEFAULT_IFRAME_SANDBOX);
234
+ applyDefaultSandbox(el, src);
213
235
  return true;
214
236
  }
215
237
  try {
@@ -5,10 +5,14 @@
5
5
 
6
6
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = { };
7
7
 
8
- RTE_DefaultConfig.editablePaddingTop = 2;
9
- RTE_DefaultConfig.editablePaddingBottom = 2;
10
- RTE_DefaultConfig.editablePaddingLeft = 2;
11
- RTE_DefaultConfig.editablePaddingRight = 2;
8
+ // Writing gutters. These are applied as an INLINE style on the editing body, so
9
+ // they win over any stylesheet - this config is the one place to change them.
10
+ // 2px left text hard against the frame, which is the clearest "unstyled textarea"
11
+ // tell there is; these values give the content room to read as a document.
12
+ RTE_DefaultConfig.editablePaddingTop = 18;
13
+ RTE_DefaultConfig.editablePaddingBottom = 24;
14
+ RTE_DefaultConfig.editablePaddingLeft = 22;
15
+ RTE_DefaultConfig.editablePaddingRight = 22;
12
16
 
13
17
  // 2026-05-08: empty-editor placeholder ("Type something…" hint that
14
18
  // disappears the moment the user starts typing). Off by default — set
@@ -42,6 +46,8 @@ RTE_DefaultConfig.enableDragDrop = true; // Enables or disables drag-and-drop su
42
46
  RTE_DefaultConfig.enableObjectResizing = true; //Specifies whether or not to allow the users resize an object winthin the RichTextEditor.
43
47
  RTE_DefaultConfig.toggleBorder = true; //Specifies the ToggleBorder state. ToggleBorder is a handy function which allows you to see the borders without setting things to border = 1 or something like that in code.
44
48
  RTE_DefaultConfig.readOnly = false; //Gets or sets a value which indicates whether the RichTextEditor should be an active HTML editor, or a read-only document viewer.
49
+ RTE_DefaultConfig.reviewCanDecide = null;
50
+ RTE_DefaultConfig.onReviewDecisionDenied = null;
45
51
  RTE_DefaultConfig.commentsOnly = false; //Comments-only review mode: when true, the editor blocks document edits but still allows inline comments. Toggle at runtime via editor.setCommentsOnly(true).
46
52
  RTE_DefaultConfig.autoLinkOnType = true; // When true, typing a URL/email followed by space auto-wraps it in an <a> tag.
47
53
  RTE_DefaultConfig.showFloatLinkUrlPreview = true; // Show clickable URL preview in the float toolbar when caret is in an <a>.
@@ -61,6 +67,11 @@ RTE_DefaultConfig.imageHoverQuickBar = true; // Show a small "Alt / Replace / X"
61
67
  RTE_DefaultConfig.inlineMath = true; // When true, editor.applyMathMarkup() wraps $...$ runs in <span class="rte-math-inline" data-tex="..."> so a math renderer (KaTeX/MathJax) can target them.
62
68
  RTE_DefaultConfig.imageLazyLoad = true; // Newly-inserted <img> tags get loading="lazy" + decoding="async" so big documents stay responsive.
63
69
  RTE_DefaultConfig.documentUploadAccept = ".txt,.text,.rtf,.md,.markdown,.html,.htm,.pdf,.doc,.docx,.odt,.xls,.xlsx,.csv,.tsv,.json,.xml,.zip,.rar"; // Default Insert Document chooser filter.
70
+ RTE_DefaultConfig.maxUploadFileSize = 0; // Largest file the editor will insert or upload, in BYTES. 0 = no limit. Applies to the image and document dialogs, paste and drag-and-drop. Over the limit the file is refused before it is read, so nothing is embedded or sent.
71
+ RTE_DefaultConfig.allowBase64Images = true; // Without file_upload_handler an inserted image is embedded in the HTML as base64. false = never embed; an upload handler is then required.
72
+ RTE_DefaultConfig.base64ImageMaxSize = 204800; // Largest base64 image, in BYTES, after shrinking (200 KB). Larger images are refused with a message. 0 = no cap (the behaviour before core 2.6.5).
73
+ RTE_DefaultConfig.base64ImageShrink = true; // Resize and re-encode photos in the browser before embedding them as base64. GIF and SVG are never re-encoded.
74
+ RTE_DefaultConfig.base64ImageMaxDimension = 1600; // Longest side, in pixels, of an image shrunk for base64 embedding.
64
75
  RTE_DefaultConfig.fileUploadLocalObjectUrl = true; // Without file_upload_handler, local document files insert as temporary blob: links instead of failing.
65
76
  // Reading-mode persistence key. Default is session-only so refresh restores the toolbar.
66
77
  // Set to true for per-editor localStorage persistence, or to a custom string to share state across editors.
@@ -112,8 +123,80 @@ RTE_DefaultConfig.maxTextLength = 0; //Gets or sets the maximum number of charac
112
123
  RTE_DefaultConfig.tagWhiteList = []; // The white list contains a list of tags that can be used in the editor.
113
124
  RTE_DefaultConfig.tagBlackList = []; // The black list contains a list of tags that cannot be used in the editor.
114
125
 
126
+ // ---- Content sanitizer (plugins/sanitizer.js) --------------------------------
127
+ // An allowlist filter that runs on INPUT, on OUTPUT and over the live DOM. It is
128
+ // what stops a stored XSS surviving into saved HTML - keep it on unless you
129
+ // sanitize server-side instead. Note this is a different mechanism from
130
+ // tagWhiteList/tagBlackList above: those pick which tags an AUTHOR may produce,
131
+ // the sanitizer decides what is safe to keep at all.
132
+ //
133
+ // Every default below is the behaviour you already get; setting them explicitly
134
+ // only makes the options discoverable.
135
+
136
+ RTE_DefaultConfig.contentSanitizer = true; // false disables the sanitizer entirely.
137
+
138
+ // Elements the editor does not know are UNWRAPPED by default - their text is kept
139
+ // and the tag is dropped. That is what silently removes custom elements, and it
140
+ // is also why schema.org microdata disappears (itemscope/itemtype/itemprop are
141
+ // not in the attribute allowlist). Both are recoverable with the two lists below;
142
+ // this is the equivalent of what CKEditor calls "General HTML Support".
143
+ //
144
+ // Entries may be exact names, "*" wildcards, or RegExp:
145
+ // sanitizerAllowTags = ["custom-widget", "x-*", /^ui-/]
146
+ // sanitizerAllowAttributes = ["my-attr", "data-cms-*", "itemscope", "itemtype", "itemprop"]
147
+ // data-* and aria-* are always allowed and need no entry here.
148
+ RTE_DefaultConfig.sanitizerAllowTags = [];
149
+ RTE_DefaultConfig.sanitizerAllowAttributes = [];
150
+
151
+ RTE_DefaultConfig.sanitizerAllowStyleTags = false; // true keeps <style> blocks inside content.
152
+
153
+ // Iframes: false drops them outright. Left true, only http(s) sources survive and
154
+ // an iframe with no sandbox attribute is given a restrictive one. Naming hosts is
155
+ // the stronger control - with a list, everything else is dropped.
156
+ // sanitizerAllowedIframeHosts = ["www.youtube.com", "player.vimeo.com"]
157
+ RTE_DefaultConfig.sanitizerAllowIframes = true;
158
+ RTE_DefaultConfig.sanitizerAllowedIframeHosts = [];
159
+
160
+ // Also filter the LIVE editing DOM, not just input and output. This is the guard
161
+ // that matters for collaboration: a peer's CRDT update writes into the DOM
162
+ // directly and never passes through setHTMLCode, so with this off a collaborator
163
+ // could run script in every other participant's browser.
164
+ RTE_DefaultConfig.sanitizerGuardLiveDom = true;
165
+
166
+ // Inspect what the last pass removed: editor.getSanitizerReport()
167
+ // Sanitize a string yourself: editor.sanitizeHtml(html)
168
+
169
+
115
170
  RTE_DefaultConfig.tabSpaces = 4; //Gets or sets the number of spaces to be inserted when the user hits the "tab" key.
116
171
  RTE_DefaultConfig.enterKeyTag = "p" // Determines what happens when the "enter" key is pressed in the editor. div, p or br.
172
+ RTE_DefaultConfig.boldTag = "b" // Tag the Bold command writes to the saved HTML: "b" (default) or "strong".
173
+ RTE_DefaultConfig.italicTag = "i" // Tag the Italic command writes to the saved HTML: "i" (default) or "em".
174
+ RTE_DefaultConfig.strikeTag = "s" // Tag the Strikethrough command writes to the saved HTML: "s" (default), "strike" or "del".
175
+ // <s> is the default because <strike> was removed from the HTML spec, and because
176
+ // three spellings reach the document: execCommand emits <strike>, markdown ~~x~~
177
+ // emits <s>, and pasted content arrives as <del>. There is no underlineTag: the
178
+ // only alternative to <u> is <ins>, which would assert "inserted text".
179
+ // Applies to the OUTPUT of getHTMLCode(), so it also unifies content typed as
180
+ // markdown (**bold**), which is produced as <strong>/<em> internally.
181
+
182
+ RTE_DefaultConfig.htmlcode_forcehexformat = false; // Write colors as #993300 instead of rgb(153, 51, 0) in the saved HTML.
183
+ // Why the default output is rgb() even though the color picker passes "#993300":
184
+ // the editor sets the value as-is (style.cssText += ";color:#993300"), and it is
185
+ // the BROWSER's CSSOM that normalises it to rgb(153, 51, 0) when the style is read
186
+ // back. Nothing in this editor converts it, which is why there is no "use rgb"
187
+ // setting to turn off - this option converts it BACK on the way out instead.
188
+ //
189
+ // Set true to get hex. Applies in __fixHTML(), which runs on both getHTMLCode()
190
+ // and setHTMLCode(), so it normalises colors that were already rgb() in loaded or
191
+ // pasted content as well as colors the user applies. Covers color and
192
+ // background-color, on spans and on table cells.
193
+ //
194
+ // rgba() is deliberately left alone: it carries an alpha channel that a 6-digit
195
+ // hex cannot represent, so converting it would silently drop the transparency.
196
+ //
197
+ // Availability: 2.x only. In 1.x this flag is compiled to a constant false and
198
+ // no configuration can enable it - for 1.x, post-process the output of
199
+ // getHTMLCode() instead.
117
200
 
118
201
  RTE_DefaultConfig.pasteMode = "Auto"; // Specifies the manner in which the editor handles pasted text. Auto,Disabled,PasteText,PasteWord.
119
202
  RTE_DefaultConfig.markdownShortcutsEnabled = true; // Enables Markdown-style block shortcuts such as "# ", "> ", "- ", "1. ", and "---" + Enter.
@@ -264,18 +347,33 @@ RTE_DefaultConfig.revisionHistoryEnabled = true;
264
347
  RTE_DefaultConfig.revisionHistoryMaxEntries = 50;
265
348
  RTE_DefaultConfig.revisionHistoryAutoSnapshotMs = 0;
266
349
  RTE_DefaultConfig.revisionHistoryUrl = "";
350
+ RTE_DefaultConfig.revisionHistoryRequestHeaders = null;
351
+ RTE_DefaultConfig.onRevisionHistorySyncError = null;
267
352
 
268
- // Yjs collaboration (Option B: presence + shared review ledger; NO content CRDT in v1).
353
+ // Yjs collaboration: presence and shared review ledger, with optional per-node CRDT text synchronization.
269
354
  // Yjs + a provider (y-websocket / y-webrtc / y-indexeddb) are PEER DEPENDENCIES — customers
270
355
  // load them separately and call editor.collab.attach({ doc, provider, user }).
271
- // The plugin handles: awareness (live cursors + presence), and bridging editor.reviewLedger
356
+ // The plugin handles awareness, optional CRDT text sync, and bridges editor.reviewLedger
272
357
  // into a shared Y.Map so AI suggestions, tracked changes, and comments replicate across peers.
273
358
  RTE_DefaultConfig.collabEnabled = true;
274
359
  RTE_DefaultConfig.collabLedgerMapName = "reviewLedger";
275
360
  RTE_DefaultConfig.collabShowPresence = true;
276
361
  RTE_DefaultConfig.collabShowRemoteCursors = true;
362
+ RTE_DefaultConfig.collabTextSync = false;
363
+ RTE_DefaultConfig.collabRequireCrdt = false;
364
+ RTE_DefaultConfig.onCollabStatus = null;
365
+
277
366
 
367
+ RTE_DefaultConfig.indentation = "40px"; // Step size for indent/outdent on non-list blocks. Any CSS length (px, pt, em, %).
368
+ RTE_DefaultConfig.indentUseMargin = true; // true: indent writes margin-left/right. false: padding-left/right. Margin is the default because Word's w:ind and pasted Word content both use margin.
278
369
 
370
+ RTE_DefaultConfig.stylesMerge = false; // true: the Quick Styles lists below are APPENDED to the shipped defaults instead of replacing them (TinyMCE calls this style_formats_merge).
371
+
372
+ // The four Quick Styles lists accept, in the same array:
373
+ // ["Red", "color:red", "color:red"] title, value, optional preview CSS
374
+ // { title: "Red", value: "color:red" } object form of the same thing
375
+ // { title: "Callouts", items: [ ... ] } a labelled GROUP; items are indented under a heading
376
+ // Groups render as a heading plus indented items, not as a flyout submenu.
279
377
  RTE_DefaultConfig.inlineStyles = [["Red", "color:red", "color:red"], ["Bold", "font-weight:bold", "font-weight:bold"], ["Mark", "my-cls-mark"], ["Warning", "my-cls-warning"]]; // Default CSS styles for inline styles dropdown.
280
378
  RTE_DefaultConfig.paragraphStyles = [["Red", "color:red", "color:red"], ["Bold", "font-weight:bold", "font-weight:bold"], ["Quote", "my-cls-quote"], ["LargeCenter", "my-cls-largecenter"]]; // Default CSS styles for paragraph styles dropdown.
281
379
  RTE_DefaultConfig.imageStyles = [["Border", "border: 1px solid #ddd; border-radius: 4px; padding: 5px;"], ["grayscale", "filter: grayscale(100%);"], ["Shadow", "box-shadow:0 0 8px gray"], ["Margin10", "margin:10px"], ["Padding:10", "padding:10px"]
@@ -298,18 +396,24 @@ RTE_DefaultConfig.toolbar_mobile = "{bold,italic,underline|fontname:toggle,fonts
298
396
  RTE_DefaultConfig.toolbar_basic = "{bold,italic,underline}|{fontname,fontsize}|{insertlink,insertemoji,insertimage,insertvideo}|removeformat|code"
299
397
  + "#{toggleborder,fullscreenenter,fullscreenexit,undo,redo,togglemore}"; // Basic set of buttons that appears in the rich text editor's toolbar.
300
398
 
301
- RTE_DefaultConfig.toolbar_full = "{bold,italic,underline,forecolor,backcolor}|{justifyleft,justifycenter,justifyright,justifyfull}|{insertorderedlist,insertunorderedlist,indent,outdent}{superscript,subscript}|{aiassist}"
399
+ RTE_DefaultConfig.toolbar_full = "{bold,italic,underline,strike,forecolor,backcolor}|{justifyleft,justifycenter,justifyright,justifyfull}|{insertorderedlist,insertunorderedlist,indent,outdent}{superscript,subscript,inlinecode}|{aiassist}"
302
400
  + " #{paragraphs:toggle,fontname:toggle,fontsize:toggle,inlinestyle,lineheight}"
303
- + " / {spellcheck,insertcomment,removeformat,cut,copy,paste,delete,find}|{insertlink,unlink,insertblockquote,insertemoji,insertchars,inserttable,menu_tablecell,menu_tablerow,menu_tablecolumn,insertimage,insertgallery,insertvideo,insertdocument,inserttemplate,insertcode}"
401
+ + " / {spellcheck,textpartlanguage,insertcomment,removeformat,cut,copy,paste,delete,find}|{insertlink,unlink,insertblockquote,insertemoji,insertchars,inserttable,menu_tablecell,menu_tablerow,menu_tablecolumn,insertimage,insertgallery,insertvideo,insertdocument,inserttemplate,insertcode}"
304
402
  + "#{preview,code,selectall}"
305
403
  + " /{paragraphs:dropdown | fontname:dropdown | fontsize:dropdown} {paragraphstyle,toggle_paragraphop,menu_paragraphop}"
306
404
  + "#{insertmergefield,insertfootnote,inserttoc,insertpagebreak,revisionhistory,newdoc,save,toggleborder,fullscreenenter,fullscreenexit,undo,redo,togglemore}"; // Full set of buttons that appears in the rich text editor's toolbar.
307
405
 
308
406
  RTE_DefaultConfig.toolbar_richtextboxjs = RTE_DefaultConfig.toolbar_full; // Compatibility alias for older demo pages still referencing the temporary preset name.
309
407
 
310
- RTE_DefaultConfig.toolbar_office = "<@COMMON,ribbonpaste,pastetext,pasteword,{save,new,print}/{cut,copy,delete,find}/{undo,redo|formatpainter}><@FORMAT,[fontname,fontsize]/{bold,italic,underlinemenu|forecolor,backcolor}/{superscript,subscript,changecase|removeformat,cleancode,selectall}><@PARAGRAPHS,[paragraphs,styles]/{justifymenu,lineheight,ltr,rtl,insertlinemenu}/{insertorderedlist,insertunorderedlist,indent,outdent,insertblockquote}><@INSERT,ribbontable,insertgallery,insertimage,{insertform,insertbox,insertlayer,insertfieldset,pageproperties,help,toggleborder,fullscreen}/{insertlink,unlink,insertanchor,insertimagemap,insertdate,insertchars,virtualkeyboard}/{inserttemplate,insertdocument,insertvideo,syntaxhighlighter,insertyoutube,html5,googlemap}>";
408
+ RTE_DefaultConfig.toolbar_office = "<@COMMON,ribbonpaste,pastetext,pasteword,{save,new,print}/{cut,copy,delete,find}/{undo,redo|formatpainter}><@FORMAT,[fontname,fontsize]/{bold,italic,underlinemenu|forecolor,backcolor}/{superscript,subscript,changecase|removeformat,cleancode,selectall}><@PARAGRAPHS,[paragraphs,styles]/{justifymenu,lineheight,ltr,rtl,textpartlanguage,insertlinemenu}/{insertorderedlist,insertunorderedlist,indent,outdent,insertblockquote}><@INSERT,ribbontable,insertgallery,insertimage,{insertform,insertbox,insertlayer,insertfieldset,pageproperties,help,toggleborder,fullscreen}/{insertlink,unlink,insertanchor,insertimagemap,insertdate,insertchars,virtualkeyboard}/{inserttemplate,insertdocument,insertvideo,syntaxhighlighter,insertyoutube,html5,googlemap}>";
311
409
 
312
- RTE_DefaultConfig.subtoolbar_more = "{strike,superscript,subscript,ucase,lcase,titlecase,inserthorizontalrule,highlight,sortlines,insertcallout,insertcolumns,inserttoggle,inserttodolist,insertmath,insertdiagram,insertbookmark,insertdatechip,spellcheck,emailexport,readaloud,html2pdf,insertdate} #{newdoc,load,save,print,printpreview,readingmode,help}"; // A set of buttons that appears in the subtoolbar of default toolbar set.
410
+ RTE_DefaultConfig.subtoolbar_more = "{strike,superscript,subscript,inlinecode,ucase,lcase,titlecase,inserthorizontalrule,highlight,sortlines,insertcallout,insertcolumns,inserttoggle,inserttodolist,insertmath,insertdiagram,insertbookmark,insertdatechip,spellcheck,emailexport,readaloud,html2pdf,insertdate} #{newdoc,load,save,print,printpreview,readingmode,help}"; // A set of buttons that appears in the subtoolbar of default toolbar set.
411
+ // `inlinecode` is deliberately absent from subtoolbar_more_full below: it is
412
+ // already in toolbar_full's main row, so adding it here would render the button
413
+ // TWICE in that preset. It belongs in subtoolbar_more (above) because that is the
414
+ // overflow set for the DEFAULT toolbar, and toolbar_default carries neither strike
415
+ // nor inlinecode - without that line the feature is unreachable for every user who
416
+ // never configured a toolbar, which is how it shipped in 2.5.8.
313
417
  RTE_DefaultConfig.subtoolbar_more_full = "{strike,ucase,lcase,titlecase,inserthorizontalrule,highlight,sortlines,html2pdf,insertdate} #{newdoc,save,print,printpreview,readingmode,help}";// A set of buttons that appears in the subtoolbar of full toolbar set.
314
418
  RTE_DefaultConfig.subtoolbar_more_mobile = "{save} #{newdoc,help}"; // A set of buttons that appears in the subtoolbar of mobile toolbar set.
315
419
  RTE_DefaultConfig.subtoolbar_paste = "pasteauto,pastetext,pasteword"; // A set of buttons that appears in the rich text editor's paste subtoolbar.
@@ -442,7 +546,9 @@ RTE_DefaultConfig.svgCode_newdoc = '<svg viewBox="0 0 24 24" fill="none" stroke=
442
546
  RTE_DefaultConfig.svgCode_lineheight = '<svg viewBox="-2 -2 36 36" fill="currentColor"><rect width="13" height="2" x="17" y="6"></rect><rect width="10" height="2" x="17" y="12"></rect><rect width="13" height="2" x="17" y="18"></rect><rect width="10" height="2" x="17" y="24"></rect><polygon points="11.59 13.41 8 9.83 8 9.83 4.41 13.42 3 12 8 7 13 12 11.59 13.41"></polygon><polygon points="11.59 18.59 8 22.17 8 22.17 4.41 18.58 3 20 8 25 13 20 11.59 18.59"></polygon></svg>';
443
547
  RTE_DefaultConfig.svgCode_insertemoji = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>';
444
548
  RTE_DefaultConfig.svgCode_insertchars = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 20h4l-2.3-2.6a6 6 0 1 1 8.6 0L14 20h4"/></svg>';
445
- RTE_DefaultConfig.svgCode_selectall = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="3 3"/><path d="M8.5 12.5l2.5 2.5 4.5-5"/></svg>';
549
+ // Language of parts (WCAG 3.1.2). A speech bubble over a globe: the phrase, not the page.
550
+ RTE_DefaultConfig.svgCode_textpartlanguage = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a15 15 0 0 1 0 18a15 15 0 0 1 0-18z"/></svg>';
551
+ RTE_DefaultConfig.svgCode_selectall ='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="3 3"/><path d="M8.5 12.5l2.5 2.5 4.5-5"/></svg>';
446
552
  RTE_DefaultConfig.svgCode_inserthorizontalrule = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/></svg>';
447
553
  RTE_DefaultConfig.svgCode_insertdate = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
448
554
  RTE_DefaultConfig.svgCode_forecolor = '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.6,12.6h1.2l-4.3-9.8H9.3L5,12.6h1.2l1-2.3h5.4L13.6,12.6z M7.8,9.2l2.1-4.8H10l2.1,4.8L7.8,9.2z M3.8,14.4h12.3v2.3H3.8V14.4z"/></svg>';
@@ -627,6 +733,8 @@ RTE_DefaultConfig.text_revisionempty = "No text captured in this snapshot.";
627
733
  RTE_DefaultConfig.text_html2pdf = "Create PDF";
628
734
  RTE_DefaultConfig.text_insertemoji = "Insert Emoji";
629
735
  RTE_DefaultConfig.text_insertchars = "Special characters";
736
+ RTE_DefaultConfig.text_textpartlanguage = "Language";// Mark the language of a phrase (WCAG 3.1.2).
737
+ RTE_DefaultConfig.text_removelanguage = "Remove language";
630
738
  RTE_DefaultConfig.text_characters = "Characters";
631
739
  RTE_DefaultConfig.text_words = "Words";
632
740
 
@@ -829,6 +937,11 @@ RTE_DefaultConfig.text_searchemojis = "Search";
829
937
  RTE_DefaultConfig.text_insertgallerytitle = "@insertgallery";
830
938
  RTE_DefaultConfig.text_inserttemplatetitle = "@inserttemplate";
831
939
 
940
+ RTE_DefaultConfig.text_uploadfailed = "The file could not be uploaded."; // Shown when the upload handler reports an error; the error code follows in brackets.
941
+ RTE_DefaultConfig.text_uploadnothandler = "This file cannot be attached, because file uploads are not set up on this site."; // Shown when no file_upload_handler is configured and fileUploadLocalObjectUrl is false.
942
+ RTE_DefaultConfig.text_uploadtoobig = "This file is {0}. The maximum allowed size is {1}."; // {0} = the file size, {1} = maxUploadFileSize. Shown when a file is refused; handle the "uploadtoobig" customdialog hook for your own dialog.
943
+ RTE_DefaultConfig.text_base64toobig = "This image is {0} even after resizing. Images larger than {1} need an image upload handler - see the upload documentation."; // {0} = embedded size, {1} = base64ImageMaxSize. customdialog hook: "base64toobig".
944
+ RTE_DefaultConfig.text_base64disabled = "Images cannot be embedded in this editor. An image upload handler must be configured (file_upload_handler)."; // Shown when allowBase64Images is false and no handler is set. customdialog hook: "base64disabled".
832
945
  RTE_DefaultConfig.text_reachmaxlength = "The text to be added has reached the character limit for this field.";
833
946
 
834
947
  RTE_DefaultConfig.translation = RTE_DefaultConfig.translation || {
@@ -846,27 +959,29 @@ RTE_DefaultConfig.plugin_insertcomment = function () {
846
959
  var selectedText = sel && !sel.isCollapsed ? sel.toString() : "";
847
960
 
848
961
  var dialoginner = editor.createDialog("Add Comment", "rte-dialog-insertcomment");
849
- dialoginner.style.padding = "16px";
962
+ dialoginner.style.padding = "14px";
850
963
  if (selectedText) {
851
964
  var preview = document.createElement("div");
852
- preview.style.cssText = "background:#f5f5f5;border-left:3px solid #0f8b8d;padding:8px 12px;margin-bottom:12px;font-size:12px;color:#555;border-radius:0 4px 4px 0;max-height:60px;overflow:hidden;";
965
+ preview.className = "rte-comment-selection-preview";
853
966
  preview.innerText = selectedText.substring(0, 120) + (selectedText.length > 120 ? "..." : "");
854
967
  dialoginner.appendChild(preview);
855
968
  }
856
969
  var label = document.createElement("label");
857
970
  label.innerText = "Comment:";
858
- label.style.cssText = "display:block;margin-bottom:6px;font-size:13px;font-weight:600;";
971
+ label.className = "rte-comment-dialog-label";
859
972
  dialoginner.appendChild(label);
860
973
  var textarea = document.createElement("textarea");
974
+ textarea.id = "rte-comment-input-" + new Date().getTime();
975
+ label.htmlFor = textarea.id;
861
976
  textarea.placeholder = "Type your comment...";
862
- textarea.style.cssText = "width:100%;height:80px;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:13px;box-sizing:border-box;resize:vertical;";
977
+ textarea.className = "rte-comment-dialog-input";
863
978
  dialoginner.appendChild(textarea);
864
979
  var btnRow = document.createElement("div");
865
- btnRow.style.cssText = "margin-top:12px;text-align:right;";
980
+ btnRow.className = "rte-comment-dialog-actions";
866
981
  var insertBtn = document.createElement("button");
867
982
  insertBtn.innerText = "Add Comment";
868
983
  insertBtn.type = "button";
869
- insertBtn.style.cssText = "padding:6px 18px;background:#0f8b8d;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;";
984
+ insertBtn.className = "rte-comment-dialog-submit";
870
985
  insertBtn.onclick = function () {
871
986
  var comment = textarea.value.replace(/^\s+|\s+$/g, "");
872
987
  if (comment && selectedText) {
@@ -877,11 +992,30 @@ RTE_DefaultConfig.plugin_insertcomment = function () {
877
992
  mark.setAttribute("data-comment", comment);
878
993
  try { range.surroundContents(mark); } catch (e) {}
879
994
  } else if (comment) {
880
- editor.insertHTML('<span class="rte-comment-marker" contenteditable="false" style="background:#fff9c4;border:1px solid #f9a825;border-radius:3px;padding:1px 6px;font-size:11px;color:#f57f17;cursor:pointer;" title="' + comment.replace(/"/g, "&quot;") + '">&#128172; Comment</span>');
995
+ var safeComment = comment.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
996
+ editor.insertHTML('<span class="rte-comment-marker" contenteditable="false" style="background:#fff9c4;border:1px solid #f9a825;border-radius:3px;padding:1px 6px;font-size:11px;color:#f57f17;cursor:pointer;" title="' + safeComment + '">&#128172; Comment</span>');
881
997
  }
882
998
  dialoginner.close();
883
999
  editor.focus();
884
1000
  };
1001
+ var cancelBtn = document.createElement("button");
1002
+ cancelBtn.innerText = "Cancel";
1003
+ cancelBtn.type = "button";
1004
+ cancelBtn.className = "rte-comment-dialog-cancel";
1005
+ cancelBtn.onclick = function () {
1006
+ dialoginner.close();
1007
+ editor.focus();
1008
+ };
1009
+ textarea.onkeydown = function (event) {
1010
+ if (event.key === "Escape") {
1011
+ event.preventDefault();
1012
+ cancelBtn.click();
1013
+ } else if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
1014
+ event.preventDefault();
1015
+ insertBtn.click();
1016
+ }
1017
+ };
1018
+ btnRow.appendChild(cancelBtn);
885
1019
  btnRow.appendChild(insertBtn);
886
1020
  dialoginner.appendChild(btnRow);
887
1021
  setTimeout(function () { textarea.focus(); }, 100);
@@ -931,3 +1065,9 @@ RTE_DefaultConfig.plugin_insertcomment = function () {
931
1065
  add("highlight", mod + "+" + shift + "+H");
932
1066
  add("readingmode", mod + "+" + shift + "+M");
933
1067
  })();
1068
+
1069
+ // 2026-09-04 State inspector (developer tool). The button is opt-in via
1070
+ // stateInspectorToolbarButton; these strings exist for hosts that turn it on.
1071
+ RTE_DefaultConfig.svgCode_stateinspector = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 8h3v3"/><path d="M8 16h8"/><path d="M14 8l2 2-2 2"/></svg>';
1072
+ RTE_DefaultConfig.text_stateinspector = "State inspector";
1073
+ RTE_DefaultConfig.text_stateinspectorhint = "Derived document model, structural checks, and a copyable snapshot for bug reports.";