@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.
Files changed (60) hide show
  1. package/migration/README.md +100 -0
  2. package/migration/cli.js +294 -0
  3. package/migration/index.d.ts +18 -0
  4. package/migration/index.js +242 -0
  5. package/migration/report-schema-v2.json +127 -0
  6. package/package.json +14 -2
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14982 -4831
  9. package/richtexteditor/all_plugins.min.js +62 -42
  10. package/richtexteditor/plugins/accessibilitychecker.js +8 -7
  11. package/richtexteditor/plugins/aitoolkit.css +17 -8
  12. package/richtexteditor/plugins/aitoolkit.js +8 -8
  13. package/richtexteditor/plugins/all_plugins.js +14982 -4831
  14. package/richtexteditor/plugins/all_plugins.min.js +62 -42
  15. package/richtexteditor/plugins/autocorrect.js +353 -0
  16. package/richtexteditor/plugins/bookmarkcard.js +12 -1
  17. package/richtexteditor/plugins/changecase.js +238 -0
  18. package/richtexteditor/plugins/comments.js +15 -28
  19. package/richtexteditor/plugins/contentminimap.js +6 -5
  20. package/richtexteditor/plugins/crdt-engine.js +2201 -198
  21. package/richtexteditor/plugins/crdt-engine.min.js +5 -5
  22. package/richtexteditor/plugins/crossreference.js +408 -0
  23. package/richtexteditor/plugins/documentimport.js +915 -21
  24. package/richtexteditor/plugins/documentoutline.js +5 -4
  25. package/richtexteditor/plugins/docxexport.js +960 -0
  26. package/richtexteditor/plugins/draghandle.js +309 -0
  27. package/richtexteditor/plugins/footnotes.js +398 -0
  28. package/richtexteditor/plugins/formatpainter.js +298 -0
  29. package/richtexteditor/plugins/formattingmarks.js +246 -0
  30. package/richtexteditor/plugins/html2pdf.js +108 -4
  31. package/richtexteditor/plugins/imageeditor.js +37 -7
  32. package/richtexteditor/plugins/insertgallery.js +553 -170
  33. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  34. package/richtexteditor/plugins/linenumbers.js +352 -0
  35. package/richtexteditor/plugins/linkchecker.js +396 -0
  36. package/richtexteditor/plugins/mentionplugin.js +4 -3
  37. package/richtexteditor/plugins/mergefields.js +328 -0
  38. package/richtexteditor/plugins/multilevellist.js +137 -0
  39. package/richtexteditor/plugins/pagination.js +505 -0
  40. package/richtexteditor/plugins/pdfexport.js +1137 -0
  41. package/richtexteditor/plugins/permanentpen.js +211 -0
  42. package/richtexteditor/plugins/revisionhistory.js +47 -34
  43. package/richtexteditor/plugins/rtlui.js +150 -0
  44. package/richtexteditor/plugins/sanitizer.js +510 -0
  45. package/richtexteditor/plugins/slashcommand.js +142 -17
  46. package/richtexteditor/plugins/spellcheck.js +6 -3
  47. package/richtexteditor/plugins/tableofcontents.js +353 -0
  48. package/richtexteditor/plugins/tabletools.js +342 -0
  49. package/richtexteditor/plugins/textdirection.js +313 -0
  50. package/richtexteditor/plugins/trackedchanges.js +37 -6
  51. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  52. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  53. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  54. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  55. package/richtexteditor/plugins/watermark.js +214 -0
  56. package/richtexteditor/plugins/wordexport.js +0 -0
  57. package/richtexteditor/plugins/yjscollab.js +49 -11
  58. package/richtexteditor/rte-config.js +44 -44
  59. package/richtexteditor/rte.js +91 -63
  60. package/richtexteditor/rte_theme_default.css +483 -384
@@ -1,3 +1,27 @@
1
+ // 2026-08-05 Image gallery browser.
2
+ //
3
+ // Two implementations of this dialog had drifted apart: the flagship shipped a
4
+ // flat grid built from config.galleryImages, while the RichTextBox package
5
+ // shipped a server-backed file manager (folders, breadcrumb, New folder, upload
6
+ // into the current folder). The tier audit requires one identical plugin
7
+ // everywhere, so keeping both meant one of them silently overwriting the other
8
+ // on the next sync — which is exactly what happened: the folder browser's CSS
9
+ // was dropped and its folder tiles rendered as blank boxes on the live site.
10
+ //
11
+ // This is the merged implementation. It is server-backed when an endpoint is
12
+ // configured and falls back to the packaged preset list otherwise, so a host
13
+ // with no gallery endpoint behaves exactly as the flat grid always did.
14
+ //
15
+ // config.galleryEndpoint URL returning { currentFolder, currentFolderDisplay,
16
+ // parentFolder, folders: [{folder,name}],
17
+ // images: [{url,name,folder,size,source}] }.
18
+ // Absent -> local mode: presets only, no request.
19
+ // config.galleryImages Preset images. String, [url, text], or an object
20
+ // with url/src/href plus optional thumbnail/name/meta.
21
+ //
22
+ // Uploads use window.richTextBoxUploadFile when present (it accepts a target
23
+ // folder), otherwise the generic window.rte_file_upload_handler.
24
+
1
25
  RTE_DefaultConfig.plugin_insertgallery = RTE_Plugin_InsertGallery;
