suneditor 3.1.2 → 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/README.md +1 -1
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +9 -6
- package/src/assets/suneditor.css +28 -2
- package/src/core/event/handlers/handler_ww_dragDrop.js +20 -0
- package/src/core/logic/dom/html.js +7 -15
- package/src/core/logic/panel/menu.js +4 -0
- package/src/core/logic/shell/ui.js +13 -2
- package/src/core/schema/options.js +16 -16
- package/src/core/section/constructor.js +29 -22
- package/src/core/section/documentType.js +55 -29
- package/src/modules/contract/Browser.js +15 -11
- package/src/modules/contract/Controller.js +3 -0
- package/src/modules/contract/Figure.js +26 -4
- package/src/modules/contract/Modal.js +14 -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/dropdown/table/services/table.style.js +21 -12
- package/src/plugins/modal/embed.js +41 -1
- package/src/plugins/modal/image/index.js +2 -1
- package/types/core/schema/options.d.ts +29 -37
- package/types/core/section/documentType.d.ts +17 -1
- 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
|
@@ -162,28 +162,19 @@ class DocumentType {
|
|
|
162
162
|
|
|
163
163
|
// page break
|
|
164
164
|
let pageBreakHeight = 0;
|
|
165
|
-
|
|
166
|
-
let additionalPages = 0;
|
|
165
|
+
const breakPoints = [];
|
|
167
166
|
if (pageBreaks.length > 0) {
|
|
168
167
|
pageBreakHeight = pageBreaks[0].offsetHeight;
|
|
169
168
|
for (let i = 0; i < pageBreaks.length; i++) {
|
|
170
|
-
|
|
171
|
-
const sectionHeight = breakPosition - lastBreakPosition;
|
|
172
|
-
if (sectionHeight % A4_PAGE_HEIGHT !== 0) additionalPages++;
|
|
173
|
-
lastBreakPosition = breakPosition;
|
|
169
|
+
breakPoints.push({ top: pageBreaks[i].offsetTop, end: pageBreaks[i].offsetTop + pageBreakHeight / 2 });
|
|
174
170
|
}
|
|
175
|
-
|
|
176
|
-
const lastSectionHeight = mirrorHeight - lastBreakPosition;
|
|
177
|
-
if (lastSectionHeight > 0 && lastSectionHeight % A4_PAGE_HEIGHT !== 0) additionalPages++;
|
|
178
171
|
}
|
|
179
172
|
|
|
180
173
|
const scrollTop = !this.#isScrollable(this.#fc) ? 0 : this._getWWScrollTop();
|
|
181
|
-
const
|
|
182
|
-
const wwWidth = this.#wwFrame.offsetWidth + 1;
|
|
183
|
-
const pages = [];
|
|
174
|
+
const pages = [{ number: 0, top: 0 }];
|
|
184
175
|
|
|
185
|
-
for (let i = 0; i <
|
|
186
|
-
pages.push({ number:
|
|
176
|
+
for (let i = 0; i < breakPoints.length; i++) {
|
|
177
|
+
pages.push({ number: 0, top: breakPoints[i].top + pageBreakHeight / 2, isBreak: true });
|
|
187
178
|
}
|
|
188
179
|
|
|
189
180
|
this.#mirrorCache = 0;
|
|
@@ -191,14 +182,27 @@ class DocumentType {
|
|
|
191
182
|
const mChr = this.#mirror.children;
|
|
192
183
|
this._initializeCache(mChr);
|
|
193
184
|
|
|
194
|
-
|
|
185
|
+
// Calculate page positions per section (between page breaks)
|
|
186
|
+
const sectionStarts = [0, ...breakPoints.map((b) => b.end)];
|
|
187
|
+
const sectionEnds = [...breakPoints.map((b) => b.top), mirrorHeight];
|
|
188
|
+
|
|
189
|
+
for (let s = 0; s < sectionStarts.length; s++) {
|
|
190
|
+
const sStart = sectionStarts[s];
|
|
191
|
+
const sEnd = sectionEnds[s];
|
|
192
|
+
let t = sStart;
|
|
193
|
+
let isFirst = s === 0;
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
195
|
+
while (true) {
|
|
196
|
+
t += A4_PAGE_HEIGHT + (isFirst ? this.#paddingTop + this.#paddingBottom : this.#paddingTop);
|
|
197
|
+
isFirst = false;
|
|
198
|
+
|
|
199
|
+
if (t >= sEnd) break;
|
|
200
|
+
|
|
201
|
+
if (!pages.some((p) => Math.abs(p.top - t) < 3)) {
|
|
202
|
+
const top = this._calcPageBreakTop(t, chr, mChr, pages);
|
|
203
|
+
if (top === null) break;
|
|
204
|
+
pages.push({ number: 0, top });
|
|
205
|
+
}
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -214,8 +218,8 @@ class DocumentType {
|
|
|
214
218
|
this.#page.innerHTML = '';
|
|
215
219
|
this.#pages = [];
|
|
216
220
|
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
const wwWidth = this.#wwFrame.offsetWidth + 1;
|
|
222
|
+
for (let i = 0, t; i < pages.length; i++) {
|
|
219
223
|
t = pages[i].top;
|
|
220
224
|
if (mirrorHeight < t) break;
|
|
221
225
|
|
|
@@ -249,10 +253,11 @@ class DocumentType {
|
|
|
249
253
|
* @param {number} t - The initial top position value to be adjusted.
|
|
250
254
|
* @param {HTMLCollection} chr - The elements array in the current (main) page.
|
|
251
255
|
* @param {HTMLCollection} mChr - The elements array in the mirrored page.
|
|
256
|
+
* @param {Array.<{number: number, top: number, isBreak?: boolean}>} [pages] - The pages array containing page break info.
|
|
252
257
|
* @returns {number|null} The adjusted top value.
|
|
253
258
|
*/
|
|
254
|
-
_calcPageBreakTop(t, chr, mChr) {
|
|
255
|
-
const { ci } = this._getElementAtPosition(t, mChr);
|
|
259
|
+
_calcPageBreakTop(t, chr, mChr, pages) {
|
|
260
|
+
const { ci } = this._getElementAtPosition(t, mChr, pages);
|
|
256
261
|
const mel = /** @type {HTMLElement} */ (mChr[ci]);
|
|
257
262
|
const el = /** @type {HTMLElement} */ (chr[ci]);
|
|
258
263
|
if (!mel || !el) return null;
|
|
@@ -290,22 +295,39 @@ class DocumentType {
|
|
|
290
295
|
* @description Retrieves the element at a given position.
|
|
291
296
|
* @param {number} pageTop - The vertical position to check.
|
|
292
297
|
* @param {HTMLCollection} mChr - List of mirrored elements.
|
|
298
|
+
* @param {Array.<{number: number, top: number, isBreak?: boolean}>} [pages] - The pages array containing page break info for skipping break elements.
|
|
293
299
|
* @returns {{ci: number, cm: number, ch: number}} The closest element and its related data.
|
|
294
300
|
* - ci: The index of the closest element.
|
|
295
301
|
* - cm: The distance between the top of the closest element and the given position.
|
|
296
302
|
* - ch: The height of the closest element.
|
|
297
303
|
*/
|
|
298
|
-
_getElementAtPosition(pageTop, mChr) {
|
|
304
|
+
_getElementAtPosition(pageTop, mChr, pages) {
|
|
299
305
|
let start = this.#mirrorCache;
|
|
300
306
|
let end = mChr.length - 1;
|
|
301
307
|
|
|
308
|
+
// Reset cache if target position is before cached position (crossing section boundaries)
|
|
309
|
+
if (start > 0) {
|
|
310
|
+
const cachedPos = this.#positionCache.get(start);
|
|
311
|
+
if (cachedPos && pageTop < cachedPos.top) {
|
|
312
|
+
start = 0;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
while (start <= end) {
|
|
303
317
|
const mid = Math.floor((start + end) / 2);
|
|
304
318
|
const { top, height, bottom } = this.#positionCache.get(mid);
|
|
305
319
|
|
|
306
320
|
if (pageTop >= top && pageTop <= bottom) {
|
|
307
|
-
|
|
308
|
-
|
|
321
|
+
let ci = mid;
|
|
322
|
+
// Skip page break elements — use adjacent content element
|
|
323
|
+
if (pages && dom.utils.hasClass(mChr[ci], 'se-page-break')) {
|
|
324
|
+
ci = ci + 1 < mChr.length ? ci + 1 : Math.max(0, ci - 1);
|
|
325
|
+
const adjPos = this.#positionCache.get(ci);
|
|
326
|
+
this.#mirrorCache = ci;
|
|
327
|
+
return { ci, cm: pageTop - adjPos.bottom, ch: adjPos.height };
|
|
328
|
+
}
|
|
329
|
+
this.#mirrorCache = ci;
|
|
330
|
+
return { ci, cm: pageTop - bottom, ch: height };
|
|
309
331
|
}
|
|
310
332
|
|
|
311
333
|
if (pageTop < top) {
|
|
@@ -315,7 +337,11 @@ class DocumentType {
|
|
|
315
337
|
}
|
|
316
338
|
}
|
|
317
339
|
|
|
318
|
-
|
|
340
|
+
let closestIndex = mChr[start] ? start : end;
|
|
341
|
+
// Skip page break elements for closest match
|
|
342
|
+
if (pages && dom.utils.hasClass(mChr[closestIndex], 'se-page-break')) {
|
|
343
|
+
closestIndex = closestIndex + 1 < mChr.length ? closestIndex + 1 : Math.max(0, closestIndex - 1);
|
|
344
|
+
}
|
|
319
345
|
this.#mirrorCache = closestIndex;
|
|
320
346
|
const iElement = this.#positionCache.get(closestIndex);
|
|
321
347
|
return { ci: closestIndex, cm: pageTop - iElement.bottom, ch: iElement.height };
|
|
@@ -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;
|
|
@@ -151,6 +151,7 @@ class Browser {
|
|
|
151
151
|
// init
|
|
152
152
|
browserFrame.appendChild(dom.utils.createElement('DIV', { class: 'se-browser-back' }));
|
|
153
153
|
browserFrame.appendChild(content);
|
|
154
|
+
browserFrame.setAttribute('popover', 'manual');
|
|
154
155
|
this.#$.contextProvider.carrierWrapper.appendChild(browserFrame);
|
|
155
156
|
|
|
156
157
|
this.#$.eventManager.addEvent(this.tagArea, 'click', this.#OnClickTag.bind(this));
|
|
@@ -175,7 +176,7 @@ class Browser {
|
|
|
175
176
|
* @param {string} [params.listClass] - Class name of list div. If not, use `this.listClass`.
|
|
176
177
|
* @param {string} [params.title] - File browser window title. If not, use `this.title`.
|
|
177
178
|
* @param {string} [params.url] - File server url. If not, use `this.url`.
|
|
178
|
-
* @param {Object<string, string>} [params.
|
|
179
|
+
* @param {Object<string, string>} [params.headers] - File server http header. If not, use `this.headers`.
|
|
179
180
|
* @example
|
|
180
181
|
* // Open with default settings (configured at construction):
|
|
181
182
|
* this.browser.open();
|
|
@@ -184,7 +185,7 @@ class Browser {
|
|
|
184
185
|
* this.browser.open({
|
|
185
186
|
* title: 'Select a video',
|
|
186
187
|
* url: '/api/videos',
|
|
187
|
-
*
|
|
188
|
+
* headers: { Authorization: 'Bearer token' },
|
|
188
189
|
* });
|
|
189
190
|
*/
|
|
190
191
|
open(params = {}) {
|
|
@@ -197,13 +198,14 @@ class Browser {
|
|
|
197
198
|
|
|
198
199
|
this.titleArea.textContent = params.title || this.title;
|
|
199
200
|
this.area.style.display = 'block';
|
|
201
|
+
this.area.showPopover?.();
|
|
200
202
|
this.#$.ui.opendBrowser = this;
|
|
201
203
|
this.closeArrow = this.#$.options.get('_rtl') ? this.#$.icons.menu_arrow_left : this.#$.icons.menu_arrow_right;
|
|
202
204
|
|
|
203
205
|
if (this.directData) {
|
|
204
206
|
this.#drowItems(this.directData);
|
|
205
207
|
} else {
|
|
206
|
-
this.#drawFileList(params.url || this.url, params.
|
|
208
|
+
this.#drawFileList(params.url || this.url, params.headers || this.headers, false);
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
this.body.style.maxHeight = dom.utils.getClientSize().h - (this.#$.offset.getGlobal(this.body).top - _w.scrollY) - 20 + 'px';
|
|
@@ -217,6 +219,7 @@ class Browser {
|
|
|
217
219
|
this.#removeGlobalEvent();
|
|
218
220
|
this.apiManager.cancel();
|
|
219
221
|
|
|
222
|
+
this.area.hidePopover?.();
|
|
220
223
|
this.area.style.display = 'none';
|
|
221
224
|
this.selectedTags = [];
|
|
222
225
|
this.items = [];
|
|
@@ -243,7 +246,8 @@ class Browser {
|
|
|
243
246
|
search(keyword) {
|
|
244
247
|
if (this.searchUrl) {
|
|
245
248
|
this.keyword = keyword;
|
|
246
|
-
this
|
|
249
|
+
const sep = this.searchUrl.includes('?') ? '&' : '?';
|
|
250
|
+
this.#drawFileList(this.searchUrl + sep + 'keyword=' + _w.encodeURIComponent(keyword), this.searchHeaders, false);
|
|
247
251
|
} else {
|
|
248
252
|
this.keyword = keyword.toLowerCase();
|
|
249
253
|
this.#drawListItem(this.#allItems.length > 0 ? this.#allItems : this.items, false);
|
|
@@ -299,11 +303,11 @@ class Browser {
|
|
|
299
303
|
/**
|
|
300
304
|
* @description Fetches the file list from the server.
|
|
301
305
|
* @param {string} url - The file server URL.
|
|
302
|
-
* @param {Object<string, string>}
|
|
306
|
+
* @param {Object<string, string>} headers - The HTTP headers for the request.
|
|
303
307
|
* @param {boolean} pageLoading - Indicates if this is a paginated request.
|
|
304
308
|
*/
|
|
305
|
-
#drawFileList(url,
|
|
306
|
-
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) });
|
|
307
311
|
if (!pageLoading) {
|
|
308
312
|
this.sideOpenBtn.style.display = 'none';
|
|
309
313
|
this.showBrowserLoading();
|
|
@@ -598,7 +602,7 @@ class Browser {
|
|
|
598
602
|
this.selectedTags = [];
|
|
599
603
|
|
|
600
604
|
if (typeof data === 'string') {
|
|
601
|
-
this.#drawFileList(data, this.
|
|
605
|
+
this.#drawFileList(data, this.headers, true);
|
|
602
606
|
} else {
|
|
603
607
|
this.#drawListItem(data, true);
|
|
604
608
|
}
|
|
@@ -110,6 +110,7 @@ class Controller {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// add element
|
|
113
|
+
this.form.setAttribute('popover', 'manual');
|
|
113
114
|
this.#$.contextProvider.carrierWrapper.appendChild(element);
|
|
114
115
|
|
|
115
116
|
// init
|
|
@@ -394,6 +395,7 @@ class Controller {
|
|
|
394
395
|
* @description Hide controller at editor area (link button, image resize button..)
|
|
395
396
|
*/
|
|
396
397
|
#controllerOff() {
|
|
398
|
+
this.form.hidePopover?.();
|
|
397
399
|
this.form.style.display = 'none';
|
|
398
400
|
this.#$.ui.opendControllers = this.#$.ui.opendControllers.filter((v) => v.form !== this.form);
|
|
399
401
|
if (this.#$.ui.currentControllerName !== this.kind && this.#$.ui.opendControllers.length > 0) return;
|
|
@@ -464,6 +466,7 @@ class Controller {
|
|
|
464
466
|
|
|
465
467
|
controller.style.zIndex = this.toTop ? INDEX_0 : this.#reserveIndex ? INDEX_S_1 : INDEX_1;
|
|
466
468
|
controller.style.visibility = '';
|
|
469
|
+
controller.showPopover?.();
|
|
467
470
|
return true;
|
|
468
471
|
}
|
|
469
472
|
|
|
@@ -282,10 +282,18 @@ class Figure {
|
|
|
282
282
|
const cover = dom.query.getParentElement(element, 'FIGURE', 2);
|
|
283
283
|
const inlineCover = dom.query.getParentElement(element, 'SPAN', 2);
|
|
284
284
|
const anyCover = cover || inlineCover;
|
|
285
|
-
|
|
285
|
+
let target = dom.query.getParentElement(element, (current) => current.parentElement === anyCover, 0) || element;
|
|
286
|
+
|
|
287
|
+
// When image is wrapped by anchor, target becomes <a> instead of <img>
|
|
288
|
+
if (dom.check.isAnchor(target)) {
|
|
289
|
+
const imgEl = target.querySelector(':scope > img');
|
|
290
|
+
if (imgEl) {
|
|
291
|
+
target = imgEl;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
286
294
|
|
|
287
295
|
return {
|
|
288
|
-
target,
|
|
296
|
+
target: /** @type {HTMLElement} */ (target),
|
|
289
297
|
container: dom.query.getParentElement(target, Figure.is, 3) || cover,
|
|
290
298
|
cover: cover,
|
|
291
299
|
inlineCover: dom.utils.hasClass(inlineCover, 'se-inline-component') ? /** @type {HTMLElement} */ (inlineCover) : null,
|
|
@@ -734,12 +742,23 @@ class Figure {
|
|
|
734
742
|
const { container, inlineCover, target } = Figure.GetContainer(targetNode);
|
|
735
743
|
const { w, h } = this.getSize(target);
|
|
736
744
|
|
|
745
|
+
// Check if target is wrapped by an anchor
|
|
746
|
+
const anchorEl = dom.check.isAnchor(target.parentNode) ? target.parentNode : null;
|
|
747
|
+
|
|
737
748
|
const newTarget = /** @type {HTMLElement} */ (target.cloneNode(false));
|
|
738
749
|
newTarget.style.width = '';
|
|
739
750
|
newTarget.style.height = '';
|
|
740
751
|
newTarget.removeAttribute('width');
|
|
741
752
|
newTarget.removeAttribute('height');
|
|
742
753
|
|
|
754
|
+
// Preserve anchor wrapper if exists
|
|
755
|
+
let elementToInsert = newTarget;
|
|
756
|
+
if (anchorEl) {
|
|
757
|
+
const newAnchor = /** @type {HTMLElement} */ (anchorEl.cloneNode(false));
|
|
758
|
+
newAnchor.appendChild(newTarget);
|
|
759
|
+
elementToInsert = newAnchor;
|
|
760
|
+
}
|
|
761
|
+
|
|
743
762
|
switch (formatStyle) {
|
|
744
763
|
case 'inline': {
|
|
745
764
|
if (inlineCover) break;
|
|
@@ -748,7 +767,7 @@ class Figure {
|
|
|
748
767
|
const next = container.nextElementSibling;
|
|
749
768
|
const parent = container.parentElement;
|
|
750
769
|
|
|
751
|
-
const figure = Figure.CreateInlineContainer(
|
|
770
|
+
const figure = Figure.CreateInlineContainer(elementToInsert);
|
|
752
771
|
dom.utils.addClass(
|
|
753
772
|
figure.container,
|
|
754
773
|
container.className
|
|
@@ -777,7 +796,7 @@ class Figure {
|
|
|
777
796
|
dom.utils.removeItem(s.previousElementSibling);
|
|
778
797
|
}
|
|
779
798
|
|
|
780
|
-
const figure = Figure.CreateContainer(
|
|
799
|
+
const figure = Figure.CreateContainer(elementToInsert);
|
|
781
800
|
dom.utils.addClass(
|
|
782
801
|
figure.container,
|
|
783
802
|
container.className
|
|
@@ -1534,6 +1553,7 @@ class Figure {
|
|
|
1534
1553
|
this.setAlign(this._element, value);
|
|
1535
1554
|
this.selectMenu_align.close();
|
|
1536
1555
|
this.#$.component.select(this._element, this.kind);
|
|
1556
|
+
this.#$.history.push(false);
|
|
1537
1557
|
}
|
|
1538
1558
|
|
|
1539
1559
|
/**
|
|
@@ -1542,6 +1562,7 @@ class Figure {
|
|
|
1542
1562
|
#SetMenuAs(value) {
|
|
1543
1563
|
this.convertAsFormat(this._element, value);
|
|
1544
1564
|
this.selectMenu_as.close();
|
|
1565
|
+
this.#$.history.push(false);
|
|
1545
1566
|
}
|
|
1546
1567
|
|
|
1547
1568
|
/**
|
|
@@ -1564,6 +1585,7 @@ class Figure {
|
|
|
1564
1585
|
|
|
1565
1586
|
this.selectMenu_resize.close();
|
|
1566
1587
|
this.#$.component.select(this._element, this.kind);
|
|
1588
|
+
this.#$.history.push(false);
|
|
1567
1589
|
}
|
|
1568
1590
|
|
|
1569
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} */
|
|
@@ -162,6 +165,7 @@ class Modal {
|
|
|
162
165
|
|
|
163
166
|
dom.utils.addClass(this.#modalArea, 'se-backdrop-show');
|
|
164
167
|
dom.utils.addClass(this.form, 'se-modal-show');
|
|
168
|
+
this.#modalArea.showPopover?.();
|
|
165
169
|
|
|
166
170
|
if (this.#resizeBody) {
|
|
167
171
|
const offset = this.#saveOffset();
|
|
@@ -174,6 +178,8 @@ class Modal {
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
if (this.focusElement) this.focusElement.focus();
|
|
181
|
+
|
|
182
|
+
this.#targetElement = this.#$.component.currentTarget;
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
/**
|
|
@@ -197,13 +203,20 @@ class Modal {
|
|
|
197
203
|
this.#bindClose &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose);
|
|
198
204
|
|
|
199
205
|
// close
|
|
206
|
+
this.#modalArea.hidePopover?.();
|
|
200
207
|
dom.utils.removeClass(this.#modalArea, 'se-backdrop-show');
|
|
201
208
|
dom.utils.removeClass(this.form, 'se-modal-show');
|
|
202
209
|
|
|
203
210
|
this.inst.modalInit?.();
|
|
204
211
|
this.inst.modalOff?.(this.isUpdate);
|
|
205
212
|
|
|
206
|
-
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;
|
|
207
220
|
}
|
|
208
221
|
|
|
209
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',
|
|
@@ -598,16 +598,25 @@ export class TableStyleService {
|
|
|
598
598
|
this._propsCache = [];
|
|
599
599
|
|
|
600
600
|
for (let i = 0, t, isBreak; (t = targets[i]); i++) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
const {
|
|
602
|
+
cssText: t_cssText,
|
|
603
|
+
border: t_border,
|
|
604
|
+
backgroundColor: t_backgroundColor,
|
|
605
|
+
color: t_color,
|
|
606
|
+
textAlign: t_textAlign,
|
|
607
|
+
verticalAlign: t_verticalAlign,
|
|
608
|
+
fontWeight: t_fontWeight,
|
|
609
|
+
textDecoration: t_textDecoration,
|
|
610
|
+
fontStyle: t_fontStyle,
|
|
611
|
+
} = t.style;
|
|
612
|
+
this._propsCache.push([t, t_cssText]);
|
|
604
613
|
if (isBreak) continue;
|
|
605
614
|
|
|
606
|
-
const { c, s, w } = this.#getBorderStyle(
|
|
615
|
+
const { c, s, w } = this.#getBorderStyle(t_border);
|
|
607
616
|
|
|
608
617
|
// use getComputedStyle to normalize any CSS color format to rgb
|
|
609
|
-
let hexBackColor =
|
|
610
|
-
let hexColor =
|
|
618
|
+
let hexBackColor = t_backgroundColor;
|
|
619
|
+
let hexColor = t_color;
|
|
611
620
|
if (hexBackColor || hexColor) {
|
|
612
621
|
const computed = _w.getComputedStyle(t);
|
|
613
622
|
if (hexBackColor) hexBackColor = computed.backgroundColor;
|
|
@@ -619,12 +628,12 @@ export class TableStyleService {
|
|
|
619
628
|
if (b_width && cellBorder.w !== w) b_width = '';
|
|
620
629
|
if (backColor !== converter.rgb2hex(hexBackColor)) backColor = '';
|
|
621
630
|
if (fontColor !== converter.rgb2hex(hexColor)) fontColor = '';
|
|
622
|
-
if (align !== (isTable ? this.#state.figureElement?.style.float :
|
|
623
|
-
if (align_v && align_v !==
|
|
624
|
-
if (bold && bold !== /.+/.test(
|
|
625
|
-
if (underline && underline !== /underline/i.test(
|
|
626
|
-
if (strike && strike !== /line-through/i.test(
|
|
627
|
-
if (italic && italic !== /italic/i.test(
|
|
631
|
+
if (align !== (isTable ? this.#state.figureElement?.style.float : t_textAlign)) align = '';
|
|
632
|
+
if (align_v && align_v !== t_verticalAlign) align_v = '';
|
|
633
|
+
if (bold && bold !== /.+/.test(t_fontWeight)) bold = false;
|
|
634
|
+
if (underline && underline !== /underline/i.test(t_textDecoration)) underline = false;
|
|
635
|
+
if (strike && strike !== /line-through/i.test(t_textDecoration)) strike = false;
|
|
636
|
+
if (italic && italic !== /italic/i.test(t_fontStyle)) italic = false;
|
|
628
637
|
if (!b_color || !b_style || !b_width || !backColor || !fontColor) {
|
|
629
638
|
isBreak = true;
|
|
630
639
|
}
|
|
@@ -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);
|