suneditor 3.0.0-beta.2 → 3.0.0-beta.20

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 (177) hide show
  1. package/CONTRIBUTING.md +186 -184
  2. package/LICENSE +21 -21
  3. package/README.md +157 -180
  4. package/dist/suneditor.min.css +1 -1
  5. package/dist/suneditor.min.js +1 -1
  6. package/package.json +126 -123
  7. package/src/assets/design/color.css +131 -121
  8. package/src/assets/design/index.css +3 -3
  9. package/src/assets/design/size.css +37 -35
  10. package/src/assets/design/typography.css +37 -37
  11. package/src/assets/icons/defaultIcons.js +247 -232
  12. package/src/assets/suneditor-contents.css +779 -778
  13. package/src/assets/suneditor.css +43 -35
  14. package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
  15. package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
  16. package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
  17. package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
  18. package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
  19. package/src/core/base/eventManager.js +1550 -1484
  20. package/src/core/base/history.js +355 -355
  21. package/src/core/class/char.js +163 -162
  22. package/src/core/class/component.js +856 -842
  23. package/src/core/class/format.js +3433 -3422
  24. package/src/core/class/html.js +1927 -1890
  25. package/src/core/class/menu.js +357 -346
  26. package/src/core/class/nodeTransform.js +424 -424
  27. package/src/core/class/offset.js +858 -891
  28. package/src/core/class/selection.js +710 -620
  29. package/src/core/class/shortcuts.js +98 -98
  30. package/src/core/class/toolbar.js +438 -430
  31. package/src/core/class/ui.js +424 -422
  32. package/src/core/class/viewer.js +750 -750
  33. package/src/core/editor.js +1810 -1708
  34. package/src/core/section/actives.js +268 -241
  35. package/src/core/section/constructor.js +1348 -1661
  36. package/src/core/section/context.js +102 -102
  37. package/src/core/section/documentType.js +582 -561
  38. package/src/core/section/options.js +367 -0
  39. package/src/core/util/instanceCheck.js +59 -0
  40. package/src/editorInjector/_classes.js +36 -36
  41. package/src/editorInjector/_core.js +92 -92
  42. package/src/editorInjector/index.js +75 -75
  43. package/src/events.js +634 -622
  44. package/src/helper/clipboard.js +59 -59
  45. package/src/helper/converter.js +586 -564
  46. package/src/helper/dom/domCheck.js +304 -304
  47. package/src/helper/dom/domQuery.js +677 -669
  48. package/src/helper/dom/domUtils.js +618 -557
  49. package/src/helper/dom/index.js +12 -12
  50. package/src/helper/env.js +249 -240
  51. package/src/helper/index.js +25 -25
  52. package/src/helper/keyCodeMap.js +183 -183
  53. package/src/helper/numbers.js +72 -72
  54. package/src/helper/unicode.js +47 -47
  55. package/src/langs/ckb.js +231 -231
  56. package/src/langs/cs.js +231 -231
  57. package/src/langs/da.js +231 -231
  58. package/src/langs/de.js +231 -231
  59. package/src/langs/en.js +230 -230
  60. package/src/langs/es.js +231 -231
  61. package/src/langs/fa.js +231 -231
  62. package/src/langs/fr.js +231 -231
  63. package/src/langs/he.js +231 -231
  64. package/src/langs/hu.js +230 -230
  65. package/src/langs/index.js +28 -28
  66. package/src/langs/it.js +231 -231
  67. package/src/langs/ja.js +230 -230
  68. package/src/langs/km.js +230 -230
  69. package/src/langs/ko.js +230 -230
  70. package/src/langs/lv.js +231 -231
  71. package/src/langs/nl.js +231 -231
  72. package/src/langs/pl.js +231 -231
  73. package/src/langs/pt_br.js +231 -231
  74. package/src/langs/ro.js +231 -231
  75. package/src/langs/ru.js +231 -231
  76. package/src/langs/se.js +231 -231
  77. package/src/langs/tr.js +231 -231
  78. package/src/langs/uk.js +231 -231
  79. package/src/langs/ur.js +231 -231
  80. package/src/langs/zh_cn.js +231 -231
  81. package/src/modules/ApiManager.js +191 -191
  82. package/src/modules/Browser.js +669 -667
  83. package/src/modules/ColorPicker.js +364 -362
  84. package/src/modules/Controller.js +474 -454
  85. package/src/modules/Figure.js +1620 -1617
  86. package/src/modules/FileManager.js +359 -359
  87. package/src/modules/HueSlider.js +577 -565
  88. package/src/modules/Modal.js +346 -346
  89. package/src/modules/ModalAnchorEditor.js +643 -643
  90. package/src/modules/SelectMenu.js +549 -549
  91. package/src/modules/_DragHandle.js +17 -17
  92. package/src/modules/index.js +14 -14
  93. package/src/plugins/browser/audioGallery.js +83 -83
  94. package/src/plugins/browser/fileBrowser.js +103 -103
  95. package/src/plugins/browser/fileGallery.js +83 -83
  96. package/src/plugins/browser/imageGallery.js +81 -81
  97. package/src/plugins/browser/videoGallery.js +103 -103
  98. package/src/plugins/command/blockquote.js +61 -60
  99. package/src/plugins/command/exportPDF.js +134 -134
  100. package/src/plugins/command/fileUpload.js +456 -456
  101. package/src/plugins/command/list_bulleted.js +149 -148
  102. package/src/plugins/command/list_numbered.js +152 -151
  103. package/src/plugins/dropdown/align.js +157 -155
  104. package/src/plugins/dropdown/backgroundColor.js +108 -104
  105. package/src/plugins/dropdown/font.js +141 -137
  106. package/src/plugins/dropdown/fontColor.js +109 -105
  107. package/src/plugins/dropdown/formatBlock.js +170 -178
  108. package/src/plugins/dropdown/hr.js +152 -152
  109. package/src/plugins/dropdown/layout.js +83 -83
  110. package/src/plugins/dropdown/lineHeight.js +131 -130
  111. package/src/plugins/dropdown/list.js +123 -122
  112. package/src/plugins/dropdown/paragraphStyle.js +138 -138
  113. package/src/plugins/dropdown/table.js +4110 -4000
  114. package/src/plugins/dropdown/template.js +83 -83
  115. package/src/plugins/dropdown/textStyle.js +149 -149
  116. package/src/plugins/field/mention.js +242 -242
  117. package/src/plugins/index.js +120 -120
  118. package/src/plugins/input/fontSize.js +414 -410
  119. package/src/plugins/input/pageNavigator.js +71 -70
  120. package/src/plugins/modal/audio.js +677 -677
  121. package/src/plugins/modal/drawing.js +537 -531
  122. package/src/plugins/modal/embed.js +886 -886
  123. package/src/plugins/modal/image.js +1377 -1376
  124. package/src/plugins/modal/link.js +248 -240
  125. package/src/plugins/modal/math.js +563 -563
  126. package/src/plugins/modal/video.js +1226 -1226
  127. package/src/plugins/popup/anchor.js +224 -222
  128. package/src/suneditor.js +114 -107
  129. package/src/themes/dark.css +132 -122
  130. package/src/typedef.js +132 -130
  131. package/types/assets/icons/defaultIcons.d.ts +8 -0
  132. package/types/core/base/eventManager.d.ts +29 -4
  133. package/types/core/class/char.d.ts +2 -1
  134. package/types/core/class/component.d.ts +1 -2
  135. package/types/core/class/format.d.ts +8 -1
  136. package/types/core/class/html.d.ts +8 -0
  137. package/types/core/class/menu.d.ts +8 -0
  138. package/types/core/class/offset.d.ts +24 -26
  139. package/types/core/class/selection.d.ts +2 -0
  140. package/types/core/class/toolbar.d.ts +6 -0
  141. package/types/core/class/ui.d.ts +1 -1
  142. package/types/core/editor.d.ts +34 -12
  143. package/types/core/section/constructor.d.ts +5 -638
  144. package/types/core/section/documentType.d.ts +12 -2
  145. package/types/core/section/options.d.ts +740 -0
  146. package/types/core/util/instanceCheck.d.ts +50 -0
  147. package/types/editorInjector/_core.d.ts +5 -5
  148. package/types/editorInjector/index.d.ts +2 -2
  149. package/types/events.d.ts +2 -0
  150. package/types/helper/converter.d.ts +9 -0
  151. package/types/helper/dom/domQuery.d.ts +5 -5
  152. package/types/helper/dom/domUtils.d.ts +8 -0
  153. package/types/helper/env.d.ts +6 -1
  154. package/types/helper/index.d.ts +4 -1
  155. package/types/index.d.ts +122 -120
  156. package/types/langs/_Lang.d.ts +194 -194
  157. package/types/modules/ColorPicker.d.ts +5 -1
  158. package/types/modules/Controller.d.ts +8 -4
  159. package/types/modules/Figure.d.ts +2 -1
  160. package/types/modules/HueSlider.d.ts +4 -1
  161. package/types/modules/SelectMenu.d.ts +1 -1
  162. package/types/plugins/command/blockquote.d.ts +1 -0
  163. package/types/plugins/command/list_bulleted.d.ts +1 -0
  164. package/types/plugins/command/list_numbered.d.ts +1 -0
  165. package/types/plugins/dropdown/align.d.ts +1 -0
  166. package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
  167. package/types/plugins/dropdown/font.d.ts +1 -0
  168. package/types/plugins/dropdown/fontColor.d.ts +1 -0
  169. package/types/plugins/dropdown/formatBlock.d.ts +3 -2
  170. package/types/plugins/dropdown/lineHeight.d.ts +1 -0
  171. package/types/plugins/dropdown/list.d.ts +1 -0
  172. package/types/plugins/dropdown/table.d.ts +6 -0
  173. package/types/plugins/input/fontSize.d.ts +1 -0
  174. package/types/plugins/modal/drawing.d.ts +4 -0
  175. package/types/plugins/modal/link.d.ts +32 -15
  176. package/types/suneditor.d.ts +13 -9
  177. package/types/typedef.d.ts +8 -0
