suneditor 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/suneditor.min.css +2 -2
- package/dist/suneditor.min.js +1 -1
- package/package.json +3 -2
- package/src/assets/suneditor.css +5117 -5105
- package/src/core/config/eventManager.js +24 -1
- package/src/core/editor.js +6 -1
- package/src/core/event/effects/keydown.registry.js +14 -9
- package/src/core/event/effects/ruleHelpers.js +119 -1
- package/src/core/event/eventOrchestrator.js +15 -6
- package/src/core/event/rules/keydown.rule.backspace.js +10 -47
- package/src/core/event/rules/keydown.rule.delete.js +28 -66
- package/src/core/logic/dom/format.js +2 -2
- package/src/core/logic/dom/html.js +85 -5
- package/src/core/logic/dom/nodeTransform.js +5 -3
- package/src/core/logic/panel/blockHandle.js +61 -24
- package/src/core/logic/panel/blockResolver.js +41 -4
- package/src/core/logic/shell/pluginManager.js +15 -2
- package/src/core/logic/shell/ui.js +38 -28
- package/src/core/schema/options.js +20 -3
- package/src/core/section/constructor.js +12 -8
- package/src/events.js +1 -1
- package/src/helper/dom/domQuery.js +10 -4
- package/src/helper/googleDocs.js +40 -0
- package/src/helper/index.js +3 -0
- package/src/modules/contract/Figure.js +5 -2
- package/src/modules/ui/CommandMenu.js +47 -5
- package/src/modules/ui/SelectMenu.js +97 -36
- package/src/plugins/dropdown/table/index.js +25 -8
- package/src/plugins/dropdown/table/shared/table.constants.js +2 -0
- package/src/plugins/field/slashCommand.js +59 -12
- package/types/core/event/effects/ruleHelpers.d.ts +56 -0
- package/types/core/logic/panel/blockHandle.d.ts +3 -16
- package/types/core/logic/shell/ui.d.ts +14 -5
- package/types/core/schema/options.d.ts +31 -5
- package/types/events.d.ts +2 -2
- package/types/helper/dom/domQuery.d.ts +4 -2
- package/types/helper/googleDocs.d.ts +19 -0
- package/types/helper/index.d.ts +5 -0
- package/types/modules/ui/CommandMenu.d.ts +11 -0
- package/types/modules/ui/SelectMenu.d.ts +13 -7
- package/types/plugins/dropdown/table/index.d.ts +11 -0
- package/types/plugins/dropdown/table/shared/table.constants.d.ts +1 -0
- package/types/plugins/field/slashCommand.d.ts +43 -2
|
@@ -9,6 +9,7 @@ const ZWS_RUN_REGEXP = new RegExp(unicode.zeroWidthSpace + '+', 'g');
|
|
|
9
9
|
* @description All HTML related classes involved in the editing area
|
|
10
10
|
*/
|
|
11
11
|
class HTML {
|
|
12
|
+
/** @type {SunEditor.Deps} */
|
|
12
13
|
#$;
|
|
13
14
|
#store;
|
|
14
15
|
|
|
@@ -33,6 +34,7 @@ class HTML {
|
|
|
33
34
|
#disallowedTagsRegExp;
|
|
34
35
|
#disallowedTagNameRegExp;
|
|
35
36
|
#allowedTagNameRegExp;
|
|
37
|
+
#emptyLineRegExp;
|
|
36
38
|
|
|
37
39
|
/** @type {Object<string, RegExp>} */
|
|
38
40
|
#attributeWhitelist;
|
|
@@ -73,6 +75,18 @@ class HTML {
|
|
|
73
75
|
splitTagStyles[n] += tagStyles[k];
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
|
|
79
|
+
// A tag with an explicit entry that is also a line/text-style tag inherits the category
|
|
80
|
+
// styles ('@line'/'@text'), so category edits stay effective for it. (e.g. "li")
|
|
81
|
+
const formatLineReg = options.get('formatLine').reg;
|
|
82
|
+
for (const k in splitTagStyles) {
|
|
83
|
+
if (k.startsWith('@')) continue;
|
|
84
|
+
const category = formatLineReg.test(k) ? '@line' : this.#textStyleTags.includes(k) ? '@text' : '';
|
|
85
|
+
if (category && tagStyles[category]) {
|
|
86
|
+
splitTagStyles[k] += (splitTagStyles[k] ? '|' : '') + tagStyles[category];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
for (const k in splitTagStyles) {
|
|
77
91
|
splitTagStyles[k] = new RegExp(`\\s*[^-a-zA-Z](${splitTagStyles[k]})\\s*:[^;]+(?!;)*`, 'gi');
|
|
78
92
|
}
|
|
@@ -97,6 +111,12 @@ class HTML {
|
|
|
97
111
|
this.#disallowedTagNameRegExp = new RegExp(`^(${disallowedExtraTags})$`, 'i');
|
|
98
112
|
this.#allowedTagNameRegExp = new RegExp(`^(${allowedExtraTags})$`, 'i');
|
|
99
113
|
|
|
114
|
+
// empty default line probe
|
|
115
|
+
this.#emptyLineRegExp = new RegExp(
|
|
116
|
+
`<${options.get('defaultLine')}(?:\\s[^>]*)?></${options.get('defaultLine')}>`,
|
|
117
|
+
'i',
|
|
118
|
+
);
|
|
119
|
+
|
|
100
120
|
// set disallow text nodes
|
|
101
121
|
const disallowStyleNodes = Object.keys(options.get('_defaultStyleTagMap'));
|
|
102
122
|
const allowStyleNodes = !options.get('elementWhitelist')
|
|
@@ -374,6 +394,8 @@ class HTML {
|
|
|
374
394
|
});
|
|
375
395
|
}
|
|
376
396
|
|
|
397
|
+
if (formatFilter && !_freeCodeViewMode) cleanData = this.#dropEmptyLines(cleanData);
|
|
398
|
+
|
|
377
399
|
return cleanData;
|
|
378
400
|
}
|
|
379
401
|
|
|
@@ -1652,7 +1674,7 @@ class HTML {
|
|
|
1652
1674
|
) || [];
|
|
1653
1675
|
for (let i = ch.length - 1, c; i >= 0; i--) {
|
|
1654
1676
|
c = /** @type {HTMLElement} */ (ch[i]);
|
|
1655
|
-
c.
|
|
1677
|
+
c.replaceWith(...c.childNodes);
|
|
1656
1678
|
}
|
|
1657
1679
|
|
|
1658
1680
|
if (
|
|
@@ -1699,6 +1721,28 @@ class HTML {
|
|
|
1699
1721
|
return '';
|
|
1700
1722
|
}
|
|
1701
1723
|
|
|
1724
|
+
/**
|
|
1725
|
+
* @description Drops empty default lines (`<p></p>`) from a cleaned HTML string.
|
|
1726
|
+
* Wrapping block-level content in a default line is invalid HTML, so the parser tears the line
|
|
1727
|
+
* apart and leaves caret-less debris behind. An intentionally blank line always carries `<br>`.
|
|
1728
|
+
* @param {string} html Cleaned HTML string
|
|
1729
|
+
* @returns {string} The string without empty default lines
|
|
1730
|
+
*/
|
|
1731
|
+
#dropEmptyLines(html) {
|
|
1732
|
+
if (!this.#emptyLineRegExp.test(html)) return html;
|
|
1733
|
+
|
|
1734
|
+
const holder = dom.utils.createElement('DIV', null, html);
|
|
1735
|
+
const lines = holder.querySelectorAll(this.#options.get('defaultLine'));
|
|
1736
|
+
let removed = false;
|
|
1737
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1738
|
+
if (lines[i].firstChild) continue;
|
|
1739
|
+
dom.utils.removeItem(lines[i]);
|
|
1740
|
+
removed = true;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return removed ? holder.innerHTML : html;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1702
1746
|
/**
|
|
1703
1747
|
* @description Checks whether a node is a block-level container in which whitespace-only text
|
|
1704
1748
|
* children are insignificant formatting whitespace (safe to drop), as opposed to an inline/line
|
|
@@ -1861,7 +1905,20 @@ class HTML {
|
|
|
1861
1905
|
}
|
|
1862
1906
|
checkTags.push(t);
|
|
1863
1907
|
} else {
|
|
1864
|
-
p.
|
|
1908
|
+
const ref = p.nextSibling;
|
|
1909
|
+
|
|
1910
|
+
let rest = null;
|
|
1911
|
+
if (t.nextSibling) {
|
|
1912
|
+
rest = p.cloneNode(false);
|
|
1913
|
+
while (t.nextSibling) rest.appendChild(t.nextSibling);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
p.parentNode.insertBefore(t, ref);
|
|
1917
|
+
|
|
1918
|
+
if (rest) {
|
|
1919
|
+
p.parentNode.insertBefore(rest, ref);
|
|
1920
|
+
checkTags.push(rest);
|
|
1921
|
+
}
|
|
1865
1922
|
checkTags.push(p);
|
|
1866
1923
|
}
|
|
1867
1924
|
}
|
|
@@ -1898,19 +1955,42 @@ class HTML {
|
|
|
1898
1955
|
}
|
|
1899
1956
|
}
|
|
1900
1957
|
|
|
1958
|
+
// wrap top-level `li` elements without a list parent in a `ul`
|
|
1959
|
+
if (formatFilter) {
|
|
1960
|
+
const orphanCells = dom.query.getListChildNodes(
|
|
1961
|
+
documentFragment,
|
|
1962
|
+
(current) => dom.check.isListCell(current) && !dom.check.isList(current.parentNode),
|
|
1963
|
+
null,
|
|
1964
|
+
);
|
|
1965
|
+
|
|
1966
|
+
for (let i = 0, len = orphanCells.length, t, ul, n, next; i < len; i++) {
|
|
1967
|
+
t = orphanCells[i];
|
|
1968
|
+
if (dom.check.isList(t.parentNode)) continue;
|
|
1969
|
+
|
|
1970
|
+
ul = dom.utils.createElement('UL');
|
|
1971
|
+
t.parentNode.insertBefore(ul, t);
|
|
1972
|
+
n = t;
|
|
1973
|
+
while (n && (dom.check.isListCell(n) || (n.nodeType === 3 && !n.textContent.trim()))) {
|
|
1974
|
+
next = n.nextSibling;
|
|
1975
|
+
ul.appendChild(n);
|
|
1976
|
+
n = next;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1901
1981
|
for (let i = 0, len = withoutFormatCells.length, t, f; i < len; i++) {
|
|
1902
1982
|
t = withoutFormatCells[i];
|
|
1903
1983
|
|
|
1904
1984
|
f = dom.utils.createElement('DIV');
|
|
1905
|
-
f.
|
|
1985
|
+
while (t.firstChild) f.appendChild(t.firstChild);
|
|
1906
1986
|
|
|
1907
|
-
if (
|
|
1987
|
+
if (f.textContent.trim().length === 0 && this.#isAllTextStyleNodes(f)) {
|
|
1908
1988
|
let leaf = /** @type {Element} */ (f);
|
|
1909
1989
|
while (leaf.firstElementChild) leaf = leaf.firstElementChild;
|
|
1910
1990
|
leaf.innerHTML = '<br>';
|
|
1911
1991
|
}
|
|
1912
1992
|
|
|
1913
|
-
t.
|
|
1993
|
+
t.appendChild(f);
|
|
1914
1994
|
}
|
|
1915
1995
|
}
|
|
1916
1996
|
|
|
@@ -106,8 +106,9 @@ class NodeTransform {
|
|
|
106
106
|
|
|
107
107
|
if (temp) {
|
|
108
108
|
if (dom.check.isListCell(newEl) && dom.check.isList(temp) && temp.firstElementChild) {
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const firstCell = temp.firstElementChild;
|
|
110
|
+
while (firstCell.firstChild) newEl.appendChild(firstCell.firstChild);
|
|
111
|
+
dom.utils.removeItem(firstCell);
|
|
111
112
|
if (temp.children.length > 0) newEl.appendChild(temp);
|
|
112
113
|
} else {
|
|
113
114
|
newEl.appendChild(temp);
|
|
@@ -300,7 +301,8 @@ class NodeTransform {
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
} else {
|
|
303
|
-
child.
|
|
304
|
+
while (next.firstChild) child.appendChild(next.firstChild);
|
|
305
|
+
child.normalize();
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
dom.utils.removeItem(next);
|
|
@@ -37,6 +37,10 @@ class BlockHandle {
|
|
|
37
37
|
#plusBtn;
|
|
38
38
|
#dragBtn;
|
|
39
39
|
#menuConfig;
|
|
40
|
+
#menuMaxHeight;
|
|
41
|
+
#menuMinWidth;
|
|
42
|
+
/** @type {?function(SunEditor.Deps, { block: HTMLElement, openMenu: function(): void }): void} */
|
|
43
|
+
#onPlusClickHook;
|
|
40
44
|
|
|
41
45
|
/** @type {CommandMenu|null} */
|
|
42
46
|
#actionMenu = null;
|
|
@@ -74,17 +78,21 @@ class BlockHandle {
|
|
|
74
78
|
* @param {HTMLElement} blockHandle - Handle group (.se-block-handle)
|
|
75
79
|
* @param {HTMLElement} blockHandlePlus - Plus button
|
|
76
80
|
* @param {HTMLElement} blockHandleDrag - Drag button
|
|
77
|
-
* @param {Array
|
|
78
|
-
*
|
|
79
|
-
* define a custom row whose `action` is invoked with the Deps bag and the current block element.
|
|
81
|
+
* @param {Object|Array<*>|null} blockHandleOptions - The `blockHandle` option object (`{ menu, onPlusClick, maxHeight, minWidth }`).
|
|
82
|
+
* - An array is accepted as a shorthand for `{ menu: [...] }`.
|
|
80
83
|
*/
|
|
81
|
-
constructor($, blockHandleArea, blockHandle, blockHandlePlus, blockHandleDrag,
|
|
84
|
+
constructor($, blockHandleArea, blockHandle, blockHandlePlus, blockHandleDrag, blockHandleOptions) {
|
|
82
85
|
this.#$ = $;
|
|
83
86
|
this.#area = blockHandleArea;
|
|
84
87
|
this.#handle = blockHandle;
|
|
85
88
|
this.#plusBtn = blockHandlePlus;
|
|
86
89
|
this.#dragBtn = blockHandleDrag;
|
|
87
|
-
|
|
90
|
+
|
|
91
|
+
const opts = Array.isArray(blockHandleOptions) ? { menu: blockHandleOptions } : blockHandleOptions || {};
|
|
92
|
+
this.#menuConfig = opts.menu || null;
|
|
93
|
+
this.#menuMaxHeight = typeof opts.maxHeight === 'string' ? opts.maxHeight : '';
|
|
94
|
+
this.#menuMinWidth = typeof opts.minWidth === 'string' ? opts.minWidth : '200px';
|
|
95
|
+
this.#onPlusClickHook = typeof opts.onPlusClick === 'function' ? opts.onPlusClick : null;
|
|
88
96
|
|
|
89
97
|
this.#$.contextProvider.carrierWrapper.appendChild(this.#handle);
|
|
90
98
|
|
|
@@ -579,6 +587,7 @@ class BlockHandle {
|
|
|
579
587
|
/**
|
|
580
588
|
* @description Plus button click — insert new line after current block.
|
|
581
589
|
* Mirrors Enter-at-end-of-line behavior from keydown.rule.enter.
|
|
590
|
+
* Adding the line is the fixed behavior; `onPlusClick` decides what happens next (nothing by default).
|
|
582
591
|
* @param {MouseEvent} e
|
|
583
592
|
*/
|
|
584
593
|
#onPlusClick(e) {
|
|
@@ -588,10 +597,20 @@ class BlockHandle {
|
|
|
588
597
|
if (!this.#currentBlock) return;
|
|
589
598
|
|
|
590
599
|
const newLine = this.#$.format.addLineAfter(this.#currentBlock);
|
|
591
|
-
if (newLine)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
600
|
+
if (!newLine) return;
|
|
601
|
+
|
|
602
|
+
this.#$.selection.setRange(newLine, 1, newLine, 1);
|
|
603
|
+
this.#$.history.push(false);
|
|
604
|
+
|
|
605
|
+
if (!this.#onPlusClickHook) return;
|
|
606
|
+
|
|
607
|
+
this.#setCurrentBlock(newLine);
|
|
608
|
+
this.#updatePosition(newLine);
|
|
609
|
+
|
|
610
|
+
this.#onPlusClickHook(this.#$, {
|
|
611
|
+
block: newLine,
|
|
612
|
+
openMenu: () => this.#toggleActionMenu(),
|
|
613
|
+
});
|
|
595
614
|
}
|
|
596
615
|
|
|
597
616
|
/**
|
|
@@ -777,6 +796,22 @@ class BlockHandle {
|
|
|
777
796
|
// Skip if this click was actually a drag
|
|
778
797
|
if (this.#isDragging) return;
|
|
779
798
|
|
|
799
|
+
const componentInfo = this.#$.component.get(this.#currentBlock);
|
|
800
|
+
if (componentInfo) {
|
|
801
|
+
this.#actionMenu?.close();
|
|
802
|
+
this.#clearHoverLines();
|
|
803
|
+
this.#$.component.select(componentInfo.target, componentInfo.pluginName);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
this.#toggleActionMenu();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* @description Open the block action menu (or close it when already open). Shared by the drag button
|
|
812
|
+
* and by the `openMenu` helper handed to the `onPlusClick` hook.
|
|
813
|
+
*/
|
|
814
|
+
#toggleActionMenu() {
|
|
780
815
|
if (!this.#menuConfig) return;
|
|
781
816
|
|
|
782
817
|
// Lazy build — plugins are not yet instantiated when BlockHandle is constructed
|
|
@@ -786,23 +821,24 @@ class BlockHandle {
|
|
|
786
821
|
|
|
787
822
|
if (this.#actionMenu.isOpen) {
|
|
788
823
|
this.#actionMenu.close();
|
|
789
|
-
|
|
790
|
-
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
791
826
|
|
|
792
|
-
|
|
793
|
-
const lines = this.#$.format.getLines(null);
|
|
794
|
-
if (lines.length > 0) {
|
|
795
|
-
this.#setHoverLines(lines);
|
|
796
|
-
}
|
|
827
|
+
this.#expandRangeToFullLines();
|
|
797
828
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const horiz = this.#$.options.get('_rtl') ? 'left' : 'right';
|
|
803
|
-
const dir = `${horiz}-${spaceBelow >= spaceAbove ? 'bottom' : 'top'}`;
|
|
804
|
-
this.#actionMenu.open(dir);
|
|
829
|
+
// Highlight selected range lines
|
|
830
|
+
const lines = this.#$.format.getLines(null);
|
|
831
|
+
if (lines.length > 0) {
|
|
832
|
+
this.#setHoverLines(lines);
|
|
805
833
|
}
|
|
834
|
+
|
|
835
|
+
// Choose open direction based on available space.
|
|
836
|
+
const btnGlobal = this.#$.offset.getGlobal(this.#dragBtn);
|
|
837
|
+
const spaceBelow = dom.utils.getClientSize().h - (btnGlobal.top - _w.scrollY + btnGlobal.height);
|
|
838
|
+
const spaceAbove = btnGlobal.top - _w.scrollY;
|
|
839
|
+
const horiz = this.#$.options.get('_rtl') ? 'left' : 'right';
|
|
840
|
+
const dir = `${horiz}-${spaceBelow >= spaceAbove ? 'bottom' : 'top'}`;
|
|
841
|
+
this.#actionMenu.open(dir);
|
|
806
842
|
}
|
|
807
843
|
|
|
808
844
|
/**
|
|
@@ -816,7 +852,8 @@ class BlockHandle {
|
|
|
816
852
|
selectMenuParams: {
|
|
817
853
|
position: 'right-top',
|
|
818
854
|
dir: this.#$.options.get('_rtl') ? 'rtl' : 'ltr',
|
|
819
|
-
minWidth:
|
|
855
|
+
minWidth: this.#menuMinWidth,
|
|
856
|
+
maxHeight: this.#menuMaxHeight,
|
|
820
857
|
keydownTarget: _w,
|
|
821
858
|
closeMethod: () => {
|
|
822
859
|
dom.utils.removeClass(this.#dragBtn, 'on');
|
|
@@ -35,6 +35,7 @@ const TABLE_INNER_RE = /^(THEAD|TBODY|TR|TD|TH)$/;
|
|
|
35
35
|
*/
|
|
36
36
|
function classifyType(el) {
|
|
37
37
|
const tag = el.nodeName;
|
|
38
|
+
if (isComponentContainer(el)) return 'component';
|
|
38
39
|
if (tag === 'P' || tag === 'DIV') return 'p';
|
|
39
40
|
if (HEADING_RE.test(tag)) return 'heading';
|
|
40
41
|
if (tag === 'LI') return 'list-item';
|
|
@@ -74,6 +75,24 @@ function isInsideComponent(node) {
|
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @description Resolve a node to its outermost component container, but only when that container sits directly on the wysiwyg root.
|
|
80
|
+
* @param {Node} node
|
|
81
|
+
* @returns {HTMLElement|null} The top-level component container, or `null` if there is none
|
|
82
|
+
*/
|
|
83
|
+
function resolveTopLevelComponent(node) {
|
|
84
|
+
let el = node;
|
|
85
|
+
let outermost = null;
|
|
86
|
+
|
|
87
|
+
while (el && !isWysiwygFrame(el)) {
|
|
88
|
+
if (el.nodeType === 1 && isComponentContainer(/** @type {Element} */ (el)))
|
|
89
|
+
outermost = /** @type {HTMLElement} */ (el);
|
|
90
|
+
el = el.parentNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return outermost && outermost.parentNode && isWysiwygFrame(outermost.parentNode) ? outermost : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
/**
|
|
78
97
|
* @description Count block-level ancestors between element and wysiwyg root.
|
|
79
98
|
* @param {HTMLElement} element
|
|
@@ -193,8 +212,10 @@ export function resolveBlock(node, format, wysiwygFrame, mouseY) {
|
|
|
193
212
|
// Already at wysiwyg root
|
|
194
213
|
if (isWysiwygFrame(node)) return null;
|
|
195
214
|
|
|
196
|
-
|
|
197
|
-
|
|
215
|
+
if (isInsideComponent(node)) {
|
|
216
|
+
const component = resolveTopLevelComponent(node);
|
|
217
|
+
return component ? describeBlock(component, format, mouseY) : null;
|
|
218
|
+
}
|
|
198
219
|
|
|
199
220
|
let resolved = null;
|
|
200
221
|
|
|
@@ -248,8 +269,24 @@ export function resolveBlock(node, format, wysiwygFrame, mouseY) {
|
|
|
248
269
|
|
|
249
270
|
if (!resolved) return null;
|
|
250
271
|
|
|
251
|
-
// Final component check on resolved element
|
|
252
|
-
if (isInsideComponent(resolved))
|
|
272
|
+
// Final component check on the resolved element (e.g. getLine walked into a component)
|
|
273
|
+
if (isInsideComponent(resolved)) {
|
|
274
|
+
const component = resolveTopLevelComponent(resolved);
|
|
275
|
+
return component ? describeBlock(component, format, mouseY) : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return describeBlock(resolved, format, mouseY);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @description Build the `BlockInfo` for an already-resolved block element.
|
|
283
|
+
* @param {HTMLElement} element - Resolved block-level element
|
|
284
|
+
* @param {FormatAPI} format - Injected format methods
|
|
285
|
+
* @param {number} [mouseY] - Mouse clientY for nested list resolution
|
|
286
|
+
* @returns {BlockInfo}
|
|
287
|
+
*/
|
|
288
|
+
function describeBlock(element, format, mouseY) {
|
|
289
|
+
let resolved = element;
|
|
253
290
|
|
|
254
291
|
// For UL/OL, resolve to the closest child LI by mouse Y.
|
|
255
292
|
// For LI with nested sub-lists, find the deepest child LI.
|
|
@@ -151,9 +151,22 @@ class PluginManager {
|
|
|
151
151
|
let retainFilter;
|
|
152
152
|
if ((retainFilter = this.#options.get('__pluginRetainFilter'))) {
|
|
153
153
|
this.#retainFormatCheckers.forEach((plugin, query) => {
|
|
154
|
-
|
|
154
|
+
let infoLst;
|
|
155
|
+
try {
|
|
156
|
+
infoLst = domParser.querySelectorAll(query);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn(`[SUNEDITOR.retainFormat.fail]-[${plugin.key}]`, error.message);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
155
162
|
for (let i = 0, len = infoLst.length; i < len; i++) {
|
|
156
|
-
if (retainFilter
|
|
163
|
+
if (retainFilter !== true && retainFilter[plugin.key] === false) continue;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
plugin.method(infoLst[i]);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn(`[SUNEDITOR.retainFormat.fail]-[${plugin.key}]`, error.message);
|
|
169
|
+
}
|
|
157
170
|
}
|
|
158
171
|
});
|
|
159
172
|
}
|
|
@@ -72,6 +72,16 @@ class UIManager {
|
|
|
72
72
|
*/
|
|
73
73
|
#blockHandle = null;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @description Currently open `SelectMenu` instances.
|
|
77
|
+
* - There is one editor-wide "a select menu is open" flag but many `SelectMenu` instances, so it
|
|
78
|
+
* cannot be a plain boolean: the last writer would win. A field plugin that closes its own menu on
|
|
79
|
+
* every keystroke (`autocomplete`) used to flip the flag off while another menu (`slashCommand`) was
|
|
80
|
+
* still open, which let the wysiwyg keydown handler run against the open menu.
|
|
81
|
+
* @type {Set<*>}
|
|
82
|
+
*/
|
|
83
|
+
#openSelectMenus = new Set();
|
|
84
|
+
|
|
75
85
|
/**
|
|
76
86
|
* @constructor
|
|
77
87
|
* @param {SunEditor.Kernel} kernel
|
|
@@ -110,12 +120,6 @@ class UIManager {
|
|
|
110
120
|
this.#closeSignal = false;
|
|
111
121
|
this.#backWrapper = /** @type {HTMLElement} */ (this.#carrierWrapper.querySelector('.se-back-wrapper'));
|
|
112
122
|
|
|
113
|
-
/**
|
|
114
|
-
* @description Whether `SelectMenu` is open
|
|
115
|
-
* @type {boolean}
|
|
116
|
-
*/
|
|
117
|
-
this.selectMenuOn = false;
|
|
118
|
-
|
|
119
123
|
/**
|
|
120
124
|
* @description Currently open `Controller` info array
|
|
121
125
|
* @type {Array<SunEditor.Module.Controller.Info>}
|
|
@@ -136,6 +140,27 @@ class UIManager {
|
|
|
136
140
|
this._figureContainer = null;
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* @description Whether any `SelectMenu` is currently open.
|
|
145
|
+
* - Read-only: a menu announces itself through {@link setSelectMenuOpen}. Derived from the set of
|
|
146
|
+
* open instances so an unrelated menu closing cannot clear the flag for a menu that is still open.
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
get selectMenuOn() {
|
|
150
|
+
return this.#openSelectMenus.size > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @internal
|
|
155
|
+
* @description `SelectMenu` open-state notification. Called by `SelectMenu.open()` / `.close()`.
|
|
156
|
+
* @param {*} instance The `SelectMenu` instance changing state
|
|
157
|
+
* @param {boolean} open `true` on open, `false` on close
|
|
158
|
+
*/
|
|
159
|
+
setSelectMenuOpen(instance, open) {
|
|
160
|
+
if (open) this.#openSelectMenus.add(instance);
|
|
161
|
+
else this.#openSelectMenus.delete(instance);
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
/**
|
|
140
165
|
* @description Set editor frame styles.
|
|
141
166
|
* - Define the style of the edit area
|
|
@@ -291,12 +316,6 @@ class UIManager {
|
|
|
291
316
|
|
|
292
317
|
this.#activeDirBtn(rtl);
|
|
293
318
|
|
|
294
|
-
// reverse toolbar buttons
|
|
295
|
-
this.#reverseToolbarButtons(this.#context.get('toolbar_buttonTray'));
|
|
296
|
-
if (this.#context.has('toolbar_sub_buttonTray')) {
|
|
297
|
-
this.#reverseToolbarButtons(this.#context.get('toolbar_sub_buttonTray'));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
319
|
if (this.#store.mode.isBalloon) this.#$.toolbar._showBalloon();
|
|
301
320
|
else if (this.#store.mode.isSubBalloon) this.#$.subToolbar._showBalloon();
|
|
302
321
|
} catch (e) {
|
|
@@ -614,7 +633,7 @@ class UIManager {
|
|
|
614
633
|
rt.get('blockHandle'),
|
|
615
634
|
rt.get('blockHandlePlus'),
|
|
616
635
|
rt.get('blockHandleDrag'),
|
|
617
|
-
blockHandleOpt
|
|
636
|
+
blockHandleOpt,
|
|
618
637
|
);
|
|
619
638
|
}
|
|
620
639
|
}
|
|
@@ -783,20 +802,6 @@ class UIManager {
|
|
|
783
802
|
}
|
|
784
803
|
}
|
|
785
804
|
|
|
786
|
-
/**
|
|
787
|
-
* @description Reverse the order of toolbar button groups (excluding the more-layer).
|
|
788
|
-
* @param {HTMLElement} buttonTray - The `.se-btn-tray` element.
|
|
789
|
-
*/
|
|
790
|
-
#reverseToolbarButtons(buttonTray) {
|
|
791
|
-
if (!buttonTray) return;
|
|
792
|
-
const moreLayer = buttonTray.querySelector('.se-toolbar-more-layer');
|
|
793
|
-
const children = Array.from(buttonTray.children).filter((c) => c !== moreLayer);
|
|
794
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
795
|
-
buttonTray.appendChild(children[i]);
|
|
796
|
-
}
|
|
797
|
-
if (moreLayer) buttonTray.appendChild(moreLayer);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
805
|
/**
|
|
801
806
|
* @internal
|
|
802
807
|
* @description Set the disabled button list
|
|
@@ -954,7 +959,12 @@ class UIManager {
|
|
|
954
959
|
this._updatePlaceholder(fc);
|
|
955
960
|
// document type page
|
|
956
961
|
if (fc.has('documentType_use_page')) {
|
|
957
|
-
|
|
962
|
+
const mirror = fc.get('documentTypePageMirror');
|
|
963
|
+
const frag = mirror.ownerDocument.createDocumentFragment();
|
|
964
|
+
for (let n = fc.get('wysiwyg').firstChild; n; n = n.nextSibling) {
|
|
965
|
+
frag.appendChild(n.cloneNode(true));
|
|
966
|
+
}
|
|
967
|
+
mirror.replaceChildren(frag);
|
|
958
968
|
fc.get('documentType').rePage(true);
|
|
959
969
|
}
|
|
960
970
|
}
|
|
@@ -71,6 +71,7 @@ export const DEFAULTS = {
|
|
|
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
73
|
'@component': 'width|height|min-width',
|
|
74
|
+
li: 'font-family|font-size|color|background-color|font-weight|font-style',
|
|
74
75
|
'table|th|td':
|
|
75
76
|
'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
|
|
76
77
|
'table|td': 'width',
|
|
@@ -327,9 +328,25 @@ export const DEFAULTS = {
|
|
|
327
328
|
* menu: [
|
|
328
329
|
* 'p', 'heading', 'blockStyle',
|
|
329
330
|
* { title: 'Duplicate', icon: 'copy', action: ($, { block }) => block.after(block.cloneNode(true)) },
|
|
331
|
+
* // `'table'` as a string opens the size picker; a custom item inserts a default table directly
|
|
332
|
+
* { title: 'Table', icon: 'table', action: ($) => $.plugins.table.insert(3, 3) },
|
|
330
333
|
* ],
|
|
331
334
|
* }
|
|
332
335
|
* ```
|
|
336
|
+
* @property {string} [blockHandle.maxHeight=""] - Max height of the menu list. Any CSS length; the list scrolls past it.
|
|
337
|
+
* - Unset by default: the menu grows with its items and is only clamped when it would overflow the viewport.
|
|
338
|
+
* @property {string} [blockHandle.minWidth="200px"] - Min width of the menu.
|
|
339
|
+
* @property {function(SunEditor.Deps, { block: HTMLElement, openMenu: function(): void }): void} [blockHandle.onPlusClick] - Runs after the plus button inserted a new line.
|
|
340
|
+
* - Adding the line is fixed behavior; this hook decides what happens next. Nothing does by default.
|
|
341
|
+
* - `block` is the new line, already focused. `openMenu()` opens the block handle's own `menu`.
|
|
342
|
+
* ```js
|
|
343
|
+
* blockHandle: {
|
|
344
|
+
* // open the block handle menu
|
|
345
|
+
* onPlusClick: ($, { openMenu }) => openMenu(),
|
|
346
|
+
* // ...or the slash command menu
|
|
347
|
+
* onPlusClick: ($, { block }) => $.plugins.slashCommand.open(block),
|
|
348
|
+
* }
|
|
349
|
+
* ```
|
|
333
350
|
* @property {string} [type=""] - Editor type. Use `"document"` for a document-style layout, with optional sub-types after `:`.
|
|
334
351
|
* ```js
|
|
335
352
|
* // type
|
|
@@ -441,7 +458,7 @@ export const DEFAULTS = {
|
|
|
441
458
|
* - Value is a pipe-delimited list of allowed style names.
|
|
442
459
|
* - Resolution order when filtering an element: `@component` (for `.se-component` containers) → explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
|
|
443
460
|
* - `@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.
|
|
444
|
-
* - An explicit tag entry **
|
|
461
|
+
* - An explicit tag entry is **merged** with its category default when the tag belongs to one (`@line` for formatLine elements, else `@text` for textStyleTags) — the entry adds styles on top of the category's.
|
|
445
462
|
* - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
|
|
446
463
|
* ```js
|
|
447
464
|
* {
|
|
@@ -449,8 +466,8 @@ export const DEFAULTS = {
|
|
|
449
466
|
* '@text': 'color|font-size|background-color', // default for span, b, i, em, ...
|
|
450
467
|
* '@line': 'text-align|margin|line-height', // default for p, h1-h6, div, li, ...
|
|
451
468
|
* 'table|td': 'border|color|background-color', // per-tag whitelist
|
|
452
|
-
*
|
|
453
|
-
*
|
|
469
|
+
* div: 'color', // merged with the `@line` default (div is a line element)
|
|
470
|
+
* hr: 'border-top',
|
|
454
471
|
* }
|
|
455
472
|
* }
|
|
456
473
|
* ```
|
|
@@ -44,12 +44,16 @@ function Constructor(editorTargets, options) {
|
|
|
44
44
|
if (options.plugins) {
|
|
45
45
|
const excludedPlugins = options.excludedPlugins || [];
|
|
46
46
|
const originPlugins = options.plugins;
|
|
47
|
-
const
|
|
48
|
-
.
|
|
49
|
-
.map((name) => originPlugins[name]);
|
|
50
|
-
|
|
51
|
-
for (let i = 0, len =
|
|
52
|
-
|
|
47
|
+
const pluginsEntries = Array.isArray(originPlugins)
|
|
48
|
+
? originPlugins.map((plugin) => [null, plugin])
|
|
49
|
+
: Object.keys(originPlugins).map((name) => [name, originPlugins[name]]);
|
|
50
|
+
|
|
51
|
+
for (let i = 0, len = pluginsEntries.length, name, p; i < len; i++) {
|
|
52
|
+
name = pluginsEntries[i][0];
|
|
53
|
+
p = pluginsEntries[i][1];
|
|
54
|
+
p = p?.default || p;
|
|
55
|
+
if (!p?.key) continue;
|
|
56
|
+
if (excludedPlugins.includes(p.key) || (name !== null && excludedPlugins.includes(name))) continue;
|
|
53
57
|
plugins[p.key] = p;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -921,7 +925,7 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
921
925
|
'toolbar_sub_width',
|
|
922
926
|
subbar.width ? (numbers.is(subbar.width) ? subbar.width + 'px' : subbar.width) : 'auto',
|
|
923
927
|
);
|
|
924
|
-
subButtons =
|
|
928
|
+
subButtons = subbar.buttonList;
|
|
925
929
|
o.set('buttons_sub', new Set(subButtons.toString().split(',')));
|
|
926
930
|
}
|
|
927
931
|
}
|
|
@@ -1054,7 +1058,7 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
1054
1058
|
i: icons,
|
|
1055
1059
|
l: /** @type {Object<string, string>} */ (options.lang || _defaultLang),
|
|
1056
1060
|
v: (options.value = typeof options.value === 'string' ? options.value : null),
|
|
1057
|
-
buttons:
|
|
1061
|
+
buttons: buttonList,
|
|
1058
1062
|
subButtons: subButtons,
|
|
1059
1063
|
statusbarContainer:
|
|
1060
1064
|
typeof options.statusbar_container === 'string'
|
package/src/events.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* @property {Event} event - event object
|
|
18
18
|
* @property {string} data - drop data
|
|
19
19
|
* @property {boolean} maxCharCount - is max char count
|
|
20
|
-
* @property {string} from - `"SE"`|`"MS"`|`""` - source
|
|
20
|
+
* @property {string} from - `"SE"`|`"MS"`|`"GOOGLE"`|`""` - source
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
// --- media
|