suneditor 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/suneditor-contents.min.css +1 -1
  3. package/dist/suneditor.min.css +1 -1
  4. package/dist/suneditor.min.js +1 -1
  5. package/package.json +10 -7
  6. package/src/assets/design/size.css +2 -1
  7. package/src/assets/suneditor.css +75 -23
  8. package/src/core/editor.js +1 -0
  9. package/src/core/event/actions/index.js +2 -1
  10. package/src/core/event/effects/keydown.registry.js +3 -1
  11. package/src/core/event/effects/ruleHelpers.js +30 -1
  12. package/src/core/event/handlers/handler_ww_dragDrop.js +20 -0
  13. package/src/core/event/rules/keydown.rule.arrow.js +22 -16
  14. package/src/core/event/rules/keydown.rule.backspace.js +18 -5
  15. package/src/core/event/rules/keydown.rule.delete.js +7 -5
  16. package/src/core/event/rules/keydown.rule.enter.js +33 -3
  17. package/src/core/logic/panel/menu.js +4 -0
  18. package/src/core/logic/shell/ui.js +52 -3
  19. package/src/core/schema/options.js +1 -1
  20. package/src/core/section/constructor.js +44 -13
  21. package/src/core/section/documentType.js +55 -29
  22. package/src/modules/contract/Browser.js +3 -0
  23. package/src/modules/contract/Controller.js +3 -0
  24. package/src/modules/contract/Figure.js +23 -4
  25. package/src/modules/contract/Modal.js +2 -0
  26. package/src/plugins/dropdown/table/services/table.style.js +21 -12
  27. package/types/core/event/actions/index.d.ts +1 -1
  28. package/types/core/event/effects/keydown.registry.d.ts +1 -1
  29. package/types/core/event/effects/ruleHelpers.d.ts +12 -0
  30. package/types/core/logic/shell/ui.d.ts +2 -2
  31. package/types/core/schema/options.d.ts +2 -2
  32. package/types/core/section/documentType.d.ts +17 -1