@@ -1,667 +1,669 @@
1
- import CoreInjector from '../editorInjector/_core';
2
- import { dom, keyCodeMap } from '../helper';
3
- import ApiManager from './ApiManager';
4
-
5
- /**
6
- * @typedef {Object} BrowserFile
7
- * @property {string} [src=""] - Source url
8
- * @property {string} [name=""] - File name | Folder name
9
- * @property {string=} thumbnail - Thumbnail url
10
- * @property {string=} alt - Image alt
11
- * @property {Array<string>|string=} tag - Tag name list
12
- * @property {string=} type - Type (image, video, audio, etc.)
13
- * @property {string=} frame - Frame name (iframe, video, etc.)
14
- * @property {BrowserFile | string=} _data - The folder's contents or an API URL.
15
- * @property {boolean=} default - Whether this folder is the default selection.
16
- * @property {Object<string, *>=} meta - Metadata
17
- */
18
-
19
- /**
20
- * @typedef BrowserParams
21
- * @property {string} title - File browser window title. Required. Can be overridden in browser.
22
- * @property {string=} className - Class name of the file browser. Optional. Default: ''.
23
- * @property {Object<string, *>|Array<*>=} data - direct data without server calls
24
- * @property {string=} url - File server url. Required. Can be overridden in browser.
25
- * @property {Object<string, string>=} headers - File server http header. Required. Can be overridden in browser.
26
- * @property {(target: Node) => void} selectorHandler - Function that actions when an item is clicked. Required. Can be overridden in browser.
27
- * @property {boolean=} useSearch - Whether to use the search function. Optional. Default: true.
28
- * @property {string=} searchUrl - File server search url. Optional. Can be overridden in browser.
29
- * @property {Object<string, string>=} searchUrlHeader - File server search http header. Optional. Can be overridden in browser.
30
- * @property {string=} listClass - Class name of list div. Required. Can be overridden in browser.
31
- * @property {(item: BrowserFile) => string=} drawItemHandler - Function that defines the HTML of a file item. Required. Can be overridden in browser.
32
- * @property {Array<*>=} props - "props" argument to "drawItemHandler" function. Optional. Can be overridden in browser.
33
- * @property {number=} columnSize - Number of "div.se-file-item-column" to be created. Optional. Can be overridden in browser. Default: 4.
34
- * @property {((item: BrowserFile) => string)=} thumbnail - Default thumbnail
35
- */
36
-
37
- /**
38
- * @class
39
- * @description File browser plugin
40
- */
41
- class Browser extends CoreInjector {
42
- /**
43
- * @constructor
44
- * @param {*} inst The instance object that called the constructor.
45
- * @param {BrowserParams} params Browser options
46
- */
47
- constructor(inst, params) {
48
- super(inst.editor);
49
-
50
- // create HTML
51
- this.useSearch = params.useSearch ?? true;
52
- const browserFrame = dom.utils.createElement('DIV', { class: 'se-browser sun-editor-common' + (params.className ? ` ${params.className}` : '') });
53
- const contentHTML = CreateHTMLInfos(inst.editor, this.useSearch);
54
- const content = contentHTML.html;
55
-
56
- // members
57
- this.kind = inst.constructor.key || inst.constructor.name;
58
- this.inst = inst;
59
- this.area = browserFrame;
60
- this.header = contentHTML.header;
61
- this.titleArea = contentHTML.titleArea;
62
- this.tagArea = contentHTML.tagArea;
63
- this.body = contentHTML.body;
64
- this.list = contentHTML.list;
65
- this.side = contentHTML.side;
66
- this.wrapper = contentHTML.wrapper;
67
- this._loading = contentHTML._loading;
68
-
69
- this.title = params.title;
70
- this.listClass = params.listClass || 'se-preview-list';
71
- this.directData = params.data;
72
- this.url = params.url;
73
- this.urlHeader = params.headers;
74
- this.searchUrl = params.searchUrl;
75
- this.searchUrlHeader = params.searchUrlHeader;
76
- this.drawItemHandler = (params.drawItemHandler || DrawItems).bind({ thumbnail: params.thumbnail, props: params.props || [] });
77
- this.selectorHandler = params.selectorHandler;
78
- this.columnSize = params.columnSize || 4;
79
- this.folderDefaultPath = '';
80
- this.closeArrow = this.icons.menu_arrow_right;
81
- this.openArrow = this.icons.menu_arrow_down;
82
- this.icon_folder = this.icons.side_menu_folder_item;
83
- this.icon_folder_item = this.icons.side_menu_folder;
84
- this.icon_item = this.icons.side_menu_item;
85
-
86
- /**
87
- * @type {Array<BrowserFile>}
88
- */
89
- this.items = [];
90
- /**
91
- * @type {Object<string, {name: string, meta: Object<string, *>}>}
92
- */
93
- this.folders = {};
94
- /**
95
- * @type {Object<string, {key?: string, name?: string, children?: *}>}
96
- */
97
- this.tree = {};
98
- /**
99
- * @type {BrowserFile}
100
- */
101
- this.data = {};
102
- this.selectedTags = [];
103
- this.keyword = '';
104
- this.sideInner = null;
105
- this._closeSignal = false;
106
- this._bindClose = null;
107
- this.__globalEventHandler = (e) => {
108
- if (!keyCodeMap.isEsc(e.code)) return;
109
- this.close();
110
- };
111
- // api manager
112
- this.apiManager = new ApiManager(this, { method: 'GET' });
113
-
114
- // init
115
- browserFrame.appendChild(dom.utils.createElement('DIV', { class: 'se-browser-back' }));
116
- browserFrame.appendChild(content);
117
- this.carrierWrapper.appendChild(browserFrame);
118
-
119
- this.eventManager.addEvent(this.tagArea, 'click', this.#OnClickTag.bind(this));
120
- this.eventManager.addEvent(this.list, 'click', this.#OnClickFile.bind(this));
121
- this.eventManager.addEvent(this.side, 'click', this.#OnClickSide.bind(this));
122
- this.eventManager.addEvent(content, 'mousedown', this.#OnMouseDown_browser.bind(this));
123
- this.eventManager.addEvent(content, 'click', this.#OnClick_browser.bind(this));
124
- this.eventManager.addEvent(browserFrame.querySelector('form.se-browser-search-form'), 'submit', this.#Search.bind(this));
125
- this.eventManager.addEvent((this.sideOpenBtn = /** @type {HTMLButtonElement} */ (browserFrame.querySelector('.se-side-open-btn'))), 'click', this.#SideOpen.bind(this));
126
- this.eventManager.addEvent([this.header, browserFrame.querySelector('.se-browser-main')], 'mousedown', this.#SideClose.bind(this));
127
- }
128
-
129
- /**
130
- * @description Open a file browser plugin
131
- * @param {Object} [params={}]
132
- * @param {string=} params.listClass - Class name of list div. If not, use "this.listClass".
133
- * @param {string=} params.title - File browser window title. If not, use "this.title".
134
- * @param {string=} params.url - File server url. If not, use "this.url".
135
- * @param {Object<string, string>=} params.urlHeader - File server http header. If not, use "this.urlHeader".
136
- */
137
- open(params) {
138
- if (!params) params = {};
139
- this.__addGlobalEvent();
140
-
141
- const listClassName = params.listClass || this.listClass;
142
- if (!dom.utils.hasClass(this.list, listClassName)) {
143
- this.list.className = 'se-browser-list ' + listClassName;
144
- }
145
-
146
- this.titleArea.textContent = params.title || this.title;
147
- this.area.style.display = 'block';
148
- this.editor.opendBrowser = this;
149
- this.closeArrow = this.options.get('_rtl') ? this.icons.menu_arrow_left : this.icons.menu_arrow_right;
150
-
151
- if (this.directData) {
152
- this.__drowItems(this.directData);
153
- } else {
154
- this._drawFileList(params.url || this.url, params.urlHeader || this.urlHeader, false);
155
- }
156
- }
157
-
158
- /**
159
- * @description Close a browser plugin
160
- * - The plugin's "init" method is called.
161
- */
162
- close() {
163
- this.__removeGlobalEvent();
164
- this.apiManager.cancel();
165
-
166
- this.area.style.display = 'none';
167
- this.selectedTags = [];
168
- this.items = [];
169
- this.folders = {};
170
- this.tree = {};
171
- this.data = {};
172
- this.keyword = '';
173
- this.list.innerHTML = this.tagArea.innerHTML = this.titleArea.textContent = '';
174
- this.editor.opendBrowser = null;
175
- this.sideInner = null;
176
-
177
- if (typeof this.inst.init === 'function') this.inst.init();
178
- }
179
-
180
- /**
181
- * @description Search files
182
- * @param {string} keyword - Search keyword
183
- */
184
- search(keyword) {
185
- if (this.searchUrl) {
186
- this.keyword = keyword;
187
- this._drawFileList(this.searchUrl + '?keyword=' + keyword, this.searchUrlHeader, false);
188
- } else {
189
- this.keyword = keyword.toLowerCase();
190
- this._drawListItem(this.items, false);
191
- }
192
- }
193
-
194
- /**
195
- * @description Filter items by tag
196
- * @param {Array<BrowserFile>} items - Items to filter
197
- * @returns {Array<BrowserFile>}
198
- */
199
- tagfilter(items) {
200
- const selectedTags = this.selectedTags;
201
- return selectedTags.length === 0 ? items : items.filter((item) => !Array.isArray(item.tag) || item.tag.some((tag) => selectedTags.includes(tag)));
202
- }
203
-
204
- /**
205
- * @description Show file browser loading box
206
- */
207
- showBrowserLoading() {
208
- this._loading.style.display = 'block';
209
- }
210
-
211
- /**
212
- * @description Close file browser loading box
213
- */
214
- closeBrowserLoading() {
215
- this._loading.style.display = 'none';
216
- }
217
-
218
- /**
219
- * @private
220
- * @description Fetches the file list from the server.
221
- * @param {string} url - The file server URL.
222
- * @param {Object<string, string>} urlHeader - The HTTP headers for the request.
223
- * @param {boolean} pageLoading - Indicates if this is a paginated request.
224
- */
225
- _drawFileList(url, urlHeader, pageLoading) {
226
- this.apiManager.call({ method: 'GET', url, headers: urlHeader, callBack: this.#CallBackGet.bind(this), errorCallBack: this.#CallBackError.bind(this) });
227
- if (!pageLoading) {
228
- this.sideOpenBtn.style.display = 'none';
229
- this.showBrowserLoading();
230
- }
231
- }
232
-
233
- /**
234
- * @private
235
- * @description Updates the displayed list of file items.
236
- * @param {Array<BrowserFile>} items - The file items to display.
237
- * @param {boolean} update - Whether to update the tags.
238
- */
239
- _drawListItem(items, update) {
240
- const keyword = this.keyword;
241
- items = this.tagfilter(items).filter((item) => item.name.toLowerCase().indexOf(keyword) > -1);
242
-
243
- const _tags = [];
244
- const len = items.length;
245
- const columnSize = this.columnSize;
246
- const splitSize = columnSize <= 1 ? 1 : Math.round(len / columnSize) || 1;
247
- const drawItemHandler = this.drawItemHandler;
248
-
249
- let tagsHTML = '';
250
- let listHTML = '<div class="se-file-item-column">';
251
- let columns = 1;
252
- for (let i = 0, item, tags; i < len; i++) {
253
- item = items[i];
254
- tags = !item.tag ? [] : typeof item.tag === 'string' ? item.tag.split(',') : item.tag;
255
- tags = item.tag = tags.map((v) => v.trim());
256
- listHTML += drawItemHandler(item);
257
-
258
- if ((i + 1) % splitSize === 0 && columns < columnSize && i + 1 < len) {
259
- columns++;
260
- listHTML += '</div><div class="se-file-item-column">';
261
- }
262
-
263
- if (update && tags.length > 0) {
264
- for (let t = 0, tLen = tags.length, tag; t < tLen; t++) {
265
- tag = tags[t];
266
- if (tag && !_tags.includes(tag)) {
267
- _tags.push(tag);
268
- tagsHTML += `<a title="${tag}" aria-label="${tag}">${tag}</a>`;
269
- }
270
- }
271
- }
272
- }
273
- listHTML += '</div>';
274
-
275
- this.list.innerHTML = listHTML;
276
-
277
- if (update) {
278
- this.items = items;
279
- this.tagArea.innerHTML = tagsHTML;
280
- }
281
- }
282
-
283
- /**
284
- * @private
285
- * @description Adds a global event listener for closing the browser.
286
- */
287
- __addGlobalEvent() {
288
- this.__removeGlobalEvent();
289
- this._bindClose = this.eventManager.addGlobalEvent('keydown', this.__globalEventHandler, true);
290
- }
291
-
292
- /**
293
- * @private
294
- * @description Removes the global event listener for closing the browser.
295
- */
296
- __removeGlobalEvent() {
297
- if (this._bindClose) this._bindClose = this.eventManager.removeGlobalEvent(this._bindClose);
298
- }
299
-
300
- /**
301
- * @private
302
- * @description Renders the file items or folder structure from data.
303
- * @param {BrowserFile[]|BrowserFile} data - The data representing the file structure.
304
- * @returns {boolean} True if rendering was successful, false otherwise.
305
- */
306
- __drowItems(data) {
307
- if (Array.isArray(data)) {
308
- if (data.length > 0) {
309
- this._drawListItem(data, true);
310
- }
311
- return true;
312
- } else if (typeof data === 'object') {
313
- this.sideOpenBtn.style.display = '';
314
- this.__parseFolderData(data);
315
-
316
- this.side.innerHTML = '';
317
- const sideInner = (this.sideInner = dom.utils.createElement('div', null));
318
- this.__createFolderList(this.tree, sideInner);
319
- this.side.appendChild(sideInner);
320
-
321
- if (this.folderDefaultPath) {
322
- const openFolder = /** @type {HTMLButtonElement} */ (sideInner.querySelector(`[data-command="${this.folderDefaultPath}"]`));
323
- openFolder.click();
324
- if (this.folderDefaultPath.includes('/')) {
325
- dom.utils.removeClass(openFolder.parentElement, 'se-menu-hidden');
326
- openFolder.parentElement.previousElementSibling.querySelector('button').innerHTML = this.openArrow;
327
- }
328
- }
329
-
330
- return true;
331
- }
332
- return false;
333
- }
334
-
335
- /**
336
- * @private
337
- * @description Parses folder data into a structured format.
338
- * @param {BrowserFile} data - The folder data.
339
- * @param {string} [path] - The current path in the folder hierarchy.
340
- */
341
- __parseFolderData(data, path) {
342
- let current = this.tree;
343
-
344
- // _data
345
- if (data._data) {
346
- this.data[path] = data._data;
347
- if (!this.folderDefaultPath || data.default) {
348
- this.folderDefaultPath = path;
349
- }
350
-
351
- const parts = path.split('/');
352
- const len = parts.length - 1;
353
- parts.forEach((part, index) => {
354
- if (!current[part]) {
355
- current[part] = { children: {} };
356
- }
357
-
358
- if (index === len) {
359
- current[part].key = path;
360
- current[part].name = this.folders[path].name;
361
- } else {
362
- current = current[part].children;
363
- }
364
- });
365
- } else if (path) {
366
- current[path] = { name: this.folders[path].name, children: {} };
367
- }
368
-
369
- // create folders, file path
370
- Object.entries(data).forEach(([key, value]) => {
371
- if (key === '_data' || !value || typeof value !== 'object') return;
372
-
373
- const v = /** @type {BrowserFile} */ (value);
374
- const currentPath = path ? `${path}/${key}` : key;
375
-
376
- this.folders[currentPath] = {
377
- name: v.name || key,
378
- meta: v.meta || {}
379
- };
380
-
381
- this.__parseFolderData(v, currentPath);
382
- });
383
- }
384
-
385
- /**
386
- * @private
387
- * @description Creates a nested folder list from parsed data.
388
- * @param {BrowserFile[]|BrowserFile} folderData - The structured folder data.
389
- * @param {HTMLElement} parentElement - The parent element to append folder structure to.
390
- */
391
- __createFolderList(folderData, parentElement) {
392
- for (const key in folderData) {
393
- const item = folderData[key];
394
- if (!item) continue;
395
-
396
- if (Object.keys(item.children).length > 0) {
397
- const folderLabel = dom.utils.createElement(
398
- 'div',
399
- item.key ? { 'data-command': item.key, 'aria-label': item.name } : null,
400
- `<span class="se-menu-icon">${item.key ? this.icon_folder : this.icon_folder_item}</span><span>${item.name}</span>`
401
- );
402
- const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel);
403
-
404
- folderLabel.insertBefore(dom.utils.createElement('button', null, this.closeArrow), folderLabel.firstElementChild);
405
- const childContainer = document.createElement('div');
406
- dom.utils.addClass(childContainer, 'se-menu-child|se-menu-hidden');
407
- this.__createFolderList(item.children, childContainer);
408
- folderDiv.appendChild(childContainer);
409
-
410
- parentElement.appendChild(folderDiv);
411
- } else {
412
- const folderLabel = dom.utils.createElement('div', { 'data-command': item.key, 'aria-label': item.name, class: 'se-menu-folder-item' }, `<span class="se-menu-icon">${this.icon_item}</span><span>${item.name}</span>`);
413
- if (parentElement === this.sideInner) {
414
- const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel);
415
- parentElement.appendChild(folderDiv);
416
- } else {
417
- parentElement.appendChild(folderLabel);
418
- }
419
- }
420
- }
421
- }
422
-
423
- /**
424
- * @param {XMLHttpRequest} xmlHttp - XMLHttpRequest object.
425
- */
426
- #CallBackGet(xmlHttp) {
427
- try {
428
- const res = JSON.parse(xmlHttp.responseText);
429
- const data = res.result;
430
- if (this.__drowItems(data)) return;
431
-
432
- if (res.nullMessage) {
433
- this.list.innerHTML = res.nullMessage;
434
- }
435
- } catch (e) {
436
- throw Error(`[SUNEDITOR.browser.drawList.fail] cause: "${e.message}"`);
437
- } finally {
438
- this.closeBrowserLoading();
439
- this.body.style.maxHeight = dom.utils.getClientSize().h - this.header.offsetHeight - 50 + 'px';
440
- }
441
- }
442
-
443
- /**
444
- * @param {*} res - response data.
445
- * @param {XMLHttpRequest} xmlHttp - XMLHttpRequest object.
446
- */
447
- #CallBackError(res, xmlHttp) {
448
- this.closeBrowserLoading();
449
- throw Error(`[SUNEDITOR.browser.get.serverException] status: ${xmlHttp.status}, response: ${res.errorMessage || xmlHttp.responseText}`);
450
- }
451
-
452
- /**
453
- * @param {MouseEvent} e - Event object
454
- */
455
- #OnClickTag(e) {
456
- const eventTarget = dom.query.getEventTarget(e);
457
- if (!dom.check.isAnchor(eventTarget)) return;
458
-
459
- const tagName = eventTarget.textContent;
460
- const selectTag = this.tagArea.querySelector('a[title="' + tagName + '"]');
461
- const sTagIndex = this.selectedTags.indexOf(tagName);
462
-
463
- if (sTagIndex > -1) {
464
- this.selectedTags.splice(sTagIndex, 1);
465
- dom.utils.removeClass(selectTag, 'on');
466
- } else {
467
- this.selectedTags.push(tagName);
468
- dom.utils.addClass(selectTag, 'on');
469
- }
470
-
471
- this._drawListItem(this.items, false);
472
- }
473
-
474
- /**
475
- * @param {MouseEvent} e - Event object
476
- */
477
- #OnClickFile(e) {
478
- const eventTarget = dom.query.getEventTarget(e);
479
-
480
- e.preventDefault();
481
- e.stopPropagation();
482
-
483
- if (eventTarget === this.list) return;
484
-
485
- const target = dom.query.getCommandTarget(eventTarget);
486
- if (!target) return;
487
-
488
- this.close();
489
- this.selectorHandler(target);
490
- }
491
-
492
- /**
493
- * @param {MouseEvent} e - Event object
494
- */
495
- #OnClickSide(e) {
496
- const eventTarget = dom.query.getEventTarget(e);
497
- e.stopPropagation();
498
-
499
- if (/^button$/i.test(eventTarget.nodeName)) {
500
- const childContainer = eventTarget.parentElement.parentElement.querySelector('.se-menu-child');
501
- if (dom.utils.hasClass(childContainer, 'se-menu-hidden')) {
502
- dom.utils.removeClass(childContainer, 'se-menu-hidden');
503
- eventTarget.innerHTML = this.openArrow;
504
- } else {
505
- dom.utils.addClass(childContainer, 'se-menu-hidden');
506
- eventTarget.innerHTML = this.closeArrow;
507
- }
508
- return;
509
- }
510
-
511
- const cmdTarget = dom.query.getCommandTarget(eventTarget);
512
- if (!cmdTarget || dom.utils.hasClass(cmdTarget, 'active')) return;
513
-
514
- const data = this.data[cmdTarget.getAttribute('data-command')];
515
-
516
- dom.utils.removeClass(this.side.querySelectorAll('.active'), 'active');
517
- dom.utils.addClass([cmdTarget, dom.query.getParentElement(cmdTarget, '.se-menu-folder')], 'active');
518
- this.tagArea.innerHTML = '';
519
-
520
- if (typeof data === 'string') {
521
- this._drawFileList(data, this.urlHeader, true);
522
- } else {
523
- this._drawListItem(data, false);
524
- }
525
- }
526
-
527
- /**
528
- * @param {MouseEvent} e - Event object
529
- */
530
- #OnMouseDown_browser(e) {
531
- const eventTarget = dom.query.getEventTarget(e);
532
- if (/se-browser-inner/.test(eventTarget.className)) {
533
- this._closeSignal = true;
534
- } else {
535
- this._closeSignal = false;
536
- }
537
- }
538
-
539
- /**
540
- * @param {MouseEvent} e - Event object
541
- */
542
- #OnClick_browser(e) {
543
- const eventTarget = dom.query.getEventTarget(e);
544
- e.stopPropagation();
545
-
546
- if (/close/.test(eventTarget.getAttribute('data-command')) || this._closeSignal) {
547
- this.close();
548
- }
549
- }
550
-
551
- /**
552
- * @param {SubmitEvent} e - Event object
553
- */
554
- #Search(e) {
555
- const eventTarget = /** @type {HTMLElement} */ (e.currentTarget);
556
- e.preventDefault();
557
- this.search(/** @type {HTMLInputElement} */ (eventTarget.querySelector('input[type="text"]')).value);
558
- }
559
-
560
- /**
561
- * @param {MouseEvent} e - Event object
562
- */
563
- #SideOpen(e) {
564
- const eventTarget = dom.query.getEventTarget(e);
565
- if (dom.utils.hasClass(eventTarget, 'active')) {
566
- dom.utils.removeClass(this.side, 'se-side-show');
567
- dom.utils.removeClass(eventTarget, 'active');
568
- } else {
569
- dom.utils.addClass(this.side, 'se-side-show');
570
- dom.utils.addClass(eventTarget, 'active');
571
- }
572
- }
573
-
574
- /**
575
- * @param {MouseEvent} e - Event object
576
- */
577
- #SideClose({ target }) {
578
- if (target === this.sideOpenBtn) return;
579
- if (dom.utils.hasClass(this.sideOpenBtn, 'active')) {
580
- dom.utils.removeClass(this.side, 'se-side-show');
581
- dom.utils.removeClass(this.sideOpenBtn, 'active');
582
- }
583
- }
584
- }
585
-
586
- /**
587
- * @private
588
- * @param {__se__EditorCore} editor - editor instance
589
- * @param {boolean} useSearch - Whether to use the search function
590
- * @returns {{ html: HTMLElement, header: HTMLElement, titleArea: HTMLElement, tagArea: HTMLElement, body: HTMLElement, list: HTMLElement, side: HTMLElement, wrapper: HTMLElement, _loading: HTMLElement }} HTML
591
- */
592
- function CreateHTMLInfos(editor, useSearch) {
593
- const lang = editor.lang;
594
- const icons = editor.icons;
595
- const htmlString = /*html*/ `
596
- <div class="se-browser-content">
597
- <div class="se-browser-header">
598
- <button type="button" data-command="close" class="se-btn se-browser-close" class="close" title="${lang.close}" aria-label="${lang.close}">
599
- ${icons.cancel}
600
- </button>
601
- <span class="se-browser-title"></span>
602
- </div>
603
- <div class="se-browser-wrapper">
604
- <div class="se-browser-side"></div>
605
- <div class="se-browser-main">
606
- <div class="se-browser-bar">
607
- <div class="se-browser-search">
608
- <button class="se-btn se-side-open-btn">${icons.side_menu_hamburger}</button>
609
- ${
610
- useSearch
611
- ? /*html*/ `
612
- <form class="se-browser-search-form">
613
- <input type="text" class="se-input-form" placeholder="${lang.search}" aria-label="${lang.search}">
614
- <button type="submit" class="se-btn" title="${lang.search}" aria-label="${lang.search}">${icons.search}</button>
615
- </form>`
616
- : ''
617
- }
618
- </div>
619
- </div>
620
- <div class="se-browser-body">
621
- <div class="se-browser-tags"></div>
622
- <div class="se-loading-box sun-editor-common"><div class="se-loading-effect"></div></div>
623
- <div class="se-browser-menus"></div>
624
- <div class="se-browser-list"></div>
625
- </div>
626
- </div>
627
- </div>
628
- </div>`;
629
-
630
- const content = dom.utils.createElement('DIV', { class: 'se-browser-inner' }, htmlString);
631
-
632
- return {
633
- html: content,
634
- header: content.querySelector('.se-browser-header'),
635
- titleArea: content.querySelector('.se-browser-title'),
636
- tagArea: content.querySelector('.se-browser-tags'),
637
- body: content.querySelector('.se-browser-body'),
638
- list: content.querySelector('.se-browser-list'),
639
- side: content.querySelector('.se-browser-side'),
640
- wrapper: content.querySelector('.se-browser-wrapper'),
641
- _loading: content.querySelector('.se-loading-box')
642
- };
643
- }
644
-
645
- /**
646
- * @private
647
- * @this {{ thumbnail: ((...args: *) => *), props: Array<*> }}
648
- * @description Define the HTML of the item to be put in "div.se-file-item-column".
649
- * - Format: [ { src: "image src", name: "name(@option)", alt: "image alt(@option)", tag: "tag name(@option)" } ]
650
- * @param {BrowserFile} item Item of the response data's array
651
- */
652
- function DrawItems(item) {
653
- const srcName = item.src.split('/').pop();
654
- const thumbnail = item.thumbnail || '';
655
- const src = thumbnail || item.src;
656
- const customProps = this.props?.map((v) => `data-${v}="${item[v]}"`).join(' ') || '';
657
- const attrs = `data-type="${item.type}" data-command="${item.src}" data-name="${item.name || srcName}" data-thumbnail="${thumbnail}" data-extension="${item.src.split('.').pop()}" ${customProps}`;
658
- const props = `class="${thumbnail || 'se-browser-empty-image'}" src="${src}" alt="${item.alt || srcName}" ${attrs}`;
659
- return /*html*/ `
660
- <div class="se-file-item-img">
661
- ${this.thumbnail && !thumbnail && item.type !== 'image' ? `<div class="se-browser-empty-thumbnail" ${props}>${this.thumbnail(item)}</div>` : `<img class="${thumbnail || 'se-browser-empty-image'}" ${props}>`}
662
- <div class="se-file-name-image se-file-name-back"></div>
663
- <div class="se-file-name-image">${item.name || srcName}</div>
664
- </div>`;
665
- }
666
-
667
- export default Browser;
1
+ import CoreInjector from '../editorInjector/_core';
2
+ import { dom, keyCodeMap } from '../helper';
3
+ import { _w } from '../helper/env';
4
+ import ApiManager from './ApiManager';
5
+
6
+ /**
7
+ * @typedef {Object} BrowserFile
8
+ * @property {string} [src=""] - Source url
9
+ * @property {string} [name=""] - File name | Folder name
10
+ * @property {string=} thumbnail - Thumbnail url
11
+ * @property {string=} alt - Image alt
12
+ * @property {Array<string>|string=} tag - Tag name list
13
+ * @property {string=} type - Type (image, video, audio, etc.)
14
+ * @property {string=} frame - Frame name (iframe, video, etc.)
15
+ * @property {BrowserFile | string=} _data - The folder's contents or an API URL.
16
+ * @property {boolean=} default - Whether this folder is the default selection.
17
+ * @property {Object<string, *>=} meta - Metadata
18
+ */
19
+
20
+ /**
21
+ * @typedef BrowserParams
22
+ * @property {string} title - File browser window title. Required. Can be overridden in browser.
23
+ * @property {string=} className - Class name of the file browser. Optional. Default: ''.
24
+ * @property {Object<string, *>|Array<*>=} data - direct data without server calls
25
+ * @property {string=} url - File server url. Required. Can be overridden in browser.
26
+ * @property {Object<string, string>=} headers - File server http header. Required. Can be overridden in browser.
27
+ * @property {(target: Node) => void} selectorHandler - Function that actions when an item is clicked. Required. Can be overridden in browser.
28
+ * @property {boolean=} useSearch - Whether to use the search function. Optional. Default: true.
29
+ * @property {string=} searchUrl - File server search url. Optional. Can be overridden in browser.
30
+ * @property {Object<string, string>=} searchUrlHeader - File server search http header. Optional. Can be overridden in browser.
31
+ * @property {string=} listClass - Class name of list div. Required. Can be overridden in browser.
32
+ * @property {(item: BrowserFile) => string=} drawItemHandler - Function that defines the HTML of a file item. Required. Can be overridden in browser.
33
+ * @property {Array<*>=} props - "props" argument to "drawItemHandler" function. Optional. Can be overridden in browser.
34
+ * @property {number=} columnSize - Number of "div.se-file-item-column" to be created. Optional. Can be overridden in browser. Default: 4.
35
+ * @property {((item: BrowserFile) => string)=} thumbnail - Default thumbnail
36
+ */
37
+
38
+ /**
39
+ * @class
40
+ * @description File browser plugin
41
+ */
42
+ class Browser extends CoreInjector {
43
+ /**
44
+ * @constructor
45
+ * @param {*} inst The instance object that called the constructor.
46
+ * @param {BrowserParams} params Browser options
47
+ */
48
+ constructor(inst, params) {
49
+ super(inst.editor);
50
+
51
+ // create HTML
52
+ this.useSearch = params.useSearch ?? true;
53
+ const browserFrame = dom.utils.createElement('DIV', { class: 'se-browser sun-editor-common' + (params.className ? ` ${params.className}` : '') });
54
+ const contentHTML = CreateHTMLInfos(inst.editor, this.useSearch);
55
+ const content = contentHTML.html;
56
+
57
+ // members
58
+ this.kind = inst.constructor.key || inst.constructor.name;
59
+ this.inst = inst;
60
+ this.area = browserFrame;
61
+ this.header = contentHTML.header;
62
+ this.titleArea = contentHTML.titleArea;
63
+ this.tagArea = contentHTML.tagArea;
64
+ this.body = contentHTML.body;
65
+ this.list = contentHTML.list;
66
+ this.side = contentHTML.side;
67
+ this.wrapper = contentHTML.wrapper;
68
+ this._loading = contentHTML._loading;
69
+
70
+ this.title = params.title;
71
+ this.listClass = params.listClass || 'se-preview-list';
72
+ this.directData = params.data;
73
+ this.url = params.url;
74
+ this.urlHeader = params.headers;
75
+ this.searchUrl = params.searchUrl;
76
+ this.searchUrlHeader = params.searchUrlHeader;
77
+ this.drawItemHandler = (params.drawItemHandler || DrawItems).bind({ thumbnail: params.thumbnail, props: params.props || [] });
78
+ this.selectorHandler = params.selectorHandler;
79
+ this.columnSize = params.columnSize || 4;
80
+ this.folderDefaultPath = '';
81
+ this.closeArrow = this.icons.menu_arrow_right;
82
+ this.openArrow = this.icons.menu_arrow_down;
83
+ this.icon_folder = this.icons.side_menu_folder_item;
84
+ this.icon_folder_item = this.icons.side_menu_folder;
85
+ this.icon_item = this.icons.side_menu_item;
86
+
87
+ /**
88
+ * @type {Array<BrowserFile>}
89
+ */
90
+ this.items = [];
91
+ /**
92
+ * @type {Object<string, {name: string, meta: Object<string, *>}>}
93
+ */
94
+ this.folders = {};
95
+ /**
96
+ * @type {Object<string, {key?: string, name?: string, children?: *}>}
97
+ */
98
+ this.tree = {};
99
+ /**
100
+ * @type {BrowserFile}
101
+ */
102
+ this.data = {};
103
+ this.selectedTags = [];
104
+ this.keyword = '';
105
+ this.sideInner = null;
106
+ this._closeSignal = false;
107
+ this._bindClose = null;
108
+ this.__globalEventHandler = (e) => {
109
+ if (!keyCodeMap.isEsc(e.code)) return;
110
+ this.close();
111
+ };
112
+ // api manager
113
+ this.apiManager = new ApiManager(this, { method: 'GET' });
114
+
115
+ // init
116
+ browserFrame.appendChild(dom.utils.createElement('DIV', { class: 'se-browser-back' }));
117
+ browserFrame.appendChild(content);
118
+ this.carrierWrapper.appendChild(browserFrame);
119
+
120
+ this.eventManager.addEvent(this.tagArea, 'click', this.#OnClickTag.bind(this));
121
+ this.eventManager.addEvent(this.list, 'click', this.#OnClickFile.bind(this));
122
+ this.eventManager.addEvent(this.side, 'click', this.#OnClickSide.bind(this));
123
+ this.eventManager.addEvent(content, 'mousedown', this.#OnMouseDown_browser.bind(this));
124
+ this.eventManager.addEvent(content, 'click', this.#OnClick_browser.bind(this));
125
+ this.eventManager.addEvent(browserFrame.querySelector('form.se-browser-search-form'), 'submit', this.#Search.bind(this));
126
+ this.eventManager.addEvent((this.sideOpenBtn = /** @type {HTMLButtonElement} */ (browserFrame.querySelector('.se-side-open-btn'))), 'click', this.#SideOpen.bind(this));
127
+ this.eventManager.addEvent([this.header, browserFrame.querySelector('.se-browser-main')], 'mousedown', this.#SideClose.bind(this));
128
+ }
129
+
130
+ /**
131
+ * @description Open a file browser plugin
132
+ * @param {Object} [params={}]
133
+ * @param {string=} params.listClass - Class name of list div. If not, use "this.listClass".
134
+ * @param {string=} params.title - File browser window title. If not, use "this.title".
135
+ * @param {string=} params.url - File server url. If not, use "this.url".
136
+ * @param {Object<string, string>=} params.urlHeader - File server http header. If not, use "this.urlHeader".
137
+ */
138
+ open(params) {
139
+ if (!params) params = {};
140
+ this.__addGlobalEvent();
141
+
142
+ const listClassName = params.listClass || this.listClass;
143
+ if (!dom.utils.hasClass(this.list, listClassName)) {
144
+ this.list.className = 'se-browser-list ' + listClassName;
145
+ }
146
+
147
+ this.titleArea.textContent = params.title || this.title;
148
+ this.area.style.display = 'block';
149
+ this.editor.opendBrowser = this;
150
+ this.closeArrow = this.options.get('_rtl') ? this.icons.menu_arrow_left : this.icons.menu_arrow_right;
151
+
152
+ if (this.directData) {
153
+ this.__drowItems(this.directData);
154
+ } else {
155
+ this._drawFileList(params.url || this.url, params.urlHeader || this.urlHeader, false);
156
+ }
157
+
158
+ this.body.style.maxHeight = dom.utils.getClientSize().h - (this.editor.offset.getGlobal(this.body).top - _w.scrollY) - 20 + 'px';
159
+ }
160
+
161
+ /**
162
+ * @description Close a browser plugin
163
+ * - The plugin's "init" method is called.
164
+ */
165
+ close() {
166
+ this.__removeGlobalEvent();
167
+ this.apiManager.cancel();
168
+
169
+ this.area.style.display = 'none';
170
+ this.selectedTags = [];
171
+ this.items = [];
172
+ this.folders = {};
173
+ this.tree = {};
174
+ this.data = {};
175
+ this.keyword = '';
176
+ this.list.innerHTML = this.tagArea.innerHTML = this.titleArea.textContent = '';
177
+ this.editor.opendBrowser = null;
178
+ this.sideInner = null;
179
+
180
+ if (typeof this.inst.init === 'function') this.inst.init();
181
+ }
182
+
183
+ /**
184
+ * @description Search files
185
+ * @param {string} keyword - Search keyword
186
+ */
187
+ search(keyword) {
188
+ if (this.searchUrl) {
189
+ this.keyword = keyword;
190
+ this._drawFileList(this.searchUrl + '?keyword=' + keyword, this.searchUrlHeader, false);
191
+ } else {
192
+ this.keyword = keyword.toLowerCase();
193
+ this._drawListItem(this.items, false);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * @description Filter items by tag
199
+ * @param {Array<BrowserFile>} items - Items to filter
200
+ * @returns {Array<BrowserFile>}
201
+ */
202
+ tagfilter(items) {
203
+ const selectedTags = this.selectedTags;
204
+ return selectedTags.length === 0 ? items : items.filter((item) => !Array.isArray(item.tag) || item.tag.some((tag) => selectedTags.includes(tag)));
205
+ }
206
+
207
+ /**
208
+ * @description Show file browser loading box
209
+ */
210
+ showBrowserLoading() {
211
+ this._loading.style.display = 'block';
212
+ }
213
+
214
+ /**
215
+ * @description Close file browser loading box
216
+ */
217
+ closeBrowserLoading() {
218
+ this._loading.style.display = 'none';
219
+ }
220
+
221
+ /**
222
+ * @private
223
+ * @description Fetches the file list from the server.
224
+ * @param {string} url - The file server URL.
225
+ * @param {Object<string, string>} urlHeader - The HTTP headers for the request.
226
+ * @param {boolean} pageLoading - Indicates if this is a paginated request.
227
+ */
228
+ _drawFileList(url, urlHeader, pageLoading) {
229
+ this.apiManager.call({ method: 'GET', url, headers: urlHeader, callBack: this.#CallBackGet.bind(this), errorCallBack: this.#CallBackError.bind(this) });
230
+ if (!pageLoading) {
231
+ this.sideOpenBtn.style.display = 'none';
232
+ this.showBrowserLoading();
233
+ }
234
+ }
235
+
236
+ /**
237
+ * @private
238
+ * @description Updates the displayed list of file items.
239
+ * @param {Array<BrowserFile>} items - The file items to display.
240
+ * @param {boolean} update - Whether to update the tags.
241
+ */
242
+ _drawListItem(items, update) {
243
+ const keyword = this.keyword;
244
+ items = this.tagfilter(items).filter((item) => item.name.toLowerCase().indexOf(keyword) > -1);
245
+
246
+ const _tags = [];
247
+ const len = items.length;
248
+ const columnSize = this.columnSize;
249
+ const splitSize = columnSize <= 1 ? 1 : Math.round(len / columnSize) || 1;
250
+ const drawItemHandler = this.drawItemHandler;
251
+
252
+ let tagsHTML = '';
253
+ let listHTML = '<div class="se-file-item-column">';
254
+ let columns = 1;
255
+ for (let i = 0, item, tags; i < len; i++) {
256
+ item = items[i];
257
+ tags = !item.tag ? [] : typeof item.tag === 'string' ? item.tag.split(',') : item.tag;
258
+ tags = item.tag = tags.map((v) => v.trim());
259
+ listHTML += drawItemHandler(item);
260
+
261
+ if ((i + 1) % splitSize === 0 && columns < columnSize && i + 1 < len) {
262
+ columns++;
263
+ listHTML += '</div><div class="se-file-item-column">';
264
+ }
265
+
266
+ if (update && tags.length > 0) {
267
+ for (let t = 0, tLen = tags.length, tag; t < tLen; t++) {
268
+ tag = tags[t];
269
+ if (tag && !_tags.includes(tag)) {
270
+ _tags.push(tag);
271
+ tagsHTML += `<a title="${tag}" aria-label="${tag}">${tag}</a>`;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ listHTML += '</div>';
277
+
278
+ this.list.innerHTML = listHTML;
279
+
280
+ if (update) {
281
+ this.items = items;
282
+ this.tagArea.innerHTML = tagsHTML;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * @private
288
+ * @description Adds a global event listener for closing the browser.
289
+ */
290
+ __addGlobalEvent() {
291
+ this.__removeGlobalEvent();
292
+ this._bindClose = this.eventManager.addGlobalEvent('keydown', this.__globalEventHandler, true);
293
+ }
294
+
295
+ /**
296
+ * @private
297
+ * @description Removes the global event listener for closing the browser.
298
+ */
299
+ __removeGlobalEvent() {
300
+ if (this._bindClose) this._bindClose = this.eventManager.removeGlobalEvent(this._bindClose);
301
+ }
302
+
303
+ /**
304
+ * @private
305
+ * @description Renders the file items or folder structure from data.
306
+ * @param {BrowserFile[]|BrowserFile} data - The data representing the file structure.
307
+ * @returns {boolean} True if rendering was successful, false otherwise.
308
+ */
309
+ __drowItems(data) {
310
+ if (Array.isArray(data)) {
311
+ if (data.length > 0) {
312
+ this._drawListItem(data, true);
313
+ }
314
+ return true;
315
+ } else if (typeof data === 'object') {
316
+ this.sideOpenBtn.style.display = '';
317
+ this.__parseFolderData(data);
318
+
319
+ this.side.innerHTML = '';
320
+ const sideInner = (this.sideInner = dom.utils.createElement('div', null));
321
+ this.__createFolderList(this.tree, sideInner);
322
+ this.side.appendChild(sideInner);
323
+
324
+ if (this.folderDefaultPath) {
325
+ const openFolder = /** @type {HTMLButtonElement} */ (sideInner.querySelector(`[data-command="${this.folderDefaultPath}"]`));
326
+ openFolder.click();
327
+ if (this.folderDefaultPath.includes('/')) {
328
+ dom.utils.removeClass(openFolder.parentElement, 'se-menu-hidden');
329
+ openFolder.parentElement.previousElementSibling.querySelector('button').innerHTML = this.openArrow;
330
+ }
331
+ }
332
+
333
+ return true;
334
+ }
335
+ return false;
336
+ }
337
+
338
+ /**
339
+ * @private
340
+ * @description Parses folder data into a structured format.
341
+ * @param {BrowserFile} data - The folder data.
342
+ * @param {string} [path] - The current path in the folder hierarchy.
343
+ */
344
+ __parseFolderData(data, path) {
345
+ let current = this.tree;
346
+
347
+ // _data
348
+ if (data._data) {
349
+ this.data[path] = data._data;
350
+ if (!this.folderDefaultPath || data.default) {
351
+ this.folderDefaultPath = path;
352
+ }
353
+
354
+ const parts = path.split('/');
355
+ const len = parts.length - 1;
356
+ parts.forEach((part, index) => {
357
+ if (!current[part]) {
358
+ current[part] = { children: {} };
359
+ }
360
+
361
+ if (index === len) {
362
+ current[part].key = path;
363
+ current[part].name = this.folders[path].name;
364
+ } else {
365
+ current = current[part].children;
366
+ }
367
+ });
368
+ } else if (path) {
369
+ current[path] = { name: this.folders[path].name, children: {} };
370
+ }
371
+
372
+ // create folders, file path
373
+ Object.entries(data).forEach(([key, value]) => {
374
+ if (key === '_data' || !value || typeof value !== 'object') return;
375
+
376
+ const v = /** @type {BrowserFile} */ (value);
377
+ const currentPath = path ? `${path}/${key}` : key;
378
+
379
+ this.folders[currentPath] = {
380
+ name: v.name || key,
381
+ meta: v.meta || {}
382
+ };
383
+
384
+ this.__parseFolderData(v, currentPath);
385
+ });
386
+ }
387
+
388
+ /**
389
+ * @private
390
+ * @description Creates a nested folder list from parsed data.
391
+ * @param {BrowserFile[]|BrowserFile} folderData - The structured folder data.
392
+ * @param {HTMLElement} parentElement - The parent element to append folder structure to.
393
+ */
394
+ __createFolderList(folderData, parentElement) {
395
+ for (const key in folderData) {
396
+ const item = folderData[key];
397
+ if (!item) continue;
398
+
399
+ if (Object.keys(item.children).length > 0) {
400
+ const folderLabel = dom.utils.createElement(
401
+ 'div',
402
+ item.key ? { 'data-command': item.key, 'aria-label': item.name } : null,
403
+ `<span class="se-menu-icon">${item.key ? this.icon_folder : this.icon_folder_item}</span><span>${item.name}</span>`
404
+ );
405
+ const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel);
406
+
407
+ folderLabel.insertBefore(dom.utils.createElement('button', null, this.closeArrow), folderLabel.firstElementChild);
408
+ const childContainer = document.createElement('div');
409
+ dom.utils.addClass(childContainer, 'se-menu-child|se-menu-hidden');
410
+ this.__createFolderList(item.children, childContainer);
411
+ folderDiv.appendChild(childContainer);
412
+
413
+ parentElement.appendChild(folderDiv);
414
+ } else {
415
+ const folderLabel = dom.utils.createElement('div', { 'data-command': item.key, 'aria-label': item.name, class: 'se-menu-folder-item' }, `<span class="se-menu-icon">${this.icon_item}</span><span>${item.name}</span>`);
416
+ if (parentElement === this.sideInner) {
417
+ const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel);
418
+ parentElement.appendChild(folderDiv);
419
+ } else {
420
+ parentElement.appendChild(folderLabel);
421
+ }
422
+ }
423
+ }
424
+ }
425
+
426
+ /**
427
+ * @param {XMLHttpRequest} xmlHttp - XMLHttpRequest object.
428
+ */
429
+ #CallBackGet(xmlHttp) {
430
+ try {
431
+ const res = JSON.parse(xmlHttp.responseText);
432
+ const data = res.result;
433
+ if (this.__drowItems(data)) return;
434
+
435
+ if (res.nullMessage) {
436
+ this.list.innerHTML = res.nullMessage;
437
+ }
438
+ } catch (e) {
439
+ throw Error(`[SUNEDITOR.browser.drawList.fail] cause: "${e.message}"`);
440
+ } finally {
441
+ this.closeBrowserLoading();
442
+ }
443
+ }
444
+
445
+ /**
446
+ * @param {*} res - response data.
447
+ * @param {XMLHttpRequest} xmlHttp - XMLHttpRequest object.
448
+ */
449
+ #CallBackError(res, xmlHttp) {
450
+ this.closeBrowserLoading();
451
+ throw Error(`[SUNEDITOR.browser.get.serverException] status: ${xmlHttp.status}, response: ${res.errorMessage || xmlHttp.responseText}`);
452
+ }
453
+
454
+ /**
455
+ * @param {MouseEvent} e - Event object
456
+ */
457
+ #OnClickTag(e) {
458
+ const eventTarget = dom.query.getEventTarget(e);
459
+ if (!dom.check.isAnchor(eventTarget)) return;
460
+
461
+ const tagName = eventTarget.textContent;
462
+ const selectTag = this.tagArea.querySelector('a[title="' + tagName + '"]');
463
+ const sTagIndex = this.selectedTags.indexOf(tagName);
464
+
465
+ if (sTagIndex > -1) {
466
+ this.selectedTags.splice(sTagIndex, 1);
467
+ dom.utils.removeClass(selectTag, 'on');
468
+ } else {
469
+ this.selectedTags.push(tagName);
470
+ dom.utils.addClass(selectTag, 'on');
471
+ }
472
+
473
+ this._drawListItem(this.items, false);
474
+ }
475
+
476
+ /**
477
+ * @param {MouseEvent} e - Event object
478
+ */
479
+ #OnClickFile(e) {
480
+ const eventTarget = dom.query.getEventTarget(e);
481
+
482
+ e.preventDefault();
483
+ e.stopPropagation();
484
+
485
+ if (eventTarget === this.list) return;
486
+
487
+ const target = dom.query.getCommandTarget(eventTarget);
488
+ if (!target) return;
489
+
490
+ this.close();
491
+ this.selectorHandler(target);
492
+ }
493
+
494
+ /**
495
+ * @param {MouseEvent} e - Event object
496
+ */
497
+ #OnClickSide(e) {
498
+ const eventTarget = dom.query.getEventTarget(e);
499
+ e.stopPropagation();
500
+
501
+ if (/^button$/i.test(eventTarget.nodeName)) {
502
+ const childContainer = eventTarget.parentElement.parentElement.querySelector('.se-menu-child');
503
+ if (dom.utils.hasClass(childContainer, 'se-menu-hidden')) {
504
+ dom.utils.removeClass(childContainer, 'se-menu-hidden');
505
+ eventTarget.innerHTML = this.openArrow;
506
+ } else {
507
+ dom.utils.addClass(childContainer, 'se-menu-hidden');
508
+ eventTarget.innerHTML = this.closeArrow;
509
+ }
510
+ return;
511
+ }
512
+
513
+ const cmdTarget = dom.query.getCommandTarget(eventTarget);
514
+ if (!cmdTarget || dom.utils.hasClass(cmdTarget, 'active')) return;
515
+
516
+ const data = this.data[cmdTarget.getAttribute('data-command')];
517
+
518
+ dom.utils.removeClass(this.side.querySelectorAll('.active'), 'active');
519
+ dom.utils.addClass([cmdTarget, dom.query.getParentElement(cmdTarget, '.se-menu-folder')], 'active');
520
+ this.tagArea.innerHTML = '';
521
+
522
+ if (typeof data === 'string') {
523
+ this._drawFileList(data, this.urlHeader, true);
524
+ } else {
525
+ this._drawListItem(data, false);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * @param {MouseEvent} e - Event object
531
+ */
532
+ #OnMouseDown_browser(e) {
533
+ const eventTarget = dom.query.getEventTarget(e);
534
+ if (/se-browser-inner/.test(eventTarget.className)) {
535
+ this._closeSignal = true;
536
+ } else {
537
+ this._closeSignal = false;
538
+ }
539
+ }
540
+
541
+ /**
542
+ * @param {MouseEvent} e - Event object
543
+ */
544
+ #OnClick_browser(e) {
545
+ const eventTarget = dom.query.getEventTarget(e);
546
+ e.stopPropagation();
547
+
548
+ if (/close/.test(eventTarget.getAttribute('data-command')) || this._closeSignal) {
549
+ this.close();
550
+ }
551
+ }
552
+
553
+ /**
554
+ * @param {SubmitEvent} e - Event object
555
+ */
556
+ #Search(e) {
557
+ const eventTarget = /** @type {HTMLElement} */ (e.currentTarget);
558
+ e.preventDefault();
559
+ this.search(/** @type {HTMLInputElement} */ (eventTarget.querySelector('input[type="text"]')).value);
560
+ }
561
+
562
+ /**
563
+ * @param {MouseEvent} e - Event object
564
+ */
565
+ #SideOpen(e) {
566
+ const eventTarget = dom.query.getEventTarget(e);
567
+ if (dom.utils.hasClass(eventTarget, 'active')) {
568
+ dom.utils.removeClass(this.side, 'se-side-show');
569
+ dom.utils.removeClass(eventTarget, 'active');
570
+ } else {
571
+ dom.utils.addClass(this.side, 'se-side-show');
572
+ dom.utils.addClass(eventTarget, 'active');
573
+ }
574
+ }
575
+
576
+ /**
577
+ * @param {MouseEvent} e - Event object
578
+ */
579
+ #SideClose({ target }) {
580
+ if (target === this.sideOpenBtn) return;
581
+ if (dom.utils.hasClass(this.sideOpenBtn, 'active')) {
582
+ dom.utils.removeClass(this.side, 'se-side-show');
583
+ dom.utils.removeClass(this.sideOpenBtn, 'active');
584
+ }
585
+ }
586
+ }
587
+
588
+ /**
589
+ * @private
590
+ * @param {__se__EditorCore} editor - editor instance
591
+ * @param {boolean} useSearch - Whether to use the search function
592
+ * @returns {{ html: HTMLElement, header: HTMLElement, titleArea: HTMLElement, tagArea: HTMLElement, body: HTMLElement, list: HTMLElement, side: HTMLElement, wrapper: HTMLElement, _loading: HTMLElement }} HTML
593
+ */
594
+ function CreateHTMLInfos(editor, useSearch) {
595
+ const lang = editor.lang;
596
+ const icons = editor.icons;
597
+ const htmlString = /*html*/ `
598
+ <div class="se-browser-content">
599
+ <div class="se-browser-header">
600
+ <button type="button" data-command="close" class="se-btn se-browser-close" class="close" title="${lang.close}" aria-label="${lang.close}">
601
+ ${icons.cancel}
602
+ </button>
603
+ <span class="se-browser-title"></span>
604
+ </div>
605
+ <div class="se-browser-wrapper">
606
+ <div class="se-browser-side"></div>
607
+ <div class="se-browser-main">
608
+ <div class="se-browser-bar">
609
+ <div class="se-browser-search">
610
+ <button class="se-btn se-side-open-btn">${icons.side_menu_hamburger}</button>
611
+ ${
612
+ useSearch
613
+ ? /*html*/ `
614
+ <form class="se-browser-search-form">
615
+ <input type="text" class="se-input-form" placeholder="${lang.search}" aria-label="${lang.search}">
616
+ <button type="submit" class="se-btn" title="${lang.search}" aria-label="${lang.search}">${icons.search}</button>
617
+ </form>`
618
+ : ''
619
+ }
620
+ </div>
621
+ </div>
622
+ <div class="se-browser-body">
623
+ <div class="se-browser-tags"></div>
624
+ <div class="se-loading-box sun-editor-common"><div class="se-loading-effect"></div></div>
625
+ <div class="se-browser-menus"></div>
626
+ <div class="se-browser-list"></div>
627
+ </div>
628
+ </div>
629
+ </div>
630
+ </div>`;
631
+
632
+ const content = dom.utils.createElement('DIV', { class: 'se-browser-inner' }, htmlString);
633
+
634
+ return {
635
+ html: content,
636
+ header: content.querySelector('.se-browser-header'),
637
+ titleArea: content.querySelector('.se-browser-title'),
638
+ tagArea: content.querySelector('.se-browser-tags'),
639
+ body: content.querySelector('.se-browser-body'),
640
+ list: content.querySelector('.se-browser-list'),
641
+ side: content.querySelector('.se-browser-side'),
642
+ wrapper: content.querySelector('.se-browser-wrapper'),
643
+ _loading: content.querySelector('.se-loading-box')
644
+ };
645
+ }
646
+
647
+ /**
648
+ * @private
649
+ * @this {{ thumbnail: ((...args: *) => *), props: Array<*> }}
650
+ * @description Define the HTML of the item to be put in "div.se-file-item-column".
651
+ * - Format: [ { src: "image src", name: "name(@option)", alt: "image alt(@option)", tag: "tag name(@option)" } ]
652
+ * @param {BrowserFile} item Item of the response data's array
653
+ */
654
+ function DrawItems(item) {
655
+ const srcName = item.src.split('/').pop();
656
+ const thumbnail = item.thumbnail || '';
657
+ const src = thumbnail || item.src;
658
+ const customProps = this.props?.map((v) => `data-${v}="${item[v]}"`).join(' ') || '';
659
+ const attrs = `data-type="${item.type}" data-command="${item.src}" data-name="${item.name || srcName}" data-thumbnail="${thumbnail}" data-extension="${item.src.split('.').pop()}" ${customProps}`;
660
+ const props = `class="${thumbnail || 'se-browser-empty-image'}" src="${src}" alt="${item.alt || srcName}" ${attrs}`;
661
+ return /*html*/ `
662
+ <div class="se-file-item-img">
663
+ ${this.thumbnail && !thumbnail && item.type !== 'image' ? `<div class="se-browser-empty-thumbnail" ${props}>${this.thumbnail(item)}</div>` : `<img class="${thumbnail || 'se-browser-empty-image'}" ${props}>`}
664
+ <div class="se-file-name-image se-file-name-back"></div>
665
+ <div class="se-file-name-image">${item.name || srcName}</div>
666
+ </div>`;
667
+ }
668
+
669
+ export default Browser;