suneditor 3.2.2 → 3.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +1 -1
- package/src/assets/suneditor.css +6 -1
- package/src/core/editor.js +2 -0
- package/src/core/event/actions/index.js +26 -2
- package/src/core/event/effects/keydown.registry.js +83 -4
- package/src/core/event/eventOrchestrator.js +20 -3
- package/src/core/event/handlers/handler_ww_input.js +12 -5
- package/src/core/event/handlers/handler_ww_key.js +10 -7
- package/src/core/event/ports.js +16 -8
- package/src/core/event/reducers/keydown.reducer.js +19 -9
- package/src/core/event/rules/keydown.rule.backspace.js +15 -0
- package/src/core/event/rules/keydown.rule.delete.js +29 -0
- package/src/core/event/rules/keydown.rule.enter.js +15 -8
- package/src/core/kernel/store.js +2 -0
- package/src/core/logic/dom/html.js +13 -1
- package/src/core/logic/panel/blockHandle.js +50 -14
- package/src/core/logic/shell/ui.js +7 -5
- package/src/core/schema/options.js +4 -2
- package/src/core/section/constructor.js +15 -0
- package/src/helper/converter.js +3 -2
- package/src/helper/env.js +51 -0
- package/src/helper/markdown.js +3 -1
- package/src/modules/contract/Browser.js +2 -2
- package/src/modules/ui/CommandMenu.js +1 -1
- package/src/plugins/dropdown/table/index.js +3 -2
- package/src/plugins/field/slashCommand.js +17 -1
- package/types/core/event/actions/index.d.ts +4 -1
- package/types/core/event/effects/keydown.registry.d.ts +9 -2
- package/types/core/event/eventOrchestrator.d.ts +14 -3
- package/types/core/event/handlers/handler_ww_key.d.ts +1 -0
- package/types/core/event/ports.d.ts +8 -6
- package/types/core/event/reducers/keydown.reducer.d.ts +15 -5
- package/types/core/kernel/store.d.ts +5 -0
- package/types/core/schema/options.d.ts +7 -4
- package/types/helper/env.d.ts +10 -0
- package/types/helper/index.d.ts +1 -0
- package/types/plugins/field/slashCommand.d.ts +34 -2
|
@@ -3,6 +3,7 @@ import { dom, converter, markdown, numbers, unicode, clipboard, env } from '../.
|
|
|
3
3
|
const { _d } = env;
|
|
4
4
|
const REQUIRED_DATA_ATTRS = 'data-se-[^\\s]+';
|
|
5
5
|
const V2_MIG_DATA_ATTRS = '|data-index|data-file-size|data-file-name|data-exp|data-font-size';
|
|
6
|
+
const ZWS_RUN_REGEXP = new RegExp(unicode.zeroWidthSpace + '+', 'g');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @description All HTML related classes involved in the editing area
|
|
@@ -365,6 +366,14 @@ class HTML {
|
|
|
365
366
|
cleanData = this.#styleNodeConvertor(cleanData);
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
if (!_freeCodeViewMode && cleanData.includes(unicode.zeroWidthSpace)) {
|
|
370
|
+
cleanData = cleanData.replace(ZWS_RUN_REGEXP, (m, offset, str) => {
|
|
371
|
+
const boundedStart = offset === 0 || str[offset - 1] === '>';
|
|
372
|
+
const boundedEnd = offset + m.length === str.length || str[offset + m.length] === '<';
|
|
373
|
+
return boundedStart && boundedEnd ? m : '';
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
368
377
|
return cleanData;
|
|
369
378
|
}
|
|
370
379
|
|
|
@@ -2228,7 +2237,9 @@ class HTML {
|
|
|
2228
2237
|
v.push(sv[0]);
|
|
2229
2238
|
}
|
|
2230
2239
|
} else if (!v || !_RE_STYLE_EQ.test(v.toString())) {
|
|
2231
|
-
if (
|
|
2240
|
+
if (_RE_SE_COMPONENT.test(m)) {
|
|
2241
|
+
v = this.#cleanStyle(m, v, '@component');
|
|
2242
|
+
} else if (this.#cleanStyleTagKeyRegExp.test(tagName)) {
|
|
2232
2243
|
v = this.#cleanStyle(m, v, tagName);
|
|
2233
2244
|
} else if (this.#$.format.isLine(tagName)) {
|
|
2234
2245
|
v = this.#cleanStyle(m, v, '@line');
|
|
@@ -2356,6 +2367,7 @@ const _RE_TAG_NAME = /(?!<)[a-zA-Z0-9-]+/;
|
|
|
2356
2367
|
const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(?:(["'])[^"']*\1|\S+)/gi;
|
|
2357
2368
|
const _RE_STYLE_EQ = /style=/i;
|
|
2358
2369
|
const _RE_STYLE_ATTR = /style\s*=\s*(?:"|')[^"']*(?:"|')/;
|
|
2370
|
+
const _RE_SE_COMPONENT = /class\s*=\s*("|')(?:[^"']*\s)?se-component(?=[\s"'])/i;
|
|
2359
2371
|
const _RE_LEADING_SPACE = /^\s/;
|
|
2360
2372
|
|
|
2361
2373
|
// #cleanStyle
|
|
@@ -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 =
|
|
17
|
+
const num = numbers.get(trimmed, -1);
|
|
18
18
|
if (!num) return 0;
|
|
19
19
|
if (/rem$/.test(trimmed)) {
|
|
20
|
-
return num * (
|
|
20
|
+
return num * (numbers.get(_w.getComputedStyle(_d.documentElement).fontSize, -1) || 16);
|
|
21
21
|
}
|
|
22
22
|
if (/em$/.test(trimmed)) {
|
|
23
|
-
return num * (
|
|
23
|
+
return num * (numbers.get(contextStyle.fontSize, -1) || 16);
|
|
24
24
|
}
|
|
25
25
|
return num;
|
|
26
26
|
}
|
|
@@ -455,9 +455,18 @@ class BlockHandle {
|
|
|
455
455
|
const scrollX = _w.scrollX;
|
|
456
456
|
const scrollY = _w.scrollY;
|
|
457
457
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
const
|
|
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;
|
|
461
470
|
|
|
462
471
|
// Handle inline offset
|
|
463
472
|
const isRtl = !!this.#$.options.get('_rtl');
|
|
@@ -469,7 +478,7 @@ class BlockHandle {
|
|
|
469
478
|
const wysiwygFrame = this.#$.frameContext.get('wysiwyg');
|
|
470
479
|
if (wysiwygFrame) {
|
|
471
480
|
const cs = _w.getComputedStyle(wysiwygFrame);
|
|
472
|
-
const padStart =
|
|
481
|
+
const padStart = numbers.get(isRtl ? cs.paddingRight : cs.paddingLeft, -1) || 0;
|
|
473
482
|
const baseline = _resolveLengthPx(cs.getPropertyValue('--se-edit-inner-padding'), cs);
|
|
474
483
|
centeringExtra = Math.max(0, padStart - baseline);
|
|
475
484
|
}
|
|
@@ -477,10 +486,6 @@ class BlockHandle {
|
|
|
477
486
|
|
|
478
487
|
const totalOffset = indent + centeringExtra;
|
|
479
488
|
|
|
480
|
-
// First appearance after being hidden — skip transition
|
|
481
|
-
const wasHidden = this.#handle.style.display !== 'flex';
|
|
482
|
-
if (wasHidden) dom.utils.addClass(this.#handle, 'se-no-transition');
|
|
483
|
-
|
|
484
489
|
this.#handle.style.top = top + 'px';
|
|
485
490
|
if (isRtl) {
|
|
486
491
|
this.#handle.style.left = '';
|
|
@@ -497,6 +502,37 @@ class BlockHandle {
|
|
|
497
502
|
}
|
|
498
503
|
}
|
|
499
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
|
+
|
|
500
536
|
/**
|
|
501
537
|
* @description Calculate the handle's inline indent.
|
|
502
538
|
* @param {HTMLElement} blockElement
|
|
@@ -512,13 +548,13 @@ class BlockHandle {
|
|
|
512
548
|
const paddingKey = isRtl ? 'paddingRight' : 'paddingLeft';
|
|
513
549
|
let indent = 0;
|
|
514
550
|
|
|
515
|
-
indent +=
|
|
551
|
+
indent += numbers.get(_w.getComputedStyle(blockElement)[marginKey], -1) || 0;
|
|
516
552
|
|
|
517
553
|
let el = blockElement.parentElement;
|
|
518
554
|
while (el && el !== wysiwyg) {
|
|
519
555
|
if (format.isBlock(el)) {
|
|
520
556
|
const s = _w.getComputedStyle(el);
|
|
521
|
-
indent += (
|
|
557
|
+
indent += (numbers.get(s[paddingKey], -1) || 0) + (numbers.get(s[marginKey], -1) || 0);
|
|
522
558
|
}
|
|
523
559
|
el = el.parentElement;
|
|
524
560
|
}
|
|
@@ -885,13 +885,15 @@ class UIManager {
|
|
|
885
885
|
if (!text) return false;
|
|
886
886
|
|
|
887
887
|
const wysiwyg = fc.get('wysiwyg');
|
|
888
|
-
const prev = wysiwyg.querySelector('.se-placeholder-line');
|
|
889
888
|
|
|
890
889
|
const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
|
|
891
|
-
const
|
|
890
|
+
const inTableCell = !!line && !!dom.query.getParentElement(line, dom.check.isTableCell);
|
|
891
|
+
const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) && !inTableCell ? line : null;
|
|
892
892
|
|
|
893
|
-
|
|
894
|
-
|
|
893
|
+
const prevMarkers = wysiwyg.querySelectorAll('.se-placeholder-line');
|
|
894
|
+
for (let i = 0; i < prevMarkers.length; i++) {
|
|
895
|
+
const prev = prevMarkers[i];
|
|
896
|
+
if (prev === target) continue;
|
|
895
897
|
prev.classList.remove('se-placeholder-line');
|
|
896
898
|
prev.removeAttribute('data-se-placeholder-line');
|
|
897
899
|
if (!prev.getAttribute('class')) prev.removeAttribute('class');
|
|
@@ -1047,7 +1049,7 @@ class UIManager {
|
|
|
1047
1049
|
|
|
1048
1050
|
function CreateAlertHTML({ lang, icons }) {
|
|
1049
1051
|
const html =
|
|
1050
|
-
'<div><button class="close" data-command="close" title="' +
|
|
1052
|
+
'<div><button type="button" class="close" data-command="close" title="' +
|
|
1051
1053
|
lang.close +
|
|
1052
1054
|
'">' +
|
|
1053
1055
|
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',
|
package/src/helper/converter.js
CHANGED
|
@@ -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 = /&| |'|"|<|>/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 =
|
|
396
|
-
const parentPaddingRight =
|
|
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,
|
package/src/helper/markdown.js
CHANGED
|
@@ -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} ${
|
|
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',
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
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,13 +37,15 @@ 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
|
-
function
|
|
48
|
+
function caretScrollTo(range: Range): Action;
|
|
47
49
|
function enterLineAddDefault(formatEl: Element): Action;
|
|
48
50
|
function enterListAddItem(formatEl: Element, selectionNode: Node): Action;
|
|
49
51
|
function enterFormatExitEmpty(formatEl: Element, rangeEl: 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,12 +39,15 @@ 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;
|
|
47
|
+
/** [caret] */
|
|
48
|
+
/** @action caretScrollTo */
|
|
49
|
+
'caret.scrollTo': ({ ports }: EffectContext_keydown, { range }: any) => void;
|
|
43
50
|
/** [enter] */
|
|
44
|
-
/** @action enterScrollTo */
|
|
45
|
-
'enter.scrollTo': ({ ports }: EffectContext_keydown, { range }: any) => void;
|
|
46
51
|
/** @action enterLineAddDefault */
|
|
47
52
|
'enter.line.addDefault': ({ ports }: EffectContext_keydown, { formatEl }: any) => void;
|
|
48
53
|
/** @action enterListAddItem */
|
|
@@ -61,6 +66,8 @@ declare const _default: {
|
|
|
61
66
|
) => void;
|
|
62
67
|
/** @action enterBrLineInsert — insert exactly one empty row at the caret inside a normal brLine. */
|
|
63
68
|
'enter.brline.insert': ({ ports }: EffectContext_keydown, { range }: any) => void;
|
|
69
|
+
/** @action enterShiftBr — soft line break (Shift+Enter): insert a `<br>` at the caret, splitting the line. */
|
|
70
|
+
'enter.shift.br': ({ ports, ctx }: EffectContext_keydown, { range }: any) => void;
|
|
64
71
|
/** @action enterBrLineExit — consume only the caret's current (last) empty row and add a default line after the brLine. */
|
|
65
72
|
'enter.brline.exit': ({ ports }: EffectContext_keydown, { brBlock }: any) => void;
|
|
66
73
|
/** @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.
|
|
@@ -86,11 +93,15 @@ declare class EventOrchestrator extends KernelInjector {
|
|
|
86
93
|
_setDefaultLine(formatName: string | null): void;
|
|
87
94
|
/**
|
|
88
95
|
* @internal
|
|
89
|
-
* @description Normalize the
|
|
90
|
-
*
|
|
96
|
+
* @description Normalize the edit range before the reducer reads it, for every key we custom-handle
|
|
97
|
+
* (Enter, Backspace, Delete). Resets an element-level container (a `line` such as `P`) to a text node so
|
|
98
|
+
* the rules' text-offset arithmetic (`isEdgePoint`, `endOffset === textContent.length`, ...) is valid; and
|
|
99
|
+
* when the caret sits inside a zero-width text node adjacent to a `<br>`, moves it onto the `<br>`.
|
|
100
|
+
* Without this, Backspace/Delete misfire whenever the browser reports the container as the line itself
|
|
101
|
+
* (child-index offset) instead of a `<br>`/ZWS text node.
|
|
91
102
|
* @returns {?(HTMLElement|Text)} The updated selection node, or `null` when the caret is not on a line (no normalization ran).
|
|
92
103
|
*/
|
|
93
|
-
|
|
104
|
+
_normalizeEditRange(): (HTMLElement | Text) | null;
|
|
94
105
|
/**
|
|
95
106
|
* @internal
|
|
96
107
|
* @description Handles data transfer actions for `paste` and `drop` events.
|
|
@@ -168,14 +168,16 @@ export function makePorts(
|
|
|
168
168
|
formatAttrsTempCache: (attrs: any) => any;
|
|
169
169
|
setOnShortcutKey: (v: any) => any;
|
|
170
170
|
/**
|
|
171
|
-
* @description Scrolls the editor view to the caret position after
|
|
172
|
-
* @param {Range} range Pre-
|
|
171
|
+
* @description Scrolls the editor view to the caret position after an edit (Enter, Backspace, ...).
|
|
172
|
+
* @param {Range} range Pre-edit snapshot range (fallback only).
|
|
173
173
|
*/
|
|
174
|
-
|
|
174
|
+
caretScrollTo(range: Range): void;
|
|
175
175
|
/**
|
|
176
176
|
* @description Prevents the default behavior of the `Enter` key.
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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;
|
|
@@ -259,5 +261,5 @@ export type EventReducerPorts = {
|
|
|
259
261
|
formatAttrsTempCache: (attrs: { [x: string]: any }) => void;
|
|
260
262
|
setOnShortcutKey: (v: boolean) => void;
|
|
261
263
|
enterPrevent: (e: Event) => void;
|
|
262
|
-
|
|
264
|
+
caretScrollTo: (range: Range) => void;
|
|
263
265
|
};
|
|
@@ -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
|
|
32
|
-
* to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|