@@ -89,6 +89,7 @@ class UIManager {
89
89
 
90
90
  // toast
91
91
  const toastPopup = CreateToastHTML();
92
+ toastPopup.setAttribute('popover', 'manual');
92
93
  this.toastPopup = toastPopup;
93
94
  this.toastContainer = toastPopup.querySelector('.se-toast-container');
94
95
  this.toastMessage = toastPopup.querySelector('span');
@@ -200,14 +201,27 @@ class UIManager {
200
201
 
201
202
  /**
202
203
  * @description Set direction to `rtl` or `ltr`.
203
- * @param {string} dir `rtl` or `ltr`
204
+ * @param {"rtl"|"ltr"} dir `rtl` or `ltr`
204
205
  */
205
206
  setDir(dir) {
206
207
  const rtl = dir === 'rtl';
207
208
  if (this.#options.get('_rtl') === rtl) return;
208
209
 
210
+ const prevDir = this.#options.get('textDirection');
211
+ const prevEditableClass = this.#options.get('_editableClass');
212
+ const prevPrintClass = this.#options.get('printClass');
213
+
209
214
  try {
210
215
  this.#options.set('_rtl', rtl);
216
+ this.#options.set('textDirection', dir);
217
+
218
+ // update _editableClass / printClass
219
+ const editableClass = rtl ? this.#options.get('_editableClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('_editableClass').replace(/\s*se-rtl/, '');
220
+ this.#options.set('_editableClass', editableClass);
221
+ if (this.#options.get('printClass')) {
222
+ this.#options.set('printClass', rtl ? this.#options.get('printClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('printClass').replace(/\s*se-rtl/, ''));
223
+ }
224
+
211
225
  this.offCurrentController();
212
226
 
213
227
  const fc = this.#frameContext;
@@ -221,11 +235,13 @@ class UIManager {
221
235
  if (rtl) {
222
236
  this.#contextProvider.applyToRoots((e) => {
223
237
  dom.utils.addClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
238
+ e.get('wysiwyg').dir = 'rtl';
224
239
  });
225
240
  dom.utils.addClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
226
241
  } else {
227
242
  this.#contextProvider.applyToRoots((e) => {
228
243
  dom.utils.removeClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
244
+ e.get('wysiwyg').removeAttribute('dir');
229
245
  });
230
246
  dom.utils.removeClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
231
247
  }
@@ -255,6 +271,12 @@ class UIManager {
255
271
 
256
272
  this.#activeDirBtn(rtl);
257
273
 
274
+ // reverse toolbar buttons
275
+ this.#reverseToolbarButtons(this.#context.get('toolbar_buttonTray'));
276
+ if (this.#context.has('toolbar_sub_buttonTray')) {
277
+ this.#reverseToolbarButtons(this.#context.get('toolbar_sub_buttonTray'));
278
+ }
279
+
258
280
  // document type
259
281
  if (fc.has('documentType_use_header')) {
260
282
  if (rtl) fc.get('wrapper').appendChild(fc.get('documentTypeInner'));
@@ -269,6 +291,9 @@ class UIManager {
269
291
  else if (this.#store.mode.isSubBalloon) this.#$.subToolbar._showBalloon();
270
292
  } catch (e) {
271
293
  this.#options.set('_rtl', !rtl);
294
+ this.#options.set('textDirection', prevDir);
295
+ this.#options.set('_editableClass', prevEditableClass);
296
+ if (prevPrintClass !== null) this.#options.set('printClass', prevPrintClass);
272
297
  console.warn(`[SUNEDITOR.ui.setDir.fail] ${e.toString()}`);
273
298
  }
274
299
 
@@ -372,7 +397,9 @@ class UIManager {
372
397
  * @param {string} [rootKey] Root key
373
398
  */
374
399
  showLoading(rootKey) {
375
- /** @type {HTMLElement} */ ((rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector('.se-loading-box')).style.display = 'block';
400
+ const el = /** @type {HTMLElement} */ ((rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector('.se-loading-box'));
401
+ el.style.display = 'block';
402
+ el.showPopover?.();
376
403
  }
377
404
 
378
405
  /**
@@ -380,7 +407,9 @@ class UIManager {
380
407
  * @param {string} [rootKey] Root key
381
408
  */
382
409
  hideLoading(rootKey) {
383
- /** @type {HTMLElement} */ ((rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector('.se-loading-box')).style.display = 'none';
410
+ const el = /** @type {HTMLElement} */ ((rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector('.se-loading-box'));
411
+ el.hidePopover?.();
412
+ el.style.display = 'none';
384
413
  }
385
414
 
386
415
  /**
@@ -399,6 +428,7 @@ class UIManager {
399
428
  this.#bindClose = this.#eventManager.addGlobalEvent('keydown', this.#closeListener[0]);
400
429
 
401
430
  this.#alertArea.style.display = 'block';
431
+ this.#alertArea.showPopover?.();
402
432
  dom.utils.addClass(this.alertModal, 'se-modal-show');
403
433
  }
404
434
 
@@ -408,6 +438,7 @@ class UIManager {
408
438
  alertClose() {
409
439
  dom.utils.removeClass(this.alertModal, 'se-modal-show');
410
440
  dom.utils.removeClass(this.alertModal, 'se-alert-*');
441
+ this.#alertArea.hidePopover?.();
411
442
  this.#alertArea.style.display = 'none';
412
443
  this.#bindAlertClick &&= this.#eventManager.removeEvent(this.#bindAlertClick);
413
444
  this.#bindClose &&= this.#eventManager.removeGlobalEvent(this.#bindClose);
@@ -428,6 +459,7 @@ class UIManager {
428
459
  if (type) dom.utils.addClass(this.toastPopup, `se-toast-${type}`);
429
460
 
430
461
  this.toastPopup.style.display = 'block';
462
+ this.toastPopup.showPopover?.();
431
463
  this.toastMessage.textContent = message;
432
464
  dom.utils.addClass(this.toastContainer, 'se-toast-show');
433
465
 
@@ -444,6 +476,7 @@ class UIManager {
444
476
  if (this.#toastToggle) _w.clearTimeout(this.#toastToggle);
445
477
  this.#toastToggle = null;
446
478
  dom.utils.removeClass(this.toastContainer, 'se-toast-show');
479
+ this.toastPopup.hidePopover?.();
447
480
  this.toastPopup.style.display = 'none';
448
481
  }
449
482
 
@@ -485,12 +518,14 @@ class UIManager {
485
518
  enableBackWrapper(cursor) {
486
519
  this.#backWrapper.style.cursor = cursor;
487
520
  this.#backWrapper.style.display = 'block';
521
+ this.#backWrapper.showPopover?.();
488
522
  }
489
523
 
490
524
  /**
491
525
  * @description Disabled background `div`
492
526
  */
493
527
  disableBackWrapper() {
528
+ this.#backWrapper.hidePopover?.();
494
529
  this.#backWrapper.style.display = 'none';
495
530
  this.#backWrapper.style.cursor = 'default';
496
531
  }
@@ -702,6 +737,20 @@ class UIManager {
702
737
  }
703
738
  }
704
739
 
740
+ /**
741
+ * @description Reverse the order of toolbar button groups (excluding the more-layer).
742
+ * @param {HTMLElement} buttonTray - The `.se-btn-tray` element.
743
+ */
744
+ #reverseToolbarButtons(buttonTray) {
745
+ if (!buttonTray) return;
746
+ const moreLayer = buttonTray.querySelector('.se-toolbar-more-layer');
747
+ const children = Array.from(buttonTray.children).filter((c) => c !== moreLayer);
748
+ for (let i = children.length - 1; i >= 0; i--) {
749
+ buttonTray.appendChild(children[i]);
750
+ }
751
+ if (moreLayer) buttonTray.appendChild(moreLayer);
752
+ }
753
+
705
754
  /**
706
755
  * @internal
707
756
  * @description Set the disabled button list
@@ -363,7 +363,7 @@ export const DEFAULTS = {
363
363
  * attributeWhitelist: {
364
364
  * a: 'href|target',
365
365
  * img: 'src|alt',
366
- * '*': 'id|data-*'
366
+ * '*': 'id|data-[^\s]+'
367
367
  * }
368
368
  * }
369
369
  * ```
@@ -6,6 +6,7 @@ import { dom, numbers, converter, env } from '../../helper';
6
6
  import { DEFAULTS } from '../schema/options';
7
7
 
8
8
  const _d = env._d;
9
+ let editorInstanceId = 0;
9
10
 
10
11
  /**
11
12
  * @typedef {import('../schema/options').AllBaseOptions} AllBaseOptions_constructor
@@ -57,16 +58,21 @@ function Constructor(editorTargets, options) {
57
58
  const icons = optionMap.i;
58
59
  const lang = optionMap.l;
59
60
  const loadingBox = dom.utils.createElement('DIV', { class: 'se-loading-box sun-editor-common' }, '<div class="se-loading-effect"></div>');
61
+ const editorFormFieldPrefix = 'suneditor-' + ++editorInstanceId;
60
62
 
61
63
  /** --- carrier wrapper --------------------------------------------------------------- */
62
64
  const editor_carrier_wrapper = dom.utils.createElement('DIV', { class: 'sun-editor sun-editor-carrier-wrapper sun-editor-common' + o.get('_themeClass') + (o.get('_rtl') ? ' se-rtl' : '') });
63
65
  // menuTray
64
- const menuTray = dom.utils.createElement('DIV', { class: 'se-menu-tray' });
66
+ const menuTray = dom.utils.createElement('DIV', { class: 'se-menu-tray', popover: 'manual' });
65
67
  editor_carrier_wrapper.appendChild(menuTray);
66
68
  // focus temp element
67
69
  const focusTemp = /** @type {HTMLInputElement} */ (
68
70
  dom.utils.createElement('INPUT', {
71
+ type: 'text',
72
+ id: editorFormFieldPrefix + '-focus-temp',
69
73
  class: '__se__focus__temp__',
74
+ autocomplete: 'off',
75
+ 'aria-hidden': 'true',
70
76
  style: 'position: fixed !important; top: -10000px !important; left: -10000px !important; display: block !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important;',
71
77
  })
72
78
  );
@@ -74,7 +80,7 @@ function Constructor(editorTargets, options) {
74
80
  editor_carrier_wrapper.appendChild(focusTemp);
75
81
 
76
82
  // modal
77
- const modal = dom.utils.createElement('DIV', { class: 'se-modal se-modal-area sun-editor-common' });
83
+ const modal = dom.utils.createElement('DIV', { class: 'se-modal se-modal-area sun-editor-common', popover: 'manual' });
78
84
  const modal_back = dom.utils.createElement('DIV', { class: 'se-modal-back' });
79
85
  const modal_inner = dom.utils.createElement('DIV', { class: 'se-modal-inner' });
80
86
  modal.appendChild(modal_back);
@@ -82,7 +88,7 @@ function Constructor(editorTargets, options) {
82
88
  editor_carrier_wrapper.appendChild(modal);
83
89
 
84
90
  // alert
85
- const alert = dom.utils.createElement('DIV', { class: 'se-alert se-modal-area sun-editor-common', style: 'display: none;' });
91
+ const alert = dom.utils.createElement('DIV', { class: 'se-alert se-modal-area sun-editor-common', style: 'display: none;', popover: 'manual' });
86
92
  const alert_back = dom.utils.createElement('DIV', { class: 'se-modal-back' });
87
93
  const alert_inner = dom.utils.createElement('DIV', { class: 'se-modal-inner' });
88
94
  alert.appendChild(alert_back);
@@ -90,11 +96,13 @@ function Constructor(editorTargets, options) {
90
96
  editor_carrier_wrapper.appendChild(alert);
91
97
 
92
98
  // loding box, resizing back
93
- editor_carrier_wrapper.appendChild(dom.utils.createElement('DIV', { class: 'se-back-wrapper' }));
94
- editor_carrier_wrapper.appendChild(loadingBox.cloneNode(true));
99
+ editor_carrier_wrapper.appendChild(dom.utils.createElement('DIV', { class: 'se-back-wrapper', popover: 'manual' }));
100
+ const loadingBoxEl = /** @type {HTMLElement} */ (loadingBox.cloneNode(true));
101
+ loadingBoxEl.setAttribute('popover', 'manual');
102
+ editor_carrier_wrapper.appendChild(loadingBoxEl);
95
103
 
96
104
  // drag cursor
97
- const dragCursor = dom.utils.createElement('DIV', { class: 'se-drag-cursor' });
105
+ const dragCursor = dom.utils.createElement('DIV', { class: 'se-drag-cursor', popover: 'manual' });
98
106
  editor_carrier_wrapper.appendChild(dragCursor);
99
107
 
100
108
  // set carrier wrapper
@@ -146,7 +154,7 @@ function Constructor(editorTargets, options) {
146
154
  container.appendChild(dom.utils.createElement('DIV', { class: 'se-toolbar-shadow' }));
147
155
 
148
156
  // init element
149
- const initElements = _initTargetElements(editTarget.key, o, top_div, to);
157
+ const initElements = _initTargetElements(editTarget.key, o, top_div, to, editorFormFieldPrefix);
150
158
  const bottomBar = initElements.bottomBar;
151
159
  const statusbar = bottomBar.statusbar;
152
160
  const wysiwyg_div = initElements.wysiwygFrame;
@@ -195,7 +203,11 @@ function Constructor(editorTargets, options) {
195
203
  // not used code mirror
196
204
  if (textarea === codeMirrorEl) {
197
205
  // add line nubers
198
- const codeNumbers = dom.utils.createElement('TEXTAREA', { class: 'se-code-view-line', readonly: 'true' }, null);
206
+ const codeNumbers = dom.utils.createElement(
207
+ 'TEXTAREA',
208
+ { id: editorFormFieldPrefix + '-code-view-line-' + (key || 'default'), class: 'se-code-view-line', readonly: 'true', autocomplete: 'off', 'aria-hidden': 'true', tabindex: '-1' },
209
+ null,
210
+ );
199
211
  codeWrapper.insertBefore(codeNumbers, textarea);
200
212
  } else {
201
213
  textarea = codeMirrorEl;
@@ -207,7 +219,11 @@ function Constructor(editorTargets, options) {
207
219
  let markdownTextarea = null;
208
220
  if (o.get('buttons').has('markdownView')) {
209
221
  markdownTextarea = initElements.markdownView;
210
- const markdownNumbers = dom.utils.createElement('TEXTAREA', { class: 'se-markdown-view-line', readonly: 'true' }, null);
222
+ const markdownNumbers = dom.utils.createElement(
223
+ 'TEXTAREA',
224
+ { id: editorFormFieldPrefix + '-markdown-view-line-' + (key || 'default'), class: 'se-markdown-view-line', readonly: 'true', autocomplete: 'off', 'aria-hidden': 'true', tabindex: '-1' },
225
+ null,
226
+ );
211
227
  markdownWrapper = dom.utils.createElement('DIV', { class: 'se-markdown-wrapper' });
212
228
  markdownWrapper.appendChild(markdownNumbers);
213
229
  markdownWrapper.appendChild(markdownTextarea);
@@ -361,8 +377,21 @@ export function CreateShortcuts(command, button, values, keyMap, rc, reverseKeys
361
377
  }
362
378
  }
363
379
 
380
+ /**
381
+ * @description Append tooltip span
382
+ * @param {Element} tooptipBtn
383
+ * @param {boolean} shift
384
+ * @param {string} shortcut
385
+ */
364
386
  function _addTooltip(tooptipBtn, shift, shortcut) {
365
- tooptipBtn.appendChild(dom.utils.createElement('SPAN', { class: 'se-shortcut' }, env.cmdIcon + (shift ? env.shiftIcon : '') + '+<span class="se-shortcut-key">' + shortcut + '</span>'));
387
+ const tooltip = dom.utils.createElement('SPAN', { class: 'se-shortcut' }, env.cmdIcon + (shift ? env.shiftIcon : '') + '+<span class="se-shortcut-key">' + shortcut + '</span>');
388
+ const prevTooltip = tooptipBtn.querySelector('.se-shortcut');
389
+
390
+ if (prevTooltip) {
391
+ tooptipBtn.replaceChild(tooltip, prevTooltip);
392
+ } else {
393
+ tooptipBtn.appendChild(tooltip);
394
+ }
366
395
  }
367
396
 
368
397
  /**
@@ -921,9 +950,10 @@ function InitFrameOptions(o, origin) {
921
950
  * @param {Map<string, *>} options - Options
922
951
  * @param {HTMLElement} topDiv - Top div
923
952
  * @param {SunEditor.FrameOptions} targetOptions - `editor.frameOptions`
953
+ * @param {string} formFieldPrefix - Prefix for generated form field ids
924
954
  * @returns {{bottomBar: ReturnType<CreateStatusbar>, wysiwygFrame: HTMLElement, codeView: HTMLElement, markdownView: HTMLElement, placeholder: HTMLElement}}
925
955
  */
926
- function _initTargetElements(key, options, topDiv, targetOptions) {
956
+ function _initTargetElements(key, options, topDiv, targetOptions, formFieldPrefix) {
927
957
  const editorStyles = targetOptions.get('_defaultStyles');
928
958
  /** top div */
929
959
  topDiv.style.cssText = editorStyles.top;
@@ -937,6 +967,7 @@ function _initTargetElements(key, options, topDiv, targetOptions) {
937
967
 
938
968
  if (!targetOptions.get('iframe')) {
939
969
  wysiwygDiv.setAttribute('contenteditable', 'true');
970
+ if (options.get('_rtl')) wysiwygDiv.dir = 'rtl';
940
971
  wysiwygDiv.className += ' ' + options.get('_editableClass');
941
972
  wysiwygDiv.style.cssText = editorStyles.frame + editorStyles.editor;
942
973
  } else {
@@ -976,10 +1007,10 @@ function _initTargetElements(key, options, topDiv, targetOptions) {
976
1007
  }
977
1008
 
978
1009
  // textarea for code view
979
- const textarea = dom.utils.createElement('TEXTAREA', { class: 'se-wrapper-inner se-code-viewer', style: editorStyles.frame });
1010
+ const textarea = dom.utils.createElement('TEXTAREA', { id: formFieldPrefix + '-code-viewer-' + (key || 'default'), class: 'se-wrapper-inner se-code-viewer', style: editorStyles.frame, autocomplete: 'off' });
980
1011
 
981
1012
  // textarea for markdown view
982
- const markdownTextarea = dom.utils.createElement('TEXTAREA', { class: 'se-wrapper-inner se-markdown-viewer', style: editorStyles.frame });
1013
+ const markdownTextarea = dom.utils.createElement('TEXTAREA', { id: formFieldPrefix + '-markdown-viewer-' + (key || 'default'), class: 'se-wrapper-inner se-markdown-viewer', style: editorStyles.frame, autocomplete: 'off' });
983
1014
 
984
1015
  const placeholder = dom.utils.createElement('SPAN', { class: 'se-placeholder' });
985
1016
  if (targetOptions.get('placeholder')) {
@@ -162,28 +162,19 @@ class DocumentType {
162
162
 
163
163
  // page break
164
164
  let pageBreakHeight = 0;
165
- let lastBreakPosition = 0;
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
- const breakPosition = pageBreaks[i].offsetTop;
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 totalPages = Math.ceil(mirrorHeight / A4_PAGE_HEIGHT) + additionalPages;
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 < pageBreaks.length; i++) {
186
- pages.push({ number: i, top: pageBreaks[i].offsetTop + pageBreakHeight / 2 });
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
- pages.push({ number: 0, top: 0 });
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
- for (let i = 1, t = 0; i < totalPages; i++) {
197
- t += A4_PAGE_HEIGHT + (i === 1 ? this.#paddingTop + this.#paddingBottom : this.#paddingTop);
198
- if (!pages.some((p) => Math.abs(p.top - t) < 3)) {
199
- const top = this._calcPageBreakTop(t, chr, mChr);
200
- if (top === null) break;
201
- pages.push({ number: i, top });
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
- for (let i = 0, t; i < totalPages; i++) {
218
- if (!pages[i]) continue;
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
- this.#mirrorCache = mid;
308
- return { ci: mid, cm: pageTop - bottom, ch: height };
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
- const closestIndex = mChr[start] ? start : end;
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 };
@@ -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));
@@ -197,6 +198,7 @@ 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
 
@@ -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 = [];
@@ -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
- const target = dom.query.getParentElement(element, (current) => current.parentElement === anyCover, 0) || /** @type {HTMLElement} */ (element);
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(newTarget);
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(newTarget);
799
+ const figure = Figure.CreateContainer(elementToInsert);
781
800
  dom.utils.addClass(
782
801
  figure.container,
783
802
  container.className
@@ -162,6 +162,7 @@ class Modal {
162
162
 
163
163
  dom.utils.addClass(this.#modalArea, 'se-backdrop-show');
164
164
  dom.utils.addClass(this.form, 'se-modal-show');
165
+ this.#modalArea.showPopover?.();
165
166
 
166
167
  if (this.#resizeBody) {
167
168
  const offset = this.#saveOffset();
@@ -197,6 +198,7 @@ class Modal {
197
198
  this.#bindClose &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose);
198
199
 
199
200
  // close
201
+ this.#modalArea.hidePopover?.();
200
202
  dom.utils.removeClass(this.#modalArea, 'se-backdrop-show');
201
203
  dom.utils.removeClass(this.form, 'se-modal-show');
202
204
 
@@ -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
- // eslint-disable-next-line no-shadow
602
- const { cssText, border, backgroundColor, color, textAlign, verticalAlign, fontWeight, textDecoration, fontStyle } = t.style;
603
- this._propsCache.push([t, cssText]);
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(border);
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 = backgroundColor;
610
- let hexColor = color;
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 : textAlign)) align = '';
623
- if (align_v && align_v !== verticalAlign) align_v = '';
624
- if (bold && bold !== /.+/.test(fontWeight)) bold = false;
625
- if (underline && underline !== /underline/i.test(textDecoration)) underline = false;
626
- if (strike && strike !== /line-through/i.test(textDecoration)) strike = false;
627
- if (italic && italic !== /italic/i.test(fontStyle)) italic = false;
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
  }
@@ -31,7 +31,7 @@ export namespace A {
31
31
  function enterFormatCleanBrAndZWS(selectionNode: Node, selectionFormat: boolean, brBlock: Element, children: NodeList, offset: number): Action;
32
32
  function enterFormatInsertBrHtml(brBlock: Element, range: Range, wSelection: Selection, offset: number): Action;
33
33
  function enterFormatInsertBrNode(wSelection: Selection): Action;
34
- function enterFormatBreakAtEdge(formatEl: Element, selectionNode: Node, formatStartEdge: boolean, formatEndEdge: boolean): Action;
34
+ function enterFormatBreakAtEdge(formatEl: Element, selectionNode: Node, formatStartEdge: boolean, formatEndEdge: boolean, bidiSwapped?: boolean): Action;
35
35
  function enterFormatBreakWithSelection(formatEl: Element, range: Range, formatStartEdge: boolean, formatEndEdge: boolean): Action;
36
36
  function enterFormatBreakAtCursor(formatEl: Element, range: Range): Action;
37
37
  function enterFigcaptionExitInList(formatEl: Element): Action;