suneditor 3.1.3 → 3.1.4
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.js +1 -1
- package/package.json +1 -1
- package/src/core/logic/dom/html.js +7 -15
- package/src/core/schema/options.js +15 -15
- package/src/core/section/constructor.js +0 -10
- package/src/modules/contract/Browser.js +12 -11
- package/src/modules/contract/Figure.js +3 -0
- package/src/modules/contract/Modal.js +12 -1
- package/src/modules/ui/ModalAnchorEditor.js +8 -3
- package/src/plugins/browser/audioGallery.js +6 -0
- package/src/plugins/browser/fileBrowser.js +6 -0
- package/src/plugins/browser/fileGallery.js +6 -0
- package/src/plugins/browser/imageGallery.js +6 -0
- package/src/plugins/browser/videoGallery.js +6 -0
- package/src/plugins/modal/embed.js +41 -1
- package/src/plugins/modal/image/index.js +2 -1
- package/types/core/schema/options.d.ts +27 -35
- package/types/modules/contract/Browser.d.ts +7 -7
- package/types/modules/ui/ModalAnchorEditor.d.ts +6 -1
- package/types/plugins/browser/audioGallery.d.ts +16 -0
- package/types/plugins/browser/fileBrowser.d.ts +16 -0
- package/types/plugins/browser/fileGallery.d.ts +16 -0
- package/types/plugins/browser/imageGallery.d.ts +16 -0
- package/types/plugins/browser/videoGallery.d.ts +16 -0
- package/types/plugins/modal/embed.d.ts +34 -0
package/package.json
CHANGED
|
@@ -77,18 +77,10 @@ class HTML {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const stylesMap = new Map();
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
line: options.get('_lineStylesRegExp'),
|
|
83
|
-
};
|
|
84
|
-
this.#textStyleTags.forEach((v) => {
|
|
85
|
-
stylesObj[v] = options.get('_textStylesRegExp');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
for (const key in stylesObj) {
|
|
89
|
-
stylesMap.set(new RegExp(`^(${key})$`), stylesObj[key]);
|
|
80
|
+
for (const key in splitTagStyles) {
|
|
81
|
+
stylesMap.set(new RegExp(`^(${key})$`), splitTagStyles[key]);
|
|
90
82
|
}
|
|
91
|
-
this.#cleanStyleTagKeyRegExp = new RegExp(`^(${Object.keys(
|
|
83
|
+
this.#cleanStyleTagKeyRegExp = new RegExp(`^(${Object.keys(splitTagStyles).join('|')})$`, 'i');
|
|
92
84
|
this.#cleanStyleRegExpMap = stylesMap;
|
|
93
85
|
|
|
94
86
|
// font size unit
|
|
@@ -1966,12 +1958,12 @@ class HTML {
|
|
|
1966
1958
|
v.push(sv[0]);
|
|
1967
1959
|
}
|
|
1968
1960
|
} else if (!v || !_RE_STYLE_EQ.test(v.toString())) {
|
|
1969
|
-
if (this.#
|
|
1961
|
+
if (this.#cleanStyleTagKeyRegExp.test(tagName)) {
|
|
1970
1962
|
v = this.#cleanStyle(m, v, tagName);
|
|
1971
1963
|
} else if (this.#$.format.isLine(tagName)) {
|
|
1972
|
-
v = this.#cleanStyle(m, v, 'line');
|
|
1973
|
-
} else if (this.#
|
|
1974
|
-
v = this.#cleanStyle(m, v,
|
|
1964
|
+
v = this.#cleanStyle(m, v, '@line');
|
|
1965
|
+
} else if (this.#textStyleTags.includes(tagName)) {
|
|
1966
|
+
v = this.#cleanStyle(m, v, '@text');
|
|
1975
1967
|
}
|
|
1976
1968
|
}
|
|
1977
1969
|
|
|
@@ -67,6 +67,8 @@ export const DEFAULTS = {
|
|
|
67
67
|
'vertical-align|visibility|' +
|
|
68
68
|
'white-space|word-break|word-wrap',
|
|
69
69
|
TAG_STYLES: {
|
|
70
|
+
'@text': 'font-family|font-size|color|background-color|width|height',
|
|
71
|
+
'@line': 'text-align|margin|margin-left|margin-right|line-height',
|
|
70
72
|
'table|th|td': 'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
|
|
71
73
|
'table|td': 'width',
|
|
72
74
|
tr: 'height',
|
|
@@ -78,8 +80,6 @@ export const DEFAULTS = {
|
|
|
78
80
|
'img|video|iframe': 'transform|transform-origin|width|min-width|max-width|height|min-height|max-height|float|margin|margin-top',
|
|
79
81
|
hr: '',
|
|
80
82
|
},
|
|
81
|
-
SPAN_STYLES: 'font-family|font-size|color|background-color|width|height',
|
|
82
|
-
LINE_STYLES: 'text-align|margin|margin-left|margin-right|line-height',
|
|
83
83
|
|
|
84
84
|
RETAIN_STYLE_MODE: ['repeat', 'always', 'none'],
|
|
85
85
|
};
|
|
@@ -317,7 +317,7 @@ export const DEFAULTS = {
|
|
|
317
317
|
* - `classFilter`: Filters disallowed CSS class names (`allowedClassName`)
|
|
318
318
|
* - `textStyleTagFilter`: Filters text style tags (b, i, u, span, etc.)
|
|
319
319
|
* - `attrFilter`: Filters disallowed HTML attributes (`attributeWhitelist`/`attributeBlacklist`)
|
|
320
|
-
* - `styleFilter`: Filters disallowed inline styles (`
|
|
320
|
+
* - `styleFilter`: Filters disallowed inline styles (per-tag from `tagStyles`)
|
|
321
321
|
* ```js
|
|
322
322
|
* // disable only attribute and style filters
|
|
323
323
|
* {
|
|
@@ -393,19 +393,23 @@ export const DEFAULTS = {
|
|
|
393
393
|
* ```js
|
|
394
394
|
* { allUsedStyles: 'color|background-color|text-shadow' }
|
|
395
395
|
* ```
|
|
396
|
-
* @property {Object<string, string>} [tagStyles={}] - Specifies allowed styles
|
|
396
|
+
* @property {Object<string, string>} [tagStyles={}] - Specifies allowed CSS styles per HTML tag.
|
|
397
|
+
* - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`).
|
|
398
|
+
* - Value is a pipe-delimited list of allowed style names.
|
|
399
|
+
* - Resolution order when filtering an element: explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
|
|
400
|
+
* - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
|
|
401
|
+
* - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
|
|
397
402
|
* ```js
|
|
398
403
|
* {
|
|
399
404
|
* tagStyles: {
|
|
400
|
-
* '
|
|
401
|
-
*
|
|
405
|
+
* '@text': 'color|font-size|background-color', // default for span, b, i, em, ...
|
|
406
|
+
* '@line': 'text-align|margin|line-height', // default for p, h1-h6, div, li, ...
|
|
407
|
+
* 'table|td': 'border|color|background-color', // per-tag whitelist
|
|
408
|
+
* div: 'color', // explicit override; ignores `@line` default
|
|
409
|
+
* hr: 'border-top',
|
|
402
410
|
* }
|
|
403
411
|
* }
|
|
404
412
|
* ```
|
|
405
|
-
* @property {string} [spanStyles=CONSTANTS.SPAN_STYLES] - Specifies allowed styles for the `span` tag.
|
|
406
|
-
* - The default follows {@link DEFAULTS.SPAN_STYLES}
|
|
407
|
-
* @property {string} [lineStyles=CONSTANTS.LINE_STYLES] - Specifies allowed styles for the `line` element (p..).
|
|
408
|
-
* - The default follows {@link DEFAULTS.LINE_STYLES}
|
|
409
413
|
* @property {Array<string>} [fontSizeUnits=CONSTANTS.SIZE_UNITS] - Allowed font size units.
|
|
410
414
|
* - The default follows {@link DEFAULTS.SIZE_UNITS}
|
|
411
415
|
* @property {"repeat"|"always"|"none"} [retainStyleMode="repeat"] - Determines how inline elements (e.g. `<span>`, `<strong>`) are handled when deleting text.
|
|
@@ -625,8 +629,6 @@ export const DEFAULTS = {
|
|
|
625
629
|
* @property {string[]} [_reverseCommandArray] - Internal key shortcut matcher for reverse commands.
|
|
626
630
|
* @property {string} [_subMode] - Sub toolbar mode (e.g., `balloon`).
|
|
627
631
|
* @property {string[]} [_textStyleTags] - Tag names used for text styling, plus span/li.
|
|
628
|
-
* @property {RegExp} [_textStylesRegExp] - Regex to match inline styles (e.g., fontSize, color).
|
|
629
|
-
* @property {RegExp} [_lineStylesRegExp] - Regex to match line styles (e.g., text-align, padding).
|
|
630
632
|
* @property {Object<string, string>} [_defaultStyleTagMap] - Mapping HTML tag => standard tag.
|
|
631
633
|
* @property {Object<string, string>} [_styleCommandMap] - Mapping HTML tag => command (e.g., bold, underline).
|
|
632
634
|
* @property {Object<string, string>} [_defaultTagCommand] - Mapping command => preferred tag.
|
|
@@ -723,8 +725,6 @@ export const OPTION_FIXED_FLAG = {
|
|
|
723
725
|
convertTextTags: 'fixed',
|
|
724
726
|
__tagStyles: 'fixed',
|
|
725
727
|
tagStyles: 'fixed',
|
|
726
|
-
spanStyles: 'fixed',
|
|
727
|
-
lineStyles: 'fixed',
|
|
728
728
|
textDirection: true,
|
|
729
729
|
reverseButtons: 'fixed',
|
|
730
730
|
historyStackDelayTime: true,
|
|
@@ -788,7 +788,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
788
788
|
* @property {boolean} classFilter - Filters disallowed CSS class names (`allowedClassName`)
|
|
789
789
|
* @property {boolean} textStyleTagFilter - Filters text style tags (b, i, u, span, etc.)
|
|
790
790
|
* @property {boolean} attrFilter - Filters disallowed HTML attributes (`attributeWhitelist`/`attributeBlacklist`)
|
|
791
|
-
* @property {boolean} styleFilter - Filters disallowed inline styles (`
|
|
791
|
+
* @property {boolean} styleFilter - Filters disallowed inline styles (per-tag from `tagStyles`)
|
|
792
792
|
*/
|
|
793
793
|
|
|
794
794
|
/**
|
|
@@ -516,8 +516,6 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
516
516
|
return _default;
|
|
517
517
|
}, {}),
|
|
518
518
|
);
|
|
519
|
-
o.set('_textStylesRegExp', new RegExp(`\\s*[^-a-zA-Z](${DEFAULTS.SPAN_STYLES}${options.spanStyles ? '|' + options.spanStyles : ''})\\s*:[^;]+(?!;)*`, 'gi'));
|
|
520
|
-
o.set('_lineStylesRegExp', new RegExp(`\\s*[^-a-zA-Z](${DEFAULTS.LINE_STYLES}${options.lineStyles ? '|' + options.lineStyles : ''})\\s*:[^;]+(?!;)*`, 'gi'));
|
|
521
519
|
o.set('_defaultStyleTagMap', {
|
|
522
520
|
strong: textTags.bold,
|
|
523
521
|
b: textTags.bold,
|
|
@@ -760,21 +758,13 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
760
758
|
|
|
761
759
|
/** Create all used styles */
|
|
762
760
|
const allUsedStyles = new Set(DEFAULTS.CONTENT_STYLES.split('|'));
|
|
763
|
-
const _ss = options.spanStyles?.split('|') || [];
|
|
764
761
|
const _ls = o.get('__listCommonStyle');
|
|
765
|
-
const _dts = DEFAULTS.SPAN_STYLES.split('|');
|
|
766
|
-
for (let i = 0, len = _dts.length; i < len; i++) {
|
|
767
|
-
allUsedStyles.add(_dts[i]);
|
|
768
|
-
}
|
|
769
762
|
for (const _ts of Object.values(o.get('tagStyles'))) {
|
|
770
763
|
const _tss = _ts.split('|');
|
|
771
764
|
for (let i = 0, len = _tss.length; i < len; i++) {
|
|
772
765
|
allUsedStyles.add(_tss[i]);
|
|
773
766
|
}
|
|
774
767
|
}
|
|
775
|
-
for (let i = 0, len = _ss.length; i < len; i++) {
|
|
776
|
-
allUsedStyles.add(_ss[i]);
|
|
777
|
-
}
|
|
778
768
|
for (let i = 0, len = _ls.length; i < len; i++) {
|
|
779
769
|
allUsedStyles.add(_ls[i]);
|
|
780
770
|
}
|
|
@@ -40,7 +40,7 @@ import ApiManager from '../manager/ApiManager';
|
|
|
40
40
|
* ]
|
|
41
41
|
* }
|
|
42
42
|
* ```
|
|
43
|
-
* @property {Object<string, string>} [
|
|
43
|
+
* @property {Object<string, string>} [searchHeaders] - File server search http header. Optional. Can be overridden in browser.
|
|
44
44
|
* @property {string} [listClass] - Class name of list div. Required. Can be overridden in browser.
|
|
45
45
|
* @property {(item: BrowserFile) => string} [drawItemHandler] - Function that returns HTML string for rendering each file item. Required. Can be overridden in browser.
|
|
46
46
|
* ```js
|
|
@@ -114,9 +114,9 @@ class Browser {
|
|
|
114
114
|
this.listClass = params.listClass || 'se-preview-list';
|
|
115
115
|
this.directData = params.data;
|
|
116
116
|
this.url = params.url;
|
|
117
|
-
this.
|
|
117
|
+
this.headers = params.headers;
|
|
118
118
|
this.searchUrl = params.searchUrl;
|
|
119
|
-
this.
|
|
119
|
+
this.searchHeaders = params.searchHeaders;
|
|
120
120
|
this.drawItemHandler = (params.drawItemHandler || DrawItems).bind({ thumbnail: params.thumbnail, props: params.props || [] });
|
|
121
121
|
this.selectorHandler = params.selectorHandler;
|
|
122
122
|
this.columnSize = params.columnSize || 4;
|
|
@@ -176,7 +176,7 @@ class Browser {
|
|
|
176
176
|
* @param {string} [params.listClass] - Class name of list div. If not, use `this.listClass`.
|
|
177
177
|
* @param {string} [params.title] - File browser window title. If not, use `this.title`.
|
|
178
178
|
* @param {string} [params.url] - File server url. If not, use `this.url`.
|
|
179
|
-
* @param {Object<string, string>} [params.
|
|
179
|
+
* @param {Object<string, string>} [params.headers] - File server http header. If not, use `this.headers`.
|
|
180
180
|
* @example
|
|
181
181
|
* // Open with default settings (configured at construction):
|
|
182
182
|
* this.browser.open();
|
|
@@ -185,7 +185,7 @@ class Browser {
|
|
|
185
185
|
* this.browser.open({
|
|
186
186
|
* title: 'Select a video',
|
|
187
187
|
* url: '/api/videos',
|
|
188
|
-
*
|
|
188
|
+
* headers: { Authorization: 'Bearer token' },
|
|
189
189
|
* });
|
|
190
190
|
*/
|
|
191
191
|
open(params = {}) {
|
|
@@ -205,7 +205,7 @@ class Browser {
|
|
|
205
205
|
if (this.directData) {
|
|
206
206
|
this.#drowItems(this.directData);
|
|
207
207
|
} else {
|
|
208
|
-
this.#drawFileList(params.url || this.url, params.
|
|
208
|
+
this.#drawFileList(params.url || this.url, params.headers || this.headers, false);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
this.body.style.maxHeight = dom.utils.getClientSize().h - (this.#$.offset.getGlobal(this.body).top - _w.scrollY) - 20 + 'px';
|
|
@@ -246,7 +246,8 @@ class Browser {
|
|
|
246
246
|
search(keyword) {
|
|
247
247
|
if (this.searchUrl) {
|
|
248
248
|
this.keyword = keyword;
|
|
249
|
-
this
|
|
249
|
+
const sep = this.searchUrl.includes('?') ? '&' : '?';
|
|
250
|
+
this.#drawFileList(this.searchUrl + sep + 'keyword=' + _w.encodeURIComponent(keyword), this.searchHeaders, false);
|
|
250
251
|
} else {
|
|
251
252
|
this.keyword = keyword.toLowerCase();
|
|
252
253
|
this.#drawListItem(this.#allItems.length > 0 ? this.#allItems : this.items, false);
|
|
@@ -302,11 +303,11 @@ class Browser {
|
|
|
302
303
|
/**
|
|
303
304
|
* @description Fetches the file list from the server.
|
|
304
305
|
* @param {string} url - The file server URL.
|
|
305
|
-
* @param {Object<string, string>}
|
|
306
|
+
* @param {Object<string, string>} headers - The HTTP headers for the request.
|
|
306
307
|
* @param {boolean} pageLoading - Indicates if this is a paginated request.
|
|
307
308
|
*/
|
|
308
|
-
#drawFileList(url,
|
|
309
|
-
this.apiManager.call({ method: 'GET', url, headers
|
|
309
|
+
#drawFileList(url, headers, pageLoading) {
|
|
310
|
+
this.apiManager.call({ method: 'GET', url, headers, callBack: this.#CallBackGet.bind(this), errorCallBack: this.#CallBackError.bind(this) });
|
|
310
311
|
if (!pageLoading) {
|
|
311
312
|
this.sideOpenBtn.style.display = 'none';
|
|
312
313
|
this.showBrowserLoading();
|
|
@@ -601,7 +602,7 @@ class Browser {
|
|
|
601
602
|
this.selectedTags = [];
|
|
602
603
|
|
|
603
604
|
if (typeof data === 'string') {
|
|
604
|
-
this.#drawFileList(data, this.
|
|
605
|
+
this.#drawFileList(data, this.headers, true);
|
|
605
606
|
} else {
|
|
606
607
|
this.#drawListItem(data, true);
|
|
607
608
|
}
|
|
@@ -1553,6 +1553,7 @@ class Figure {
|
|
|
1553
1553
|
this.setAlign(this._element, value);
|
|
1554
1554
|
this.selectMenu_align.close();
|
|
1555
1555
|
this.#$.component.select(this._element, this.kind);
|
|
1556
|
+
this.#$.history.push(false);
|
|
1556
1557
|
}
|
|
1557
1558
|
|
|
1558
1559
|
/**
|
|
@@ -1561,6 +1562,7 @@ class Figure {
|
|
|
1561
1562
|
#SetMenuAs(value) {
|
|
1562
1563
|
this.convertAsFormat(this._element, value);
|
|
1563
1564
|
this.selectMenu_as.close();
|
|
1565
|
+
this.#$.history.push(false);
|
|
1564
1566
|
}
|
|
1565
1567
|
|
|
1566
1568
|
/**
|
|
@@ -1583,6 +1585,7 @@ class Figure {
|
|
|
1583
1585
|
|
|
1584
1586
|
this.selectMenu_resize.close();
|
|
1585
1587
|
this.#$.component.select(this._element, this.kind);
|
|
1588
|
+
this.#$.history.push(false);
|
|
1586
1589
|
}
|
|
1587
1590
|
|
|
1588
1591
|
#OffFigureContainer() {
|
|
@@ -10,6 +10,9 @@ const DIRECTION_CURSOR_MAP = { w: 'ns-resize', h: 'ew-resize', c: 'nwse-resize',
|
|
|
10
10
|
class Modal {
|
|
11
11
|
#$;
|
|
12
12
|
|
|
13
|
+
/** @type {Node} */
|
|
14
|
+
#targetElement;
|
|
15
|
+
|
|
13
16
|
/** @type {HTMLElement} */
|
|
14
17
|
#modalArea;
|
|
15
18
|
/** @type {HTMLElement} */
|
|
@@ -175,6 +178,8 @@ class Modal {
|
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
if (this.focusElement) this.focusElement.focus();
|
|
181
|
+
|
|
182
|
+
this.#targetElement = this.#$.component.currentTarget;
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
/**
|
|
@@ -205,7 +210,13 @@ class Modal {
|
|
|
205
210
|
this.inst.modalInit?.();
|
|
206
211
|
this.inst.modalOff?.(this.isUpdate);
|
|
207
212
|
|
|
208
|
-
if (!this.isUpdate)
|
|
213
|
+
if (!this.isUpdate) {
|
|
214
|
+
this.#$.focusManager.focus();
|
|
215
|
+
} else if (this.#targetElement) {
|
|
216
|
+
this.inst.componentSelect?.(this.#targetElement);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.#targetElement = null;
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
/**
|
|
@@ -209,6 +209,8 @@ class ModalAnchorEditor {
|
|
|
209
209
|
/**
|
|
210
210
|
* @description Creates an anchor (`<a>`) element with the specified attributes.
|
|
211
211
|
* @param {boolean} notText - If `true`, the anchor will not contain text content.
|
|
212
|
+
* @param {?string} [urlOverride] - Fallback URL used when the modal's `linkValue` is empty
|
|
213
|
+
* - (e.g., when called outside the modal lifecycle such as from `retainFormat`)
|
|
212
214
|
* @returns {HTMLElement|null} - The newly created anchor element, or `null` if the URL is empty.
|
|
213
215
|
* @example
|
|
214
216
|
* // In a link plugin — create anchor with text content:
|
|
@@ -221,11 +223,14 @@ class ModalAnchorEditor {
|
|
|
221
223
|
* if (anchor) {
|
|
222
224
|
* anchor.appendChild(imgElement);
|
|
223
225
|
* }
|
|
226
|
+
*
|
|
227
|
+
* // Preserve an existing parent anchor's href when rebuilding outside the modal:
|
|
228
|
+
* const anchor = this.anchor.create(true, parentAnchor.href);
|
|
224
229
|
*/
|
|
225
|
-
create(notText) {
|
|
226
|
-
|
|
230
|
+
create(notText, urlOverride) {
|
|
231
|
+
const url = this.linkValue || urlOverride || '';
|
|
232
|
+
if (url.length === 0) return null;
|
|
227
233
|
|
|
228
|
-
const url = this.linkValue;
|
|
229
234
|
const displayText = this.displayInput.value.length === 0 ? url : this.displayInput.value;
|
|
230
235
|
|
|
231
236
|
const oA = /** @type {HTMLAnchorElement} */ (this.currentTarget || dom.utils.createElement('A'));
|
|
@@ -19,6 +19,10 @@ import { Browser } from '../../modules/contract';
|
|
|
19
19
|
* }
|
|
20
20
|
* ```
|
|
21
21
|
* @property {Object<string, string>} [headers] - Server request headers
|
|
22
|
+
* @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
|
|
23
|
+
* as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
|
|
24
|
+
* already-loaded items locally.
|
|
25
|
+
* @property {Object<string, string>} [searchHeaders] - Server-side search request headers
|
|
22
26
|
* @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
|
|
23
27
|
*/
|
|
24
28
|
|
|
@@ -51,6 +55,8 @@ class AudioGallery extends PluginBrowser {
|
|
|
51
55
|
data: pluginOptions.data,
|
|
52
56
|
url: pluginOptions.url,
|
|
53
57
|
headers: pluginOptions.headers,
|
|
58
|
+
searchUrl: pluginOptions.searchUrl,
|
|
59
|
+
searchHeaders: pluginOptions.searchHeaders,
|
|
54
60
|
selectorHandler: this.#SetItem.bind(this),
|
|
55
61
|
columnSize: 4,
|
|
56
62
|
className: 'se-audio-gallery',
|
|
@@ -26,6 +26,10 @@ import { Browser } from '../../modules/contract';
|
|
|
26
26
|
* }
|
|
27
27
|
* ```
|
|
28
28
|
* @property {Object<string, string>} [headers] - Server request headers
|
|
29
|
+
* @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
|
|
30
|
+
* as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
|
|
31
|
+
* already-loaded items locally.
|
|
32
|
+
* @property {Object<string, string>} [searchHeaders] - Server-side search request headers
|
|
29
33
|
* @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail URL or a function that returns a thumbnail URL per item.
|
|
30
34
|
* @property {number} [expand=1] - Initial folder expand depth. `1` expands the first level, `Infinity` expands all. Default: `1`.
|
|
31
35
|
* @property {Array<string>} [props] - Additional tag names
|
|
@@ -64,6 +68,8 @@ class FileBrowser extends PluginBrowser {
|
|
|
64
68
|
data: pluginOptions.data,
|
|
65
69
|
url: pluginOptions.url,
|
|
66
70
|
headers: pluginOptions.headers,
|
|
71
|
+
searchUrl: pluginOptions.searchUrl,
|
|
72
|
+
searchHeaders: pluginOptions.searchHeaders,
|
|
67
73
|
selectorHandler: this.#SetItem.bind(this),
|
|
68
74
|
columnSize: 4,
|
|
69
75
|
className: 'se-file-browser',
|
|
@@ -20,6 +20,10 @@ import { Browser } from '../../modules/contract';
|
|
|
20
20
|
* }
|
|
21
21
|
* ```
|
|
22
22
|
* @property {Object<string, string>} [headers] - Server request headers
|
|
23
|
+
* @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
|
|
24
|
+
* as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
|
|
25
|
+
* already-loaded items locally.
|
|
26
|
+
* @property {Object<string, string>} [searchHeaders] - Server-side search request headers
|
|
23
27
|
* @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
|
|
24
28
|
*/
|
|
25
29
|
|
|
@@ -52,6 +56,8 @@ class FileGallery extends PluginBrowser {
|
|
|
52
56
|
data: pluginOptions.data,
|
|
53
57
|
url: pluginOptions.url,
|
|
54
58
|
headers: pluginOptions.headers,
|
|
59
|
+
searchUrl: pluginOptions.searchUrl,
|
|
60
|
+
searchHeaders: pluginOptions.searchHeaders,
|
|
55
61
|
selectorHandler: this.#SetItem.bind(this),
|
|
56
62
|
columnSize: 4,
|
|
57
63
|
className: 'se-file-gallery',
|
|
@@ -20,6 +20,10 @@ import { Browser } from '../../modules/contract';
|
|
|
20
20
|
* }
|
|
21
21
|
* ```
|
|
22
22
|
* @property {Object<string, string>} [headers] - Server request headers
|
|
23
|
+
* @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
|
|
24
|
+
* as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
|
|
25
|
+
* already-loaded items locally.
|
|
26
|
+
* @property {Object<string, string>} [searchHeaders] - Server-side search request headers
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -50,6 +54,8 @@ class ImageGallery extends PluginBrowser {
|
|
|
50
54
|
data: pluginOptions.data,
|
|
51
55
|
url: pluginOptions.url,
|
|
52
56
|
headers: pluginOptions.headers,
|
|
57
|
+
searchUrl: pluginOptions.searchUrl,
|
|
58
|
+
searchHeaders: pluginOptions.searchHeaders,
|
|
53
59
|
selectorHandler: this.#SetItem.bind(this),
|
|
54
60
|
columnSize: 4,
|
|
55
61
|
className: 'se-image-gallery',
|
|
@@ -20,6 +20,10 @@ import { Browser } from '../../modules/contract';
|
|
|
20
20
|
* }
|
|
21
21
|
* ```
|
|
22
22
|
* @property {Object<string, string>} [headers] - Server request headers
|
|
23
|
+
* @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
|
|
24
|
+
* as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
|
|
25
|
+
* already-loaded items locally.
|
|
26
|
+
* @property {Object<string, string>} [searchHeaders] - Server-side search request headers
|
|
23
27
|
* @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
|
|
24
28
|
*/
|
|
25
29
|
|
|
@@ -52,6 +56,8 @@ class VideoGallery extends PluginBrowser {
|
|
|
52
56
|
data: pluginOptions.data,
|
|
53
57
|
url: pluginOptions.url,
|
|
54
58
|
headers: pluginOptions.headers,
|
|
59
|
+
searchUrl: pluginOptions.searchUrl,
|
|
60
|
+
searchHeaders: pluginOptions.searchHeaders,
|
|
55
61
|
selectorHandler: this.#SetItem.bind(this),
|
|
56
62
|
columnSize: 4,
|
|
57
63
|
className: 'se-video-gallery',
|
|
@@ -39,6 +39,17 @@ const { _w, NO_EVENT } = env;
|
|
|
39
39
|
* { query_vimeo: 'autoplay=1' }
|
|
40
40
|
* ```
|
|
41
41
|
* @property {Array<RegExp>} [urlPatterns] - Additional URL patterns to recognize as embeddable content.
|
|
42
|
+
* @property {Array<RegExp|string>} [scriptSrcWhitelist] - Allowed `<script src=...>` patterns for raw embed HTML
|
|
43
|
+
* - (e.g. Twitter blockquote + `widgets.js`). Each entry is a `RegExp` (tested against the full src) or a `string` (matched via `startsWith`).
|
|
44
|
+
* - Defaults to `[]` — all script tags are rejected.** Inline scripts (no `src`) are always rejected.
|
|
45
|
+
* ```js
|
|
46
|
+
* {
|
|
47
|
+
* scriptSrcWhitelist: [
|
|
48
|
+
* /^https:\/\/platform\.x\.com\/widgets\.js$/,
|
|
49
|
+
* /^https:\/\/www\.instagram\.com\/embed\.js$/,
|
|
50
|
+
* ]
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
42
53
|
* @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom embed service definitions.
|
|
43
54
|
* Each key is a service name, with `pattern` to match the URL, `action` to transform it into an embed URL, and `tag` for the output element.
|
|
44
55
|
* ```js
|
|
@@ -125,6 +136,18 @@ class Embed extends PluginModal {
|
|
|
125
136
|
return false;
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @description Validate a `<script src>` against the configured whitelist for raw embed HTML.
|
|
141
|
+
* Inline scripts (no `src`) are always rejected.
|
|
142
|
+
* @param {string} src
|
|
143
|
+
* @param {Array<RegExp|string>} whitelist
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
static #isAllowedScriptSrc(src, whitelist) {
|
|
147
|
+
if (!src) return false;
|
|
148
|
+
return whitelist.some((p) => (p instanceof RegExp ? p.test(src) : src.startsWith(p)));
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
/** @type {Array<RegExp>} */
|
|
129
152
|
static #urlPatterns = null;
|
|
130
153
|
|
|
@@ -168,6 +191,7 @@ class Embed extends PluginModal {
|
|
|
168
191
|
iframeTagAttributes: pluginOptions.iframeTagAttributes || null,
|
|
169
192
|
query_youtube: pluginOptions.query_youtube || '',
|
|
170
193
|
query_vimeo: pluginOptions.query_vimeo || '',
|
|
194
|
+
scriptSrcWhitelist: Array.isArray(pluginOptions.scriptSrcWhitelist) ? pluginOptions.scriptSrcWhitelist : [],
|
|
171
195
|
insertBehavior: pluginOptions.insertBehavior,
|
|
172
196
|
};
|
|
173
197
|
|
|
@@ -446,6 +470,19 @@ class Embed extends PluginModal {
|
|
|
446
470
|
if (/^<iframe\s|^<blockquote\s/i.test(src)) {
|
|
447
471
|
const embedDOM = new DOMParser().parseFromString(src, 'text/html').body.children;
|
|
448
472
|
if (embedDOM.length === 0) return false;
|
|
473
|
+
|
|
474
|
+
// Validate every iframe in the raw HTML against the URL whitelist.
|
|
475
|
+
for (let i = 0; i < embedDOM.length; i++) {
|
|
476
|
+
const node = /** @type {Element} */ (embedDOM[i]);
|
|
477
|
+
if (/^iframe$/i.test(node.nodeName) && !Embed.#checkContentType(node.getAttribute('src') || '')) return false;
|
|
478
|
+
const nested = node.querySelectorAll?.('iframe');
|
|
479
|
+
if (nested) {
|
|
480
|
+
for (let j = 0; j < nested.length; j++) {
|
|
481
|
+
if (!Embed.#checkContentType(nested[j].getAttribute('src') || '')) return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
449
486
|
embedInfo = { children: embedDOM, ...this.#getInfo(), process: null };
|
|
450
487
|
} else {
|
|
451
488
|
const processUrl = this.findProcessUrl(src);
|
|
@@ -604,9 +641,12 @@ class Embed extends PluginModal {
|
|
|
604
641
|
container = figure.container;
|
|
605
642
|
|
|
606
643
|
const childNodes = Array.from(children);
|
|
644
|
+
const scriptWhitelist = this.pluginOptions.scriptSrcWhitelist;
|
|
607
645
|
for (const chd of childNodes) {
|
|
608
646
|
if (/^script$/i.test(chd.nodeName)) {
|
|
609
|
-
|
|
647
|
+
const scriptSrc = /** @type {Element} */ (chd).getAttribute('src') || '';
|
|
648
|
+
if (!Embed.#isAllowedScriptSrc(scriptSrc, scriptWhitelist)) continue;
|
|
649
|
+
scriptTag = dom.utils.createElement('script', { src: scriptSrc, async: 'true' }, null);
|
|
610
650
|
continue;
|
|
611
651
|
}
|
|
612
652
|
cover.appendChild(chd);
|
|
@@ -774,7 +774,7 @@ class Image_ extends PluginModal {
|
|
|
774
774
|
|
|
775
775
|
// link
|
|
776
776
|
let isNewAnchor = null;
|
|
777
|
-
const anchor = this.anchor.create(true);
|
|
777
|
+
const anchor = this.anchor.create(true, dom.check.isAnchor(this.#element.parentElement) ? this.#element.parentElement.href : null);
|
|
778
778
|
if (anchor) {
|
|
779
779
|
if (this.#linkElement !== anchor || (isNewContainer && !container.contains(anchor))) {
|
|
780
780
|
this.#linkElement = anchor.cloneNode(false);
|
|
@@ -839,6 +839,7 @@ class Image_ extends PluginModal {
|
|
|
839
839
|
*/
|
|
840
840
|
#setAnchor(imgTag, anchor) {
|
|
841
841
|
if (anchor) {
|
|
842
|
+
/** @type {HTMLAnchorElement} */ (anchor).setAttribute('data-se-non-link', 'true');
|
|
842
843
|
anchor.appendChild(imgTag);
|
|
843
844
|
return anchor;
|
|
844
845
|
}
|