suneditor 3.1.3 → 3.2.0
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-contents.min.css +1 -1
- package/dist/suneditor.min.css +28 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +2 -1
- package/src/assets/design/color.css +12 -0
- package/src/assets/design/size.css +9 -2
- package/src/assets/icons/defaultIcons.js +96 -47
- package/src/assets/suneditor-contents.css +3 -2
- package/src/assets/suneditor.css +480 -31
- package/src/core/config/contextProvider.js +2 -1
- package/src/core/config/eventManager.js +41 -2
- package/src/core/config/optionProvider.js +23 -2
- package/src/core/editor.js +32 -14
- package/src/core/event/actions/index.js +76 -9
- package/src/core/event/effects/common.registry.js +2 -1
- package/src/core/event/effects/keydown.registry.js +169 -33
- package/src/core/event/effects/ruleHelpers.js +20 -6
- package/src/core/event/eventOrchestrator.js +136 -23
- package/src/core/event/handlers/handler_toolbar.js +4 -2
- package/src/core/event/handlers/handler_ww_dragDrop.js +10 -0
- package/src/core/event/handlers/handler_ww_input.js +9 -2
- package/src/core/event/handlers/handler_ww_key.js +86 -17
- package/src/core/event/handlers/handler_ww_mouse.js +45 -8
- package/src/core/event/ports.js +23 -5
- package/src/core/event/reducers/keydown.reducer.js +15 -2
- package/src/core/event/rules/keydown.rule.arrow.js +12 -2
- package/src/core/event/rules/keydown.rule.backspace.js +110 -15
- package/src/core/event/rules/keydown.rule.delete.js +62 -7
- package/src/core/event/rules/keydown.rule.enter.js +62 -31
- package/src/core/event/support/defaultLineManager.js +9 -2
- package/src/core/event/support/selectionState.js +13 -3
- package/src/core/kernel/coreKernel.js +1 -0
- package/src/core/logic/dom/char.js +13 -3
- package/src/core/logic/dom/format.js +171 -39
- package/src/core/logic/dom/html.js +357 -87
- package/src/core/logic/dom/inline.js +229 -34
- package/src/core/logic/dom/listFormat.js +111 -20
- package/src/core/logic/dom/nodeTransform.js +36 -11
- package/src/core/logic/dom/offset.js +116 -31
- package/src/core/logic/dom/selection.js +147 -29
- package/src/core/logic/panel/blockHandle.js +785 -0
- package/src/core/logic/panel/blockResolver.js +278 -0
- package/src/core/logic/panel/finder.js +16 -6
- package/src/core/logic/panel/menu.js +119 -18
- package/src/core/logic/panel/toolbar.js +75 -19
- package/src/core/logic/panel/viewer.js +76 -20
- package/src/core/logic/shell/_commandExecutor.js +44 -9
- package/src/core/logic/shell/commandDispatcher.js +46 -4
- package/src/core/logic/shell/component.js +102 -25
- package/src/core/logic/shell/focusManager.js +13 -3
- package/src/core/logic/shell/history.js +21 -6
- package/src/core/logic/shell/pluginManager.js +21 -13
- package/src/core/logic/shell/shortcuts.js +9 -1
- package/src/core/logic/shell/ui.js +139 -37
- package/src/core/schema/frameContext.js +34 -3
- package/src/core/schema/options.js +61 -19
- package/src/core/section/constructor.js +537 -103
- package/src/core/section/documentType.js +35 -6
- package/src/helper/converter.js +24 -7
- package/src/helper/dom/domCheck.js +25 -5
- package/src/helper/dom/domQuery.js +2 -1
- package/src/helper/dom/domUtils.js +17 -4
- package/src/helper/env.js +11 -2
- package/src/helper/keyCodeMap.js +6 -2
- package/src/helper/markdown.js +24 -5
- package/src/helper/msOffice.js +9 -2
- package/src/interfaces/plugins.js +1 -1
- package/src/langs/ckb.js +0 -1
- package/src/langs/cs.js +0 -1
- package/src/langs/da.js +0 -1
- package/src/langs/de.js +0 -1
- package/src/langs/en.js +0 -1
- package/src/langs/es.js +0 -1
- package/src/langs/fa.js +0 -1
- package/src/langs/fr.js +0 -1
- package/src/langs/he.js +0 -1
- package/src/langs/hu.js +0 -1
- package/src/langs/index.js +27 -1
- package/src/langs/it.js +0 -1
- package/src/langs/ja.js +0 -1
- package/src/langs/km.js +0 -1
- package/src/langs/ko.js +0 -1
- package/src/langs/lv.js +0 -1
- package/src/langs/nl.js +0 -1
- package/src/langs/pl.js +0 -1
- package/src/langs/pt_br.js +0 -1
- package/src/langs/ro.js +0 -1
- package/src/langs/ru.js +0 -1
- package/src/langs/se.js +0 -1
- package/src/langs/tr.js +0 -1
- package/src/langs/uk.js +0 -1
- package/src/langs/ur.js +0 -1
- package/src/langs/zh_cn.js +0 -1
- package/src/modules/contract/Browser.js +68 -23
- package/src/modules/contract/ColorPicker.js +12 -4
- package/src/modules/contract/Controller.js +37 -7
- package/src/modules/contract/Figure.js +159 -36
- package/src/modules/contract/HueSlider.js +27 -4
- package/src/modules/contract/Modal.js +62 -11
- package/src/modules/manager/FileManager.js +50 -8
- package/src/modules/ui/CommandMenu.js +597 -0
- package/src/modules/ui/ModalAnchorEditor.js +41 -9
- package/src/modules/ui/SelectMenu.js +481 -36
- package/src/modules/ui/index.js +1 -0
- package/src/plugins/browser/audioGallery.js +8 -1
- package/src/plugins/browser/fileBrowser.js +15 -2
- package/src/plugins/browser/fileGallery.js +8 -1
- package/src/plugins/browser/imageGallery.js +24 -3
- package/src/plugins/browser/videoGallery.js +19 -4
- package/src/plugins/command/blockquote.js +6 -1
- package/src/plugins/command/codeBlock.js +43 -5
- package/src/plugins/command/exportPDF.js +20 -4
- package/src/plugins/command/fileUpload.js +46 -11
- package/src/plugins/command/list_bulleted.js +6 -1
- package/src/plugins/command/list_numbered.js +6 -1
- package/src/plugins/dropdown/align.js +21 -33
- package/src/plugins/dropdown/backgroundColor.js +10 -2
- package/src/plugins/dropdown/blockStyle.js +17 -22
- package/src/plugins/dropdown/font.js +34 -34
- package/src/plugins/dropdown/hr.js +15 -43
- package/src/plugins/dropdown/layout.js +10 -27
- package/src/plugins/dropdown/lineHeight.js +10 -33
- package/src/plugins/dropdown/list.js +19 -25
- package/src/plugins/dropdown/paragraphStyle.js +16 -24
- package/src/plugins/dropdown/table/index.js +72 -17
- package/src/plugins/dropdown/table/render/table.html.js +3 -1
- package/src/plugins/dropdown/table/services/table.cell.js +33 -7
- package/src/plugins/dropdown/table/services/table.clipboard.js +5 -1
- package/src/plugins/dropdown/table/services/table.grid.js +40 -9
- package/src/plugins/dropdown/table/services/table.resize.js +55 -11
- package/src/plugins/dropdown/table/services/table.selection.js +27 -7
- package/src/plugins/dropdown/table/services/table.style.js +120 -20
- package/src/plugins/dropdown/template.js +13 -29
- package/src/plugins/dropdown/textStyle.js +22 -35
- package/src/plugins/field/autocomplete.js +8 -4
- package/src/plugins/field/slashCommand.js +297 -0
- package/src/plugins/index.js +3 -0
- package/src/plugins/input/fontSize.js +37 -8
- package/src/plugins/input/pageNavigator.js +7 -1
- package/src/plugins/modal/audio.js +71 -17
- package/src/plugins/modal/drawing.js +41 -11
- package/src/plugins/modal/embed.js +132 -22
- package/src/plugins/modal/image/index.js +115 -28
- package/src/plugins/modal/image/services/image.size.js +9 -2
- package/src/plugins/modal/image/services/image.upload.js +38 -4
- package/src/plugins/modal/link.js +9 -2
- package/src/plugins/modal/math.js +10 -2
- package/src/plugins/modal/video/index.js +112 -26
- package/src/plugins/modal/video/render/video.html.js +4 -1
- package/src/plugins/modal/video/services/video.size.js +15 -4
- package/src/plugins/modal/video/services/video.upload.js +10 -2
- package/src/plugins/popup/anchor.js +14 -2
- package/src/suneditor.js +8 -2
- package/src/themes/cobalt.css +12 -0
- package/src/themes/cream.css +12 -0
- package/src/themes/dark.css +12 -0
- package/src/themes/midnight.css +12 -0
- package/src/typedef.js +32 -21
- package/types/assets/icons/defaultIcons.d.ts +1 -0
- package/types/core/config/contextProvider.d.ts +7 -1
- package/types/core/config/eventManager.d.ts +37 -4
- package/types/core/config/optionProvider.d.ts +5 -1
- package/types/core/event/actions/index.d.ts +44 -6
- package/types/core/event/effects/keydown.registry.d.ts +37 -7
- package/types/core/event/effects/ruleHelpers.d.ts +6 -1
- package/types/core/event/eventOrchestrator.d.ts +6 -1
- package/types/core/event/handlers/handler_ww_clipboard.d.ts +15 -3
- package/types/core/event/handlers/handler_ww_dragDrop.d.ts +14 -2
- package/types/core/event/handlers/handler_ww_input.d.ts +10 -2
- package/types/core/event/handlers/handler_ww_key.d.ts +10 -2
- package/types/core/event/handlers/handler_ww_mouse.d.ts +25 -5
- package/types/core/event/ports.d.ts +7 -2
- package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
- package/types/core/kernel/store.d.ts +4 -1
- package/types/core/logic/dom/format.d.ts +32 -14
- package/types/core/logic/dom/html.d.ts +11 -3
- package/types/core/logic/dom/nodeTransform.d.ts +6 -6
- package/types/core/logic/panel/blockHandle.d.ts +64 -0
- package/types/core/logic/panel/blockResolver.d.ts +50 -0
- package/types/core/logic/panel/menu.d.ts +72 -3
- package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
- package/types/core/logic/shell/component.d.ts +5 -3
- package/types/core/logic/shell/shortcuts.d.ts +10 -1
- package/types/core/logic/shell/ui.d.ts +7 -1
- package/types/core/schema/context.d.ts +7 -1
- package/types/core/schema/frameContext.d.ts +36 -4
- package/types/core/schema/options.d.ts +110 -37
- package/types/core/section/constructor.d.ts +38 -1
- package/types/events.d.ts +208 -32
- package/types/helper/converter.d.ts +4 -2
- package/types/helper/dom/domCheck.d.ts +12 -2
- package/types/helper/dom/domQuery.d.ts +35 -7
- package/types/helper/dom/domUtils.d.ts +4 -1
- package/types/interfaces/plugins.d.ts +2 -2
- package/types/langs/_Lang.d.ts +0 -1
- package/types/modules/contract/Browser.d.ts +7 -7
- package/types/modules/contract/ColorPicker.d.ts +2 -2
- package/types/modules/contract/Controller.d.ts +7 -0
- package/types/modules/contract/Figure.d.ts +31 -4
- package/types/modules/ui/CommandMenu.d.ts +262 -0
- package/types/modules/ui/ModalAnchorEditor.d.ts +6 -1
- package/types/modules/ui/SelectMenu.d.ts +56 -3
- package/types/modules/ui/index.d.ts +1 -0
- package/types/plugins/browser/audioGallery.d.ts +17 -1
- package/types/plugins/browser/fileBrowser.d.ts +17 -1
- package/types/plugins/browser/fileGallery.d.ts +17 -1
- package/types/plugins/browser/imageGallery.d.ts +17 -1
- package/types/plugins/browser/videoGallery.d.ts +17 -1
- package/types/plugins/dropdown/align.d.ts +1 -1
- package/types/plugins/dropdown/blockStyle.d.ts +3 -1
- package/types/plugins/dropdown/table/index.d.ts +4 -1
- package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
- package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
- package/types/plugins/field/autocomplete.d.ts +0 -1
- package/types/plugins/field/slashCommand.d.ts +165 -0
- package/types/plugins/index.d.ts +3 -0
- package/types/plugins/modal/embed.d.ts +34 -0
- package/types/plugins/modal/link.d.ts +4 -1
- package/types/typedef.d.ts +48 -23
|
@@ -75,7 +75,14 @@ class FileManager {
|
|
|
75
75
|
this.uploadFileLength = data.size;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
this.apiManager.call({
|
|
78
|
+
this.apiManager.call({
|
|
79
|
+
method: 'POST',
|
|
80
|
+
url: uploadUrl,
|
|
81
|
+
headers: uploadHeader,
|
|
82
|
+
data: formData,
|
|
83
|
+
callBack,
|
|
84
|
+
errorCallBack,
|
|
85
|
+
});
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
/**
|
|
@@ -105,7 +112,12 @@ class FileManager {
|
|
|
105
112
|
this.uploadFileLength = data.size;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
return await this.apiManager.asyncCall({
|
|
115
|
+
return await this.apiManager.asyncCall({
|
|
116
|
+
method: 'POST',
|
|
117
|
+
url: uploadUrl,
|
|
118
|
+
headers: uploadHeader,
|
|
119
|
+
data: formData,
|
|
120
|
+
});
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
/**
|
|
@@ -196,7 +208,14 @@ class FileManager {
|
|
|
196
208
|
|
|
197
209
|
infoList.splice(i, 1);
|
|
198
210
|
|
|
199
|
-
const params = {
|
|
211
|
+
const params = {
|
|
212
|
+
element: null,
|
|
213
|
+
index: dataIndex,
|
|
214
|
+
state: 'delete',
|
|
215
|
+
info: null,
|
|
216
|
+
remainingFilesCount: 0,
|
|
217
|
+
pluginName: this.kind,
|
|
218
|
+
};
|
|
200
219
|
if (this.actionEventName) {
|
|
201
220
|
this.#$.eventManager.triggerEvent(this.actionEventName, params);
|
|
202
221
|
}
|
|
@@ -214,9 +233,17 @@ class FileManager {
|
|
|
214
233
|
const params = { element: null, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind };
|
|
215
234
|
for (let i = 0, len = this.infoList.length; i < len; i++) {
|
|
216
235
|
if (this.actionEventName) {
|
|
217
|
-
this.#$.eventManager.triggerEvent(this.actionEventName, {
|
|
236
|
+
this.#$.eventManager.triggerEvent(this.actionEventName, {
|
|
237
|
+
...params,
|
|
238
|
+
index: this.infoList[i].index,
|
|
239
|
+
pluginName: this.kind,
|
|
240
|
+
});
|
|
218
241
|
}
|
|
219
|
-
this.#$.eventManager.triggerEvent('onFileManagerAction', {
|
|
242
|
+
this.#$.eventManager.triggerEvent('onFileManagerAction', {
|
|
243
|
+
...params,
|
|
244
|
+
index: this.infoList[i].index,
|
|
245
|
+
pluginName: this.kind,
|
|
246
|
+
});
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
this.infoList = [];
|
|
@@ -233,7 +260,14 @@ class FileManager {
|
|
|
233
260
|
if (index === this.infoList[i].index) {
|
|
234
261
|
this.infoList.splice(i, 1);
|
|
235
262
|
if (this.actionEventName) {
|
|
236
|
-
this.#$.eventManager.triggerEvent(this.actionEventName, {
|
|
263
|
+
this.#$.eventManager.triggerEvent(this.actionEventName, {
|
|
264
|
+
element: null,
|
|
265
|
+
index,
|
|
266
|
+
state: 'delete',
|
|
267
|
+
info: null,
|
|
268
|
+
remainingFilesCount: 0,
|
|
269
|
+
pluginName: this.kind,
|
|
270
|
+
});
|
|
237
271
|
}
|
|
238
272
|
return;
|
|
239
273
|
}
|
|
@@ -252,7 +286,8 @@ class FileManager {
|
|
|
252
286
|
let state = '';
|
|
253
287
|
|
|
254
288
|
file ||= {
|
|
255
|
-
name:
|
|
289
|
+
name:
|
|
290
|
+
GetAttr(element, 'file-name') || (typeof element.src === 'string' ? element.src.split('/').pop() : ''),
|
|
256
291
|
size: Number(GetAttr(element, 'file-size')) || 0,
|
|
257
292
|
};
|
|
258
293
|
|
|
@@ -316,7 +351,14 @@ class FileManager {
|
|
|
316
351
|
}
|
|
317
352
|
}.bind(this, this.#$.component, element);
|
|
318
353
|
|
|
319
|
-
const params = {
|
|
354
|
+
const params = {
|
|
355
|
+
element,
|
|
356
|
+
index: dataIndex,
|
|
357
|
+
state,
|
|
358
|
+
info,
|
|
359
|
+
remainingFilesCount: --this.uploadFileLength < 0 ? 0 : this.uploadFileLength,
|
|
360
|
+
pluginName: this.kind,
|
|
361
|
+
};
|
|
320
362
|
if (this.actionEventName) {
|
|
321
363
|
this.#$.eventManager.triggerEvent(this.actionEventName, params);
|
|
322
364
|
}
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { dom, env } from '../../helper';
|
|
2
|
+
import SelectMenu from './SelectMenu.js';
|
|
3
|
+
|
|
4
|
+
const { _w } = env;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description Resolve an icon spec to an HTML string. Accepts a key from `$.icons`,
|
|
8
|
+
* a raw HTML snippet (anything starting with `<`), or empty.
|
|
9
|
+
* @param {string} icon
|
|
10
|
+
* @param {Object} icons
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function resolveIconHTML(icon, icons) {
|
|
14
|
+
if (!icon || typeof icon !== 'string') return '';
|
|
15
|
+
if (icon.charAt(0) === '<') return icon;
|
|
16
|
+
return icons[icon] || '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @description Build the canonical command-menu row HTML. Single source of truth for the
|
|
21
|
+
* BlockHandle action menu and the SlashCommand menu, so they look identical.
|
|
22
|
+
* @param {string} label
|
|
23
|
+
* @param {string} iconHTML
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function buildRowHTML(label, iconHTML) {
|
|
27
|
+
const inner = iconHTML
|
|
28
|
+
? `<span class="se-block-menu-icon">${iconHTML}</span><span class="se-block-menu-label">${label}</span>`
|
|
29
|
+
: `<span class="se-block-menu-label se-block-menu-label-full">${label}</span>`;
|
|
30
|
+
return `<button type="button" class="se-cmd-row">${inner}</button>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} CommandMenuItem
|
|
35
|
+
* @property {string} [key] - Optional identifier.
|
|
36
|
+
* @property {string} title - Display label.
|
|
37
|
+
* @property {string} [icon] - `$.icons` key or raw HTML snippet.
|
|
38
|
+
* @property {string[]} [keywords] - Extra search terms (used by the filter).
|
|
39
|
+
* @property {function(SunEditor.Deps, *): void | Promise<void>} action - Custom callback. The second
|
|
40
|
+
* argument is the host-supplied context (e.g. `{block}` for BlockHandle, `{triggerChar, query}` for SlashCommand).
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} SubmenuChild
|
|
45
|
+
* @property {string} pluginName - Owning plugin key.
|
|
46
|
+
* @property {HTMLElement} element - The original toolbar dropdown button element. Passed to `plugin.action(element)` on click.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} ResolvedItem
|
|
51
|
+
* @property {'custom'|'plugin'|'submenu'|'dropdownFree'} kind
|
|
52
|
+
* @property {string} title
|
|
53
|
+
* @property {string} iconHTML
|
|
54
|
+
* @property {string} search - Lower-cased haystack for substring filtering.
|
|
55
|
+
* @property {CommandMenuItem} [raw]
|
|
56
|
+
* @property {string} [name] - Plugin key.
|
|
57
|
+
* @property {string} [type] - Plugin type (`'modal'`, `'command'`, etc.).
|
|
58
|
+
* @property {string} [command]
|
|
59
|
+
* @property {SubmenuChild[]} [children] - For `kind: 'submenu'` — clickable child rows (one per dropdown item).
|
|
60
|
+
* @property {string[]} [childMenus] - For `kind: 'submenu'` — HTML for each child row.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} CommandMenuParams
|
|
65
|
+
* @property {Array<string | CommandMenuItem>} items - Raw menu entries.
|
|
66
|
+
* @property {function(string, Object, *, Object, Object): ({title: string, icon: string, type: string, command: string} | null)} resolveButton
|
|
67
|
+
* Caller-provided plugin/button resolver. Inject `ResolveButton` from `core/section/constructor`.
|
|
68
|
+
* Injected (not imported) because this module lives under `src/modules/` and cannot import from `src/core/*`.
|
|
69
|
+
* @property {Object} selectMenuParams - Base SelectMenu params (`position`, `minWidth`, `keydownTarget`, etc.).
|
|
70
|
+
* @property {function(CommandMenuItem, { icons: Object }): string} [renderCustomItem] - Optional renderer
|
|
71
|
+
* applied to custom (object) items only. Plugin-string items always render with `buildRowHTML`.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @class
|
|
76
|
+
* @description Shared command-menu module used by BlockHandle and SlashCommand.
|
|
77
|
+
* - Owns the underlying `SelectMenu`, renders rows with the canonical BlockHandle row HTML.
|
|
78
|
+
* - Supports four item kinds:
|
|
79
|
+
* - `'custom'` — user-defined `{title, icon?, action}` invoked with `($, hostContext)`.
|
|
80
|
+
* - `'plugin'` — plain plugin (modal/browser/command/popup) dispatched via `plugin.open/show/action`.
|
|
81
|
+
* - `'submenu'` — dropdown plugin auto-expanded inline (children come from `$.menu.itemsMap`).
|
|
82
|
+
* - `'dropdownFree'` — dropdown-free plugin (e.g. `fontColor`, `table`) shown with a submenu arrow
|
|
83
|
+
* and an attached hover flyout that reuses the plugin's toolbar dropdown DOM.
|
|
84
|
+
* - Handles blur prevention on mousedown, hover flyout lifecycle, and click-to-toggle for dropdown-free.
|
|
85
|
+
*
|
|
86
|
+
* Host responsibilities (kept outside this module):
|
|
87
|
+
* - Pre-dispatch side effects (BlockHandle: `expandRangeToFullLines`; SlashCommand: delete trigger text).
|
|
88
|
+
* - Anchor element selection (BlockHandle anchors to the drag button; SlashCommand to a Controller wrapper).
|
|
89
|
+
* - Context object for custom-item actions.
|
|
90
|
+
*/
|
|
91
|
+
class CommandMenu {
|
|
92
|
+
#$;
|
|
93
|
+
#resolveButton;
|
|
94
|
+
#rawItems;
|
|
95
|
+
#resolved = null;
|
|
96
|
+
#renderCustomItem;
|
|
97
|
+
|
|
98
|
+
/** @type {Map<number, { name: string, plugin: any, li: HTMLElement }>} */
|
|
99
|
+
#freeMap = new Map();
|
|
100
|
+
/** @type {?{ dropdown: HTMLElement, plugin: any, originalParent: ?Node, anchorLi: HTMLElement, evClick: ?SunEditor.Event.Info }} */
|
|
101
|
+
#flyoutState = null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @type {Array<{ name: string, idx: number }>}
|
|
105
|
+
*/
|
|
106
|
+
#pendingTargets = [];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @type {Array<{ name: string, target: HTMLElement }>}
|
|
110
|
+
*/
|
|
111
|
+
#registered = [];
|
|
112
|
+
|
|
113
|
+
/** @type {SelectMenu} */
|
|
114
|
+
selectMenu;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @constructor
|
|
118
|
+
* @param {*} _host - Reserved for future host-aware behavior (kept for API symmetry with `Modal`/`Controller`).
|
|
119
|
+
* @param {SunEditor.Deps} $ - Deps bag.
|
|
120
|
+
* @param {CommandMenuParams} params
|
|
121
|
+
*/
|
|
122
|
+
constructor(_host, $, params) {
|
|
123
|
+
this.#$ = $;
|
|
124
|
+
this.#resolveButton = params.resolveButton;
|
|
125
|
+
this.#rawItems = Array.isArray(params.items) ? params.items : [];
|
|
126
|
+
this.#renderCustomItem = typeof params.renderCustomItem === 'function' ? params.renderCustomItem : null;
|
|
127
|
+
|
|
128
|
+
// Wrap the host's closeMethod so the flyout is always torn down with the menu.
|
|
129
|
+
const userClose = params.selectMenuParams?.closeMethod;
|
|
130
|
+
this.selectMenu = new SelectMenu($, {
|
|
131
|
+
...params.selectMenuParams,
|
|
132
|
+
closeMethod: () => {
|
|
133
|
+
this.#closeFlyout();
|
|
134
|
+
this.#unregisterAll();
|
|
135
|
+
userClose?.();
|
|
136
|
+
},
|
|
137
|
+
subEscMethod: () => {
|
|
138
|
+
if (!this.#flyoutState) return false;
|
|
139
|
+
const anchorLi = this.#flyoutState.anchorLi;
|
|
140
|
+
this.#closeFlyout();
|
|
141
|
+
const idx = anchorLi ? Number(anchorLi.getAttribute('data-index')) : NaN;
|
|
142
|
+
if (!Number.isNaN(idx)) this.selectMenu.setItem(idx);
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @description Bind the menu to a reference element and register the click handler.
|
|
150
|
+
* @param {HTMLElement} referElement
|
|
151
|
+
* @param {(item: ResolvedItem|null) => void} onSelect - Called with the resolved item picked by the user.
|
|
152
|
+
* Hosts run their pre-dispatch hook here (delete trigger, etc.) and then call `dispatch(item, ctx)`.
|
|
153
|
+
* @param {{ class?: string, style?: string }} [attr]
|
|
154
|
+
*/
|
|
155
|
+
attach(referElement, onSelect, attr = {}) {
|
|
156
|
+
this.selectMenu.on(referElement, /** @type {*} */ (onSelect), attr);
|
|
157
|
+
|
|
158
|
+
this.#$.eventManager.addEvent(this.selectMenu.form, 'mousedown', (e) => {
|
|
159
|
+
if (env.isMobile) {
|
|
160
|
+
this.#$.store.set('_preventBlur', true);
|
|
161
|
+
} else {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.#$.eventManager.addEvent(this.selectMenu.form, 'mousemove', this.#onMenuMouseMove.bind(this));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @description Replace the items list (e.g. when host options change). Invalidates the resolved cache.
|
|
171
|
+
* @param {Array<string | CommandMenuItem>} items
|
|
172
|
+
*/
|
|
173
|
+
setItems(items) {
|
|
174
|
+
this.#rawItems = Array.isArray(items) ? items : [];
|
|
175
|
+
this.#resolved = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @description Lazily normalize raw items. Lazy because plugin instances (`$.plugins`) may not
|
|
180
|
+
* be available during the host's constructor.
|
|
181
|
+
* @returns {ResolvedItem[]}
|
|
182
|
+
*/
|
|
183
|
+
getItems() {
|
|
184
|
+
if (this.#resolved) return this.#resolved;
|
|
185
|
+
/** @type {ResolvedItem[]} */
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const entry of this.#rawItems) {
|
|
188
|
+
if (typeof entry === 'string') {
|
|
189
|
+
const r = this.#resolveButton(entry, this.#$.plugins, this.#$.options, this.#$.icons, this.#$.lang);
|
|
190
|
+
if (!r) continue;
|
|
191
|
+
|
|
192
|
+
const type = r.type || '';
|
|
193
|
+
|
|
194
|
+
// Dropdown-free
|
|
195
|
+
if (/dropdown-free/.test(type)) {
|
|
196
|
+
out.push({
|
|
197
|
+
kind: 'dropdownFree',
|
|
198
|
+
title: r.title,
|
|
199
|
+
iconHTML: r.icon || '',
|
|
200
|
+
search: (r.title + ' ' + entry).toLowerCase(),
|
|
201
|
+
name: entry,
|
|
202
|
+
type,
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Dropdown
|
|
208
|
+
if (/dropdown/.test(type)) {
|
|
209
|
+
const menuItems = this.#$.menu?.itemsMap?.[entry] || [];
|
|
210
|
+
if (menuItems.length > 0) {
|
|
211
|
+
out.push({
|
|
212
|
+
kind: 'submenu',
|
|
213
|
+
title: r.title,
|
|
214
|
+
iconHTML: r.icon || '',
|
|
215
|
+
search: (r.title + ' ' + entry).toLowerCase(),
|
|
216
|
+
name: entry,
|
|
217
|
+
children: menuItems.map((mi) => ({ pluginName: entry, element: mi._element })),
|
|
218
|
+
childMenus: menuItems.map((mi) => {
|
|
219
|
+
const iconEl =
|
|
220
|
+
mi._element?.querySelector('.se-list-icon') || mi._element?.querySelector('svg');
|
|
221
|
+
const icon = iconEl ? iconEl.outerHTML : '';
|
|
222
|
+
const label = icon ? `${icon}<span>${mi.title}</span>` : mi.title;
|
|
223
|
+
return `<span class="se-block-submenu-item">${label}</span>`;
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
out.push({
|
|
231
|
+
kind: 'plugin',
|
|
232
|
+
title: r.title,
|
|
233
|
+
iconHTML: r.icon || '',
|
|
234
|
+
search: (r.title + ' ' + entry).toLowerCase(),
|
|
235
|
+
name: entry,
|
|
236
|
+
type,
|
|
237
|
+
command: r.command,
|
|
238
|
+
});
|
|
239
|
+
} else if (entry && typeof entry === 'object' && typeof entry.action === 'function') {
|
|
240
|
+
const title = entry.title || entry.key || '';
|
|
241
|
+
const keywords = Array.isArray(entry.keywords) ? entry.keywords.join(' ') : '';
|
|
242
|
+
out.push({
|
|
243
|
+
kind: 'custom',
|
|
244
|
+
title,
|
|
245
|
+
iconHTML: resolveIconHTML(entry.icon, this.#$.icons),
|
|
246
|
+
search: (title + ' ' + keywords).toLowerCase(),
|
|
247
|
+
raw: entry,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.#resolved = out;
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @description Case-insensitive substring filter on `title + keywords + plugin name`.
|
|
257
|
+
* Empty query returns all items (up to `limit`).
|
|
258
|
+
* @param {string} query
|
|
259
|
+
* @param {number} [limit=Infinity]
|
|
260
|
+
* @returns {ResolvedItem[]}
|
|
261
|
+
*/
|
|
262
|
+
filter(query, limit = Infinity) {
|
|
263
|
+
const items = this.getItems();
|
|
264
|
+
const q = (query || '').toLowerCase().trim();
|
|
265
|
+
const out = [];
|
|
266
|
+
for (const it of items) {
|
|
267
|
+
if (!q || it.search.indexOf(q) !== -1) out.push(it);
|
|
268
|
+
if (out.length >= limit) break;
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @description Build row HTML for a list of resolved items.
|
|
275
|
+
* @param {ResolvedItem[]} items
|
|
276
|
+
* @returns {string[]}
|
|
277
|
+
*/
|
|
278
|
+
renderRows(items) {
|
|
279
|
+
const ctx = { icons: this.#$.icons };
|
|
280
|
+
const arrow = `<span class="se-submenu-arrow">${this.#$.icons.menu_arrow_right}</span>`;
|
|
281
|
+
return items.map((it) => {
|
|
282
|
+
if (it.kind === 'custom' && this.#renderCustomItem) return this.#renderCustomItem(it.raw, ctx);
|
|
283
|
+
const row = buildRowHTML(it.title, it.iconHTML);
|
|
284
|
+
return it.kind === 'dropdownFree' ? row + arrow : row;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @description Populate the SelectMenu rows.
|
|
290
|
+
* @param {ResolvedItem[]} items
|
|
291
|
+
*/
|
|
292
|
+
createRows(items) {
|
|
293
|
+
// SlashCommand rebuilds rows on every keystroke; previous `li`s become detached. Drop their
|
|
294
|
+
// registrations before the SelectMenu rewrite so toolbar map never points at orphaned nodes.
|
|
295
|
+
this.#unregisterAll();
|
|
296
|
+
|
|
297
|
+
const selectItems = items.map((it) =>
|
|
298
|
+
it.kind === 'submenu' ? { children: it.children, childMenus: it.childMenus } : it,
|
|
299
|
+
);
|
|
300
|
+
this.selectMenu.create(selectItems, this.renderRows(items));
|
|
301
|
+
|
|
302
|
+
this.#freeMap.clear();
|
|
303
|
+
this.#pendingTargets.length = 0;
|
|
304
|
+
items.forEach((it, i) => {
|
|
305
|
+
if (it.kind === 'dropdownFree') {
|
|
306
|
+
this.#freeMap.set(i, {
|
|
307
|
+
name: it.name,
|
|
308
|
+
plugin: this.#$.plugins[it.name],
|
|
309
|
+
li: this.selectMenu.menus[i],
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (it.kind === 'plugin' || it.kind === 'dropdownFree') {
|
|
313
|
+
this.#pendingTargets.push({ name: it.name, idx: i });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// SlashCommand stays "open" across createRows calls — re-register immediately so the freshly
|
|
318
|
+
// built `li` set takes over from the discarded one without waiting for the next open().
|
|
319
|
+
if (this.selectMenu.isOpen) this.#registerAll();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @description Open the SelectMenu. Lazily (re-)registers menu rows into
|
|
324
|
+
* `commandDispatcher.targets` so `selectionState.update` paints the `active` class on them through
|
|
325
|
+
* @param {string} [position]
|
|
326
|
+
*/
|
|
327
|
+
open(position) {
|
|
328
|
+
this.selectMenu.open(position);
|
|
329
|
+
if (this.#registered.length === 0) this.#registerAll();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @description Close the SelectMenu. Also unregisters menu rows from `commandDispatcher.targets`
|
|
334
|
+
*/
|
|
335
|
+
close() {
|
|
336
|
+
this.#unregisterAll();
|
|
337
|
+
this.selectMenu.close();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @description Whether a sub-panel (native submenu or dropdown-free flyout) is currently open.
|
|
342
|
+
* Lets an owning Controller keep the menu open on ESC and close only the sub-panel.
|
|
343
|
+
* @returns {boolean}
|
|
344
|
+
*/
|
|
345
|
+
hasOpenSubPanel() {
|
|
346
|
+
return this.selectMenu.hasOpenSubmenu() || !!this.#flyoutState;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @description Push every `#pendingTargets` row's inner `<button>` into the toolbar target map
|
|
351
|
+
*/
|
|
352
|
+
#registerAll() {
|
|
353
|
+
const cd = this.#$.commandDispatcher;
|
|
354
|
+
for (const { name, idx } of this.#pendingTargets) {
|
|
355
|
+
const btn = this.#resolveButtonTarget(idx);
|
|
356
|
+
if (!btn) continue;
|
|
357
|
+
cd.registerTargets(name, btn);
|
|
358
|
+
this.#registered.push({ name, target: btn });
|
|
359
|
+
}
|
|
360
|
+
// Bust the dedup cache so applyTagEffect re-runs even when the selection node is unchanged.
|
|
361
|
+
this.#$.store.set('_lastSelectionNode', null);
|
|
362
|
+
this.#$.eventManager.applyTagEffect();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @description Drop every `(cmd, target)` pair this instance registered. Safe to call repeatedly.
|
|
367
|
+
*/
|
|
368
|
+
#unregisterAll() {
|
|
369
|
+
if (this.#registered.length === 0) return;
|
|
370
|
+
const cd = this.#$.commandDispatcher;
|
|
371
|
+
for (const { name, target } of this.#registered) cd.unregisterTargets(name, target);
|
|
372
|
+
this.#registered.length = 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @description Resolve the registration target for menu index `idx`.
|
|
377
|
+
* (defensive — should always exist after {@link buildRowHTML}).
|
|
378
|
+
* @param {number} idx
|
|
379
|
+
* @returns {?HTMLButtonElement}
|
|
380
|
+
*/
|
|
381
|
+
#resolveButtonTarget(idx) {
|
|
382
|
+
const li = this.selectMenu.menus[idx];
|
|
383
|
+
if (!li) return null;
|
|
384
|
+
return /** @type {?HTMLButtonElement} */ (li.querySelector('button.se-cmd-row') || li);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @description Highlight a row.
|
|
389
|
+
* @param {number} index
|
|
390
|
+
*/
|
|
391
|
+
setItem(index) {
|
|
392
|
+
this.selectMenu.setItem(index);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @description Soft-hide / soft-show without changing open state. Pass-through.
|
|
397
|
+
* @param {boolean} hidden
|
|
398
|
+
*/
|
|
399
|
+
setHidden(hidden) {
|
|
400
|
+
this.selectMenu.setHidden(hidden);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
get isOpen() {
|
|
404
|
+
return this.selectMenu.isOpen;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @description Dispatch a resolved item. For custom items, calls `raw.action($, ctx)`. For plugin
|
|
409
|
+
* items, routes to `plugin.open/show/action` or `commandDispatcher.run` (built-ins). For dropdown-free
|
|
410
|
+
* items, toggles the hover flyout
|
|
411
|
+
* @param {ResolvedItem | SubmenuChild | null} item
|
|
412
|
+
* @param {*} [ctx] - Context passed to custom-item `action` (host-specific shape).
|
|
413
|
+
* @returns {boolean} `true` if a plugin was dispatched
|
|
414
|
+
*/
|
|
415
|
+
dispatch(item, ctx) {
|
|
416
|
+
if (!item) return false;
|
|
417
|
+
const asChild = /** @type {SubmenuChild} */ (/** @type {unknown} */ (item));
|
|
418
|
+
if (asChild.pluginName && asChild.element) {
|
|
419
|
+
const plugin = this.#$.plugins[asChild.pluginName];
|
|
420
|
+
plugin?.action?.(asChild.element);
|
|
421
|
+
this.#$.history.push(false);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
const resolved = /** @type {ResolvedItem} */ (item);
|
|
425
|
+
if (resolved.kind === 'custom') {
|
|
426
|
+
try {
|
|
427
|
+
resolved.raw.action(this.#$, ctx);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.error('[SUNEDITOR.commandMenu.customAction]', e);
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
if (resolved.kind === 'dropdownFree') {
|
|
434
|
+
const plugin = this.#$.plugins[resolved.name];
|
|
435
|
+
if (!plugin) return false;
|
|
436
|
+
if (this.#flyoutState?.plugin === plugin) {
|
|
437
|
+
this.#closeFlyout();
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
const idx = this.selectMenu.items.indexOf(/** @type {*} */ (resolved));
|
|
441
|
+
const anchorLi = idx >= 0 ? this.selectMenu.menus[idx] : null;
|
|
442
|
+
if (anchorLi) this.#openFlyout(resolved.name, plugin, anchorLi);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const plugin = resolved.name ? this.#$.plugins[resolved.name] : null;
|
|
446
|
+
if (plugin) {
|
|
447
|
+
const type = resolved.type || '';
|
|
448
|
+
if (/modal/.test(type)) plugin.open?.();
|
|
449
|
+
else if (/browser/.test(type)) plugin.open?.(null);
|
|
450
|
+
else if (/popup/.test(type)) plugin.show?.();
|
|
451
|
+
else plugin.action?.(dom.utils.createElement('BUTTON', { 'data-command': resolved.name }));
|
|
452
|
+
this.#$.history.push(false);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
if (resolved.command) {
|
|
456
|
+
this.#$.commandDispatcher.run(resolved.command, null, null);
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @description Mousemove handler on the menu form. For dropdown-free rows.
|
|
463
|
+
* @param {MouseEvent} e
|
|
464
|
+
*/
|
|
465
|
+
#onMenuMouseMove(e) {
|
|
466
|
+
if (this.#freeMap.size === 0) return;
|
|
467
|
+
const target = /** @type {HTMLElement} */ (e.target);
|
|
468
|
+
if (this.#flyoutState?.dropdown?.contains(target)) return;
|
|
469
|
+
|
|
470
|
+
const li = target.closest?.('li[data-index]');
|
|
471
|
+
if (!li) return;
|
|
472
|
+
|
|
473
|
+
const idx = Number(li.getAttribute('data-index'));
|
|
474
|
+
const free = this.#freeMap.get(idx);
|
|
475
|
+
if (free) {
|
|
476
|
+
if (this.#flyoutState?.plugin !== free.plugin) {
|
|
477
|
+
this.#openFlyout(free.name, free.plugin, free.li);
|
|
478
|
+
}
|
|
479
|
+
} else if (this.#flyoutState) {
|
|
480
|
+
this.#closeFlyout();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @description Open a dropdown-free plugin's flyout.
|
|
486
|
+
* @param {string} name
|
|
487
|
+
* @param {*} plugin
|
|
488
|
+
* @param {HTMLElement} anchorLi
|
|
489
|
+
*/
|
|
490
|
+
#openFlyout(name, plugin, anchorLi) {
|
|
491
|
+
if (this.#flyoutState) this.#closeFlyout();
|
|
492
|
+
const dropdown = this.#$.menu?.targetMap?.[name];
|
|
493
|
+
if (!dropdown || !this.selectMenu.form) return;
|
|
494
|
+
|
|
495
|
+
const originalParent = dropdown.parentNode;
|
|
496
|
+
this.selectMenu.form.appendChild(dropdown);
|
|
497
|
+
this.#positionFlyout(dropdown, anchorLi, this.selectMenu.form);
|
|
498
|
+
|
|
499
|
+
dom.utils.addClass(anchorLi, 'se-submenu-open');
|
|
500
|
+
plugin.on?.(anchorLi);
|
|
501
|
+
|
|
502
|
+
// Clicking inside the flyout's dropdown should dismiss both the flyout and the parent menu —
|
|
503
|
+
// matches the toolbar behavior where a click commits the selection.
|
|
504
|
+
const evClick = this.#$.eventManager.addEvent(dropdown, 'click', () =>
|
|
505
|
+
_w.setTimeout(() => {
|
|
506
|
+
this.#closeFlyout();
|
|
507
|
+
this.selectMenu.close();
|
|
508
|
+
}, 0),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
this.#flyoutState = { dropdown, plugin, originalParent, anchorLi, evClick };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @description Close the open flyout (if any). Restores the dropdown DOM to its original parent
|
|
516
|
+
* (the menuTray) so the toolbar can use it again.
|
|
517
|
+
*/
|
|
518
|
+
#closeFlyout() {
|
|
519
|
+
const s = this.#flyoutState;
|
|
520
|
+
if (!s) return;
|
|
521
|
+
this.#flyoutState = null;
|
|
522
|
+
|
|
523
|
+
this.#$?.eventManager.removeEvent(s.evClick);
|
|
524
|
+
s.dropdown.style.cssText = '';
|
|
525
|
+
s.dropdown.style.display = 'none';
|
|
526
|
+
|
|
527
|
+
if (s.anchorLi) dom.utils.removeClass(s.anchorLi, 'se-submenu-open');
|
|
528
|
+
|
|
529
|
+
if (s.originalParent && s.originalParent !== s.dropdown.parentNode) {
|
|
530
|
+
s.originalParent.appendChild(s.dropdown);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
s.plugin?.off?.();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* @description Place `el` next to `anchor`, coordinates relative to `container`. Mirrors
|
|
538
|
+
* `SelectMenu.#openSubmenu`: prefers the text-direction side, flips on overflow, shifts on
|
|
539
|
+
* double overflow.
|
|
540
|
+
* @param {HTMLElement} el
|
|
541
|
+
* @param {HTMLElement} anchor
|
|
542
|
+
* @param {HTMLElement} container
|
|
543
|
+
*/
|
|
544
|
+
#positionFlyout(el, anchor, container) {
|
|
545
|
+
const a = anchor.getBoundingClientRect();
|
|
546
|
+
const c = container.getBoundingClientRect();
|
|
547
|
+
const isRtl = !!this.#$.options.get('_rtl');
|
|
548
|
+
|
|
549
|
+
el.style.position = 'absolute';
|
|
550
|
+
el.style.right = '';
|
|
551
|
+
el.style.visibility = 'hidden';
|
|
552
|
+
el.style.display = 'block';
|
|
553
|
+
|
|
554
|
+
const elW = el.offsetWidth;
|
|
555
|
+
const elH = el.offsetHeight;
|
|
556
|
+
const vpW = _w.innerWidth;
|
|
557
|
+
const vpH = _w.innerHeight;
|
|
558
|
+
const gap = 4;
|
|
559
|
+
|
|
560
|
+
// Horizontal: preferred side follows text direction. Flip if it overflows; if both overflow,
|
|
561
|
+
// keep the smaller-overflow side and shift inward by the missing amount.
|
|
562
|
+
const rightVP = a.right + gap;
|
|
563
|
+
const leftVP = a.left - elW - gap;
|
|
564
|
+
const rightOverflow = Math.max(0, rightVP + elW - vpW);
|
|
565
|
+
const leftOverflow = Math.max(0, -leftVP);
|
|
566
|
+
const prefer = isRtl ? 'left' : 'right';
|
|
567
|
+
let leftPx;
|
|
568
|
+
if (prefer === 'right') {
|
|
569
|
+
if (rightOverflow === 0) leftPx = rightVP;
|
|
570
|
+
else if (leftOverflow === 0) leftPx = leftVP;
|
|
571
|
+
else if (rightOverflow <= leftOverflow) leftPx = rightVP - rightOverflow;
|
|
572
|
+
else leftPx = leftVP + leftOverflow;
|
|
573
|
+
} else {
|
|
574
|
+
if (leftOverflow === 0) leftPx = leftVP;
|
|
575
|
+
else if (rightOverflow === 0) leftPx = rightVP;
|
|
576
|
+
else if (leftOverflow <= rightOverflow) leftPx = leftVP + leftOverflow;
|
|
577
|
+
else leftPx = rightVP - rightOverflow;
|
|
578
|
+
}
|
|
579
|
+
el.style.left = leftPx - c.left + 'px';
|
|
580
|
+
|
|
581
|
+
// Vertical: top-aligned with anchor, flip on overflow.
|
|
582
|
+
const topVP = a.top;
|
|
583
|
+
const bottomVP = a.bottom - elH;
|
|
584
|
+
const downOverflow = Math.max(0, topVP + elH - vpH);
|
|
585
|
+
const upOverflow = Math.max(0, -bottomVP);
|
|
586
|
+
let topPx;
|
|
587
|
+
if (downOverflow === 0) topPx = topVP;
|
|
588
|
+
else if (upOverflow === 0) topPx = bottomVP;
|
|
589
|
+
else if (downOverflow <= upOverflow) topPx = topVP - downOverflow;
|
|
590
|
+
else topPx = bottomVP + upOverflow;
|
|
591
|
+
el.style.top = topPx - c.top + 'px';
|
|
592
|
+
|
|
593
|
+
el.style.visibility = '';
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export default CommandMenu;
|