suneditor 3.0.4 → 3.0.6

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 (43) hide show
  1. package/dist/suneditor.min.css +1 -1
  2. package/dist/suneditor.min.js +1 -1
  3. package/package.json +1 -1
  4. package/src/assets/suneditor.css +51 -3
  5. package/src/core/logic/panel/viewer.js +9 -5
  6. package/src/core/logic/shell/_commandExecutor.js +38 -1
  7. package/src/core/section/constructor.js +1 -1
  8. package/src/helper/markdown.js +18 -3
  9. package/src/modules/contract/Browser.js +98 -10
  10. package/src/modules/contract/Controller.js +1 -2
  11. package/src/modules/contract/Figure.js +1 -1
  12. package/src/modules/ui/ModalAnchorEditor.js +16 -1
  13. package/src/plugins/browser/audioGallery.js +13 -0
  14. package/src/plugins/browser/fileBrowser.js +22 -0
  15. package/src/plugins/browser/fileGallery.js +14 -0
  16. package/src/plugins/browser/imageGallery.js +14 -0
  17. package/src/plugins/browser/videoGallery.js +14 -0
  18. package/src/plugins/command/codeBlock.js +1 -1
  19. package/src/plugins/command/fileUpload.js +12 -0
  20. package/src/plugins/command/list_bulleted.js +1 -1
  21. package/src/plugins/command/list_numbered.js +1 -1
  22. package/src/plugins/field/mention.js +2 -2
  23. package/src/plugins/input/fontSize.js +4 -4
  24. package/src/plugins/modal/audio.js +12 -0
  25. package/src/plugins/modal/embed.js +12 -0
  26. package/src/plugins/modal/image/index.js +12 -0
  27. package/src/plugins/modal/link.js +12 -0
  28. package/src/plugins/modal/video/index.js +12 -0
  29. package/types/langs/_Lang.d.ts +1 -0
  30. package/types/modules/contract/Browser.d.ts +32 -0
  31. package/types/modules/ui/ModalAnchorEditor.d.ts +32 -2
  32. package/types/plugins/browser/audioGallery.d.ts +26 -0
  33. package/types/plugins/browser/fileBrowser.d.ts +45 -0
  34. package/types/plugins/browser/fileGallery.d.ts +28 -0
  35. package/types/plugins/browser/imageGallery.d.ts +28 -0
  36. package/types/plugins/browser/videoGallery.d.ts +28 -0
  37. package/types/plugins/command/fileUpload.d.ts +24 -0
  38. package/types/plugins/field/mention.d.ts +1 -1
  39. package/types/plugins/modal/audio.d.ts +24 -0
  40. package/types/plugins/modal/embed.d.ts +24 -0
  41. package/types/plugins/modal/image/index.d.ts +24 -0
  42. package/types/plugins/modal/link.d.ts +28 -1
  43. package/types/plugins/modal/video/index.d.ts +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suneditor",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -368,6 +368,26 @@
368
368
  z-index: 1;
369
369
  }
370
370
 
371
+ /** -- plain text button */
372
+ .sun-editor .se-btn.se-btn-plain {
373
+ background-color: transparent;
374
+ border-color: transparent;
375
+ box-shadow: none;
376
+ }
377
+ .sun-editor .se-btn.se-btn-plain:not(.on):not(.active):enabled:hover,
378
+ .sun-editor .se-btn.se-btn-plain:not(.on):not(.active):enabled:focus {
379
+ background-color: transparent;
380
+ border-color: transparent;
381
+ box-shadow: none;
382
+ }
383
+ .sun-editor .se-btn.se-btn-plain:not(.on):not(.active):enabled:active,
384
+ .sun-editor .se-btn.se-btn-plain:not(.on):not(.active):enabled.__se__active {
385
+ background-color: transparent;
386
+ border-color: transparent !important;
387
+ box-shadow: none;
388
+ outline: none !important;
389
+ }
390
+
371
391
  /** --- primary button */
372
392
  .sun-editor .se-btn-primary {
373
393
  background-color: var(--se-active-light2-color);
@@ -3232,11 +3252,14 @@
3232
3252
  margin-left: 0;
3233
3253
  margin-right: 0;
3234
3254
  }
