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.
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +1 -1
- package/src/assets/suneditor.css +51 -3
- package/src/core/logic/panel/viewer.js +9 -5
- package/src/core/logic/shell/_commandExecutor.js +38 -1
- package/src/core/section/constructor.js +1 -1
- package/src/helper/markdown.js +18 -3
- package/src/modules/contract/Browser.js +98 -10
- package/src/modules/contract/Controller.js +1 -2
- package/src/modules/contract/Figure.js +1 -1
- package/src/modules/ui/ModalAnchorEditor.js +16 -1
- package/src/plugins/browser/audioGallery.js +13 -0
- package/src/plugins/browser/fileBrowser.js +22 -0
- package/src/plugins/browser/fileGallery.js +14 -0
- package/src/plugins/browser/imageGallery.js +14 -0
- package/src/plugins/browser/videoGallery.js +14 -0
- package/src/plugins/command/codeBlock.js +1 -1
- package/src/plugins/command/fileUpload.js +12 -0
- package/src/plugins/command/list_bulleted.js +1 -1
- package/src/plugins/command/list_numbered.js +1 -1
- package/src/plugins/field/mention.js +2 -2
- package/src/plugins/input/fontSize.js +4 -4
- package/src/plugins/modal/audio.js +12 -0
- package/src/plugins/modal/embed.js +12 -0
- package/src/plugins/modal/image/index.js +12 -0
- package/src/plugins/modal/link.js +12 -0
- package/src/plugins/modal/video/index.js +12 -0
- package/types/langs/_Lang.d.ts +1 -0
- package/types/modules/contract/Browser.d.ts +32 -0
- package/types/modules/ui/ModalAnchorEditor.d.ts +32 -2
- package/types/plugins/browser/audioGallery.d.ts +26 -0
- package/types/plugins/browser/fileBrowser.d.ts +45 -0
- package/types/plugins/browser/fileGallery.d.ts +28 -0
- package/types/plugins/browser/imageGallery.d.ts +28 -0
- package/types/plugins/browser/videoGallery.d.ts +28 -0
- package/types/plugins/command/fileUpload.d.ts +24 -0
- package/types/plugins/field/mention.d.ts +1 -1
- package/types/plugins/modal/audio.d.ts +24 -0
- package/types/plugins/modal/embed.d.ts +24 -0
- package/types/plugins/modal/image/index.d.ts +24 -0
- package/types/plugins/modal/link.d.ts +28 -1
- package/types/plugins/modal/video/index.d.ts +24 -0
package/package.json
CHANGED
package/src/assets/suneditor.css
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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, '
|
|
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],
|
package/src/helper/markdown.js
CHANGED
|
@@ -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 (
|
|
463
|
+
// Figure - process children (table, media)
|
|
450
464
|
if (tag === 'figure') {
|
|
451
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
* {
|
|
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
|