2
26
 
3
27
  function RTE_Plugin_InsertGallery() {
@@ -35,12 +59,50 @@ function RTE_Plugin_InsertGallery() {
35
59
  return tag;
36
60
  }
37
61
 
38
- function clear(node) {
39
- while (node.firstChild) {
40
- node.removeChild(node.firstChild);
62
+ function getGalleryEndpoint() {
63
+ return config.galleryEndpoint || window.RichTextBoxGalleryUrl || "";
64
+ }
65
+
66
+ function withFolder(url, folder) {
67
+ var separator = url.indexOf("?") >= 0 ? "&" : "?";
68
+ return url + separator + "folder=" + encodeURIComponent(folder || "");
69
+ }
70
+
71
+ function requestJson(method, url, body, callback) {
72
+ var request = new XMLHttpRequest();
73
+ request.open(method, url, true);
74
+
75
+ request.onreadystatechange = function () {
76
+ if (request.readyState !== 4) {
77
+ return;
78
+ }
79
+
80
+ if (request.status < 200 || request.status >= 300) {
81
+ callback(null, "http-" + request.status);
82
+ return;
83
+ }
84
+
85
+ try {
86
+ callback(JSON.parse(request.responseText), null);
87
+ } catch (ex) {
88
+ callback(null, "invalid-json");
89
+ }
90
+ };
91
+
92
+ request.onerror = function () {
93
+ callback(null, "network-error");
94
+ };
95
+
96
+ if (body) {
97
+ request.send(body);
98
+ return;
41
99
  }
100
+
101
+ request.send();
42
102
  }
43
103
 
104
+ // Query strings and percent-escapes both show up in real upload URLs, so
105
+ // strip and decode rather than splitting on "/" alone.
44
106
  function getFileName(url) {
45
107
  var value = String(url || "").split("#")[0].split("?")[0];
46
108
  var lastSlash = value.lastIndexOf("/");
@@ -52,25 +114,24 @@ function RTE_Plugin_InsertGallery() {
52
114
  return name || "Image";
53
115
  }
54
116
 
55
- function getMetaText(item) {
56
- if (item.meta) {
57
- return item.meta;
117
+ function humanSize(size) {
118
+ if (!size) {
119
+ return "";
120
+ }
121
+ if (size < 1024) {
122
+ return size + " B";
58
123
  }
59
- if (item.alt) {
60
- return item.alt;
124
+ if (size < 1024 * 1024) {
125
+ return Math.round(size / 1024) + " KB";
61
126
  }
62
- return item.url;
127
+ return (Math.round(size * 10 / (1024 * 1024)) / 10) + " MB";
63
128
  }
64
129
 
65
- function normalizeGalleryItem(item) {
130
+ function normalizePreset(item) {
66
131
  var normalized = null;
67
132
 
68
133
  if (typeof item === "string") {
69
- normalized = {
70
- url: item,
71
- thumbnail: item,
72
- name: getFileName(item)
73
- };
134
+ normalized = { url: item, thumbnail: item, name: getFileName(item) };
74
135
  } else if (item instanceof Array) {
75
136
  normalized = {
76
137
  url: item[0],
@@ -83,7 +144,6 @@ function RTE_Plugin_InsertGallery() {
83
144
  if (!url) {
84
145
  return null;
85
146
  }
86
-
87
147
  normalized = {
88
148
  url: url,
89
149
  thumbnail: item.thumbnail || item.thumb || item.preview || url,
@@ -96,64 +156,72 @@ function RTE_Plugin_InsertGallery() {
96
156
  return null;
97
157
  }
98
158
 
99
- if (!normalized.thumbnail) {
100
- normalized.thumbnail = normalized.url;
101
- }
102
-
103
- if (!normalized.name) {
104
- normalized.name = getFileName(normalized.url);
105
- }
106
-
159
+ normalized.thumbnail = normalized.thumbnail || normalized.url;
160
+ normalized.name = normalized.name || getFileName(normalized.url);
161
+ normalized.source = "preset";
162
+ normalized.folder = "";
163
+ normalized.searchText = (normalized.name + " " + (normalized.meta || "") + " " + normalized.url).toLowerCase();
107
164
  return normalized;
108
165
  }
109
166
 
110
- function uploadFiles(fileList, onUploaded, onFinished, onFailed) {
111
- var files = [];
112
- var handler = window.rte_file_upload_handler;
113
- var i;
114
-
115
- for (i = 0; i < fileList.length; i++) {
116
- files.push(fileList[i]);
167
+ function normalizeServerFolder(item) {
168
+ if (!item || !item.folder) {
169
+ return null;
117
170
  }
171
+ var name = item.name || getFileName(item.folder) || item.folder;
172
+ return {
173
+ name: name,
174
+ folder: item.folder,
175
+ searchText: (name + " " + item.folder).toLowerCase()
176
+ };
177
+ }
118
178
 
119
- if (!files.length) {
120
- onFinished();
121
- return;
179
+ function normalizeServerImage(item) {
180
+ if (!item || !item.url) {
181
+ return null;
122
182
  }
183
+ var name = item.name || getFileName(item.url);
184
+ return {
185
+ name: name,
186
+ url: item.url,
187
+ thumbnail: item.thumbnail || item.url,
188
+ folder: item.folder || "",
189
+ source: item.source || "upload",
190
+ size: item.size || 0,
191
+ searchText: (name + " " + item.url).toLowerCase()
192
+ };
193
+ }
123
194
 
124
- if (typeof handler !== "function") {
125
- if (onFailed) {
126
- onFailed("Upload handler is not configured.");
127
- }
128
- onFinished();
129
- return;
195
+ function setDisabled(button, disabled) {
196
+ button.disabled = !!disabled;
197
+ if (disabled) {
198
+ button.setAttribute("aria-disabled", "true");
199
+ } else {
200
+ button.removeAttribute("aria-disabled");
130
201
  }
202
+ }
131
203
 
132
- var index = 0;
204
+ function show(element, visible) {
205
+ element.style.display = visible ? "" : "none";
206
+ }
133
207
 
134
- function next() {
135
- if (index >= files.length) {
136
- onFinished();
137
- return;
208
+ obj.DoInsertGallery = function () {
209
+ var endpoint = getGalleryEndpoint();
210
+ var serverMode = !!endpoint;
211
+ var uploadToFolder = typeof window.richTextBoxUploadFile === "function";
212
+ var genericUpload = typeof window.rte_file_upload_handler === "function";
213
+ var canUpload = uploadToFolder || genericUpload;
214
+
215
+ var presetImages = [];
216
+ var presetSource = config.galleryImages || [];
217
+ var i;
218
+ for (i = 0; i < presetSource.length; i++) {
219
+ var preset = normalizePreset(presetSource[i]);
220
+ if (preset) {
221
+ presetImages.push(preset);
138
222
  }
139
-
140
- var file = files[index];
141
- handler(file, function (url, error) {
142
- if (url) {
143
- onUploaded(url, file, index, files);
144
- } else if (onFailed) {
145
- onFailed(error || ("Upload failed for " + file.name), file);
146
- }
147
-
148
- index++;
149
- next();
150
- }, index, files);
151
223
  }
152
224
 
153
- next();
154
- }
155
-
156
- obj.DoInsertGallery = function () {
157
225
  var dialoginner = editor.createDialog(editor.getLangText("insertgallerytitle") || "Image gallery", "rte-dialog-insertgallery");
158
226
  var closeDialog = typeof dialoginner.close === "function" ? function () {
159
227
  dialoginner.close();
@@ -162,50 +230,79 @@ function RTE_Plugin_InsertGallery() {
162
230
  };
163
231
 
164
232
  var browser = append(dialoginner, "div", "", "rte-gallery-browser");
165
- // The outer dialog frame already supplies a title bar + close button via
166
- // __UI_CreateDialogFrame, so we only render a short subtitle here (no
167
- // duplicate "Image gallery" heading underneath the frame title).
233
+
234
+ // The dialog frame already draws a title bar and close button, so this is
235
+ // a one-line subtitle, not a second heading.
168
236
  var header = append(browser, "div", "", "rte-dialog-browser-header");
169
237
  var copy = append(header, "div", "", "rte-dialog-browser-copy");
170
- copy.innerText = "Browse uploaded assets, filter by name, and insert the selected image into the editor.";
238
+ copy.innerText = serverMode
239
+ ? "Browse folders, upload new files, and insert the selected image into the editor."
240
+ : "Browse the available images, filter by name, and insert the selected image into the editor.";
171
241
 
172
242
  var toolbar = append(browser, "div", "", "rte-gallery-browser-toolbar");
173
- var path = append(toolbar, "div", "", "rte-gallery-browser-path");
243
+ var toolbarLeft = append(toolbar, "div", "", "rte-gallery-browser-toolbar-group");
244
+ var toolbarRight = append(toolbar, "div", "", "rte-gallery-browser-toolbar-group rte-gallery-browser-toolbar-group-right");
245
+
246
+ var upButton = append(toolbarLeft, "button", "", "rte-gallery-browser-button");
247
+ upButton.type = "button";
248
+ upButton.innerText = "Up";
249
+ upButton.setAttribute("aria-label", "Go to the parent folder");
250
+
251
+ var path = append(toolbarLeft, "div", "", "rte-gallery-browser-path");
174
252
  path.innerText = "/";
175
- var type = append(toolbar, "div", "", "rte-gallery-browser-type");
176
- type.innerText = "Image Files";
177
253
 
178
- var uploadButton = append(toolbar, "button", "", "rte-gallery-browser-button");
254
+ var type = append(toolbarLeft, "div", "", "rte-gallery-browser-type");
255
+ type.innerText = "Image files";
256
+
257
+ var createFolderButton = append(toolbarLeft, "button", "", "rte-gallery-browser-button");
258
+ createFolderButton.type = "button";
259
+ createFolderButton.innerText = "New folder";
260
+
261
+ var uploadButton = append(toolbarLeft, "button", "", "rte-gallery-browser-button rte-gallery-browser-button-primary");
179
262
  uploadButton.type = "button";
180
263
  uploadButton.innerText = "Upload";
181
264
 
182
- var refreshButton = append(toolbar, "button", "", "rte-gallery-browser-button");
265
+ var refreshButton = append(toolbarLeft, "button", "", "rte-gallery-browser-button");
183
266
  refreshButton.type = "button";
184
267
  refreshButton.innerText = "Refresh";
185
268
 
186
- var search = append(toolbar, "input", "", "rte-gallery-browser-search");
269
+ var search = append(toolbarRight, "input", "", "rte-gallery-browser-search");
187
270
  search.type = "search";
188
271
  search.placeholder = "Search images";
189
272
  search.setAttribute("aria-label", "Search images");
190
273
 
191
- var fileInput = append(toolbar, "input", "display:none;");
274
+ var fileInput = append(toolbarRight, "input", "display:none;");
192
275
  fileInput.type = "file";
193
276
  fileInput.accept = "image/*,.jpg,.jpeg,.png,.gif,.bmp,.webp,.svg";
194
277
  fileInput.multiple = true;
195
278
 
279
+ // Without a server there is no folder tree to walk and nothing to create
280
+ // a folder in, so those controls are hidden rather than shown disabled.
281
+ show(upButton, serverMode);
282
+ show(path, serverMode);
283
+ show(createFolderButton, serverMode);
284
+ show(uploadButton, canUpload);
285
+
196
286
  var status = append(browser, "div", "", "rte-gallery-browser-status");
197
287
  status.setAttribute("role", "status");
198
288
  status.setAttribute("aria-live", "polite");
289
+
199
290
  var surface = append(browser, "div", "", "rte-gallery-browser-surface");
291
+
292
+ // Folders are navigation and images are choices, so they cannot share one
293
+ // listbox — a non-option child of a listbox is invalid ARIA.
294
+ var folderGrid = append(surface, "div", "", "rte-gallery-browser-grid rte-gallery-browser-grid-folders");
295
+ folderGrid.setAttribute("role", "group");
296
+ folderGrid.setAttribute("aria-label", "Folders");
297
+
200
298
  var grid = append(surface, "div", "", "rte-gallery-browser-grid");
201
299
  grid.setAttribute("role", "listbox");
202
300
  grid.setAttribute("aria-label", "Available images");
301
+
203
302
  var empty = append(surface, "div", "", "rte-gallery-browser-empty");
204
- empty.innerText = "No images match this search. Upload a file or adjust the filter.";
205
303
 
206
304
  var footer = append(browser, "div", "", "rte-gallery-browser-footer");
207
305
  var footerText = append(footer, "div", "", "rte-gallery-browser-footer-text");
208
- footerText.innerText = "Choose an image to enable insert.";
209
306
 
210
307
  var cancelButton = append(footer, "button", "", "rte-gallery-browser-button");
211
308
  cancelButton.type = "button";
@@ -214,149 +311,435 @@ function RTE_Plugin_InsertGallery() {
214
311
  var insertButton = append(footer, "button", "", "rte-gallery-browser-button rte-gallery-browser-button-primary");
215
312
  insertButton.type = "button";
216
313
  insertButton.innerText = "Insert";
217
- insertButton.disabled = true;
218
-
219
- var selectedUrl = "";
220
-
221
- function getNormalizedItems() {
222
- var list = [];
223
- var items = config.galleryImages || [];
224
- var i;
225
- for (i = 0; i < items.length; i++) {
226
- var normalized = normalizeGalleryItem(items[i]);
227
- if (normalized) {
228
- list.push(normalized);
229
- }
314
+
315
+ var state = {
316
+ currentFolder: "",
317
+ currentFolderDisplay: "/",
318
+ parentFolder: null,
319
+ folders: [],
320
+ images: presetImages.slice(0),
321
+ selectedUrl: "",
322
+ loading: false,
323
+ error: "",
324
+ fallbackMode: !serverMode
325
+ };
326
+
327
+ function filterList(list) {
328
+ var term = search.value.toLowerCase();
329
+ if (!term) {
330
+ return list.slice(0);
230
331
  }
231
- return list;
232
- }
233
332
 
234
- function getFilteredItems() {
235
- var keyword = search.value.replace(/^\s+|\s+$/g, "").toLowerCase();
236
- var items = getNormalizedItems();
237
- if (!keyword) {
238
- return items;
333
+ var items = [];
334
+ for (var index = 0; index < list.length; index++) {
335
+ if (list[index].searchText.indexOf(term) >= 0) {
336
+ items.push(list[index]);
337
+ }
239
338
  }
339
+ return items;
340
+ }
240
341
 
241
- return items.filter(function (item) {
242
- return (item.name && item.name.toLowerCase().indexOf(keyword) >= 0)
243
- || (item.meta && item.meta.toLowerCase().indexOf(keyword) >= 0)
244
- || (item.url && item.url.toLowerCase().indexOf(keyword) >= 0);
245
- });
342
+ function setSelected(url) {
343
+ state.selectedUrl = url || "";
344
+ render();
246
345
  }
247
346
 
248
- function updateStatus(items) {
249
- var selectedName = "";
250
- var i;
251
- for (i = 0; i < items.length; i++) {
252
- if (items[i].url === selectedUrl) {
253
- selectedName = items[i].name;
347
+ function insertSelected() {
348
+ var selected = null;
349
+ for (var index = 0; index < state.images.length; index++) {
350
+ if (state.images[index].url === state.selectedUrl) {
351
+ selected = state.images[index];
254
352
  break;
255
353
  }
256
354
  }
257
355
 
258
- status.innerText = items.length + " item" + (items.length === 1 ? "" : "s") + " available."
259
- + (selectedName ? " " + selectedName + " selected." : " No image selected.");
260
- footerText.innerText = selectedName ? ("Ready to insert " + selectedName + ".") : "Choose an image to enable insert.";
261
- insertButton.disabled = !selectedName;
262
- }
263
-
264
- function insertSelected() {
265
- if (!selectedUrl) {
356
+ if (!selected) {
266
357
  return;
267
358
  }
268
- editor.insertImageByUrl(selectedUrl);
359
+
360
+ editor.insertImageByUrl(selected.url);
269
361
  closeDialog();
270
362
  editor.focus();
271
363
  }
272
364
 
365
+ function updateStatus(folderCount, imageCount) {
366
+ var total = folderCount + imageCount;
367
+ var where = serverMode ? (" in " + state.currentFolderDisplay) : "";
368
+ var message = total + " item" + (total === 1 ? "" : "s") + where + ". ";
369
+
370
+ if (!state.selectedUrl) {
371
+ status.innerText = message + "No image selected.";
372
+ footerText.innerText = state.fallbackMode && serverMode
373
+ ? "Showing packaged gallery items. Uploads still save into the current folder."
374
+ : "Choose an image to enable insert.";
375
+ setDisabled(insertButton, true);
376
+ return;
377
+ }
378
+
379
+ var selectedName = getFileName(state.selectedUrl);
380
+ status.innerText = message + "Selected: " + selectedName + ".";
381
+ setDisabled(insertButton, state.loading);
382
+
383
+ for (var index = 0; index < state.images.length; index++) {
384
+ if (state.images[index].url === state.selectedUrl) {
385
+ var size = humanSize(state.images[index].size);
386
+ footerText.innerText = selectedName + " ready to insert" + (size ? " (" + size + ")." : ".");
387
+ return;
388
+ }
389
+ }
390
+
391
+ footerText.innerText = selectedName + " ready to insert.";
392
+ }
393
+
394
+ function createFolderCard(item) {
395
+ var button = append(folderGrid, "button", "", "rte-gallery-browser-card rte-gallery-folder-card");
396
+ button.type = "button";
397
+ button.title = "Open " + item.name;
398
+
399
+ var selection = append(button, "div", "", "rte-gallery-browser-selection");
400
+ selection.innerHTML = "&nbsp;";
401
+
402
+ var thumbnail = append(button, "div", "", "rte-gallery-browser-thumbnail rte-gallery-browser-thumbnail-folder");
403
+ append(thumbnail, "div", "", "rte-gallery-browser-folder-icon");
404
+
405
+ var label = append(button, "div", "", "rte-gallery-browser-name");
406
+ label.innerText = item.name;
407
+
408
+ var meta = append(button, "div", "", "rte-gallery-browser-meta");
409
+ meta.innerText = "Folder";
410
+
411
+ button.onclick = function () {
412
+ loadFolder(item.folder);
413
+ };
414
+ }
415
+
416
+ function createImageCard(item) {
417
+ var isSelected = item.url === state.selectedUrl;
418
+ var button = append(grid, "button", "", "rte-gallery-browser-card rte-gallery-image-card" + (isSelected ? " is-selected" : ""));
419
+ button.type = "button";
420
+ button.title = item.name;
421
+ button.setAttribute("role", "option");
422
+ button.setAttribute("aria-selected", isSelected ? "true" : "false");
423
+
424
+ var selection = append(button, "div", "", "rte-gallery-browser-selection");
425
+ selection.innerHTML = isSelected ? "&#10003;" : "&nbsp;";
426
+
427
+ var thumbnail = append(button, "div", "", "rte-gallery-browser-thumbnail");
428
+ var image = append(thumbnail, "img", "", "rte-gallery-browser-thumbnail-image");
429
+ image.src = item.thumbnail || item.url;
430
+ image.alt = item.name;
431
+
432
+ var label = append(button, "div", "", "rte-gallery-browser-name");
433
+ label.innerText = item.name;
434
+
435
+ var meta = append(button, "div", "", "rte-gallery-browser-meta");
436
+ meta.innerText = item.meta || (item.source === "preset" ? "Preset image" : humanSize(item.size)) || item.url;
437
+
438
+ button.onclick = function () {
439
+ setSelected(item.url);
440
+ };
441
+
442
+ button.ondblclick = function () {
443
+ setSelected(item.url);
444
+ insertSelected();
445
+ };
446
+ }
447
+
448
+ // Presets belong to no folder, so they are only offered at the root —
449
+ // otherwise every folder would appear to contain them.
450
+ function mergePresets(serverImages) {
451
+ var merged = [];
452
+ var seen = {};
453
+ var index;
454
+
455
+ for (index = 0; index < serverImages.length; index++) {
456
+ merged.push(serverImages[index]);
457
+ seen[serverImages[index].url] = true;
458
+ }
459
+
460
+ if (state.currentFolder) {
461
+ return merged;
462
+ }
463
+
464
+ for (index = 0; index < presetImages.length; index++) {
465
+ if (!seen[presetImages[index].url]) {
466
+ merged.push(presetImages[index]);
467
+ }
468
+ }
469
+
470
+ return merged;
471
+ }
472
+
273
473
  function render() {
274
- clear(grid);
275
- var items = getFilteredItems();
276
- var i;
277
-
278
- empty.style.display = items.length ? "none" : "block";
279
-
280
- for (i = 0; i < items.length; i++) {
281
- (function (item) {
282
- var card = append(grid, "button", "", "rte-gallery-browser-card");
283
- card.type = "button";
284
- card.setAttribute("role", "option");
285
- card.setAttribute("aria-selected", item.url === selectedUrl ? "true" : "false");
286
- if (item.url === selectedUrl) {
287
- card.classList.add("is-selected");
288
- }
474
+ var visibleFolders = filterList(state.folders);
475
+ var visibleImages = filterList(state.images);
476
+
477
+ path.innerText = state.currentFolderDisplay || "/";
478
+ setDisabled(upButton, state.loading || state.parentFolder === null);
479
+ setDisabled(createFolderButton, state.loading);
480
+ setDisabled(uploadButton, state.loading);
481
+ setDisabled(refreshButton, state.loading);
482
+
483
+ folderGrid.innerHTML = "";
484
+ grid.innerHTML = "";
485
+ show(empty, false);
486
+ show(folderGrid, false);
487
+ show(grid, true);
488
+
489
+ if (state.loading) {
490
+ empty.innerText = "Loading gallery...";
491
+ show(empty, true);
492
+ show(grid, false);
493
+ updateStatus(0, 0);
494
+ return;
495
+ }
496
+
497
+ if (state.error) {
498
+ empty.innerText = state.error;
499
+ show(empty, true);
500
+ show(grid, false);
501
+ updateStatus(0, 0);
502
+ return;
503
+ }
289
504
 
290
- var selection = append(card, "div", "", "rte-gallery-browser-selection");
291
- selection.innerText = item.url === selectedUrl ? "Selected" : "";
505
+ for (var folderIndex = 0; folderIndex < visibleFolders.length; folderIndex++) {
506
+ createFolderCard(visibleFolders[folderIndex]);
507
+ }
508
+ show(folderGrid, visibleFolders.length > 0);
292
509
 
293
- var thumb = append(card, "div", "", "rte-gallery-browser-thumbnail");
294
- var image = append(thumb, "img", "", "rte-gallery-browser-thumbnail-image");
295
- image.src = item.thumbnail;
296
- image.alt = item.name;
510
+ for (var imageIndex = 0; imageIndex < visibleImages.length; imageIndex++) {
511
+ createImageCard(visibleImages[imageIndex]);
512
+ }
513
+ show(grid, visibleImages.length > 0);
297
514
 
298
- var name = append(card, "div", "", "rte-gallery-browser-name");
299
- name.innerText = item.name;
515
+ updateStatus(visibleFolders.length, visibleImages.length);
300
516
 
301
- var meta = append(card, "div", "", "rte-gallery-browser-meta");
302
- meta.innerText = getMetaText(item);
517
+ if (!visibleFolders.length && !visibleImages.length) {
518
+ empty.innerText = search.value
519
+ ? "No folders or images match the current filter."
520
+ : (serverMode
521
+ ? "This folder is empty. Create a folder or upload an image to get started."
522
+ : "No images are available. Upload a file to get started.");
523
+ show(empty, true);
524
+ }
525
+ }
303
526
 
304
- card.onclick = function () {
305
- selectedUrl = item.url;
306
- render();
307
- };
527
+ function applyResponse(payload, fallbackMode) {
528
+ var nextFolders = [];
529
+ var nextImages = [];
530
+ var index;
531
+
532
+ state.currentFolder = payload.currentFolder || "";
533
+ state.currentFolderDisplay = payload.currentFolderDisplay || "/";
534
+ state.parentFolder = typeof payload.parentFolder === "undefined" ? null : payload.parentFolder;
535
+ state.fallbackMode = !!fallbackMode;
536
+ state.error = "";
537
+
538
+ if (payload.folders) {
539
+ for (index = 0; index < payload.folders.length; index++) {
540
+ var normalizedFolder = normalizeServerFolder(payload.folders[index]);
541
+ if (normalizedFolder) {
542
+ nextFolders.push(normalizedFolder);
543
+ }
544
+ }
545
+ }
308
546
 
309
- card.ondblclick = function () {
310
- selectedUrl = item.url;
311
- insertSelected();
312
- };
313
- })(items[i]);
547
+ if (payload.images) {
548
+ for (index = 0; index < payload.images.length; index++) {
549
+ var normalizedImage = normalizeServerImage(payload.images[index]);
550
+ if (normalizedImage) {
551
+ nextImages.push(normalizedImage);
552
+ }
553
+ }
314
554
  }
315
555
 
316
- if (selectedUrl) {
556
+ state.folders = nextFolders;
557
+ state.images = mergePresets(nextImages);
558
+
559
+ if (state.selectedUrl) {
317
560
  var stillVisible = false;
318
- for (i = 0; i < items.length; i++) {
319
- if (items[i].url === selectedUrl) {
561
+ for (index = 0; index < state.images.length; index++) {
562
+ if (state.images[index].url === state.selectedUrl) {
320
563
  stillVisible = true;
321
564
  break;
322
565
  }
323
566
  }
324
567
  if (!stillVisible) {
325
- selectedUrl = "";
568
+ state.selectedUrl = "";
326
569
  }
327
570
  }
571
+ }
328
572
 
329
- updateStatus(items);
573
+ function loadFallback(errorCode) {
574
+ applyResponse({
575
+ currentFolder: "",
576
+ currentFolderDisplay: "/",
577
+ parentFolder: null,
578
+ folders: [],
579
+ images: []
580
+ }, true);
581
+ state.images = presetImages.slice(0);
582
+ state.error = presetImages.length ? "" : ("The image gallery is unavailable right now (" + errorCode + ").");
583
+ state.loading = false;
584
+ render();
330
585
  }
331
586
 
587
+ function loadFolder(folder, selectAfterLoad) {
588
+ if (!serverMode) {
589
+ state.images = presetImages.slice(0);
590
+ render();
591
+ return;
592
+ }
593
+
594
+ state.loading = true;
595
+ state.error = "";
596
+ render();
597
+
598
+ requestJson("GET", withFolder(endpoint, folder), null, function (payload, errorCode) {
599
+ if (errorCode) {
600
+ loadFallback(errorCode);
601
+ return;
602
+ }
603
+
604
+ applyResponse(payload || {}, false);
605
+ if (selectAfterLoad) {
606
+ state.selectedUrl = selectAfterLoad;
607
+ }
608
+ state.loading = false;
609
+ render();
610
+ });
611
+ }
612
+
613
+ function createFolder() {
614
+ var folderName = window.prompt("New folder name", "");
615
+ if (!folderName) {
616
+ return;
617
+ }
618
+
619
+ state.loading = true;
620
+ state.error = "";
621
+ render();
622
+
623
+ var formData = new FormData();
624
+ formData.append("action", "create-folder");
625
+ formData.append("folder", state.currentFolder || "");
626
+ formData.append("name", folderName);
627
+
628
+ requestJson("POST", endpoint, formData, function (payload, errorCode) {
629
+ if (errorCode) {
630
+ state.loading = false;
631
+ state.error = "The folder could not be created right now.";
632
+ render();
633
+ return;
634
+ }
635
+
636
+ applyResponse(payload || {}, false);
637
+ state.loading = false;
638
+ render();
639
+ });
640
+ }
641
+
642
+ function uploadFiles(fileList) {
643
+ var files = [];
644
+ var index;
645
+ for (index = 0; index < fileList.length; index++) {
646
+ files.push(fileList[index]);
647
+ }
648
+
649
+ if (!files.length || !canUpload) {
650
+ return;
651
+ }
652
+
653
+ var lastUploadedUrl = "";
654
+
655
+ function uploadNext(nextIndex) {
656
+ if (nextIndex >= files.length) {
657
+ if (serverMode) {
658
+ loadFolder(state.currentFolder, lastUploadedUrl);
659
+ return;
660
+ }
661
+
662
+ // No server to re-list from, so fold the upload into the
663
+ // preset list directly and keep it selected.
664
+ if (lastUploadedUrl) {
665
+ var uploaded = normalizePreset(lastUploadedUrl);
666
+ if (uploaded) {
667
+ uploaded.source = "upload";
668
+ presetImages.unshift(uploaded);
669
+ config.galleryImages.unshift(lastUploadedUrl);
670
+ }
671
+ state.selectedUrl = lastUploadedUrl;
672
+ }
673
+ state.images = presetImages.slice(0);
674
+ state.loading = false;
675
+ render();
676
+ return;
677
+ }
678
+
679
+ var file = files[nextIndex];
680
+
681
+ function done(url, errorCode) {
682
+ if (!url) {
683
+ state.error = errorCode || ("Upload failed for " + file.name + ".");
684
+ state.loading = false;
685
+ render();
686
+ return;
687
+ }
688
+
689
+ lastUploadedUrl = url;
690
+ uploadNext(nextIndex + 1);
691
+ }
692
+
693
+ if (uploadToFolder) {
694
+ window.richTextBoxUploadFile(file, function (url, errorCode) {
695
+ done(errorCode ? "" : url, errorCode ? ("Upload failed for " + file.name + ".") : "");
696
+ }, { folder: state.currentFolder }, nextIndex, files);
697
+ return;
698
+ }
699
+
700
+ window.rte_file_upload_handler(file, function (url, error) {
701
+ done(url, error);
702
+ }, nextIndex, files);
703
+ }
704
+
705
+ state.loading = true;
706
+ state.error = "";
707
+ render();
708
+ uploadNext(0);
709
+ }
710
+
711
+ upButton.onclick = function () {
712
+ if (state.parentFolder === null) {
713
+ return;
714
+ }
715
+ loadFolder(state.parentFolder);
716
+ };
717
+
718
+ createFolderButton.onclick = createFolder;
719
+
332
720
  uploadButton.onclick = function () {
333
721
  fileInput.click();
334
722
  };
335
723
 
336
724
  fileInput.onchange = function () {
337
- var lastUploaded = "";
338
- status.innerText = "Uploading images...";
339
-
340
- uploadFiles(fileInput.files, function (url) {
341
- lastUploaded = url;
342
- config.galleryImages.unshift(url);
343
- }, function () {
344
- if (lastUploaded) {
345
- selectedUrl = lastUploaded;
346
- }
347
- fileInput.value = "";
348
- render();
349
- }, function (error) {
350
- status.innerText = error || "Upload failed.";
351
- });
725
+ uploadFiles(this.files);
726
+ this.value = "";
727
+ };
728
+
729
+ refreshButton.onclick = function () {
730
+ loadFolder(state.currentFolder, state.selectedUrl);
352
731
  };
353
732
 
354
- refreshButton.onclick = render;
355
733
  search.oninput = render;
356
734
  cancelButton.onclick = closeDialog;
357
735
  insertButton.onclick = insertSelected;
358
736
 
359
- render();
737
+ if (serverMode) {
738
+ loadFolder("");
739
+ } else {
740
+ render();
741
+ }
742
+
360
743
  search.focus();
361
744
  };
362
745
  }