suneditor 3.2.1 → 3.2.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 (39) hide show
  1. package/dist/suneditor.min.css +2 -2
  2. package/dist/suneditor.min.js +1 -1
  3. package/package.json +1 -1
  4. package/src/assets/suneditor.css +8 -1
  5. package/src/core/editor.js +2 -0
  6. package/src/core/event/actions/index.js +21 -0
  7. package/src/core/event/effects/keydown.registry.js +77 -0
  8. package/src/core/event/eventOrchestrator.js +13 -0
  9. package/src/core/event/handlers/handler_ww_input.js +12 -5
  10. package/src/core/event/handlers/handler_ww_key.js +5 -3
  11. package/src/core/event/ports.js +12 -2
  12. package/src/core/event/reducers/keydown.reducer.js +19 -9
  13. package/src/core/event/rules/keydown.rule.backspace.js +14 -0
  14. package/src/core/event/rules/keydown.rule.delete.js +29 -0
  15. package/src/core/event/rules/keydown.rule.enter.js +7 -0
  16. package/src/core/kernel/store.js +2 -0
  17. package/src/core/logic/dom/html.js +13 -1
  18. package/src/core/logic/panel/blockHandle.js +94 -29
  19. package/src/core/logic/shell/ui.js +3 -2
  20. package/src/core/schema/options.js +4 -2
  21. package/src/core/section/constructor.js +16 -1
  22. package/src/helper/converter.js +3 -2
  23. package/src/helper/env.js +51 -0
  24. package/src/helper/markdown.js +3 -1
  25. package/src/modules/contract/Browser.js +2 -2
  26. package/src/modules/ui/CommandMenu.js +1 -1
  27. package/src/plugins/dropdown/table/index.js +3 -2
  28. package/src/plugins/field/slashCommand.js +17 -1
  29. package/types/core/event/actions/index.d.ts +3 -0
  30. package/types/core/event/effects/keydown.registry.d.ts +6 -0
  31. package/types/core/event/eventOrchestrator.d.ts +7 -0
  32. package/types/core/event/handlers/handler_ww_key.d.ts +1 -0
  33. package/types/core/event/ports.d.ts +4 -2
  34. package/types/core/event/reducers/keydown.reducer.d.ts +15 -5
  35. package/types/core/kernel/store.d.ts +5 -0
  36. package/types/core/schema/options.d.ts +7 -4
  37. package/types/helper/env.d.ts +10 -0
  38. package/types/helper/index.d.ts +1 -0
  39. package/types/plugins/field/slashCommand.d.ts +34 -2
@@ -1,4 +1,4 @@
1
- import { dom, env } from '../../../helper';
1
+ import { dom, env, numbers } from '../../../helper';
2
2
  import { resolveBlock } from './blockResolver';
3
3
  import { ResolveButton } from '../../section/constructor';
4
4
  import CommandMenu from '../../../modules/ui/CommandMenu.js';
@@ -14,13 +14,13 @@ const { _w, _d } = env;
14
14
  */
15
15
  function _resolveLengthPx(value, contextStyle) {
16
16
  const trimmed = (value || '').trim();
17
- const num = parseFloat(trimmed);
17
+ const num = numbers.get(trimmed, -1);
18
18
  if (!num) return 0;
19
19
  if (/rem$/.test(trimmed)) {
20
- return num * (parseFloat(_w.getComputedStyle(_d.documentElement).fontSize) || 16);
20
+ return num * (numbers.get(_w.getComputedStyle(_d.documentElement).fontSize, -1) || 16);
21
21
  }
22
22
  if (/em$/.test(trimmed)) {
23
- return num * (parseFloat(contextStyle.fontSize) || 16);
23
+ return num * (numbers.get(contextStyle.fontSize, -1) || 16);
24
24
  }
25
25
  return num;
26
26
  }