3235
-
3236
- .sun-editor .se-browser .se-browser-search input {
3255
+ .sun-editor .se-browser .se-browser-search-input-wrap {
3256
+ position: relative;
3237
3257
  flex: auto;
3258
+ }
3259
+ .sun-editor .se-browser .se-browser-search-input-wrap input {
3260
+ width: 100%;
3261
+ padding: 3px 24px 3px 6px;
3238
3262
  background-color: transparent;
3239
- padding: 3px 6px;
3240
3263
  color: var(--se-main-font-color);
3241
3264
  text-decoration: none;
3242
3265
  border-radius: var(--se-border-radius);
@@ -3245,6 +3268,18 @@
3245
3268
  -moz-background-clip: padding;
3246
3269
  -webkit-background-clip: padding-box;
3247
3270
  background-clip: padding-box;
3271
+ box-sizing: border-box;
3272
+ }
3273
+ .sun-editor .se-browser .se-browser-search-clear {
3274
+ position: absolute;
3275
+ right: 2px;
3276
+ top: 50%;
3277
+ transform: translateY(-50%);
3278
+ padding: 0 2px;
3279
+ opacity: 0.35;
3280
+ }
3281
+ .sun-editor .se-browser .se-browser-search-clear:hover {
3282
+ opacity: 0.8;
3248
3283
  }
3249
3284
 
3250
3285
  .sun-editor .se-browser .se-browser-search button {
@@ -3396,6 +3431,12 @@
3396
3431
  opacity: 0.6;
3397
3432
  pointer-events: none;
3398
3433
  }
3434
+ .sun-editor .se-browser .se-file-name-image mark {
3435
+ background-color: var(--se-active-color);
3436
+ color: var(--se-active-font-color);
3437
+ padding: 0 1px;
3438
+ border-radius: 2px;
3439
+ }
3399
3440
 
3400
3441
  .sun-editor .se-browser .se-browser-list.se-preview-list .se-file-item-img > * {
3401
3442
  display: flex;
@@ -4148,6 +4189,13 @@
4148
4189
  margin: 2px 8px;
4149
4190
  flex-direction: row-reverse;
4150
4191
  }
4192
+ .sun-editor.se-rtl .se-browser .se-browser-search-input-wrap input {
4193
+ padding: 3px 6px 3px 24px;
4194
+ }
4195
+ .sun-editor.se-rtl .se-browser .se-browser-search-clear {
4196
+ right: auto;
4197
+ left: 2px;
4198
+ }
4151
4199
  .sun-editor.se-rtl .se-browser .se-browser-side .se-menu-icon {
4152
4200
  margin: 0 0 0 10px;
4153
4201
  }
@@ -300,7 +300,7 @@ class Viewer {
300
300
  this.#originCssText = topArea.style.cssText;
301
301
  this.#editorAreaOriginCssText = editorArea.style.cssText;
302
302
  this.#wysiwygOriginCssText = wysiwygFrame.style.cssText;
303
- this.#codeWrapperOriginCssText = codeWrapper.style.cssText;
303
+ this.#codeWrapperOriginCssText = codeWrapper?.style.cssText;
304
304
  this.#codeOriginCssText = code.style.cssText;
305
305
  this.#codeNumberOriginCssText = codeNumbers?.style.cssText;
306
306
  this.#markdownWrapperOriginCssText = markdownWrapper?.style.cssText;
@@ -344,9 +344,11 @@ class Viewer {
344
344
  wysiwygFrame.style.cssText = (wysiwygFrame.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + this.#frameOptions.get('_defaultStyles').editor + (isCodeView || isMarkdownView ? 'display: none;' : '');
345
345
 
346
346
  // code wrapper
347
- codeWrapper.style.cssText = (codeWrapper.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
348
- codeWrapper.style.overflow = 'auto';
349
- codeWrapper.style.height = '100%';
347
+ if (codeWrapper) {
348
+ codeWrapper.style.cssText = (codeWrapper.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
349
+ codeWrapper.style.overflow = 'auto';
350
+ codeWrapper.style.height = '100%';
351
+ }
350
352
 
351
353
  // markdown wrapper
352
354
  if (markdownWrapper) {
@@ -384,7 +386,9 @@ class Viewer {
384
386
  wysiwygFrame.style.cssText = this.#wysiwygOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + (isCodeView || isMarkdownView ? 'display: none;' : '');
385
387
 
386
388
  // code wrapper
387
- codeWrapper.style.cssText = this.#codeWrapperOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
389
+ if (codeWrapper) {
390
+ codeWrapper.style.cssText = this.#codeWrapperOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
391
+ }
388
392
 
389
393
  // code
390
394
  code.style.cssText = this.#codeOriginCssText;
@@ -59,12 +59,15 @@ export default class CommandExecutor {
59
59
  * @description Execute default command of command button
60
60
  */
61
61
  async execute(command, button) {
62
- if (this.#frameContext.get('isReadOnly') && !/copy|cut|selectAll|codeView|markdownView|fullScreen|print|preview|showBlocks|finder/.test(command)) return;
62
+ if (this.#frameContext.get('isReadOnly') && !/copy|cut|selectAll|selectAll_full|codeView|markdownView|fullScreen|print|preview|showBlocks|finder/.test(command)) return;
63
63
 
64
64
  switch (command) {
65
65
  case 'selectAll':
66
66
  this.#SELECT_ALL();
67
67
  break;
68
+ case 'selectAll_full':
69
+ this.#SELECT_ALL_FULL();
70
+ break;
68
71
  case 'copy': {
69
72
  const range = this.#$.selection.getRange();
70
73
  if (range.collapsed) break;
@@ -254,6 +257,40 @@ export default class CommandExecutor {
254
257
  this.#$.toolbar._showBalloon(this.#$.selection.setRange(first, 0, last, last.textContent.length));
255
258
  }
256
259
 
260
+ /**
261
+ * @description Selects all content in the entire editor without scope stepping.
262
+ */
263
+ #SELECT_ALL_FULL() {
264
+ this.#$.ui.offCurrentController();
265
+ this.#$.menu.containerOff();
266
+
267
+ const ww = this.#frameContext.get('wysiwyg');
268
+ let { first, last } = __findFirstAndLast(ww);
269
+
270
+ if (!first || !last) return;
271
+
272
+ let info = null;
273
+ if (dom.check.isMedia(first) || (info = this.#$.component.get(first)) || dom.check.isTableElements(first)) {
274
+ info ||= this.#$.component.get(first);
275
+ const br = dom.utils.createElement('BR');
276
+ const format = dom.utils.createElement(this.#options.get('defaultLine'), null, br);
277
+ first = info ? info.container || info.cover : first;
278
+ first.parentElement.insertBefore(format, first);
279
+ first = br;
280
+ }
281
+
282
+ if (dom.check.isMedia(last) || (info = this.#$.component.get(last)) || dom.check.isTableElements(last)) {
283
+ info ||= this.#$.component.get(last);
284
+ const br = dom.utils.createElement('BR');
285
+ const format = dom.utils.createElement(this.#options.get('defaultLine'), null, br);
286
+ last = info ? info.container || info.cover : last;
287
+ last.parentElement.appendChild(format);
288
+ last = br;
289
+ }
290
+
291
+ this.#$.toolbar._showBalloon(this.#$.selection.setRange(first, 0, last, last.textContent.length));
292
+ }
293
+
257
294
  /**
258
295
  * @description Saves the editor content.
259
296
  * @returns {Promise<void>}
@@ -1132,7 +1132,7 @@ function _defaultButtons(isRTL, icons, lang) {
1132
1132
  finder: ['se-component-enabled', lang.find, 'finder', '', icons.finder],
1133
1133
  save: ['se-component-enabled', lang.save, 'save', '', icons.save],
1134
1134
  newDocument: ['se-component-enabled', lang.newDocument, 'newDocument', '', icons.new_document],
1135
- selectAll: ['se-component-enabled', lang.selectAll, 'selectAll', '', icons.select_all],
1135
+ selectAll: ['se-component-enabled', lang.selectAll, 'selectAll_full', '', icons.select_all],
1136
1136
  pageBreak: ['se-component-enabled', lang.pageBreak, 'pageBreak', '', icons.page_break],
1137
1137
  // document type buttons
1138
1138
  pageUp: ['se-component-enabled', lang.pageUp, 'pageUp', '', icons.page_up],
@@ -1,6 +1,21 @@
1
1
  /**
2
2
  * @fileoverview Markdown converter module
3
3
  * - Supports GitHub Flavored Markdown (GFM) syntax
4
+ *
5
+ * @description Limitations — Style loss during roundtrip
6
+ *
7
+ * Markdown syntax cannot represent HTML inline styles, classes, or data attributes.
8
+ * When switching between WYSIWYG and Markdown view, the following behavior applies:
9
+ *
10
+ * | Content type | Behavior | Example |
11
+ * |-------------------------------------|---------------------------------------------|------------------------------------------------------|
12
+ * | Media components (`div.se-component`) | Converted to markdown — styles are lost | Image alignment, data-se-* attrs |
13
+ * | Styled `<span>` elements | Preserved as raw HTML in markdown | Font color, background, custom classes |
14
+ * | Tables, figures | Converted to markdown — styles are lost | Table cell styles, figure width, colgroup widths |
15
+ * | General elements (p, h1, blockquote) | Converted to markdown — styles are lost | text-align, color, font-size on paragraphs/headings |
16
+ *
17
+ * This is a fundamental limitation of the Markdown format.
18
+ * To preserve all HTML attributes without loss, use Code View instead.
4
19
  */
5
20
 
6
21
  const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i;
@@ -439,16 +454,16 @@ function nodeToMarkdown(node, indent, isBlock) {
439
454
 
440
455
  // Span - pass through as inline (may contain styles)
441
456
  if (tag === 'span') {
442
- // Check if it has meaningful attributes that need HTML fallback
443
457
  if (attributes.style || attributes.class) {
444
458
  return nodeToHtmlFallback(node);
445
459
  }
446
460
  return childrenToInline(children);
447
461
  }
448
462
 
449
- // Figure - process children (usually contains img or other media)
463
+ // Figure - process children (table, media)
450
464
  if (tag === 'figure') {
451
- return children.map((c) => nodeToMarkdown(c, indent, true)).join('');
465
+ const inner = children.map((c) => nodeToMarkdown(c, indent, true)).join('');
466
+ return /\n$/.test(inner) ? inner : inner + '\n\n';
452
467
  }
453
468
  if (tag === 'figcaption') {
454
469
  const content = childrenToInline(children).trim();
@@ -27,6 +27,19 @@ import ApiManager from '../manager/ApiManager';
27
27
  * @property {(target: Node) => void} selectorHandler - Function that actions when an item is clicked. Required. Can be overridden in browser.
28
28
  * @property {boolean} [useSearch] - Whether to use the search function. Optional. Default: `true`.
29
29
  * @property {string} [searchUrl] - File server search url. Optional. Can be overridden in browser.
30
+ * - Requested as `searchUrl + '?keyword=' + keyword`. The server must return:
31
+ * ```js
32
+ * {
33
+ * "result": [
34
+ * {
35
+ * "src": "https://example.com/file.jpg",
36
+ * "name": "file.jpg",
37
+ * "thumbnail": "https://example.com/file_thumb.jpg",
38
+ * "tag": ["photo"]
39
+ * }
40
+ * ]
41
+ * }
42
+ * ```
30
43
  * @property {Object<string, string>} [searchUrlHeader] - File server search http header. Optional. Can be overridden in browser.
31
44
  * @property {string} [listClass] - Class name of list div. Required. Can be overridden in browser.
32
45
  * @property {(item: BrowserFile) => string} [drawItemHandler] - Function that returns HTML string for rendering each file item. Required. Can be overridden in browser.
@@ -37,6 +50,7 @@ import ApiManager from '../manager/ApiManager';
37
50
  * @property {Array<*>} [props] - `props` argument to `drawItemHandler` function. Optional. Can be overridden in browser.
38
51
  * @property {number} [columnSize] - Number of `div.se-file-item-column` to be created.
39
52
  * - Optional. Can be overridden in browser. Default: 4.
53
+ * @property {number} [expand=1] - Initial folder expand depth. `1` expands the first level, `Infinity` expands all. Default: `1`.
40
54
  * @property {((item: BrowserFile) => string)} [thumbnail] - Default thumbnail
41
55
  */
42
56
 
@@ -50,6 +64,10 @@ class Browser {
50
64
  #loading;
51
65
  #globalEventHandler;
52
66
 
67
+ /** @type {Array<BrowserFile>} */
68
+ #allItems = [];
69
+ #searchInput;
70
+ #searchClearBtn;
53
71
  #closeSignal = false;
54
72
  #bindClose = null;
55
73
 
@@ -102,6 +120,7 @@ class Browser {
102
120
  this.drawItemHandler = (params.drawItemHandler || DrawItems).bind({ thumbnail: params.thumbnail, props: params.props || [] });
103
121
  this.selectorHandler = params.selectorHandler;
104
122
  this.columnSize = params.columnSize || 4;
123
+ this.expand = params.expand ?? 1;
105
124
  this.folderDefaultPath = '';
106
125
  this.closeArrow = this.#$.icons.menu_arrow_right;
107
126
  this.openArrow = this.#$.icons.menu_arrow_down;
@@ -139,9 +158,15 @@ class Browser {
139
158
  this.#$.eventManager.addEvent(this.side, 'click', this.#OnClickSide.bind(this));
140
159
  this.#$.eventManager.addEvent(content, 'mousedown', this.#OnMouseDown_browser.bind(this));
141
160
  this.#$.eventManager.addEvent(content, 'click', this.#OnClick_browser.bind(this));
142
- this.#$.eventManager.addEvent(browserFrame.querySelector('form.se-browser-search-form'), 'submit', this.#Search.bind(this));
143
161
  this.#$.eventManager.addEvent((this.sideOpenBtn = /** @type {HTMLButtonElement} */ (browserFrame.querySelector('.se-side-open-btn'))), 'click', this.#SideOpen.bind(this));
144
162
  this.#$.eventManager.addEvent([this.header, browserFrame.querySelector('.se-browser-main')], 'mousedown', this.#SideClose.bind(this));
163
+
164
+ // search
165
+ const searchForm = browserFrame.querySelector('form.se-browser-search-form');
166
+ this.#searchInput = /** @type {HTMLInputElement} */ (searchForm?.querySelector('input[type="text"]'));
167
+ this.#searchClearBtn = /** @type {HTMLButtonElement} */ (browserFrame.querySelector('.se-browser-search-clear'));
168
+ this.#$.eventManager.addEvent(searchForm, 'submit', this.#Search.bind(this));
169
+ this.#$.eventManager.addEvent(this.#searchClearBtn, 'click', this.#ClearSearch.bind(this));
145
170
  }
146
171
 
147
172
  /**
@@ -195,11 +220,16 @@ class Browser {
195
220
  this.area.style.display = 'none';
196
221
  this.selectedTags = [];
197
222
  this.items = [];
223
+ this.#allItems = [];
198
224
  this.folders = {};
199
225
  this.tree = {};
200
226
  this.data = {};
201
227
  this.keyword = '';
202
228
  this.list.innerHTML = this.tagArea.innerHTML = this.titleArea.textContent = '';
229
+
230
+ if (this.#searchInput) this.#searchInput.value = '';
231
+ if (this.#searchClearBtn) this.#searchClearBtn.style.display = 'none';
232
+
203
233
  this.#$.ui.opendBrowser = null;
204
234
  this.sideInner = null;
205
235
 
@@ -216,8 +246,25 @@ class Browser {
216
246
  this.#drawFileList(this.searchUrl + '?keyword=' + keyword, this.searchUrlHeader, false);
217
247
  } else {
218
248
  this.keyword = keyword.toLowerCase();
219
- this.#drawListItem(this.items, false);
249
+ this.#drawListItem(this.#allItems.length > 0 ? this.#allItems : this.items, false);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * @description Collects all file items from every folder in `this.data`.
255
+ * @returns {Array<BrowserFile>}
256
+ */
257
+ #collectAllItems() {
258
+ const all = [];
259
+ for (const key in this.data) {
260
+ const items = this.data[key];
261
+ if (Array.isArray(items)) {
262
+ for (let i = 0; i < items.length; i++) {
263
+ all.push(items[i]);
264
+ }
265
+ }
220
266
  }
267
+ return all;
221
268
  }
222
269
 
223
270
  /**
@@ -306,6 +353,10 @@ class Browser {
306
353
 
307
354
  this.list.innerHTML = listHTML;
308
355
 
356
+ if (keyword) {
357
+ this.#highlightKeyword(keyword);
358
+ }
359
+
309
360
  if (update) {
310
361
  this.items = items;
311
362
  this.tagArea.innerHTML = tagsHTML;
@@ -341,6 +392,7 @@ class Browser {
341
392
  } else if (typeof data === 'object') {
342
393
  this.sideOpenBtn.style.display = '';
343
394
  this.#parseFolderData(data);
395
+ this.#allItems = this.#collectAllItems();
344
396
 
345
397
  this.side.innerHTML = '';
346
398
  const sideInner = (this.sideInner = dom.utils.createElement('div', null));
@@ -412,8 +464,11 @@ class Browser {
412
464
  * @description Creates a nested folder list from parsed data.
413
465
  * @param {BrowserFile[]|BrowserFile} folderData - The structured folder data.
414
466
  * @param {HTMLElement} parentElement - The parent element to append folder structure to.
467
+ * @param {number} [depth=0] - Current depth level.
415
468
  */
416
- #createFolderList(folderData, parentElement) {
469
+ #createFolderList(folderData, parentElement, depth = 0) {
470
+ const expanded = depth < this.expand;
471
+
417
472
  for (const key in folderData) {
418
473
  const item = folderData[key];
419
474
  if (!item) continue;
@@ -426,10 +481,10 @@ class Browser {
426
481
  );
427
482
  const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel);
428
483
 
429
- folderLabel.insertBefore(dom.utils.createElement('button', null, this.closeArrow), folderLabel.firstElementChild);
484
+ folderLabel.insertBefore(dom.utils.createElement('button', null, expanded ? this.openArrow : this.closeArrow), folderLabel.firstElementChild);
430
485
  const childContainer = document.createElement('div');
431
- dom.utils.addClass(childContainer, 'se-menu-child|se-menu-hidden');
432
- this.#createFolderList(item.children, childContainer);
486
+ dom.utils.addClass(childContainer, expanded ? 'se-menu-child' : 'se-menu-child|se-menu-hidden');
487
+ this.#createFolderList(item.children, childContainer, depth + 1);
433
488
  folderDiv.appendChild(childContainer);
434
489
 
435
490
  parentElement.appendChild(folderDiv);
@@ -544,7 +599,7 @@ class Browser {
544
599
  if (typeof data === 'string') {
545
600
  this.#drawFileList(data, this.urlHeader, true);
546
601
  } else {
547
- this.#drawListItem(data, false);
602
+ this.#drawListItem(data, true);
548
603
  }
549
604
  }
550
605
 
@@ -576,9 +631,39 @@ class Browser {
576
631
  * @param {SubmitEvent} e - Event object
577
632
  */
578
633
  #Search(e) {
579
- const eventTarget = /** @type {HTMLElement} */ (e.currentTarget);
580
634
  e.preventDefault();
581
- this.search(/** @type {HTMLInputElement} */ (eventTarget.querySelector('input[type="text"]')).value);
635
+
636
+ const keyword = this.#searchInput.value;
637
+ this.search(keyword);
638
+
639
+ if (this.#searchClearBtn) this.#searchClearBtn.style.display = keyword ? '' : 'none';
640
+ }
641
+
642
+ /**
643
+ * @description Clears the search keyword and restores the current folder's item list.
644
+ */
645
+ #ClearSearch() {
646
+ if (this.#searchInput) this.#searchInput.value = '';
647
+ if (this.#searchClearBtn) this.#searchClearBtn.style.display = 'none';
648
+
649
+ this.keyword = '';
650
+ this.#drawListItem(this.items, false);
651
+ }
652
+
653
+ /**
654
+ * @description Highlights the search keyword in file name elements.
655
+ * @param {string} keyword - Lowercase keyword to highlight.
656
+ */
657
+ #highlightKeyword(keyword) {
658
+ const nameEls = this.list.querySelectorAll('.se-file-name-image:not(.se-file-name-back)');
659
+ for (let i = 0; i < nameEls.length; i++) {
660
+ const el = nameEls[i];
661
+ const text = el.textContent;
662
+ const idx = text.toLowerCase().indexOf(keyword);
663
+ if (idx > -1) {
664
+ el.innerHTML = text.substring(0, idx) + '<mark>' + text.substring(idx, idx + keyword.length) + '</mark>' + text.substring(idx + keyword.length);
665
+ }
666
+ }
582
667
  }
583
668
 
584
669
  /**
@@ -633,7 +718,10 @@ function CreateHTMLInfos($, useSearch) {
633
718
  useSearch
634
719
  ? /*html*/ `
635
720
  <form class="se-browser-search-form">
636
- <input type="text" class="se-input-form" placeholder="${lang.search}" aria-label="${lang.search}">
721
+ <div class="se-browser-search-input-wrap">
722
+ <input type="text" class="se-input-form" placeholder="${lang.search}" aria-label="${lang.search}">
723
+ <button type="button" class="se-btn se-btn-plain se-browser-search-clear" title="${lang.cancel}" aria-label="${lang.cancel}" style="display:none">${icons.cancel}</button>
724
+ </div>
637
725
  <button type="submit" class="se-btn" title="${lang.search}" aria-label="${lang.search}">${icons.search}</button>
638
726
  </form>`
639
727
  : ''
@@ -377,14 +377,13 @@ class Controller {
377
377
  if (!passive) {
378
378
  this.#$.ui.onControllerContext();
379
379
  this.#$.store.set('controlActive', true);
380
+ this.#$.store.set('_preventBlur', true);
380
381
  }
381
382
 
382
383
  if (!this.isOpen) {
383
384
  this.#$.ui.opendControllers.push(info);
384
385
  }
385
386
 
386
- this.#$.store.set('_preventBlur', true);
387
-
388
387
  this.isOpen = true;
389
388
 
390
389
  this.host.controllerOn?.(form, target);
@@ -904,7 +904,7 @@ class Figure {
904
904
  retainFigureFormat(container, originEl, anchorCover, fileManagerInst) {
905
905
  const isInline = this.#$.component.isInline(container);
906
906
  const originParent = originEl.parentNode;
907
- let existElement = this.#$.format.isBlock(originParent) || dom.check.isWysiwygFrame(originParent) ? originEl : Figure.GetContainer(originEl)?.container || originParent || originEl;
907
+ let existElement = this.#$.format.isBlock(originParent) || dom.check.isWysiwygFrame(originParent) || originParent.nodeType >= 9 ? originEl : Figure.GetContainer(originEl)?.container || originParent || originEl;
908
908
 
909
909
  if (dom.query.getParentElement(originEl, dom.check.isExcludeFormat)) {
910
910
  existElement = anchorCover && anchorCover !== originEl ? anchorCover : originEl;
@@ -13,9 +13,24 @@ const { _w, NO_EVENT } = env;
13
13
  * @property {{default?: string, check_new_window?: string, check_bookmark?: string}} [defaultRel={}] - Default `rel` values auto-applied by condition.
14
14
  * `default` is always applied, `check_new_window` when "Open in new window" is checked, `check_bookmark` for bookmark links.
15
15
  * ```js
16
- * { relList: ['nofollow', 'noreferrer', 'noopener'], defaultRel: { default: 'noopener', check_new_window: 'noreferrer' } }
16
+ * {
17
+ * relList: ['nofollow', 'noreferrer', 'noopener'],
18
+ * defaultRel: { default: 'noopener', check_new_window: 'noreferrer' }
19
+ * }
17
20
  * ```
18
21
  * @property {string} [uploadUrl] - File upload URL.
22
+ * - The server must return:
23
+ * ```js
24
+ * {
25
+ * "result": [
26
+ * {
27
+ * "url": "https://example.com/file.pdf",
28
+ * "name": "file.pdf",
29
+ * "size": 1048576
30
+ * }
31
+ * ]
32
+ * }
33
+ * ```
19
34
  * @property {Object<string, string>} [uploadHeaders] - File upload headers.
20
35
  * @property {number} [uploadSizeLimit] - File upload size limit.
21
36
  * @property {number} [uploadSingleSizeLimit] - File upload single size limit.
@@ -5,6 +5,19 @@ import { Browser } from '../../modules/contract';
5
5
  * @typedef {Object} AudioGalleryPluginOptions
6
6
  * @property {Array<SunEditor.Module.Browser.File>} [data] - Direct data without server calls
7
7
  * @property {string} [url] - Server request URL
8
+ * - The server must return:
9
+ * ```js
10
+ * {
11
+ * "result": [
12
+ * {
13
+ * "src": "https://example.com/audio.mp3",
14
+ * "name": "audio.mp3",
15
+ * "thumbnail": "https://example.com/audio_icon.png",
16
+ * "tag": ["music"]
17
+ * }
18
+ * ]
19
+ * }
20
+ * ```
8
21
  * @property {Object<string, string>} [headers] - Server request headers
9
22
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
10
23
  */
@@ -5,8 +5,29 @@ import { Browser } from '../../modules/contract';
5
5
  * @typedef {Object} FileBrowserPluginOptions
6
6
  * @property {Object<string, *>|Array<*>} [data] - Direct data without server calls (bypasses URL fetch).
7
7
  * @property {string} [url] - Server request URL
8
+ * - The server must return a nested folder structure.
9
+ * - `_data`: array (inline) or string URL (lazy-loaded on folder click).
10
+ * - `"default": true` sets the initially selected folder.
11
+ * ```js
12
+ * {
13
+ * "result": {
14
+ * "root": {
15
+ * "name": "Root",
16
+ * "default": true,
17
+ * "_data": [
18
+ * { "src": "https://example.com/file1.pdf", "name": "file1.pdf" }
19
+ * ],
20
+ * "documents": {
21
+ * "name": "Documents",
22
+ * "_data": "https://api.example.com/files/documents"
23
+ * }
24
+ * }
25
+ * }
26
+ * }
27
+ * ```
8
28
  * @property {Object<string, string>} [headers] - Server request headers
9
29
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail URL or a function that returns a thumbnail URL per item.
30
+ * @property {number} [expand=1] - Initial folder expand depth. `1` expands the first level, `Infinity` expands all. Default: `1`.
10
31
  * @property {Array<string>} [props] - Additional tag names
11
32
  * ```js
12
33
  * { url: '/api/files', headers: { Authorization: 'Bearer token' }, thumbnail: (item) => item.thumbUrl }
@@ -48,6 +69,7 @@ class FileBrowser extends PluginBrowser {
48
69
  className: 'se-file-browser',
49
70
  thumbnail: typeof pluginOptions.thumbnail === 'function' ? pluginOptions.thumbnail : (item) => thumbnail[item.type] || defaultThumbnail,
50
71
  props: [...new Set((pluginOptions.props ?? []).concat(['frame']))],
72
+ expand: pluginOptions.expand,
51
73
  });
52
74
  }
53
75
 
@@ -5,6 +5,20 @@ import { Browser } from '../../modules/contract';
5
5
  * @typedef {Object} FileGalleryPluginOptions
6
6
  * @property {Array<SunEditor.Module.Browser.File>} [data] - Direct data without server calls
7
7
  * @property {string} [url] - Server request URL
8
+ * - The server must return:
9
+ * ```js
10
+ * {
11
+ * "result": [
12
+ * {
13
+ * "src": "https://example.com/doc.pdf",
14
+ * "name": "doc.pdf",
15
+ * "thumbnail": "https://example.com/pdf_icon.png",
16
+ * "type": "file", // video, image ..[plugin name]
17
+ * "tag": ["document"]
18
+ * }
19
+ * ]
20
+ * }
21
+ * ```
8
22
  * @property {Object<string, string>} [headers] - Server request headers
9
23
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
10
24
  */
@@ -5,6 +5,20 @@ import { Browser } from '../../modules/contract';
5
5
  * @typedef ImageGalleryPluginOptions
6
6
  * @property {Array<*>} [data] - Direct data without server calls
7
7
  * @property {string} [url] - Server request URL
8
+ * - The server must return:
9
+ * ```js
10
+ * {
11
+ * "result": [
12
+ * {
13
+ * "src": "https://example.com/img.jpg",
14
+ * "name": "img.jpg",
15
+ * "thumbnail": "https://example.com/img_thumb.jpg",
16
+ * "alt": "description",
17
+ * "tag": ["nature"]
18
+ * }
19
+ * ]
20
+ * }
21
+ * ```
8
22
  * @property {Object<string, string>} [headers] - Server request headers
9
23
  */
10
24
 
@@ -5,6 +5,20 @@ import { Browser } from '../../modules/contract';
5
5
  * @typedef {Object} VideoGalleryPluginOptions
6
6
  * @property {Array<SunEditor.Module.Browser.File>} [data] - Direct data without server calls
7
7
  * @property {string} [url] - Server request URL
8
+ * - The server must return:
9
+ * ```js
10
+ * {
11
+ * "result": [
12
+ * {
13
+ * "src": "https://example.com/video.mp4",
14
+ * "name": "video.mp4",
15
+ * "thumbnail": "https://example.com/video_thumb.jpg",
16
+ * "frame": "video",
17
+ * "tag": ["tutorial"]
18
+ * }
19
+ * ]
20
+ * }
21
+ * ```
8
22
  * @property {Object<string, string>} [headers] - Server request headers
9
23
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
10
24
  */
@@ -66,7 +66,7 @@ class CodeBlock extends PluginCommand {
66
66
  // ───────────────── [[toolbar dropdown type]] ─────────────────
67
67
  this.afterItem = dom.utils.createElement(
68
68
  'button',
69
- { class: 'se-btn se-tooltip se-sub-arrow-btn', 'data-command': CodeBlock.key, 'data-type': 'dropdown' },
69
+ { class: 'se-btn se-tooltip se-sub-arrow-btn', type: 'button', 'data-command': CodeBlock.key, 'data-type': 'dropdown' },
70
70
  `${this.$.icons.arrow_down}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.codeLanguage || 'Language'}</span></span>`,
71
71
  );
72
72
 
@@ -8,6 +8,18 @@ const { NO_EVENT } = env;
8
8
  /**
9
9
  * @typedef FileUploadPluginOptions
10
10
  * @property {string} uploadUrl - Server request URL for file upload
11
+ * - The server must return:
12
+ * ```js
13
+ * {
14
+ * "result": [
15
+ * {
16
+ * "url": "https://example.com/file.pdf",
17
+ * "name": "file.pdf",
18
+ * "size": 1048576
19
+ * }
20
+ * ]
21
+ * }
22
+ * ```
11
23
  * @property {Object<string, string>} [uploadHeaders] - Server request headers
12
24
  * @property {number} [uploadSizeLimit] - Total upload size limit in bytes
13
25
  * @property {number} [uploadSingleSizeLimit] - Single file size limit in bytes