@richscripts/richtexteditor 2.4.1 → 2.5.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 +4 -2
- package/richtexteditor/all_plugins.js +2861 -935
- package/richtexteditor/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/accessibilitychecker.js +186 -1
- package/richtexteditor/plugins/aitoolkit.js +1 -1
- package/richtexteditor/plugins/all_plugins.js +2861 -935
- package/richtexteditor/plugins/all_plugins.min.js +58 -58
- package/richtexteditor/plugins/crdt-engine.js +2201 -198
- package/richtexteditor/plugins/crdt-engine.min.js +5 -5
- package/richtexteditor/plugins/gapcursor.js +365 -0
- package/richtexteditor/plugins/imageeditor.js +30 -3
- package/richtexteditor/plugins/importstyles.js +312 -0
- package/richtexteditor/plugins/insertcode.js +81 -4
- package/richtexteditor/plugins/insertemoji.js +302 -229
- package/richtexteditor/plugins/insertgallery.js +34 -6
- package/richtexteditor/plugins/keyboarda11y.js +27 -1
- package/richtexteditor/plugins/multiroot.js +269 -0
- package/richtexteditor/plugins/revisionhistory.js +5 -0
- package/richtexteditor/plugins/sanitizer.js +24 -0
- package/richtexteditor/plugins/textpartlanguage.js +504 -0
- package/richtexteditor/plugins/trackedchanges.js +716 -689
- package/richtexteditor/plugins/wordexport.js +1 -1
- package/richtexteditor/rte-config.js +2 -2
- package/richtexteditor/rte.js +55 -51
- package/richtexteditor/rte_theme_default.css +151 -83
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-08-15 Gap cursor — reach the unreachable places in a document.
|
|
4
|
+
//
|
|
5
|
+
// A document that ENDS with a table cannot be added to. The caret goes into the
|
|
6
|
+
// last cell and stays there: ArrowDown, ArrowRight and End all leave it where it
|
|
7
|
+
// was, because there is no node after the table to put it in. Same story for a
|
|
8
|
+
// document that STARTS with a table, and for two adjacent tables with no
|
|
9
|
+
// paragraph between them — there is no caret position in the gap, so the author
|
|
10
|
+
// cannot type there at all. Verified on the running editor before writing this:
|
|
11
|
+
// the caret stayed at TABLE>TBODY>TR>TD through every key.
|
|
12
|
+
//
|
|
13
|
+
// This is the single most-reported "the editor is broken" issue in every
|
|
14
|
+
// WYSIWYG that lacks it. Froala ships it as `lineBreaker`, Tiptap as
|
|
15
|
+
// `gapcursor`, and ProseMirror has it in core. We had it under no name.
|
|
16
|
+
//
|
|
17
|
+
// Design notes:
|
|
18
|
+
// - ON DEMAND, never eager. The obvious fix — always append a trailing empty
|
|
19
|
+
// paragraph — mutates every document that ends in a table, so getHTMLCode()
|
|
20
|
+
// starts returning markup the author did not write and a round-trip through
|
|
21
|
+
// the editor is no longer lossless. A paragraph is created only when the
|
|
22
|
+
// author actually asks for one by pressing a key or clicking the gap.
|
|
23
|
+
// - Clicking the blank space below the content is the gesture people already
|
|
24
|
+
// have from Google Docs and Notion, and it needs no overlay DOM: a click
|
|
25
|
+
// that lands on the editable ITSELF (rather than on a child) is by
|
|
26
|
+
// definition a click in a gap. The child list then says which gap.
|
|
27
|
+
// - Only TRAPPING blocks count. A trailing <p> is already reachable, so
|
|
28
|
+
// offering to add another one after it would just spam empty paragraphs.
|
|
29
|
+
// - The inserted paragraph carries a <br> filler. An empty <p> has zero
|
|
30
|
+
// height in most engines, so without it the caret lands somewhere invisible
|
|
31
|
+
// and the author thinks nothing happened.
|
|
32
|
+
RTE_DefaultConfig.plugin_gapcursor = RTE_Plugin_GapCursor;
|
|
33
|
+
|
|
34
|
+
// Click the empty space below/around content to create a paragraph there.
|
|
35
|
+
if (typeof RTE_DefaultConfig.gapCursorClick === "undefined") RTE_DefaultConfig.gapCursorClick = true;
|
|
36
|
+
// Arrow/Home/End keys escape a trapping block at the document edge.
|
|
37
|
+
if (typeof RTE_DefaultConfig.gapCursorKeys === "undefined") RTE_DefaultConfig.gapCursorKeys = true;
|
|
38
|
+
// Show a thin insertion hint when hovering a gap between two trapping blocks.
|
|
39
|
+
if (typeof RTE_DefaultConfig.gapCursorHint === "undefined") RTE_DefaultConfig.gapCursorHint = true;
|
|
40
|
+
|
|
41
|
+
function RTE_Plugin_GapCursor() {
|
|
42
|
+
var obj = this;
|
|
43
|
+
var config, editor;
|
|
44
|
+
var boundDoc = null;
|
|
45
|
+
|
|
46
|
+
obj.PluginName = "GapCursor";
|
|
47
|
+
|
|
48
|
+
// Blocks that cannot host a caret directly beside them. A caret can always
|
|
49
|
+
// be placed inside a paragraph or heading, so those are never "trapping".
|
|
50
|
+
var TRAPPING = {
|
|
51
|
+
TABLE: 1, HR: 1, IMG: 1, FIGURE: 1, IFRAME: 1, VIDEO: 1, AUDIO: 1,
|
|
52
|
+
OBJECT: 1, EMBED: 1, CANVAS: 1, SVG: 1, PRE: 1
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
56
|
+
|
|
57
|
+
obj.InitEditor = function (argeditor) {
|
|
58
|
+
editor = argeditor;
|
|
59
|
+
|
|
60
|
+
bind();
|
|
61
|
+
try { editor.attachEvent("ready", bind); } catch (e) {}
|
|
62
|
+
try { editor.attachEvent("aftersethtml", bind); } catch (e) {}
|
|
63
|
+
setTimeout(bind, 0);
|
|
64
|
+
|
|
65
|
+
// Public API.
|
|
66
|
+
editor.insertParagraphAt = function (index) { return insertAt(index); };
|
|
67
|
+
editor.insertParagraphAfter = function (el) { return insertAround(el, true); };
|
|
68
|
+
editor.insertParagraphBefore = function (el) { return insertAround(el, false); };
|
|
69
|
+
editor.getGapPositions = function () { return gaps(); };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
73
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
74
|
+
|
|
75
|
+
function bind() {
|
|
76
|
+
var doc = getDoc();
|
|
77
|
+
var ed = getEditable();
|
|
78
|
+
if (!doc || !ed || doc === boundDoc) return;
|
|
79
|
+
boundDoc = doc;
|
|
80
|
+
injectStyles(doc);
|
|
81
|
+
|
|
82
|
+
if (config.gapCursorKeys !== false) ed.addEventListener("keydown", onKeyDown, true);
|
|
83
|
+
if (config.gapCursorClick !== false) ed.addEventListener("click", onClick, true);
|
|
84
|
+
if (config.gapCursorHint !== false) ed.addEventListener("mousemove", onMove, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isTrapping(el) {
|
|
88
|
+
return !!(el && el.nodeType === 1 && TRAPPING[el.nodeName]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isHint(el) {
|
|
92
|
+
return !!(el && el.nodeType === 1 && el.className === "rte-gap-hint");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The editable's real children, with the hover hint filtered out.
|
|
96
|
+
//
|
|
97
|
+
// The hint is itself a child of the editable, so any code that reasons about
|
|
98
|
+
// positions via ed.children sees it as a block: it shifts every index after
|
|
99
|
+
// it, and because a <div> is not "trapping" its presence as the last child
|
|
100
|
+
// made the trailing gap disappear entirely — hovering below a table removed
|
|
101
|
+
// the very gap the hover was advertising, and the click then did nothing.
|
|
102
|
+
function blocks() {
|
|
103
|
+
var ed = getEditable();
|
|
104
|
+
var out = [];
|
|
105
|
+
if (!ed) return out;
|
|
106
|
+
for (var i = 0; i < ed.children.length; i++) {
|
|
107
|
+
if (!isHint(ed.children[i])) out.push(ed.children[i]);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Every index where a paragraph could usefully be inserted: before a leading
|
|
113
|
+
// trapping block, after a trailing one, and between two adjacent ones.
|
|
114
|
+
function gaps() {
|
|
115
|
+
var out = [];
|
|
116
|
+
var kids = blocks();
|
|
117
|
+
if (!kids.length) return out;
|
|
118
|
+
if (isTrapping(kids[0])) out.push(0);
|
|
119
|
+
for (var i = 0; i < kids.length - 1; i++) {
|
|
120
|
+
if (isTrapping(kids[i]) && isTrapping(kids[i + 1])) out.push(i + 1);
|
|
121
|
+
}
|
|
122
|
+
if (isTrapping(kids[kids.length - 1])) out.push(kids.length);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- insertion -------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
function makeParagraph(doc) {
|
|
129
|
+
var p = doc.createElement("p");
|
|
130
|
+
// An empty <p> collapses to zero height, so the caret would land
|
|
131
|
+
// somewhere the author cannot see.
|
|
132
|
+
p.appendChild(doc.createElement("br"));
|
|
133
|
+
return p;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function insertAt(index) {
|
|
137
|
+
var ed = getEditable();
|
|
138
|
+
var doc = getDoc();
|
|
139
|
+
if (!ed || !doc) return null;
|
|
140
|
+
// Resolve against the hint-free list, then insert relative to that node.
|
|
141
|
+
hideHint();
|
|
142
|
+
var p = makeParagraph(doc);
|
|
143
|
+
var before = blocks()[index] || null;
|
|
144
|
+
ed.insertBefore(p, before);
|
|
145
|
+
focusParagraph(p);
|
|
146
|
+
fireChange();
|
|
147
|
+
return p;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function insertAround(el, after) {
|
|
151
|
+
var ed = getEditable();
|
|
152
|
+
var doc = getDoc();
|
|
153
|
+
if (!ed || !doc || !el) return null;
|
|
154
|
+
var p = makeParagraph(doc);
|
|
155
|
+
if (after) el.parentNode.insertBefore(p, el.nextSibling);
|
|
156
|
+
else el.parentNode.insertBefore(p, el);
|
|
157
|
+
focusParagraph(p);
|
|
158
|
+
fireChange();
|
|
159
|
+
return p;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function focusParagraph(p) {
|
|
163
|
+
try {
|
|
164
|
+
var doc = p.ownerDocument;
|
|
165
|
+
var r = doc.createRange();
|
|
166
|
+
r.setStart(p, 0);
|
|
167
|
+
r.collapse(true);
|
|
168
|
+
var sel = editor.getSelection();
|
|
169
|
+
sel.removeAllRanges();
|
|
170
|
+
sel.addRange(r);
|
|
171
|
+
editor.focus();
|
|
172
|
+
} catch (e) {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- keyboard --------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
// The top-level child of the editable that contains this node.
|
|
178
|
+
function topBlockOf(node) {
|
|
179
|
+
var ed = getEditable();
|
|
180
|
+
var n = (node && node.nodeType === 3) ? node.parentNode : node;
|
|
181
|
+
while (n && n.parentNode && n.parentNode !== ed) n = n.parentNode;
|
|
182
|
+
return (n && n.parentNode === ed) ? n : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function onKeyDown(e) {
|
|
186
|
+
var key = e.key;
|
|
187
|
+
var forward = (key === "ArrowDown" || key === "ArrowRight" || key === "End" || key === "PageDown");
|
|
188
|
+
var back = (key === "ArrowUp" || key === "ArrowLeft" || key === "Home" || key === "PageUp");
|
|
189
|
+
if (!forward && !back) return;
|
|
190
|
+
|
|
191
|
+
var ed = getEditable();
|
|
192
|
+
if (!ed || !ed.children.length) return;
|
|
193
|
+
|
|
194
|
+
var sel = editor.getSelection();
|
|
195
|
+
if (!sel || !sel.rangeCount) return;
|
|
196
|
+
var block = topBlockOf(sel.getRangeAt(0).startContainer);
|
|
197
|
+
if (!isTrapping(block)) return;
|
|
198
|
+
|
|
199
|
+
var kids = blocks();
|
|
200
|
+
var index = kids.indexOf(block);
|
|
201
|
+
if (index < 0) return;
|
|
202
|
+
|
|
203
|
+
if (forward) {
|
|
204
|
+
var next = kids[index + 1];
|
|
205
|
+
// Escape only where the author is genuinely stuck: nothing after the
|
|
206
|
+
// block, or another trapping block immediately after it.
|
|
207
|
+
if (next && !isTrapping(next)) return;
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
insertAt(index + 1);
|
|
211
|
+
} else {
|
|
212
|
+
var prev = kids[index - 1];
|
|
213
|
+
if (prev && !isTrapping(prev)) return;
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
insertAt(index);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- click in a gap --------------------------------------------------
|
|
221
|
+
|
|
222
|
+
// A click that lands on the editable itself, not on any child, is a click in
|
|
223
|
+
// the space between or after blocks.
|
|
224
|
+
function onClick(e) {
|
|
225
|
+
var ed = getEditable();
|
|
226
|
+
if (!ed || e.target !== ed) return;
|
|
227
|
+
// Measure the gaps as they are without the hint, or the hint's own box
|
|
228
|
+
// shifts the geometry the click is being resolved against.
|
|
229
|
+
hideHint();
|
|
230
|
+
var index = gapIndexAt(e.clientY);
|
|
231
|
+
if (index === null) return;
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
e.stopPropagation();
|
|
234
|
+
insertAt(index);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Which gap does this viewport Y fall in? Below every block means the end.
|
|
238
|
+
function gapIndexAt(clientY) {
|
|
239
|
+
var kids = blocks();
|
|
240
|
+
if (!kids.length) return null;
|
|
241
|
+
var available = gaps();
|
|
242
|
+
if (!available.length) return null;
|
|
243
|
+
|
|
244
|
+
for (var i = 0; i < kids.length; i++) {
|
|
245
|
+
var rect = kids[i].getBoundingClientRect();
|
|
246
|
+
if (clientY < rect.top) return allowed(available, i);
|
|
247
|
+
if (clientY <= rect.bottom) return null; // on the block itself
|
|
248
|
+
}
|
|
249
|
+
return allowed(available, kids.length);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function allowed(available, index) {
|
|
253
|
+
return available.indexOf(index) >= 0 ? index : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- hover hint ------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
var hint = null;
|
|
259
|
+
var hintIndex = null;
|
|
260
|
+
|
|
261
|
+
// How close to a block boundary counts as "aiming at the gap".
|
|
262
|
+
var EDGE_TOLERANCE = 10;
|
|
263
|
+
|
|
264
|
+
function onMove(e) {
|
|
265
|
+
var ed = getEditable();
|
|
266
|
+
if (!ed) return;
|
|
267
|
+
if (isHint(e.target)) return; // already on the hint; leave it alone
|
|
268
|
+
|
|
269
|
+
// Blank space below/around the content: the click will land on the
|
|
270
|
+
// editable itself and the geometry alone is enough.
|
|
271
|
+
if (e.target === ed) {
|
|
272
|
+
var index = gapIndexAt(e.clientY);
|
|
273
|
+
if (index === null) { hideHint(); return; }
|
|
274
|
+
showHint(index);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Two adjacent tables have ZERO pixels between them, so a click there
|
|
279
|
+
// can never reach the editable — it always lands on one of the tables.
|
|
280
|
+
// The gap is unreachable by mouse unless we put something in it, so
|
|
281
|
+
// hovering near the boundary materialises the hint and the hint itself
|
|
282
|
+
// becomes the click target.
|
|
283
|
+
var near = gapNearEdge(e.clientY);
|
|
284
|
+
if (near === null) { hideHint(); return; }
|
|
285
|
+
showHint(near);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// A gap whose boundary is within EDGE_TOLERANCE of this Y.
|
|
289
|
+
function gapNearEdge(clientY) {
|
|
290
|
+
var kids = blocks();
|
|
291
|
+
var available = gaps();
|
|
292
|
+
for (var g = 0; g < available.length; g++) {
|
|
293
|
+
var i = available[g];
|
|
294
|
+
var edge;
|
|
295
|
+
if (i === 0) edge = kids[0].getBoundingClientRect().top;
|
|
296
|
+
else edge = kids[i - 1].getBoundingClientRect().bottom;
|
|
297
|
+
if (Math.abs(clientY - edge) <= EDGE_TOLERANCE) return i;
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function showHint(index) {
|
|
303
|
+
var ed = getEditable();
|
|
304
|
+
var doc = getDoc();
|
|
305
|
+
if (!ed || !doc) return;
|
|
306
|
+
if (!hint) {
|
|
307
|
+
hint = doc.createElement("div");
|
|
308
|
+
hint.className = "rte-gap-hint";
|
|
309
|
+
hint.setAttribute("contenteditable", "false");
|
|
310
|
+
hint.setAttribute("aria-hidden", "true");
|
|
311
|
+
// The hint is the only clickable thing in a zero-height gap.
|
|
312
|
+
hint.addEventListener("mousedown", function (ev) {
|
|
313
|
+
ev.preventDefault();
|
|
314
|
+
ev.stopPropagation();
|
|
315
|
+
if (hintIndex !== null) insertAt(hintIndex);
|
|
316
|
+
}, true);
|
|
317
|
+
}
|
|
318
|
+
var before = blocks()[index] || null;
|
|
319
|
+
// Re-inserting in place would thrash the DOM on every mousemove.
|
|
320
|
+
if (hint.parentNode === ed && hint.nextSibling === before) { hintIndex = index; return; }
|
|
321
|
+
ed.insertBefore(hint, before);
|
|
322
|
+
hintIndex = index;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hideHint() {
|
|
326
|
+
if (hint && hint.parentNode) hint.parentNode.removeChild(hint);
|
|
327
|
+
hintIndex = null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function fireChange() {
|
|
331
|
+
// The hint is decoration; it must never be in the document when the
|
|
332
|
+
// content is read.
|
|
333
|
+
hideHint();
|
|
334
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
335
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---- styles ----------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
function css() {
|
|
341
|
+
// A real hit area, not just a line: in a zero-height gap between two
|
|
342
|
+
// tables this element IS the click target, so it needs height.
|
|
343
|
+
return ".rte-gap-hint{height:6px;margin:1px 0;position:relative;cursor:text;" +
|
|
344
|
+
"pointer-events:auto;background:transparent;}" +
|
|
345
|
+
".rte-gap-hint::before{content:'';position:absolute;left:0;right:0;top:2px;" +
|
|
346
|
+
"border-top:2px solid #2563eb;opacity:.65;}" +
|
|
347
|
+
".rte-gap-hint::after{content:'';position:absolute;left:-3px;top:0;width:6px;height:6px;" +
|
|
348
|
+
"border-radius:50%;background:#2563eb;}";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function injectStyles(doc) {
|
|
352
|
+
if (!doc) return;
|
|
353
|
+
var text = css();
|
|
354
|
+
var existing = doc.getElementById("rte-gapcursor-styles");
|
|
355
|
+
if (existing) {
|
|
356
|
+
if (existing.getAttribute("data-css") === text) return;
|
|
357
|
+
existing.parentNode && existing.parentNode.removeChild(existing);
|
|
358
|
+
}
|
|
359
|
+
var st = doc.createElement("style");
|
|
360
|
+
st.id = "rte-gapcursor-styles";
|
|
361
|
+
st.setAttribute("data-css", text);
|
|
362
|
+
st.appendChild(doc.createTextNode(text));
|
|
363
|
+
(doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -43,7 +43,22 @@ function RTE_Plugin_ImageEditor() {
|
|
|
43
43
|
return tag;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
var MIME_EXTENSION = {
|
|
47
|
+
"image/png": ".png",
|
|
48
|
+
"image/jpeg": ".jpg",
|
|
49
|
+
"image/jpg": ".jpg",
|
|
50
|
+
"image/webp": ".webp",
|
|
51
|
+
"image/gif": ".gif",
|
|
52
|
+
"image/bmp": ".bmp",
|
|
53
|
+
"image/avif": ".avif"
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Returns a File, not a bare Blob. Upload handlers identify the file by its
|
|
57
|
+
// .name - a Blob has none, so the edited image was posted with an undefined
|
|
58
|
+
// name and every server rejected it ("Invalid file extension"), surfacing as
|
|
59
|
+
// "Upload failed. Check your connection and try again." when the network was
|
|
60
|
+
// perfectly fine.
|
|
61
|
+
function dataURLToBlob(dataurl, filename) {
|
|
47
62
|
var arr = dataurl.split(',');
|
|
48
63
|
var mime = arr[0].match(/:(.*?);/)[1];
|
|
49
64
|
var bstr = atob(arr[1]);
|
|
@@ -52,7 +67,16 @@ function RTE_Plugin_ImageEditor() {
|
|
|
52
67
|
while (n--) {
|
|
53
68
|
u8arr[n] = bstr.charCodeAt(n);
|
|
54
69
|
}
|
|
55
|
-
|
|
70
|
+
var name = filename || ("image-" + new Date().getTime() + (MIME_EXTENSION[mime] || ".png"));
|
|
71
|
+
try {
|
|
72
|
+
return new File([u8arr], name, { type: mime });
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Very old browsers have no File constructor. Keep the name reachable
|
|
75
|
+
// so a handler reading file.name still works.
|
|
76
|
+
var blob = new Blob([u8arr], { type: mime });
|
|
77
|
+
blob.name = name;
|
|
78
|
+
return blob;
|
|
79
|
+
}
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
obj.DoImageEditor = function () {
|
|
@@ -159,8 +183,11 @@ function RTE_Plugin_ImageEditor() {
|
|
|
159
183
|
}
|
|
160
184
|
savebtn.disabled = false;
|
|
161
185
|
savebtn.innerText = "Save";
|
|
186
|
+
// Show what the server actually said. Blaming the connection sent
|
|
187
|
+
// people chasing a network problem when the real answer was a
|
|
188
|
+
// rejected file type or a size limit.
|
|
162
189
|
status.innerText = error
|
|
163
|
-
? "Upload failed
|
|
190
|
+
? ("Upload failed: " + error)
|
|
164
191
|
: "The upload did not return a file URL. Please try again.";
|
|
165
192
|
});
|
|
166
193
|
|