@richscripts/richtexteditor 2.6.4 → 2.7.1
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/LICENSE.md +66 -0
- package/README.md +22 -2
- package/package.json +146 -142
- package/richtexteditor/all_plugins.js +866 -17
- package/richtexteditor/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/accessibilitychecker.js +283 -0
- package/richtexteditor/plugins/all_plugins.js +866 -17
- package/richtexteditor/plugins/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/dialoga11y.js +50 -6
- package/richtexteditor/plugins/documentimport.js +59 -2
- package/richtexteditor/plugins/docxexport.js +57 -3
- package/richtexteditor/plugins/inlinecode.js +270 -0
- package/richtexteditor/plugins/keyboarda11y.js +108 -0
- package/richtexteditor/plugins/pdfexport.js +35 -5
- package/richtexteditor/plugins/wordexport.js +3 -1
- package/richtexteditor/rte.js +54 -4
|
@@ -82,9 +82,24 @@ function RTE_Plugin_DialogA11y() {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// 2026-09-05 Widened. The original selector only matched
|
|
86
|
+
// <rte-dropdown-panel>/.rte-panel-general, but Find&Replace, Insert Link and
|
|
87
|
+
// their siblings render as <rte-dialog-float class="rte-panel-find"> wrapping
|
|
88
|
+
// an <rte-dialog-inner role="dialog">. Those carry EXACTLY the markup this
|
|
89
|
+
// plugin consumes - an <rte-dialog-header> and <rte-dialog-line-X> field
|
|
90
|
+
// wrappers - and were simply never visited, so the plugin reported nothing
|
|
91
|
+
// and covered nothing. Measured before the change: calling
|
|
92
|
+
// editor.applyDialogAccessibility() with the find dialog open labelled 0 of
|
|
93
|
+
// its 4 inputs, leaving "Find" and "Replace" announced as bare "edit"
|
|
94
|
+
// (WCAG 1.3.1 / 3.3.2 / 4.1.2) while the two checkboxes were fine because
|
|
95
|
+
// the core happens to wrap those in <label>.
|
|
96
|
+
//
|
|
97
|
+
// Widening is safe because enhancePanel() still requires an
|
|
98
|
+
// <rte-dialog-header>, which is what keeps colour/font dropdowns out.
|
|
85
99
|
function isPanel(el) {
|
|
86
100
|
return el && el.nodeType === 1 &&
|
|
87
|
-
(el.tagName === "RTE-DROPDOWN-PANEL" ||
|
|
101
|
+
(el.tagName === "RTE-DROPDOWN-PANEL" || el.tagName === "RTE-DIALOG-FLOAT" ||
|
|
102
|
+
/rte-panel-general/.test(el.className || ""));
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
function schedule(panel) {
|
|
@@ -104,7 +119,7 @@ function RTE_Plugin_DialogA11y() {
|
|
|
104
119
|
|
|
105
120
|
function enhanceAll(host) {
|
|
106
121
|
if (!host || !host.querySelectorAll) return;
|
|
107
|
-
var ps = host.querySelectorAll("rte-dropdown-panel,[class*='rte-panel-general']");
|
|
122
|
+
var ps = host.querySelectorAll("rte-dropdown-panel,rte-dialog-float,[class*='rte-panel-general']");
|
|
108
123
|
for (var i = 0; i < ps.length; i++) enhancePanel(ps[i]);
|
|
109
124
|
}
|
|
110
125
|
|
|
@@ -116,12 +131,22 @@ function RTE_Plugin_DialogA11y() {
|
|
|
116
131
|
var name = (headerEl.textContent || "").trim()
|
|
117
132
|
.replace(/\s*\((?:Ctrl|Cmd|Alt|Shift|⌘|⇧|⌥)[^)]*\)\s*$/i, "").trim();
|
|
118
133
|
|
|
134
|
+
// Name the element that IS the dialog. The float-style panels already
|
|
135
|
+
// carry role="dialog" on an inner <rte-dialog-inner>; adding a second
|
|
136
|
+
// role="dialog" to the wrapper would announce two nested dialogs, so
|
|
137
|
+
// when an inner one exists it is the target and the wrapper is left
|
|
138
|
+
// alone. When there is none (the original dropdown-panel shape) this
|
|
139
|
+
// resolves to the panel itself and behaves exactly as before.
|
|
140
|
+
var dialogEl = panel.getAttribute("role") === "dialog"
|
|
141
|
+
? panel
|
|
142
|
+
: (panel.querySelector('[role="dialog"]') || panel);
|
|
143
|
+
|
|
119
144
|
// A titled, form-bearing popup is a dialog, not a menu — override the
|
|
120
145
|
// core's default role="menu" (a menu must not contain text fields).
|
|
121
|
-
var role =
|
|
122
|
-
if (!role || role === "menu")
|
|
123
|
-
if (name && !
|
|
124
|
-
|
|
146
|
+
var role = dialogEl.getAttribute("role");
|
|
147
|
+
if (!role || role === "menu") dialogEl.setAttribute("role", "dialog");
|
|
148
|
+
if (name && !dialogEl.getAttribute("aria-label") && !dialogEl.getAttribute("aria-labelledby")) {
|
|
149
|
+
dialogEl.setAttribute("aria-label", name);
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
var fields = panel.querySelectorAll("input,select,textarea");
|
|
@@ -136,6 +161,25 @@ function RTE_Plugin_DialogA11y() {
|
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
function fieldLabel(f, dialogName) {
|
|
164
|
+
// 0) THE VISIBLE LABEL WINS. 2026-09-05: deriving the name from the
|
|
165
|
+
// <rte-dialog-line-X> suffix alone produced "Keyword" for a field whose
|
|
166
|
+
// visible label reads "Find". That fails WCAG 2.5.3 Label in Name, and
|
|
167
|
+
// it breaks speech input concretely: a user says "Find" and nothing
|
|
168
|
+
// matches, because the accessible name does not contain the words they
|
|
169
|
+
// can see. The markup already carries the right text in a sibling
|
|
170
|
+
// <rte-dialog-input-label>, so prefer it and keep the suffix as the
|
|
171
|
+
// fallback for fields that have no visible label.
|
|
172
|
+
var lineEl = f.parentElement;
|
|
173
|
+
for (var d = 0; d < 8 && lineEl; d++) {
|
|
174
|
+
if (/^RTE-DIALOG-LINE-/.test(lineEl.tagName || "")) break;
|
|
175
|
+
lineEl = lineEl.parentElement;
|
|
176
|
+
}
|
|
177
|
+
if (lineEl && lineEl.querySelector) {
|
|
178
|
+
var visEl = lineEl.querySelector("rte-dialog-input-label");
|
|
179
|
+
var vis = visEl ? (visEl.textContent || "").replace(/^\s+|\s+$/g, "").replace(/[::]\s*$/, "") : "";
|
|
180
|
+
if (vis) return vis;
|
|
181
|
+
}
|
|
182
|
+
|
|
139
183
|
// 1) nearest <rte-dialog-line-X> ancestor — the suffix is the field meaning
|
|
140
184
|
var n = f.parentElement;
|
|
141
185
|
for (var i = 0; i < 8 && n; i++) {
|
|
@@ -951,6 +951,25 @@ function RTE_Plugin_DocumentImport() {
|
|
|
951
951
|
var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
|
|
952
952
|
if (hv && hv !== "none") css += "background-color:" + hv + ";";
|
|
953
953
|
}
|
|
954
|
+
// w:shd is the OTHER way Word carries a background, and it is the
|
|
955
|
+
// one our own exporter writes (w:highlight only accepts a fixed
|
|
956
|
+
// set of colour NAMES, shd takes any hex). Reading only
|
|
957
|
+
// w:highlight meant every highlight we exported came back
|
|
958
|
+
// unhighlighted - the two sides used different vocabulary for the
|
|
959
|
+
// same thing.
|
|
960
|
+
var shd = firstChildEl(rpr, "shd");
|
|
961
|
+
if (shd) {
|
|
962
|
+
var fill = shd.getAttributeNS(W, "fill") || shd.getAttribute("w:fill") || "";
|
|
963
|
+
if (fill && /^[0-9A-Fa-f]{6}$/.test(fill)) css += "background-color:#" + fill + ";";
|
|
964
|
+
}
|
|
965
|
+
// Monospace run -> inline code. Our exporter marks <code>/<kbd>/
|
|
966
|
+
// <samp>/<pre> with w:rFonts Consolas; nothing read it back.
|
|
967
|
+
var rf = firstChildEl(rpr, "rFonts");
|
|
968
|
+
if (rf) {
|
|
969
|
+
var asc = rf.getAttributeNS(W, "ascii") || rf.getAttribute("w:ascii") || "";
|
|
970
|
+
if (/consolas|courier|monospace|mono$/i.test(asc)) txt = "<code>" + txt + "</code>";
|
|
971
|
+
else if (asc) css += "font-family:" + asc.replace(/[<>"]/g, "") + ";";
|
|
972
|
+
}
|
|
954
973
|
var sz = firstChildEl(rpr, "sz");
|
|
955
974
|
if (sz) {
|
|
956
975
|
var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
|
|
@@ -1123,7 +1142,16 @@ function RTE_Plugin_DocumentImport() {
|
|
|
1123
1142
|
// without this they came back as plain <p> and the round-trip lost
|
|
1124
1143
|
// every blockquote. Match Quote / IntenseQuote / BlockQuote.
|
|
1125
1144
|
else if (/^(intense)?quote$|blockquote/i.test(v.replace(/\s+/g, ""))) info.quote = true;
|
|
1145
|
+
// docxexport.js writes <w:pStyle w:val="Code"/> for <pre>; without
|
|
1146
|
+
// this a code block came back as an ordinary paragraph and lost
|
|
1147
|
+
// both its monospace and its whitespace.
|
|
1148
|
+
else if (/^(code|sourcecode|htmlpre|preformatted)$/i.test(v.replace(/\s+/g, ""))) info.pre = true;
|
|
1126
1149
|
}
|
|
1150
|
+
// A horizontal rule is exported as an EMPTY paragraph carrying a
|
|
1151
|
+
// bottom border - Word has no <hr>. Nothing read it back, so every
|
|
1152
|
+
// rule became a stray blank paragraph on import.
|
|
1153
|
+
var pbdr = firstChildEl(ppr, "pBdr");
|
|
1154
|
+
if (pbdr && firstChildEl(pbdr, "bottom")) info.rule = true;
|
|
1127
1155
|
var numPr = firstChildEl(ppr, "numPr");
|
|
1128
1156
|
if (numPr) {
|
|
1129
1157
|
info.list = true;
|
|
@@ -1138,6 +1166,19 @@ function RTE_Plugin_DocumentImport() {
|
|
|
1138
1166
|
// matches the old behaviour rather than inventing <ol>s.
|
|
1139
1167
|
info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
|
|
1140
1168
|
}
|
|
1169
|
+
// Word paragraph indentation. Nothing read this before 2026-09-04,
|
|
1170
|
+
// so every indented paragraph in every imported .docx arrived
|
|
1171
|
+
// flush left and the indentation was lost silently.
|
|
1172
|
+
// w:left is in twips; 15 twips = 1px.
|
|
1173
|
+
var ind = firstChildEl(ppr, "ind");
|
|
1174
|
+
if (ind) {
|
|
1175
|
+
var lft = ind.getAttributeNS(W, "left") || ind.getAttribute("w:left") || "";
|
|
1176
|
+
var tw = parseInt(lft, 10);
|
|
1177
|
+
// 720 twips alongside the Quote style is the blockquote's own
|
|
1178
|
+
// indent, already carried by the blockquote element itself.
|
|
1179
|
+
if (tw > 0 && !(info.quote && tw === 720))
|
|
1180
|
+
info.indentPx = Math.round(tw / 15);
|
|
1181
|
+
}
|
|
1141
1182
|
var jc = firstChildEl(ppr, "jc");
|
|
1142
1183
|
if (jc) {
|
|
1143
1184
|
var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
|
|
@@ -1204,10 +1245,26 @@ function RTE_Plugin_DocumentImport() {
|
|
|
1204
1245
|
continue;
|
|
1205
1246
|
}
|
|
1206
1247
|
closeListsTo(0);
|
|
1207
|
-
|
|
1248
|
+
// Only a rule when the paragraph carries no text of its own -
|
|
1249
|
+
// a bordered paragraph WITH content is a real bordered paragraph.
|
|
1250
|
+
if (st.rule && !(inner || "").replace(/<[^>]*>/g, "").trim()) {
|
|
1251
|
+
closeListsTo(0); out.push("<hr>"); continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (st.pre) { out.push("<pre>" + (inner || "<br>") + "</pre>"); continue; }
|
|
1254
|
+
if (st.heading) {
|
|
1255
|
+
// Headings carry w:ind too; without this an indented
|
|
1256
|
+
// heading imported flush left.
|
|
1257
|
+
var hs = st.indentPx ? ' style="margin-left:' + st.indentPx + 'px"' : "";
|
|
1258
|
+
out.push("<h" + st.heading + hs + ">" + (inner || "") + "</h" + st.heading + ">");
|
|
1259
|
+
}
|
|
1208
1260
|
else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
|
|
1209
1261
|
else {
|
|
1210
|
-
var
|
|
1262
|
+
var decl = [];
|
|
1263
|
+
if (st.align) decl.push("text-align:" + st.align);
|
|
1264
|
+
// indentUseMargin defaults to margin, and margin is what
|
|
1265
|
+
// w:ind means, so import writes margin-left to match.
|
|
1266
|
+
if (st.indentPx) decl.push("margin-left:" + st.indentPx + "px");
|
|
1267
|
+
var style = decl.length ? ' style="' + decl.join(";") + '"' : "";
|
|
1211
1268
|
// A paragraph that contained ONLY a text box has no text of
|
|
1212
1269
|
// its own; emitting an empty <p> before the box adds a blank
|
|
1213
1270
|
// line that was never in the document.
|
|
@@ -194,7 +194,7 @@ function RTE_Plugin_DocxExport() {
|
|
|
194
194
|
bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
|
|
195
195
|
strike: inherited.strike, sizeHalfPoints: inherited.sizeHalfPoints,
|
|
196
196
|
color: inherited.color, highlight: inherited.highlight, vertAlign: inherited.vertAlign,
|
|
197
|
-
mono: inherited.mono
|
|
197
|
+
mono: inherited.mono, font: inherited.font
|
|
198
198
|
};
|
|
199
199
|
var tag = node.tagName ? node.tagName.toLowerCase() : "";
|
|
200
200
|
if (tag === "b" || tag === "strong") s.bold = true;
|
|
@@ -204,6 +204,10 @@ function RTE_Plugin_DocxExport() {
|
|
|
204
204
|
if (tag === "sup") s.vertAlign = "superscript";
|
|
205
205
|
if (tag === "sub") s.vertAlign = "subscript";
|
|
206
206
|
if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "pre") s.mono = true;
|
|
207
|
+
// <mark> carries no inline style, so the CSS branch below never sees it
|
|
208
|
+
// and highlighted text was dropped entirely on export. Word's own
|
|
209
|
+
// default highlight is yellow.
|
|
210
|
+
if (tag === "mark") s.highlight = s.highlight || "FFFF00";
|
|
207
211
|
|
|
208
212
|
var css = node.style;
|
|
209
213
|
if (css) {
|
|
@@ -214,6 +218,12 @@ function RTE_Plugin_DocxExport() {
|
|
|
214
218
|
if (deco.indexOf("line-through") >= 0) s.strike = true;
|
|
215
219
|
if (css.color) { var c = hexColor(css.color); if (c) s.color = c; }
|
|
216
220
|
if (css.backgroundColor) { var h = hexColor(css.backgroundColor); if (h) s.highlight = h; }
|
|
221
|
+
// Font family was read by nothing, so an exported document lost every
|
|
222
|
+
// typeface choice. Word wants ONE family name, not a CSS stack.
|
|
223
|
+
if (css.fontFamily) {
|
|
224
|
+
var fam = String(css.fontFamily).split(",")[0].replace(/^\s*['"]?|['"]?\s*$/g, "");
|
|
225
|
+
if (fam) s.font = fam;
|
|
226
|
+
}
|
|
217
227
|
if (css.fontSize) {
|
|
218
228
|
var pt = cssToPoints(css.fontSize);
|
|
219
229
|
// w:sz is in HALF-POINTS. Writing points here makes every
|
|
@@ -250,7 +260,10 @@ function RTE_Plugin_DocxExport() {
|
|
|
250
260
|
|
|
251
261
|
function runProps(s) {
|
|
252
262
|
var p = "";
|
|
253
|
-
|
|
263
|
+
// One rFonts element only: mono wins, because <code> inside a styled span
|
|
264
|
+
// should still read as code.
|
|
265
|
+
var fontName = s.mono ? "Consolas" : (s.font || "");
|
|
266
|
+
if (fontName) p += '<w:rFonts w:ascii="' + esc(fontName) + '" w:hAnsi="' + esc(fontName) + '"/>';
|
|
254
267
|
if (s.bold) p += "<w:b/>";
|
|
255
268
|
if (s.italic) p += "<w:i/>";
|
|
256
269
|
if (s.underline) p += '<w:u w:val="single"/>';
|
|
@@ -612,6 +625,31 @@ function RTE_Plugin_DocxExport() {
|
|
|
612
625
|
var revSeq = { n: 1 };
|
|
613
626
|
var base = { bold: false, italic: false, underline: false, strike: false, sizeHalfPoints: 0, color: null, highlight: null, vertAlign: null, mono: false };
|
|
614
627
|
|
|
628
|
+
|
|
629
|
+
// A block indented in the editor carries margin-left (or padding-left,
|
|
630
|
+
// per indentUseMargin). Word expresses the same thing as w:ind, in
|
|
631
|
+
// TWIPS. Before 2026-09-04 indentation reached Word only when it was
|
|
632
|
+
// spelled <blockquote>, which dragged the Quote style along with it -
|
|
633
|
+
// so indented body text arrived italic and accent-coloured.
|
|
634
|
+
// 1px = 0.75pt and 1pt = 20 twips, hence px * 15.
|
|
635
|
+
function indentTwips(node) {
|
|
636
|
+
if (!node || !node.style) return 0;
|
|
637
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
638
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
639
|
+
if (!m) return 0;
|
|
640
|
+
var n = parseFloat(m[1]);
|
|
641
|
+
if (!n || n <= 0) return 0;
|
|
642
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
643
|
+
var px = unit === "px" ? n
|
|
644
|
+
: unit === "pt" ? n * (4 / 3)
|
|
645
|
+
: unit === "in" ? n * 96
|
|
646
|
+
: unit === "cm" ? n * 37.795
|
|
647
|
+
: unit === "mm" ? n * 3.7795
|
|
648
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
649
|
+
: 0;
|
|
650
|
+
return Math.round(px * 15);
|
|
651
|
+
}
|
|
652
|
+
|
|
615
653
|
function para(node, style, opts) {
|
|
616
654
|
opts = opts || {};
|
|
617
655
|
var pr = "";
|
|
@@ -620,6 +658,12 @@ function RTE_Plugin_DocxExport() {
|
|
|
620
658
|
if (opts.pre) pr += '<w:pStyle w:val="Code"/>';
|
|
621
659
|
if (opts.numId) pr += "<w:numPr><w:ilvl w:val=\"" + (opts.level || 0) + "\"/><w:numId w:val=\"" + opts.numId + "\"/></w:numPr>";
|
|
622
660
|
if (opts.pageBreakBefore) pr += "<w:pageBreakBefore/>";
|
|
661
|
+
// Independent of the Quote style: a real blockquote already got
|
|
662
|
+
// its w:ind above, so only emit this for everything else.
|
|
663
|
+
if (!opts.quote) {
|
|
664
|
+
var indTw = indentTwips(node);
|
|
665
|
+
if (indTw > 0) pr += '<w:ind w:left="' + indTw + '"/>';
|
|
666
|
+
}
|
|
623
667
|
var align = node && node.style ? node.style.textAlign : "";
|
|
624
668
|
if (align === "center" || align === "right") pr += '<w:jc w:val="' + align + '"/>';
|
|
625
669
|
else if (align === "justify") pr += '<w:jc w:val="both"/>';
|
|
@@ -637,7 +681,17 @@ function RTE_Plugin_DocxExport() {
|
|
|
637
681
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
638
682
|
var li = node.childNodes[i];
|
|
639
683
|
if (li.nodeType !== 1 || li.tagName.toLowerCase() !== "li") continue;
|
|
640
|
-
|
|
684
|
+
// Serialise the item's OWN content only. para() walks every
|
|
685
|
+
// descendant, so passing the whole <li> emitted the nested list's
|
|
686
|
+
// text here AND again as its own items below - a round trip turned
|
|
687
|
+
// "xx" with a child "yy" into "xxyy". Content duplication, not loss.
|
|
688
|
+
var own = li.ownerDocument.createElement("li");
|
|
689
|
+
for (var k = 0; k < li.childNodes.length; k++) {
|
|
690
|
+
var ch = li.childNodes[k];
|
|
691
|
+
if (ch.nodeType === 1 && /^(ul|ol)$/.test(ch.tagName.toLowerCase())) continue;
|
|
692
|
+
own.appendChild(ch.cloneNode(true));
|
|
693
|
+
}
|
|
694
|
+
xml += para(own, styleFrom(li, style), { numId: numId, level: level, pageBreakBefore: pendingBreak });
|
|
641
695
|
pendingBreak = false;
|
|
642
696
|
for (var j = 0; j < li.childNodes.length; j++) {
|
|
643
697
|
var sub = li.childNodes[j];
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-09-04 Inline code — CKEditor 5's "Code" feature (`<code>`), the one
|
|
4
|
+
// basic-style it ships that this editor had no command for. Markdown `x` was
|
|
5
|
+
// the only way to produce inline code here, which meant there was no way to
|
|
6
|
+
// REMOVE it and no toolbar affordance at all.
|
|
7
|
+
//
|
|
8
|
+
// The command is "inlinecode", NOT "code". "code" is already taken by the
|
|
9
|
+
// HTML-source-view toggle in the core (__Is_Cmd_Active case "code" ->
|
|
10
|
+
// ___Is_CodeMode()), so registering "code" here would have collided with source
|
|
11
|
+
// view. keyboarda11y.js has carried "inlinecode" in its TOGGLE_CMD list all
|
|
12
|
+
// along, with a comment that it matched nothing in this build; a core
|
|
13
|
+
// __Is_Cmd_Active case added alongside this plugin is what makes it match, so
|
|
14
|
+
// the button reports aria-pressed and not just a highlight.
|
|
15
|
+
//
|
|
16
|
+
// Design notes:
|
|
17
|
+
// - Toggling is done by SPLITTING the <code> element around the selection
|
|
18
|
+
// rather than by lifting text nodes out of it. Splitting preserves nested
|
|
19
|
+
// inline markup: un-coding the middle of <code>a <em>b</em> c</code> keeps
|
|
20
|
+
// the <em>. Lifting text nodes would have dropped it.
|
|
21
|
+
// - A collapsed selection acts on the word under the caret. That is the same
|
|
22
|
+
// choice changecase.js makes, and for the same reason: it makes the command
|
|
23
|
+
// usable from a keyboard shortcut without selecting first. The alternative -
|
|
24
|
+
// inserting an empty <code> and putting the caret inside - is the caret trap
|
|
25
|
+
// documented in the footnotes work, so it is deliberately not done.
|
|
26
|
+
// - Never nests inside <pre> or an existing <code>: both already mean
|
|
27
|
+
// "preformatted", and nesting produces markup no serializer round-trips.
|
|
28
|
+
// - Emits a bare <code> element and no rte-* class. That keeps the styling in
|
|
29
|
+
// the content stylesheet where authors can override it, and keeps the plugin
|
|
30
|
+
// out of check-css-contract.mjs, which only scans the four theme sheets.
|
|
31
|
+
RTE_DefaultConfig.plugin_inlinecode = RTE_Plugin_InlineCode;
|
|
32
|
+
|
|
33
|
+
if (!RTE_DefaultConfig.text_inlinecode) RTE_DefaultConfig.text_inlinecode = "Inline code";
|
|
34
|
+
|
|
35
|
+
if (!RTE_DefaultConfig.svgCode_inlinecode) {
|
|
36
|
+
RTE_DefaultConfig.svgCode_inlinecode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 8 3 12 7 16"/><polyline points="17 8 21 12 17 16"/></svg>';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function RTE_Plugin_InlineCode() {
|
|
40
|
+
var obj = this;
|
|
41
|
+
var config, editor;
|
|
42
|
+
|
|
43
|
+
obj.PluginName = "InlineCode";
|
|
44
|
+
|
|
45
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
46
|
+
|
|
47
|
+
obj.InitEditor = function (argeditor) {
|
|
48
|
+
editor = argeditor;
|
|
49
|
+
|
|
50
|
+
editor.attachEvent("exec_command_inlinecode", function (state) {
|
|
51
|
+
state.returnValue = true;
|
|
52
|
+
obj.Toggle();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Public API, matching the shape the other toggle plugins expose.
|
|
56
|
+
editor.toggleInlineCode = function () { return obj.Toggle(); };
|
|
57
|
+
editor.isInlineCode = function () { return obj.IsActive(); };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
61
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
62
|
+
|
|
63
|
+
function getRange(doc) {
|
|
64
|
+
var sel = doc && doc.getSelection ? doc.getSelection() : null;
|
|
65
|
+
if (!sel || !sel.rangeCount) return null;
|
|
66
|
+
return sel.getRangeAt(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Nearest <code> ancestor that is still inside the editable area.
|
|
70
|
+
function codeAncestor(node, editable) {
|
|
71
|
+
while (node && node !== editable) {
|
|
72
|
+
if (node.nodeType === 1 && node.nodeName === "CODE") return node;
|
|
73
|
+
node = node.parentNode;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function inPre(node, editable) {
|
|
79
|
+
while (node && node !== editable) {
|
|
80
|
+
if (node.nodeType === 1 && node.nodeName === "PRE") return true;
|
|
81
|
+
node = node.parentNode;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
obj.IsActive = function () {
|
|
87
|
+
var doc = getDoc(), editable = getEditable();
|
|
88
|
+
if (!doc || !editable) return false;
|
|
89
|
+
var range = getRange(doc);
|
|
90
|
+
if (!range) return false;
|
|
91
|
+
return !!codeAncestor(range.startContainer, editable);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---- selection helpers -----------------------------------------------
|
|
95
|
+
|
|
96
|
+
// Expand a collapsed caret to the word under it. Returns false when the
|
|
97
|
+
// caret is not sitting in a word, in which case the command does nothing
|
|
98
|
+
// rather than creating an empty element.
|
|
99
|
+
function expandToWord(range) {
|
|
100
|
+
var node = range.startContainer;
|
|
101
|
+
if (!node || node.nodeType !== 3) return false;
|
|
102
|
+
var text = node.data || "";
|
|
103
|
+
var offset = range.startOffset;
|
|
104
|
+
var start = offset, end = offset;
|
|
105
|
+
while (start > 0 && /\S/.test(text.charAt(start - 1))) start--;
|
|
106
|
+
while (end < text.length && /\S/.test(text.charAt(end))) end++;
|
|
107
|
+
if (start === end) return false;
|
|
108
|
+
range.setStart(node, start);
|
|
109
|
+
range.setEnd(node, end);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Every text node the range actually covers. Boundary text nodes are split
|
|
114
|
+
// first so a partially-selected node is not treated as fully selected.
|
|
115
|
+
function selectedTextNodes(doc, range, editable) {
|
|
116
|
+
if (range.startContainer.nodeType === 3 && range.startOffset > 0) {
|
|
117
|
+
var s = range.startContainer;
|
|
118
|
+
if (range.startOffset < s.data.length) {
|
|
119
|
+
var rest = s.splitText(range.startOffset);
|
|
120
|
+
var endSame = (range.endContainer === s);
|
|
121
|
+
var endOff = range.endOffset;
|
|
122
|
+
range.setStart(rest, 0);
|
|
123
|
+
if (endSame) range.setEnd(rest, endOff - s.data.length);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (range.endContainer.nodeType === 3 && range.endOffset < range.endContainer.data.length && range.endOffset > 0) {
|
|
127
|
+
range.endContainer.splitText(range.endOffset);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
var out = [];
|
|
131
|
+
var walker = doc.createTreeWalker(editable, 4 /* SHOW_TEXT */, null, false);
|
|
132
|
+
var n;
|
|
133
|
+
while ((n = walker.nextNode())) {
|
|
134
|
+
if (!n.data) continue;
|
|
135
|
+
if (range.intersectsNode ? range.intersectsNode(n) : true) {
|
|
136
|
+
// intersectsNode is true for merely-touching nodes, so confirm
|
|
137
|
+
// the node is genuinely within the boundaries.
|
|
138
|
+
var r = doc.createRange();
|
|
139
|
+
r.selectNodeContents(n);
|
|
140
|
+
if (range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
|
|
141
|
+
range.compareBoundaryPoints(Range.START_TO_END, r) > 0) {
|
|
142
|
+
out.push(n);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- apply / remove ---------------------------------------------------
|
|
150
|
+
|
|
151
|
+
// Split `code` around the selection and unwrap only the overlapping part,
|
|
152
|
+
// so partial removal keeps the untouched halves coded.
|
|
153
|
+
function unwrapPart(doc, code, range) {
|
|
154
|
+
var parent = code.parentNode;
|
|
155
|
+
if (!parent) return;
|
|
156
|
+
|
|
157
|
+
var startsInside = code.contains(range.startContainer);
|
|
158
|
+
var endsInside = code.contains(range.endContainer);
|
|
159
|
+
|
|
160
|
+
// Tail first: extracting it cannot disturb the head's boundaries.
|
|
161
|
+
var after = doc.createRange();
|
|
162
|
+
if (endsInside) after.setStart(range.endContainer, range.endOffset);
|
|
163
|
+
else after.setStart(code, code.childNodes.length);
|
|
164
|
+
after.setEnd(code, code.childNodes.length);
|
|
165
|
+
var afterFrag = after.extractContents();
|
|
166
|
+
|
|
167
|
+
var before = doc.createRange();
|
|
168
|
+
before.setStart(code, 0);
|
|
169
|
+
if (startsInside) before.setEnd(range.startContainer, range.startOffset);
|
|
170
|
+
else before.setEnd(code, 0);
|
|
171
|
+
var beforeFrag = before.extractContents();
|
|
172
|
+
|
|
173
|
+
if (beforeFrag && beforeFrag.textContent !== "") {
|
|
174
|
+
var head = doc.createElement("code");
|
|
175
|
+
head.appendChild(beforeFrag);
|
|
176
|
+
parent.insertBefore(head, code);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// What is left in `code` is exactly the selected part: unwrap it.
|
|
180
|
+
while (code.firstChild) parent.insertBefore(code.firstChild, code);
|
|
181
|
+
|
|
182
|
+
if (afterFrag && afterFrag.textContent !== "") {
|
|
183
|
+
var tail = doc.createElement("code");
|
|
184
|
+
tail.appendChild(afterFrag);
|
|
185
|
+
parent.insertBefore(tail, code);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
parent.removeChild(code);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function wrapNodes(doc, nodes, editable) {
|
|
192
|
+
// Group contiguous siblings so a run of text under one parent becomes a
|
|
193
|
+
// single <code>, not one per text node.
|
|
194
|
+
var group = [], groups = [];
|
|
195
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
196
|
+
var n = nodes[i];
|
|
197
|
+
if (inPre(n, editable) || codeAncestor(n, editable)) continue;
|
|
198
|
+
if (group.length && group[group.length - 1].parentNode === n.parentNode &&
|
|
199
|
+
group[group.length - 1].nextSibling === n) {
|
|
200
|
+
group.push(n);
|
|
201
|
+
} else {
|
|
202
|
+
if (group.length) groups.push(group);
|
|
203
|
+
group = [n];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (group.length) groups.push(group);
|
|
207
|
+
|
|
208
|
+
for (var g = 0; g < groups.length; g++) {
|
|
209
|
+
var run = groups[g];
|
|
210
|
+
// Whitespace-only runs would produce a <code> containing nothing but
|
|
211
|
+
// a space, which is invisible formatting debris.
|
|
212
|
+
var joined = "";
|
|
213
|
+
for (var k = 0; k < run.length; k++) joined += run[k].data;
|
|
214
|
+
if (!/\S/.test(joined)) continue;
|
|
215
|
+
|
|
216
|
+
var code = doc.createElement("code");
|
|
217
|
+
run[0].parentNode.insertBefore(code, run[0]);
|
|
218
|
+
for (var j = 0; j < run.length; j++) code.appendChild(run[j]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Adjacent <code><code> pairs read as one to the user but serialize as two.
|
|
223
|
+
function mergeAdjacent(editable) {
|
|
224
|
+
var all = editable.querySelectorAll("code");
|
|
225
|
+
for (var i = 0; i < all.length; i++) {
|
|
226
|
+
var code = all[i];
|
|
227
|
+
var next = code.nextSibling;
|
|
228
|
+
while (next && next.nodeType === 1 && next.nodeName === "CODE") {
|
|
229
|
+
while (next.firstChild) code.appendChild(next.firstChild);
|
|
230
|
+
next.parentNode.removeChild(next);
|
|
231
|
+
next = code.nextSibling;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
obj.Toggle = function () {
|
|
237
|
+
var doc = getDoc(), editable = getEditable();
|
|
238
|
+
if (!doc || !editable) return false;
|
|
239
|
+
var range = getRange(doc);
|
|
240
|
+
if (!range) return false;
|
|
241
|
+
if (!editable.contains(range.startContainer)) return false;
|
|
242
|
+
|
|
243
|
+
if (range.collapsed && !expandToWord(range)) return false;
|
|
244
|
+
|
|
245
|
+
var nodes = selectedTextNodes(doc, range, editable);
|
|
246
|
+
if (!nodes.length) return false;
|
|
247
|
+
|
|
248
|
+
// Remove when every selected text node is already coded; otherwise apply.
|
|
249
|
+
var allCoded = true;
|
|
250
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
251
|
+
if (!/\S/.test(nodes[i].data)) continue;
|
|
252
|
+
if (!codeAncestor(nodes[i], editable)) { allCoded = false; break; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (allCoded) {
|
|
256
|
+
var codes = [], seen = [];
|
|
257
|
+
for (var n = 0; n < nodes.length; n++) {
|
|
258
|
+
var c = codeAncestor(nodes[n], editable);
|
|
259
|
+
if (c && seen.indexOf(c) < 0) { seen.push(c); codes.push(c); }
|
|
260
|
+
}
|
|
261
|
+
for (var x = 0; x < codes.length; x++) unwrapPart(doc, codes[x], range);
|
|
262
|
+
} else {
|
|
263
|
+
wrapNodes(doc, nodes, editable);
|
|
264
|
+
mergeAdjacent(editable);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try { editor.updateToolbarStatus && editor.updateToolbarStatus(); } catch (e) { }
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
}
|