@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.
- package/package.json +1 -1
- package/richtexteditor/all_plugins.js +158 -47
- package/richtexteditor/all_plugins.min.js +55 -55
- package/richtexteditor/plugins/aitoolkit.js +27 -4
- package/richtexteditor/plugins/all_plugins.js +158 -47
- package/richtexteditor/plugins/all_plugins.min.js +55 -55
- package/richtexteditor/plugins/insertgallery.js +108 -42
- package/richtexteditor/plugins/sanitizer.js +23 -1
- package/richtexteditor/rte-config.js +158 -18
- package/richtexteditor/rte-headless.js +40 -0
- package/richtexteditor/rte.js +21 -2
- package/richtexteditor/runtime/colorpicker_more_ns.htm +129 -35
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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,
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
962
|
+
dialoginner.style.padding = "14px";
|
|
850
963
|
if (selectedText) {
|
|
851
964
|
var preview = document.createElement("div");
|
|
852
|
-
preview.
|
|
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.
|
|
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.
|
|
977
|
+
textarea.className = "rte-comment-dialog-input";
|
|
863
978
|
dialoginner.appendChild(textarea);
|
|
864
979
|
var btnRow = document.createElement("div");
|
|
865
|
-
btnRow.
|
|
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.
|
|
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
|
-
|
|
995
|
+
var safeComment = comment.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 + '">💬 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.";
|