@@ -119,6 +119,30 @@ class BlockHandle {
119
119
  em.addEvent(this.#$.frameContext.get('eventWysiwyg'), 'keydown', this.#onWrapperKeyDown.bind(this), true);
120
120
  }
121
121
 
122
+ /**
123
+ * @description Show the handle.
124
+ */
125
+ #showHandle() {
126
+ this.#handle.style.display = 'flex';
127
+ try {
128
+ this.#handle.showPopover?.();
129
+ } catch {
130
+ // already open
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @description Hide the handle and drop it out of the top layer.
136
+ */
137
+ #hideHandle() {
138
+ try {
139
+ this.#handle.hidePopover?.();
140
+ } catch {
141
+ // already hidden
142
+ }
143
+ this.#handle.style.display = 'none';
144
+ }
145
+
122
146
  /**
123
147
  * @description Position the block handle for the given mouse target. Uses rAF throttle.
124
148
  * Called from wysiwyg mousemove.
@@ -156,7 +180,7 @@ class BlockHandle {
156
180
  */
157
181
  hideNow() {
158
182
  this.#cancelHide();
159
- this.#handle.style.display = 'none';
183
+ this.#hideHandle();
160
184
  this.#actionMenu?.close();
161
185
  this.#setCurrentBlock(null);
162
186
  }
@@ -267,7 +291,7 @@ class BlockHandle {
267
291
  if (this.#hideTimer) return;
268
292
  this.#hideTimer = _w.setTimeout(() => {
269
293
  this.#hideTimer = null;
270
- this.#handle.style.display = 'none';
294
+ this.#hideHandle();
271
295
  this.#actionMenu?.close();
272
296
  this.#setCurrentBlock(null);
273
297
  }, 200);
@@ -323,11 +347,8 @@ class BlockHandle {
323
347
  */
324
348
  #onAreaMouseLeave(e) {
325
349
  if (this.#actionMenu?.isOpen) return;
326
-
327
- const related = /** @type {Node} */ (e?.relatedTarget);
328
- if (related && this.#handle?.contains(related)) return;
329
-
330
- this.#scheduleHide();
350
+ if (this.#stayAlive(/** @type {Node} */ (e?.relatedTarget))) return;
351
+ this.hideNow();
331
352
  }
332
353
 
333
354
  /**
@@ -335,11 +356,19 @@ class BlockHandle {
335
356
  */
336
357
  #onWrapperMouseLeave(e) {
337
358
  if (this.#actionMenu?.isOpen) return;
338
- const related = /** @type {Node} */ (e?.relatedTarget);
339
- if (related && this.#handle?.contains(related)) return;
340
- this.#cancelHide();
341
- this.#handle.style.display = 'none';
342
- this.#setCurrentBlock(null);
359
+ if (this.#stayAlive(/** @type {Node} */ (e?.relatedTarget))) return;
360
+ this.hideNow();
361
+ }
362
+
363
+ /**
364
+ * @description Whether a mouseleave's `relatedTarget` is still within the editor interaction zone
365
+ * @param {Node|null} related - `relatedTarget` of the mouseleave event
366
+ * @returns {boolean}
367
+ */
368
+ #stayAlive(related) {
369
+ if (!related) return false;
370
+ const wrapper = this.#area?.parentElement;
371
+ return !!(this.#handle?.contains(related) || wrapper?.contains(related));
343
372
  }
344
373
 
345
374
  /**
@@ -412,13 +441,13 @@ class BlockHandle {
412
441
  if (isIframe) {
413
442
  const iframeH = wysiwygFrameEl.clientHeight || 0;
414
443
  if (blockRect.bottom <= 0 || blockRect.top >= iframeH) {
415
- this.#handle.style.display = 'none';
444
+ this.#hideHandle();
416
445
  return;
417
446
  }
418
447
  } else {
419
448
  const wwFrameRect = wysiwygFrameEl.getBoundingClientRect();
420
449
  if (blockRect.bottom <= wwFrameRect.top || blockRect.top >= wwFrameRect.bottom) {
421
- this.#handle.style.display = 'none';
450
+ this.#hideHandle();
422
451
  return;
423
452
  }
424
453
  }
@@ -426,9 +455,18 @@ class BlockHandle {
426
455
  const scrollX = _w.scrollX;
427
456
  const scrollY = _w.scrollY;
428
457
 
429
- // parent-viewport top to convert to parent coordinates
430
- const blockTopVP = isIframe ? blockRect.top + iframeRect.top : blockRect.top;
431
- const top = blockTopVP + scrollY;
458
+ // First appearance after being hidden — skip transition. Make the handle
459
+ // measurable (display) before reading its height so we can center it.
460
+ const wasHidden = this.#handle.style.display !== 'flex';
461
+ if (wasHidden) {
462
+ dom.utils.addClass(this.#handle, 'se-no-transition');
463
+ this.#handle.style.display = 'flex';
464
+ }
465
+
466
+ const handleHeight = this.#handle.offsetHeight || 0;
467
+ const firstLineCenter = this.#getFirstLineCenter(blockElement, blockRect);
468
+ const firstLineCenterVP = isIframe ? firstLineCenter + iframeRect.top : firstLineCenter;
469
+ const top = firstLineCenterVP + scrollY - handleHeight / 2;
432
470
 
433
471
  // Handle inline offset
434
472
  const isRtl = !!this.#$.options.get('_rtl');
@@ -440,7 +478,7 @@ class BlockHandle {
440
478
  const wysiwygFrame = this.#$.frameContext.get('wysiwyg');
441
479
  if (wysiwygFrame) {
442
480
  const cs = _w.getComputedStyle(wysiwygFrame);
443
- const padStart = parseFloat(isRtl ? cs.paddingRight : cs.paddingLeft) || 0;
481
+ const padStart = numbers.get(isRtl ? cs.paddingRight : cs.paddingLeft, -1) || 0;
444
482
  const baseline = _resolveLengthPx(cs.getPropertyValue('--se-edit-inner-padding'), cs);
445
483
  centeringExtra = Math.max(0, padStart - baseline);
446
484
  }
@@ -448,10 +486,6 @@ class BlockHandle {
448
486
 
449
487
  const totalOffset = indent + centeringExtra;
450
488
 
451
- // First appearance after being hidden — skip transition
452
- const wasHidden = this.#handle.style.display !== 'flex';
453
- if (wasHidden) dom.utils.addClass(this.#handle, 'se-no-transition');
454
-
455
489
  this.#handle.style.top = top + 'px';
456
490
  if (isRtl) {
457
491
  this.#handle.style.left = '';
@@ -460,7 +494,7 @@ class BlockHandle {
460
494
  this.#handle.style.right = '';
461
495
  this.#handle.style.left = areaRect.left + scrollX + totalOffset + 'px';
462
496
  }
463
- this.#handle.style.display = 'flex';
497
+ this.#showHandle();
464
498
 
465
499
  if (wasHidden) {
466
500
  void this.#handle.offsetHeight;
@@ -468,6 +502,37 @@ class BlockHandle {
468
502
  }
469
503
  }
470
504
 
505
+ /**
506
+ * @description Viewport-Y center of the block's first line box (in the block's own document coordinates).
507
+ * @param {HTMLElement} blockElement
508
+ * @param {DOMRect} blockRect - `blockElement.getBoundingClientRect()`
509
+ * @returns {number} Viewport-Y coordinate of the first line's vertical center
510
+ */
511
+ #getFirstLineCenter(blockElement, blockRect) {
512
+ try {
513
+ const doc = blockElement.ownerDocument;
514
+ const walker = doc.createTreeWalker(blockElement, NodeFilter.SHOW_TEXT, null);
515
+
516
+ let textNode = /** @type {Text|null} */ (null);
517
+ while ((textNode = /** @type {Text} */ (walker.nextNode()))) {
518
+ if (textNode.textContent?.trim()) break;
519
+ }
520
+
521
+ if (textNode) {
522
+ const range = doc.createRange();
523
+ range.setStart(textNode, 0);
524
+ range.setEnd(textNode, Math.min(1, textNode.length));
525
+ const rects = range.getClientRects();
526
+ if (rects.length) return (rects[0].top + rects[0].bottom) / 2;
527
+ }
528
+ } catch {
529
+ // Fall through to the line-height estimate below
530
+ }
531
+
532
+ const lineHeight = numbers.get(_w.getComputedStyle(blockElement).lineHeight, -1) || blockRect.height;
533
+ return blockRect.top + Math.min(lineHeight, blockRect.height) / 2;
534
+ }
535
+
471
536
  /**
472
537
  * @description Calculate the handle's inline indent.
473
538
  * @param {HTMLElement} blockElement
@@ -483,13 +548,13 @@ class BlockHandle {
483
548
  const paddingKey = isRtl ? 'paddingRight' : 'paddingLeft';
484
549
  let indent = 0;
485
550
 
486
- indent += parseFloat(_w.getComputedStyle(blockElement)[marginKey]) || 0;
551
+ indent += numbers.get(_w.getComputedStyle(blockElement)[marginKey], -1) || 0;
487
552
 
488
553
  let el = blockElement.parentElement;
489
554
  while (el && el !== wysiwyg) {
490
555
  if (format.isBlock(el)) {
491
556
  const s = _w.getComputedStyle(el);
492
- indent += (parseFloat(s[paddingKey]) || 0) + (parseFloat(s[marginKey]) || 0);
557
+ indent += (numbers.get(s[paddingKey], -1) || 0) + (numbers.get(s[marginKey], -1) || 0);
493
558
  }
494
559
  el = el.parentElement;
495
560
  }
@@ -888,7 +888,8 @@ class UIManager {
888
888
  const prev = wysiwyg.querySelector('.se-placeholder-line');
889
889
 
890
890
  const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
891
- const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) ? line : null;
891
+ const inTableCell = !!line && !!dom.query.getParentElement(line, dom.check.isTableCell);
892
+ const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) && !inTableCell ? line : null;
892
893
 
893
894
  // Single-marker invariant: drop the previous marker unless it is still the target line.
894
895
  if (prev && prev !== target) {
@@ -1047,7 +1048,7 @@ class UIManager {
1047
1048
 
1048
1049
  function CreateAlertHTML({ lang, icons }) {
1049
1050
  const html =
1050
- '<div><button class="close" data-command="close" title="' +
1051
+ '<div><button type="button" class="close" data-command="close" title="' +
1051
1052
  lang.close +
1052
1053
  '">' +
1053
1054
  icons.cancel +
@@ -70,6 +70,7 @@ export const DEFAULTS = {
70
70
  TAG_STYLES: {
71
71
  '@text': 'font-family|font-size|color|background-color|width|height',
72
72
  '@line': 'text-align|margin|margin-left|margin-right|line-height',
73
+ '@component': 'width|height|min-width',
73
74
  'table|th|td':
74
75
  'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
75
76
  'table|td': 'width',
@@ -422,9 +423,10 @@ export const DEFAULTS = {
422
423
  * { allUsedStyles: 'color|background-color|text-shadow' }
423
424
  * ```
424
425
  * @property {Object<string, string>} [tagStyles={}] - Specifies allowed CSS styles per HTML tag.
425
- * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`).
426
+ * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`, `@component`).
426
427
  * - Value is a pipe-delimited list of allowed style names.
427
- * - Resolution order when filtering an element: explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
428
+ * - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
429
+ * - `@component` guards the inline sizing (`width`/`height`/`min-width`) the editor writes on a media component's container for percentage-based sizes; keep these so a clean() round-trip does not reset the component to full width.
428
430
  * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
429
431
  * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
430
432
  * ```js
@@ -77,6 +77,21 @@ function Constructor(editorTargets, options) {
77
77
  const menuTray = dom.utils.createElement('DIV', { class: 'se-menu-tray', popover: 'manual' });
78
78
  editor_carrier_wrapper.appendChild(menuTray);
79
79
 
80
+ // focus temp element — used by the legacy `keydown` Enter path (environments without `beforeinput`)
81
+ // to force-end a mobile virtual-keyboard IME session. See ports.enterPrevent / useEnterFromBeforeInput.
82
+ const focusTemp = /** @type {HTMLInputElement} */ (
83
+ dom.utils.createElement('INPUT', {
84
+ type: 'text',
85
+ id: editorFormFieldPrefix + '-focus-temp',
86
+ class: '__se__focus__temp__',
87
+ autocomplete: 'off',
88
+ 'aria-hidden': 'true',
89
+ 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;',
90
+ })
91
+ );
92
+ focusTemp.tabIndex = 0;
93
+ editor_carrier_wrapper.appendChild(focusTemp);
94
+
80
95
  // modal
81
96
  const modal = dom.utils.createElement('DIV', {
82
97
  class: 'se-modal se-modal-area sun-editor-common',
@@ -299,7 +314,7 @@ function Constructor(editorTargets, options) {
299
314
  if (isBlockHandle) {
300
315
  blockHandleArea = dom.utils.createElement('DIV', { class: 'se-block-handle-area' });
301
316
 
302
- const blockHandleGroup = dom.utils.createElement('DIV', { class: 'se-block-handle' });
317
+ const blockHandleGroup = dom.utils.createElement('DIV', { class: 'se-block-handle', popover: 'manual' });
303
318
  const blockHandlePlus = dom.utils.createElement(
304
319
  'DIV',
305
320
  { class: 'se-block-handle-btn se-block-handle-plus' },
@@ -1,4 +1,5 @@
1
1
  import { _d, _w } from './env';
2
+ import { get as getNumber } from './numbers';
2
3
 
3
4
  const _RE_HTML_CHARS = /&|\u00A0|'|"|<|>/g;
4
5
  const _RE_HTML_ENTITIES = /&amp;|&nbsp;|&apos;|&quot;|&lt;|&gt;/g;
@@ -392,8 +393,8 @@ export function rgb2hex(rgba) {
392
393
  export function getWidthInPercentage(target, parentTarget) {
393
394
  const parent = /** @type {HTMLElement} */ (parentTarget || target.parentElement);
394
395
  const parentStyle = _w.getComputedStyle(parent);
395
- const parentPaddingLeft = parseFloat(parentStyle.paddingLeft);
396
- const parentPaddingRight = parseFloat(parentStyle.paddingRight);
396
+ const parentPaddingLeft = getNumber(parentStyle.paddingLeft, -1);
397
+ const parentPaddingRight = getNumber(parentStyle.paddingRight, -1);
397
398
  const scrollbarWidth = parent.offsetWidth - parent.clientWidth;
398
399
  const parentWidth = parent.offsetWidth - parentPaddingLeft - parentPaddingRight - scrollbarWidth;
399
400
  const widthInPercentage = (target.offsetWidth / parentWidth) * 100;
package/src/helper/env.js CHANGED
@@ -216,6 +216,56 @@ export const shiftIcon = isOSX_IOS ? '⇧' : '+SHIFT';
216
216
  */
217
217
  export const DPI = _w.devicePixelRatio;
218
218
 
219
+ /** @type {?boolean} */
220
+ let _canUseBeforeInputCache = null;
221
+
222
+ /**
223
+ * @description Runtime probe: does this environment actually deliver input events for edits?
224
+ * Some managed/corporate environments (keyboard-hooking security SW, DLP, VDI) silently drop `beforeinput`
225
+ * **and** `input` at runtime even though the browser statically supports them — static feature-detection
226
+ * (`window.InputEvent`) cannot see that. This synthetically triggers an edit (`execCommand('insertText')`)
227
+ * on an offscreen contenteditable and observes whether an input-family event fires **synchronously**.
228
+ * @returns {boolean} `true` if an input-family event fired (environment delivers edits), else `false`.
229
+ */
230
+ export function canUseBeforeInput() {
231
+ if (_canUseBeforeInputCache !== null) return _canUseBeforeInputCache;
232
+ if (!_d.body || typeof _d.execCommand !== 'function') return true;
233
+
234
+ let seen = false;
235
+
236
+ try {
237
+ const probe = _d.createElement('div');
238
+ probe.setAttribute('contenteditable', 'true');
239
+ probe.setAttribute('aria-hidden', 'true');
240
+ probe.style.cssText =
241
+ 'position: fixed; top: 0; left: -9999px; width: 1px; height: 1px; opacity: 0; pointer-events: none;';
242
+
243
+ const active = /** @type {?HTMLElement} */ (_d.activeElement);
244
+ _d.body.appendChild(probe);
245
+
246
+ const onInput = () => {
247
+ seen = true;
248
+ };
249
+
250
+ probe.addEventListener('beforeinput', onInput, true);
251
+ probe.addEventListener('input', onInput, true);
252
+
253
+ probe.focus({ preventScroll: true });
254
+ _d.execCommand('insertText', false, '\u200b');
255
+
256
+ probe.removeEventListener('beforeinput', onInput, true);
257
+ probe.removeEventListener('input', onInput, true);
258
+
259
+ probe.remove();
260
+
261
+ if (active && active !== _d.body && typeof active.focus === 'function') active.focus({ preventScroll: true });
262
+ } catch {
263
+ seen = true;
264
+ }
265
+
266
+ return (_canUseBeforeInputCache = seen);
267
+ }
268
+
219
269
  /** --- editor env --- */
220
270
  export const KATEX_WEBSITE = 'https://katex.org/docs/supported.html';
221
271
  export const MATHJAX_WEBSITE = 'https://www.mathjax.org/';
@@ -239,6 +289,7 @@ const env = {
239
289
  isAndroid,
240
290
  isMobile,
241
291
  isTouchDevice,
292
+ canUseBeforeInput,
242
293
  cmdIcon,
243
294
  shiftIcon,
244
295
  DPI,
@@ -322,9 +322,11 @@ function nodeToMarkdown(node, indent, isBlock) {
322
322
  // Headings
323
323
  const headingMatch = /^h([1-6])$/.exec(tag);
324
324
  if (headingMatch) {
325
+ const content = childrenToInline(children).trim();
326
+ if (!content) return '\n';
325
327
  const level = parseInt(headingMatch[1], 10);
326
328
  const prefix = '#'.repeat(level);
327
- return `${prefix} ${childrenToInline(children).trim()}\n\n`;
329
+ return `${prefix} ${content}\n\n`;
328
330
  }
329
331
 
330
332
  if (tag === 'p') {
@@ -516,7 +516,7 @@ class Browser {
516
516
  const folderDiv = dom.utils.createElement('div', { class: 'se-menu-folder' }, folderLabel);
517
517
 
518
518
  folderLabel.insertBefore(
519
- dom.utils.createElement('button', null, expanded ? this.openArrow : this.closeArrow),
519
+ dom.utils.createElement('button', { type: 'button' }, expanded ? this.openArrow : this.closeArrow),
520
520
  folderLabel.firstElementChild,
521
521
  );
522
522
  const childContainer = document.createElement('div');
@@ -762,7 +762,7 @@ function CreateHTMLInfos($, useSearch) {
762
762
  <div class="se-browser-main">
763
763
  <div class="se-browser-bar">
764
764
  <div class="se-browser-search">
765
- <button class="se-btn se-side-open-btn">${icons.side_menu_hamburger}</button>
765
+ <button type="button" class="se-btn se-side-open-btn">${icons.side_menu_hamburger}</button>
766
766
  ${
767
767
  useSearch
768
768
  ? /*html*/ `
@@ -448,7 +448,7 @@ class CommandMenu {
448
448
  if (/modal/.test(type)) plugin.open?.();
449
449
  else if (/browser/.test(type)) plugin.open?.(null);
450
450
  else if (/popup/.test(type)) plugin.show?.();
451
- else plugin.action?.(dom.utils.createElement('BUTTON', { 'data-command': resolved.name }));
451
+ else plugin.action?.(dom.utils.createElement('BUTTON', { type: 'button', 'data-command': resolved.name }));
452
452
  this.#$.history.push(false);
453
453
  return true;
454
454
  }
@@ -339,7 +339,7 @@ class Table extends PluginDropdownFree {
339
339
  if (currentLogicalCol + i >= maxColumnCount || !cellWidth) continue;
340
340
 
341
341
  rowColOccupancy[currentLogicalCol + i] = true;
342
- const currentPxWidth = parseFloat(cellWidth);
342
+ const currentPxWidth = numbers.get(cellWidth, -1);
343
343
 
344
344
  for (let j = 0; j < colSpan; j++) {
345
345
  const targetColIndex = currentLogicalCol + j;
@@ -349,8 +349,9 @@ class Table extends PluginDropdownFree {
349
349
  if (existingWidth === null) {
350
350
  colWidths[targetColIndex] = `width: ${cellWidth};`;
351
351
  } else {
352
- const existingPxWidth = parseFloat(
352
+ const existingPxWidth = numbers.get(
353
353
  existingWidth.replace('width: ', '').replace(';', ''),
354
+ -1,
354
355
  );
355
356
  if (colSpan === 1 && currentPxWidth !== existingPxWidth) {
356
357
  colWidths[targetColIndex] = `width: ${cellWidth};`;
@@ -46,7 +46,23 @@ const { debounce } = converter;
46
46
  * title: 'Heading 1',
47
47
  * icon: 'h1',
48
48
  * keywords: ['header', 'title'],
49
- * action: ($) => $.format.applyBlock(document.createElement('H1')),
49
+ * // A line-level tag (H1-H6, P): `setLine` CHANGES the current line's tag → `<h1>…</h1>`.
50
+ * // Do NOT use `applyBlock` here — it WRAPS (`<h1><p>…</p></h1>`) and traps Enter inside.
51
+ * action: ($) => $.format.setLine(document.createElement('H1')),
52
+ * },
53
+ * {
54
+ * key: 'code',
55
+ * title: 'Code block',
56
+ * keywords: ['pre'],
57
+ * // A br-line block (PRE): `setBrLine` converts the line to a `<br>`-separated code block.
58
+ * action: ($) => $.format.setBrLine(document.createElement('PRE')),
59
+ * },
60
+ * {
61
+ * key: 'quote',
62
+ * title: 'Quote',
63
+ * keywords: ['blockquote'],
64
+ * // A container block (BLOCKQUOTE, DIV…): `applyBlock` WRAPS the selected lines → `<blockquote>…</blockquote>`.
65
+ * action: ($) => $.format.applyBlock(document.createElement('BLOCKQUOTE')),
50
66
  * },
51
67
  * 'bold',
52
68
  * 'image',
@@ -37,11 +37,13 @@ export namespace A {
37
37
  function backspaceListRemoveNested(range: Range): Action;
38
38
  function backspaceEmptyLineMergePrev(formatEl: Element, prev: Element): Action;
39
39
  function backspaceBrLineRowMerge(rowEndBr: Node, rowStartBr: Node): Action;
40
+ function backspaceSoftBreakMerge(zws: Node): Action;
40
41
  function deleteComponentSelect(formatEl: Element, fileComponentInfo: SunEditor.ComponentInfo): Action;
41
42
  function deleteComponentSelectNext(formatEl: any, nextEl: Element): Action;
42
43
  function deleteListRemoveNested(range: Range, formatEl: Element, rangeEl: Element): Action;
43
44
  function deleteEmptyLineMergeNext(formatEl: Element, next: Element): Action;
44
45
  function deleteBrLineRowMerge(rowEndBr: Node): Action;
46
+ function deleteSoftBreakMerge(br: Node): Action;
45
47
  function tabFormatIndent(range: Range, formatEl: Element, shift: boolean): Action;
46
48
  function enterScrollTo(range: Range): Action;
47
49
  function enterLineAddDefault(formatEl: Element): Action;
@@ -57,6 +59,7 @@ export namespace A {
57
59
  function enterFormatInsertBrHtml(brBlock: Element, range: Range, wSelection: Selection, offset: number): Action;
58
60
  function enterFormatInsertBrNode(wSelection: Selection): Action;
59
61
  function enterBrLineInsert(range: Range): Action;
62
+ function enterShiftBr(range: Range): Action;
60
63
  function enterBrLineExit(brBlock: Element): Action;
61
64
  function enterFormatBreakAtEdge(
62
65
  formatEl: Element,
@@ -26,6 +26,8 @@ declare const _default: {
26
26
  'backspace.emptyLine.mergePrev': ({ ports }: EffectContext_keydown, { formatEl, prev }: any) => void;
27
27
  /** @action backspaceBrLineRowMerge */
28
28
  'backspace.brline.rowMerge': ({ ports }: EffectContext_keydown, { rowEndBr, rowStartBr }: any) => void;
29
+ /** @action backspaceSoftBreakMerge */
30
+ 'backspace.softBreak.merge': ({ ports }: EffectContext_keydown, { zws }: any) => void;
29
31
  /** [delete] */
30
32
  /** @action deleteComponentSelect */
31
33
  'delete.component.select': ({ ports }: EffectContext_keydown, { formatEl, fileComponentInfo }: any) => void;
@@ -37,6 +39,8 @@ declare const _default: {
37
39
  'delete.emptyLine.mergeNext': ({ ports }: EffectContext_keydown, { formatEl, next }: any) => void;
38
40
  /** @action deleteBrLineRowMerge — remove an empty row inside a brLine (PRE), pull the next row up */
39
41
  'delete.brline.rowMerge': ({ ports }: EffectContext_keydown, { rowEndBr }: any) => void;
42
+ /** @action deleteSoftBreakMerge */
43
+ 'delete.softBreak.merge': ({ ports }: EffectContext_keydown, { br }: any) => void;
40
44
  /** [tab] */
41
45
  /** @action tabFormatIndent */
42
46
  'tab.format.indent': ({ ports, ctx }: EffectContext_keydown, { range, formatEl, shift }: any) => boolean;
@@ -61,6 +65,8 @@ declare const _default: {
61
65
  ) => void;
62
66
  /** @action enterBrLineInsert — insert exactly one empty row at the caret inside a normal brLine. */
63
67
  'enter.brline.insert': ({ ports }: EffectContext_keydown, { range }: any) => void;
68
+ /** @action enterShiftBr — soft line break (Shift+Enter): insert a `<br>` at the caret, splitting the line. */
69
+ 'enter.shift.br': ({ ports, ctx }: EffectContext_keydown, { range }: any) => void;
64
70
  /** @action enterBrLineExit — consume only the caret's current (last) empty row and add a default line after the brLine. */
65
71
  'enter.brline.exit': ({ ports }: EffectContext_keydown, { brBlock }: any) => void;
66
72
  /** @action enterFormatInsertBrNode */
@@ -9,6 +9,11 @@ declare class EventOrchestrator extends KernelInjector {
9
9
  * @type {boolean}
10
10
  */
11
11
  isComposing: boolean;
12
+ /**
13
+ * @description Real Shift-key state of the most recent Enter keydown.
14
+ * @type {boolean}
15
+ */
16
+ _enterKeyShift: boolean;
12
17
  /**
13
18
  * @description An array of parent containers that can be scrolled (in descending order)
14
19
  * @type {Array<Element>}
@@ -51,6 +56,8 @@ declare class EventOrchestrator extends KernelInjector {
51
56
  __eventDoc: Document;
52
57
  /** @type {string} */
53
58
  __secopy: string;
59
+ /** @type {HTMLInputElement} */
60
+ __focusTemp: HTMLInputElement;
54
61
  /**
55
62
  * @description Activates the corresponding button with the tags information of the current cursor position,
56
63
  * - such as `bold`, `underline`, etc., and executes the `active` method of the plugins.
@@ -23,6 +23,7 @@ export class OnKeyDown_wysiwyg {
23
23
  */
24
24
  constructor(this: import('../eventOrchestrator').default, fc: SunEditor.FrameContext, e: KeyboardEvent);
25
25
  isComposing: boolean;
26
+ _enterKeyShift: boolean;
26
27
  _onShortcutKey: boolean;
27
28
  }
28
29
  /**
@@ -174,8 +174,10 @@ export function makePorts(
174
174
  enterScrollTo(range: Range): void;
175
175
  /**
176
176
  * @description Prevents the default behavior of the `Enter` key.
177
- * Enter now runs from `beforeinput` (post-IME-commit), so the former mobile focus-shuffle
178
- * (temp-focus → refocus, to force-end a virtual-keyboard IME session) is unnecessary.
177
+ * On the `beforeinput` Enter path the IME has already committed, so a plain `preventDefault` suffices.
178
+ * On the legacy `keydown` path (environment doesn't deliver `beforeinput` — see `useEnterFromBeforeInput`)
179
+ * a mobile virtual keyboard can leave an open IME session, so we force it to end with a focus-shuffle
180
+ * (temp-focus → refocus wysiwyg); otherwise the just-committed marked-text is re-trapped.
179
181
  * @param {Event} e The keyboard/input event
180
182
  */
181
183
  enterPrevent(e: Event): void;
@@ -1,4 +1,12 @@
1
1
  import type {} from '../../../typedef';
2
+ /**
3
+ * @description Routing decision for Enter. Auto-repeat (held key) fires `keydown` but not `beforeinput`,
4
+ * so a repeat must stay on `keydown` or only the first line break would land.
5
+ * @param {SunEditor.Store} store - Editor store object
6
+ * @param {KeyboardEvent|InputEvent} [e] - Source event; a `repeat` flag forces the `keydown` path.
7
+ * @returns {boolean} `true` to route Enter through `beforeinput`, `false` to keep it on `keydown`.
8
+ */
9
+ export function useEnterFromBeforeInput(store: SunEditor.Store, e?: KeyboardEvent | InputEvent): boolean;
2
10
  /**
3
11
  * @typedef {import('../ports').EventReducerPorts} EventPorts
4
12
  */
@@ -28,11 +36,13 @@ import type {} from '../../../typedef';
28
36
  */
29
37
  export function reduceKeydown(ports: EventPorts, ctx: KeydownReducerCtx): Promise<EventActions>;
30
38
  /**
31
- * @description Enter is processed from the `beforeinput` event (post-IME-commit) instead of `keydown`,
32
- * to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see `handler_ww_input.js`).
33
- * Flip to `false` to instantly restore the legacy synchronous `keydown` Enter path — no other file needs
34
- * touching for rollback (the `keydown` Enter gate, the `handler_ww_key` normalization guard, and the
35
- * `beforeinput` dispatch all key off this single flag).
39
+ * @description Master switch for processing Enter from the `beforeinput` event (post-IME-commit) instead
40
+ * of `keydown`, to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see
41
+ * `handler_ww_input.js`).
42
+ *
43
+ * This is only the compile-time master; the effective per-environment decision is
44
+ * {@link useEnterFromBeforeInput}, which additionally requires that this environment actually delivers
45
+ * `beforeinput` at runtime (some corporate security SW / DLP / VDI drop it).
36
46
  * @type {boolean}
37
47
  */
38
48
  export const ENTER_FROM_BEFOREINPUT: boolean;
@@ -65,6 +65,10 @@ export type StoreState = {
65
65
  * - Suppress `focus` event handling.
66
66
  */
67
67
  _preventFocus: boolean;
68
+ /**
69
+ * - Whether this environment actually delivers `beforeinput` at runtime (probed once at editor load; some corporate security SW / DLP / VDI drop it). Defaults `true`.
70
+ */
71
+ _canUseBeforeInput: boolean;
68
72
  };
69
73
  /**
70
74
  * - Toolbar display mode flags (immutable after init).
@@ -117,6 +121,7 @@ export type StoreMode = {
117
121
  * @property {boolean} _mousedown - Whether `mousedown` is pressed.
118
122
  * @property {boolean} _preventBlur - Suppress `blur` event handling.
119
123
  * @property {boolean} _preventFocus - Suppress `focus` event handling.
124
+ * @property {boolean} _canUseBeforeInput - Whether this environment actually delivers `beforeinput` at runtime (probed once at editor load; some corporate security SW / DLP / VDI drop it). Defaults `true`.
120
125
  */
121
126
  /**
122
127
  * @typedef {Object} StoreMode - Toolbar display mode flags (immutable after init).
@@ -27,6 +27,7 @@ export namespace DEFAULTS {
27
27
  let TAG_STYLES: {
28
28
  '@text': string;
29
29
  '@line': string;
30
+ '@component': string;
30
31
  'table|th|td': string;
31
32
  'table|td': string;
32
33
  tr: string;
@@ -365,9 +366,10 @@ export namespace DEFAULTS {
365
366
  * { allUsedStyles: 'color|background-color|text-shadow' }
366
367
  * ```
367
368
  * @property {Object<string, string>} [tagStyles={}] - Specifies allowed CSS styles per HTML tag.
368
- * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`).
369
+ * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`, `@component`).
369
370
  * - Value is a pipe-delimited list of allowed style names.
370
- * - Resolution order when filtering an element: explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
371
+ * - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
372
+ * - `@component` guards the inline sizing (`width`/`height`/`min-width`) the editor writes on a media component's container for percentage-based sizes; keep these so a clean() round-trip does not reset the component to full width.
371
373
  * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
372
374
  * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
373
375
  * ```js
@@ -1177,9 +1179,10 @@ export type EditorBaseOptions = {
1177
1179
  allUsedStyles?: string;
1178
1180
  /**
1179
1181
  * - Specifies allowed CSS styles per HTML tag.
1180
- * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`).
1182
+ * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`, `@component`).
1181
1183
  * - Value is a pipe-delimited list of allowed style names.
1182
- * - Resolution order when filtering an element: explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
1184
+ * - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
1185
+ * - `@component` guards the inline sizing (`width`/`height`/`min-width`) the editor writes on a media component's container for percentage-based sizes; keep these so a clean() round-trip does not reset the component to full width.
1183
1186
  * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
1184
1187
  * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
1185
1188
  * ```js