@richscripts/richtexteditor 2.4.0 → 2.4.2
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/migration/README.md +100 -0
- package/migration/cli.js +294 -0
- package/migration/index.d.ts +18 -0
- package/migration/index.js +242 -0
- package/migration/report-schema-v2.json +127 -0
- package/package.json +14 -2
- package/richtexteditor/CHANGELOG.md +326 -1
- package/richtexteditor/all_plugins.js +14982 -4831
- package/richtexteditor/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/accessibilitychecker.js +8 -7
- package/richtexteditor/plugins/aitoolkit.css +17 -8
- package/richtexteditor/plugins/aitoolkit.js +8 -8
- package/richtexteditor/plugins/all_plugins.js +14982 -4831
- package/richtexteditor/plugins/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/autocorrect.js +353 -0
- package/richtexteditor/plugins/bookmarkcard.js +12 -1
- package/richtexteditor/plugins/changecase.js +238 -0
- package/richtexteditor/plugins/comments.js +15 -28
- package/richtexteditor/plugins/contentminimap.js +6 -5
- package/richtexteditor/plugins/crdt-engine.js +2201 -198
- package/richtexteditor/plugins/crdt-engine.min.js +5 -5
- package/richtexteditor/plugins/crossreference.js +408 -0
- package/richtexteditor/plugins/documentimport.js +915 -21
- package/richtexteditor/plugins/documentoutline.js +5 -4
- package/richtexteditor/plugins/docxexport.js +960 -0
- package/richtexteditor/plugins/draghandle.js +309 -0
- package/richtexteditor/plugins/footnotes.js +398 -0
- package/richtexteditor/plugins/formatpainter.js +298 -0
- package/richtexteditor/plugins/formattingmarks.js +246 -0
- package/richtexteditor/plugins/html2pdf.js +108 -4
- package/richtexteditor/plugins/imageeditor.js +37 -7
- package/richtexteditor/plugins/insertgallery.js +553 -170
- package/richtexteditor/plugins/keyboarda11y.js +316 -0
- package/richtexteditor/plugins/linenumbers.js +352 -0
- package/richtexteditor/plugins/linkchecker.js +396 -0
- package/richtexteditor/plugins/mentionplugin.js +4 -3
- package/richtexteditor/plugins/mergefields.js +328 -0
- package/richtexteditor/plugins/multilevellist.js +137 -0
- package/richtexteditor/plugins/pagination.js +505 -0
- package/richtexteditor/plugins/pdfexport.js +1137 -0
- package/richtexteditor/plugins/permanentpen.js +211 -0
- package/richtexteditor/plugins/revisionhistory.js +47 -34
- package/richtexteditor/plugins/rtlui.js +150 -0
- package/richtexteditor/plugins/sanitizer.js +510 -0
- package/richtexteditor/plugins/slashcommand.js +142 -17
- package/richtexteditor/plugins/spellcheck.js +6 -3
- package/richtexteditor/plugins/tableofcontents.js +353 -0
- package/richtexteditor/plugins/tabletools.js +342 -0
- package/richtexteditor/plugins/textdirection.js +313 -0
- package/richtexteditor/plugins/trackedchanges.js +37 -6
- package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
- package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
- package/richtexteditor/plugins/watermark.js +214 -0
- package/richtexteditor/plugins/wordexport.js +0 -0
- package/richtexteditor/plugins/yjscollab.js +49 -11
- package/richtexteditor/rte-config.js +44 -44
- package/richtexteditor/rte.js +91 -63
- package/richtexteditor/rte_theme_default.css +483 -384
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-07-27 Format painter. Pick up the character formatting at the caret and
|
|
4
|
+
// paint it onto another selection — the Word/Docs clipboard-brush interaction.
|
|
5
|
+
//
|
|
6
|
+
// Premium in both majors:
|
|
7
|
+
// - CKEditor 5 format painter: "Unlock this feature with selected CKEditor Plans".
|
|
8
|
+
// - TinyMCE formatpainter: "This plugin is only available for paid TinyMCE
|
|
9
|
+
// subscriptions".
|
|
10
|
+
//
|
|
11
|
+
// Design notes:
|
|
12
|
+
// - Formatting is captured from the COMPUTED style, not from the tag soup, so
|
|
13
|
+
// it works the same whether the source was <b>, <strong>, style="font-weight:700"
|
|
14
|
+
// or a class — which is the whole point of a painter.
|
|
15
|
+
// - Painting wraps the target in one <span> and then strips the character
|
|
16
|
+
// formatting already inside it. Without that strip the paint silently loses:
|
|
17
|
+
// a descendant <strong> beats an ancestor span's font-weight:normal, so
|
|
18
|
+
// "paint unbold onto bold text" would do nothing.
|
|
19
|
+
// - Sticky mode (paint repeatedly until cancelled) mirrors double-clicking the
|
|
20
|
+
// brush in Word. Escape cancels, as it does there.
|
|
21
|
+
RTE_DefaultConfig.plugin_formatpainter = RTE_Plugin_FormatPainter;
|
|
22
|
+
|
|
23
|
+
// The character-formatting properties the brush carries. Deliberately excludes
|
|
24
|
+
// block-level properties (alignment, margins, line-height): a painter that moved
|
|
25
|
+
// those would reflow the document, which is not what users expect from it.
|
|
26
|
+
if (typeof RTE_DefaultConfig.formatPainterProperties === "undefined") {
|
|
27
|
+
RTE_DefaultConfig.formatPainterProperties = [
|
|
28
|
+
"font-family", "font-size", "font-weight", "font-style",
|
|
29
|
+
"text-decoration-line", "color", "background-color",
|
|
30
|
+
"letter-spacing", "text-transform", "font-variant"
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function RTE_Plugin_FormatPainter() {
|
|
35
|
+
var obj = this;
|
|
36
|
+
var config, editor;
|
|
37
|
+
var captured = null; // {props:{...}} or null
|
|
38
|
+
var sticky = false;
|
|
39
|
+
var boundDoc = null;
|
|
40
|
+
|
|
41
|
+
// Character-level tags that must be unwrapped inside a painted region,
|
|
42
|
+
// because their UA styling outranks the wrapper span.
|
|
43
|
+
var CHAR_TAGS = { B: 1, STRONG: 1, I: 1, EM: 1, U: 1, S: 1, STRIKE: 1, FONT: 1, BIG: 1, SMALL: 1 };
|
|
44
|
+
|
|
45
|
+
obj.PluginName = "FormatPainter";
|
|
46
|
+
|
|
47
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
48
|
+
|
|
49
|
+
obj.InitEditor = function (argeditor) {
|
|
50
|
+
editor = argeditor;
|
|
51
|
+
|
|
52
|
+
editor.attachEvent("exec_command_formatpainter", function (state) {
|
|
53
|
+
state.returnValue = true;
|
|
54
|
+
obj.Toggle();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
setup();
|
|
58
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
59
|
+
try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
|
|
60
|
+
setTimeout(setup, 0);
|
|
61
|
+
|
|
62
|
+
// Public API.
|
|
63
|
+
editor.copyFormatting = function () { return obj.Copy(); };
|
|
64
|
+
editor.pasteFormatting = function () { return obj.Paint(); };
|
|
65
|
+
editor.toggleFormatPainter = function (isSticky) { return obj.Toggle(isSticky); };
|
|
66
|
+
editor.cancelFormatPainter = function () { return obj.Cancel(); };
|
|
67
|
+
editor.isFormatPainterActive = function () { return !!captured; };
|
|
68
|
+
editor.getCapturedFormatting = function () { return captured ? cloneProps(captured.props) : null; };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function setup() {
|
|
72
|
+
var doc = getDoc();
|
|
73
|
+
if (!doc) return;
|
|
74
|
+
injectStyles(doc);
|
|
75
|
+
if (doc === boundDoc) return;
|
|
76
|
+
boundDoc = doc;
|
|
77
|
+
var editable = getEditable();
|
|
78
|
+
if (!editable) return;
|
|
79
|
+
// Mouse-up rather than click: the selection is final by then, and click
|
|
80
|
+
// fires before the browser has settled a drag-selection.
|
|
81
|
+
editable.addEventListener("mouseup", function () {
|
|
82
|
+
if (!captured) return;
|
|
83
|
+
setTimeout(function () { if (captured) obj.Paint(); }, 0);
|
|
84
|
+
});
|
|
85
|
+
editable.addEventListener("keydown", function (e) {
|
|
86
|
+
if (e.key === "Escape" && captured) { obj.Cancel(); e.preventDefault(); }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
91
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
92
|
+
|
|
93
|
+
function cloneProps(p) { var o = {}; for (var k in p) if (p.hasOwnProperty(k)) o[k] = p[k]; return o; }
|
|
94
|
+
|
|
95
|
+
// ---- capture ---------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function selectionElement() {
|
|
98
|
+
try {
|
|
99
|
+
var sel = editor.getSelection();
|
|
100
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
101
|
+
var n = sel.getRangeAt(0).startContainer;
|
|
102
|
+
if (n && n.nodeType === 3) n = n.parentNode;
|
|
103
|
+
return n && n.nodeType === 1 ? n : null;
|
|
104
|
+
} catch (e) { return null; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
obj.Copy = function () {
|
|
108
|
+
var el = selectionElement();
|
|
109
|
+
var doc = getDoc();
|
|
110
|
+
if (!el || !doc) return null;
|
|
111
|
+
var view = doc.defaultView || window;
|
|
112
|
+
var cs = view.getComputedStyle(el);
|
|
113
|
+
// Diff against the editable root rather than recording the whole computed
|
|
114
|
+
// style. Everything the source merely INHERITED (the body font stack,
|
|
115
|
+
// letter-spacing:normal, text-transform:none...) is identical at the root,
|
|
116
|
+
// so this drops it and the painted span carries only the formatting the
|
|
117
|
+
// user can actually see. Without the diff every paint emits a ~250-char
|
|
118
|
+
// style attribute of defaults, which is exactly the markup bloat this
|
|
119
|
+
// product sells against.
|
|
120
|
+
var baseEl = getEditable();
|
|
121
|
+
var base = baseEl ? view.getComputedStyle(baseEl) : null;
|
|
122
|
+
var props = {};
|
|
123
|
+
var list = config.formatPainterProperties || [];
|
|
124
|
+
for (var i = 0; i < list.length; i++) {
|
|
125
|
+
var name = list[i];
|
|
126
|
+
var v = cs.getPropertyValue(name);
|
|
127
|
+
if (!v) continue;
|
|
128
|
+
if (base && v === base.getPropertyValue(name)) continue;
|
|
129
|
+
props[name] = v;
|
|
130
|
+
}
|
|
131
|
+
// A transparent background is the default, not a formatting choice —
|
|
132
|
+
// carrying it would wipe highlights on every paint.
|
|
133
|
+
if (props["background-color"] && /^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/.test(props["background-color"])) {
|
|
134
|
+
delete props["background-color"];
|
|
135
|
+
}
|
|
136
|
+
captured = { props: props };
|
|
137
|
+
setBrushState(true);
|
|
138
|
+
return cloneProps(props);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ---- paint -----------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
obj.Paint = function () {
|
|
144
|
+
if (!captured) return false;
|
|
145
|
+
var doc = getDoc();
|
|
146
|
+
var sel;
|
|
147
|
+
try { sel = editor.getSelection(); } catch (e) { return false; }
|
|
148
|
+
if (!doc || !sel || sel.rangeCount === 0) return false;
|
|
149
|
+
if (sel.getRangeAt(0).collapsed) return false; // nothing selected: wait for one
|
|
150
|
+
|
|
151
|
+
var span = doc.createElement("span");
|
|
152
|
+
span.className = "rte-painted";
|
|
153
|
+
var props = captured.props;
|
|
154
|
+
for (var k in props) if (props.hasOwnProperty(k)) span.style.setProperty(k, props[k]);
|
|
155
|
+
|
|
156
|
+
var placed = null;
|
|
157
|
+
try { placed = editor.surroundElement(span); } catch (e) { placed = null; }
|
|
158
|
+
if (!placed) return false;
|
|
159
|
+
|
|
160
|
+
// Strip the FULL configured property list, not just the captured subset.
|
|
161
|
+
// Copy() only keeps properties that differ from the root, so painting
|
|
162
|
+
// "no bold" captures no font-weight at all — but a descendant carrying
|
|
163
|
+
// style="font-weight:700" still has to go, or the paint does nothing.
|
|
164
|
+
stripInnerFormatting(placed, config.formatPainterProperties || []);
|
|
165
|
+
collapseRedundantAncestors(placed);
|
|
166
|
+
|
|
167
|
+
if (!sticky) obj.Cancel();
|
|
168
|
+
fireChange();
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Remove the character formatting already inside the painted region so the
|
|
173
|
+
// wrapper actually wins. Both halves matter: inline style properties on
|
|
174
|
+
// descendants beat the ancestor by specificity, and <b>/<i>/<font> beat it
|
|
175
|
+
// by UA stylesheet.
|
|
176
|
+
function stripInnerFormatting(root, propNames) {
|
|
177
|
+
var els = root.getElementsByTagName("*");
|
|
178
|
+
// Live HTMLCollection + unwrapping = skipped nodes; snapshot first.
|
|
179
|
+
var all = Array.prototype.slice.call(els);
|
|
180
|
+
for (var i = 0; i < all.length; i++) {
|
|
181
|
+
var el = all[i];
|
|
182
|
+
if (el.style) {
|
|
183
|
+
for (var p = 0; p < propNames.length; p++) el.style.removeProperty(propNames[p]);
|
|
184
|
+
// Legacy shorthands the loop above cannot see.
|
|
185
|
+
el.style.removeProperty("font");
|
|
186
|
+
el.style.removeProperty("text-decoration");
|
|
187
|
+
}
|
|
188
|
+
if (el.nodeName === "FONT") {
|
|
189
|
+
el.removeAttribute("color"); el.removeAttribute("face"); el.removeAttribute("size");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (var j = 0; j < all.length; j++) {
|
|
193
|
+
var e2 = all[j];
|
|
194
|
+
if (CHAR_TAGS[e2.nodeName] && e2.parentNode) unwrap(e2);
|
|
195
|
+
}
|
|
196
|
+
tidy(root);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Stripping properties leaves behind style="" and spans that no longer carry
|
|
200
|
+
// anything. Left in place they accumulate on every repaint, so the "clean
|
|
201
|
+
// HTML" the product promises would erode one brush stroke at a time.
|
|
202
|
+
function tidy(root) {
|
|
203
|
+
var all = Array.prototype.slice.call(root.getElementsByTagName("*"));
|
|
204
|
+
for (var i = 0; i < all.length; i++) {
|
|
205
|
+
var el = all[i];
|
|
206
|
+
if (el.getAttribute && el.getAttribute("style") === "") el.removeAttribute("style");
|
|
207
|
+
// A wrapper from an EARLIER paint that now sits inside this one is
|
|
208
|
+
// superseded by definition — its properties were just stripped. Left
|
|
209
|
+
// alone, painting the same run N times nests N wrappers.
|
|
210
|
+
if (el.nodeName === "SPAN" && el.classList && el.classList.contains("rte-painted") && el.parentNode) {
|
|
211
|
+
unwrap(el);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (el.nodeName === "SPAN" && !el.attributes.length && el.parentNode) unwrap(el);
|
|
215
|
+
}
|
|
216
|
+
if (root.getAttribute && root.getAttribute("style") === "") root.removeAttribute("style");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function unwrap(el) {
|
|
220
|
+
var parent = el.parentNode;
|
|
221
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
222
|
+
parent.removeChild(el);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Repainting a run that was already painted puts the new wrapper INSIDE the
|
|
226
|
+
// old one, because surroundElement only ever wraps the extracted selection —
|
|
227
|
+
// so the stale wrapper is an ANCESTOR and tidy(), which walks descendants,
|
|
228
|
+
// cannot see it. Paint the same words five times and you get five nested
|
|
229
|
+
// spans. Only collapse a parent that wraps nothing but us: if it has other
|
|
230
|
+
// children, its formatting still belongs to them.
|
|
231
|
+
function collapseRedundantAncestors(span) {
|
|
232
|
+
for (var guard = 0; guard < 32; guard++) {
|
|
233
|
+
var p = span.parentNode;
|
|
234
|
+
if (!p || p.nodeType !== 1) return;
|
|
235
|
+
if (p.nodeName !== "SPAN") return;
|
|
236
|
+
if (!p.classList || !p.classList.contains("rte-painted")) return;
|
|
237
|
+
if (!wrapsNothingElse(p, span)) return;
|
|
238
|
+
unwrap(p);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// NOT childNodes.length === 1: Range.extractContents() leaves zero-length
|
|
243
|
+
// text nodes on both sides of the extraction point, so the stale wrapper
|
|
244
|
+
// reads as having three children when it really holds only the new span.
|
|
245
|
+
// Counting those as content made the collapse above silently never fire.
|
|
246
|
+
function wrapsNothingElse(parent, child) {
|
|
247
|
+
for (var i = 0; i < parent.childNodes.length; i++) {
|
|
248
|
+
var n = parent.childNodes[i];
|
|
249
|
+
if (n === child) continue;
|
|
250
|
+
if (n.nodeType === 3 && n.nodeValue.length === 0) continue;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- brush state -----------------------------------------------------
|
|
257
|
+
|
|
258
|
+
obj.Toggle = function (isSticky) {
|
|
259
|
+
sticky = !!isSticky;
|
|
260
|
+
if (captured) { obj.Cancel(); return false; }
|
|
261
|
+
obj.Copy();
|
|
262
|
+
return !!captured;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
obj.Cancel = function () {
|
|
266
|
+
captured = null;
|
|
267
|
+
sticky = false;
|
|
268
|
+
setBrushState(false);
|
|
269
|
+
return true;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// A cursor change is the only affordance the user gets that the brush is
|
|
273
|
+
// loaded, so it is worth doing properly rather than leaving it invisible.
|
|
274
|
+
function setBrushState(on) {
|
|
275
|
+
var editable = getEditable();
|
|
276
|
+
if (!editable || !editable.classList) return;
|
|
277
|
+
if (on) editable.classList.add("rte-format-painting");
|
|
278
|
+
else editable.classList.remove("rte-format-painting");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function fireChange() {
|
|
282
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
283
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function css() {
|
|
287
|
+
return ".rte-format-painting,.rte-format-painting *{cursor:copy !important;}";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function injectStyles(doc) {
|
|
291
|
+
if (!doc) return;
|
|
292
|
+
if (doc.getElementById("rte-format-painter-styles")) return;
|
|
293
|
+
var st = doc.createElement("style");
|
|
294
|
+
st.id = "rte-format-painter-styles";
|
|
295
|
+
st.appendChild(doc.createTextNode(css()));
|
|
296
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-07-28 Formatting marks — Word's ¶ button. Shows paragraph marks, block
|
|
4
|
+
// outlines, tabs, line breaks and (the useful one) non-breaking and zero-width
|
|
5
|
+
// characters that are otherwise invisible and cause mysterious layout bugs.
|
|
6
|
+
//
|
|
7
|
+
// Positioning note: this is PARITY, not a gap win. TinyMCE ships `visualchars`
|
|
8
|
+
// and `visualblocks` in its FREE core, and CKEditor has show-blocks. Do not
|
|
9
|
+
// claim it as something competitors charge for.
|
|
10
|
+
//
|
|
11
|
+
// Design notes:
|
|
12
|
+
// - Everything that can be done in CSS is done in CSS: pilcrows via ::after,
|
|
13
|
+
// block outlines via outline. CSS cannot change the document, so those marks
|
|
14
|
+
// are incapable of corrupting content no matter what the user does.
|
|
15
|
+
// - Invisible CHARACTERS cannot be reached from CSS, so those get a wrapper
|
|
16
|
+
// span — but every wrapper is stripped around serialize (the pagination.js
|
|
17
|
+
// contract) and removed when the feature is switched off. A marking feature
|
|
18
|
+
// that leaves debris in the saved HTML is worse than no marking feature.
|
|
19
|
+
RTE_DefaultConfig.plugin_formattingmarks = RTE_Plugin_FormattingMarks;
|
|
20
|
+
|
|
21
|
+
// Which marks to draw.
|
|
22
|
+
if (typeof RTE_DefaultConfig.formattingMarkPilcrow === "undefined") RTE_DefaultConfig.formattingMarkPilcrow = true;
|
|
23
|
+
if (typeof RTE_DefaultConfig.formattingMarkBlocks === "undefined") RTE_DefaultConfig.formattingMarkBlocks = true;
|
|
24
|
+
if (typeof RTE_DefaultConfig.formattingMarkInvisibles === "undefined") RTE_DefaultConfig.formattingMarkInvisibles = true;
|
|
25
|
+
|
|
26
|
+
function RTE_Plugin_FormattingMarks() {
|
|
27
|
+
var obj = this;
|
|
28
|
+
var config, editor;
|
|
29
|
+
var boundDoc = null;
|
|
30
|
+
var active = false;
|
|
31
|
+
var wrapped = false;
|
|
32
|
+
var queued = false;
|
|
33
|
+
|
|
34
|
+
// Characters that render as nothing (or as a normal space) but behave
|
|
35
|
+
// differently. These are what people are actually hunting when they turn
|
|
36
|
+
// formatting marks on after pasting from somewhere else.
|
|
37
|
+
var INVISIBLES = [
|
|
38
|
+
{ ch: " ", cls: "rte-fm-nbsp", label: "·" }, // non-breaking space
|
|
39
|
+
{ ch: "", cls: "rte-fm-zwsp", label: "␀" }, // zero-width space
|
|
40
|
+
{ ch: "", cls: "rte-fm-bidi", label: "" }, // LTR mark
|
|
41
|
+
{ ch: "", cls: "rte-fm-bidi", label: "" }, // RTL mark
|
|
42
|
+
{ ch: "", cls: "rte-fm-shy", label: "-" } // soft hyphen
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
obj.PluginName = "FormattingMarks";
|
|
46
|
+
|
|
47
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
48
|
+
|
|
49
|
+
obj.InitEditor = function (argeditor) {
|
|
50
|
+
editor = argeditor;
|
|
51
|
+
|
|
52
|
+
editor.attachEvent("exec_command_formattingmarks", function (state) {
|
|
53
|
+
state.returnValue = true;
|
|
54
|
+
obj.Toggle();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
setup();
|
|
58
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
59
|
+
try { editor.attachEvent("aftersethtml", function () { setup(); if (active) queue(); }); } catch (e) {}
|
|
60
|
+
setTimeout(setup, 0);
|
|
61
|
+
|
|
62
|
+
editor.setFormattingMarks = function (on) { active = !!on; apply(); return active; };
|
|
63
|
+
editor.toggleFormattingMarks = function () { return obj.Toggle(); };
|
|
64
|
+
editor.isFormattingMarks = function () { return active; };
|
|
65
|
+
editor.getFormattingMarksCss = function () { return css(); };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
obj.Toggle = function () { active = !active; apply(); return active; };
|
|
69
|
+
|
|
70
|
+
function setup() {
|
|
71
|
+
var doc = getDoc();
|
|
72
|
+
if (!doc) return;
|
|
73
|
+
injectStyles(doc);
|
|
74
|
+
wrapSerializers();
|
|
75
|
+
if (doc === boundDoc) return;
|
|
76
|
+
boundDoc = doc;
|
|
77
|
+
var editable = getEditable();
|
|
78
|
+
if (!editable) return;
|
|
79
|
+
editable.addEventListener("keyup", function () { if (active) queue(); });
|
|
80
|
+
editable.addEventListener("paste", function () { if (active) queue(); });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
84
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
85
|
+
|
|
86
|
+
function queue() {
|
|
87
|
+
if (queued) return;
|
|
88
|
+
queued = true;
|
|
89
|
+
setTimeout(function () { queued = false; markInvisibles(); }, 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function apply() {
|
|
93
|
+
var editable = getEditable();
|
|
94
|
+
if (!editable) return;
|
|
95
|
+
if (editable.classList) {
|
|
96
|
+
editable.classList.toggle("rte-fm-on", active);
|
|
97
|
+
editable.classList.toggle("rte-fm-pilcrow", active && config.formattingMarkPilcrow !== false);
|
|
98
|
+
editable.classList.toggle("rte-fm-blocks", active && config.formattingMarkBlocks !== false);
|
|
99
|
+
}
|
|
100
|
+
if (active && config.formattingMarkInvisibles !== false) markInvisibles();
|
|
101
|
+
else unmarkInvisibles();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- invisible characters (the only part that touches the DOM) --------
|
|
105
|
+
|
|
106
|
+
function unmarkInvisibles() {
|
|
107
|
+
var editable = getEditable();
|
|
108
|
+
if (!editable) return;
|
|
109
|
+
var spans = editable.querySelectorAll("span[data-rte-fm]");
|
|
110
|
+
for (var i = 0; i < spans.length; i++) {
|
|
111
|
+
var s = spans[i];
|
|
112
|
+
var parent = s.parentNode;
|
|
113
|
+
if (!parent) continue;
|
|
114
|
+
// Put the ORIGINAL character back, not the label glyph.
|
|
115
|
+
parent.replaceChild(s.ownerDocument.createTextNode(s.getAttribute("data-rte-fm-char") || ""), s);
|
|
116
|
+
parent.normalize();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function markInvisibles() {
|
|
121
|
+
var editable = getEditable();
|
|
122
|
+
var doc = getDoc();
|
|
123
|
+
if (!editable || !doc) return;
|
|
124
|
+
unmarkInvisibles();
|
|
125
|
+
|
|
126
|
+
var chars = {};
|
|
127
|
+
for (var i = 0; i < INVISIBLES.length; i++) chars[INVISIBLES[i].ch] = INVISIBLES[i];
|
|
128
|
+
|
|
129
|
+
// Collect first: splitting text nodes while walking invalidates the walk.
|
|
130
|
+
var walker = doc.createTreeWalker(editable, 4 /* SHOW_TEXT */, null, false);
|
|
131
|
+
var targets = [];
|
|
132
|
+
var n;
|
|
133
|
+
while ((n = walker.nextNode())) {
|
|
134
|
+
if (!n.nodeValue) continue;
|
|
135
|
+
for (var c = 0; c < INVISIBLES.length; c++) {
|
|
136
|
+
if (n.nodeValue.indexOf(INVISIBLES[c].ch) >= 0) { targets.push(n); break; }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (var t = 0; t < targets.length; t++) wrapNode(doc, targets[t], chars);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function wrapNode(doc, node, chars) {
|
|
144
|
+
var text = node.nodeValue;
|
|
145
|
+
var parent = node.parentNode;
|
|
146
|
+
if (!parent) return;
|
|
147
|
+
// Never mark inside our own markers, or inside code where the character
|
|
148
|
+
// may be deliberate.
|
|
149
|
+
if (parent.nodeName === "CODE" || parent.nodeName === "PRE") return;
|
|
150
|
+
|
|
151
|
+
var frag = doc.createDocumentFragment();
|
|
152
|
+
var buf = "";
|
|
153
|
+
for (var i = 0; i < text.length; i++) {
|
|
154
|
+
var def = chars[text[i]];
|
|
155
|
+
if (!def) { buf += text[i]; continue; }
|
|
156
|
+
if (buf) { frag.appendChild(doc.createTextNode(buf)); buf = ""; }
|
|
157
|
+
var span = doc.createElement("span");
|
|
158
|
+
span.setAttribute("data-rte-fm", "1");
|
|
159
|
+
span.setAttribute("data-rte-fm-char", text[i]);
|
|
160
|
+
span.className = def.cls;
|
|
161
|
+
span.setAttribute("contenteditable", "false");
|
|
162
|
+
span.textContent = def.label;
|
|
163
|
+
frag.appendChild(span);
|
|
164
|
+
}
|
|
165
|
+
if (buf) frag.appendChild(doc.createTextNode(buf));
|
|
166
|
+
parent.replaceChild(frag, node);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- serialization safety -------------------------------------------
|
|
170
|
+
|
|
171
|
+
function stripFor() {
|
|
172
|
+
var editable = getEditable();
|
|
173
|
+
if (!editable) return function () {};
|
|
174
|
+
var spans = Array.prototype.slice.call(editable.querySelectorAll("span[data-rte-fm]"));
|
|
175
|
+
if (!spans.length) return function () {};
|
|
176
|
+
var parked = [];
|
|
177
|
+
for (var i = 0; i < spans.length; i++) {
|
|
178
|
+
var s = spans[i];
|
|
179
|
+
var textNode = s.ownerDocument.createTextNode(s.getAttribute("data-rte-fm-char") || "");
|
|
180
|
+
parked.push({ span: s, parent: s.parentNode, text: textNode });
|
|
181
|
+
if (s.parentNode) s.parentNode.replaceChild(textNode, s);
|
|
182
|
+
}
|
|
183
|
+
return function restore() {
|
|
184
|
+
for (var j = 0; j < parked.length; j++) {
|
|
185
|
+
var p = parked[j];
|
|
186
|
+
if (p.text.parentNode) p.text.parentNode.replaceChild(p.span, p.text);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function wrapSerializers() {
|
|
192
|
+
if (wrapped) return;
|
|
193
|
+
var names = ["getHTMLCode", "getJSON", "getHTMLContent", "getText"];
|
|
194
|
+
var did = false;
|
|
195
|
+
for (var i = 0; i < names.length; i++) {
|
|
196
|
+
(function (name) {
|
|
197
|
+
var orig = editor[name];
|
|
198
|
+
if (typeof orig !== "function" || orig.__rteFmWrapped) return;
|
|
199
|
+
var w = function () {
|
|
200
|
+
var restore = stripFor();
|
|
201
|
+
try { return orig.apply(editor, arguments); } finally { restore(); }
|
|
202
|
+
};
|
|
203
|
+
w.__rteFmWrapped = true;
|
|
204
|
+
editor[name] = w;
|
|
205
|
+
did = true;
|
|
206
|
+
})(names[i]);
|
|
207
|
+
}
|
|
208
|
+
if (did) wrapped = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- styles ----------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function css() {
|
|
214
|
+
return (
|
|
215
|
+
// Pilcrow at the end of every block, drawn by CSS so it can never
|
|
216
|
+
// become part of the document.
|
|
217
|
+
".rte-fm-pilcrow p::after,.rte-fm-pilcrow h1::after,.rte-fm-pilcrow h2::after," +
|
|
218
|
+
".rte-fm-pilcrow h3::after,.rte-fm-pilcrow h4::after,.rte-fm-pilcrow h5::after," +
|
|
219
|
+
".rte-fm-pilcrow h6::after,.rte-fm-pilcrow li::after,.rte-fm-pilcrow blockquote::after" +
|
|
220
|
+
"{content:'\\00b6';color:#9aa4b5;opacity:.65;font-weight:400;}" +
|
|
221
|
+
".rte-fm-blocks p,.rte-fm-blocks h1,.rte-fm-blocks h2,.rte-fm-blocks h3," +
|
|
222
|
+
".rte-fm-blocks h4,.rte-fm-blocks h5,.rte-fm-blocks h6,.rte-fm-blocks ul," +
|
|
223
|
+
".rte-fm-blocks ol,.rte-fm-blocks blockquote,.rte-fm-blocks table," +
|
|
224
|
+
".rte-fm-blocks div{outline:1px dashed rgba(120,140,170,.45);outline-offset:1px;}" +
|
|
225
|
+
"span[data-rte-fm]{color:#c2410c;opacity:.75;user-select:none;}" +
|
|
226
|
+
".rte-fm-nbsp{background:rgba(194,65,12,.10);border-radius:2px;}" +
|
|
227
|
+
".rte-fm-zwsp,.rte-fm-shy,.rte-fm-bidi{background:rgba(194,65,12,.18);border-radius:2px;" +
|
|
228
|
+
"font-size:.8em;vertical-align:middle;}"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function injectStyles(doc) {
|
|
233
|
+
if (!doc) return;
|
|
234
|
+
var existing = doc.getElementById("rte-formatting-marks-styles");
|
|
235
|
+
var text = css();
|
|
236
|
+
if (existing) {
|
|
237
|
+
if (existing.getAttribute("data-css") === text) return;
|
|
238
|
+
existing.parentNode && existing.parentNode.removeChild(existing);
|
|
239
|
+
}
|
|
240
|
+
var st = doc.createElement("style");
|
|
241
|
+
st.id = "rte-formatting-marks-styles";
|
|
242
|
+
st.setAttribute("data-css", text);
|
|
243
|
+
st.appendChild(doc.createTextNode(text));
|
|
244
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
245
|
+
}
|
|
246
|
+
}
|