@wpmoo/ui 0.7.1 → 0.8.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/README.md +18 -9
- package/certification.json +4 -2
- package/dist/assets/css/moo-ui.css +1105 -35
- package/dist/assets/css/moo-ui.min.css +2 -2
- package/dist/assets/css/moo.css +940 -17
- package/dist/assets/css/moo.min.css +2 -2
- package/dist/js/context-menu.js +274 -0
- package/dist/js/datatable.js +1080 -0
- package/package.json +6 -3
- package/THIRD_PARTY_NOTICES.md +0 -41
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const instances = new WeakMap();
|
|
2
|
+
|
|
3
|
+
function pointerRect(event) {
|
|
4
|
+
const { clientX: x, clientY: y } = event;
|
|
5
|
+
return { top: y, left: x, right: x, bottom: y, width: 0, height: 0, x, y };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isDisabledTrigger(trigger) {
|
|
9
|
+
return Boolean(trigger?.disabled || trigger?.getAttribute?.("aria-disabled") === "true");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function menuItemFromEvent(event) {
|
|
13
|
+
const target = event.target;
|
|
14
|
+
const item = target.closest?.(".dropdown-item");
|
|
15
|
+
if (item) {
|
|
16
|
+
return item;
|
|
17
|
+
}
|
|
18
|
+
return target.closest?.("li")?.querySelector(":scope > .dropdown-item") || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default class ContextMenu {
|
|
22
|
+
static getInstance(element) {
|
|
23
|
+
return element?.nodeType === 1 ? instances.get(element) || null : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static getOrCreateInstance(element, config = {}) {
|
|
27
|
+
return ContextMenu.getInstance(element) || new ContextMenu(element, config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructor(element, config = {}) {
|
|
31
|
+
if (element?.nodeType !== 1 || !element.matches(".context-menu")) {
|
|
32
|
+
throw new TypeError("ContextMenu requires a .context-menu root element.");
|
|
33
|
+
}
|
|
34
|
+
const existing = instances.get(element);
|
|
35
|
+
if (existing) {
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this._element = element;
|
|
40
|
+
this._document = element.ownerDocument;
|
|
41
|
+
this._window = this._document.defaultView;
|
|
42
|
+
this._menu = element.querySelector(":scope > .context-menu-menu");
|
|
43
|
+
this._fallback = element.querySelector(":scope > [data-context-menu-fallback]");
|
|
44
|
+
if (!this._menu) {
|
|
45
|
+
throw new TypeError("ContextMenu requires a .context-menu-menu element.");
|
|
46
|
+
}
|
|
47
|
+
if (!this._fallback) {
|
|
48
|
+
throw new TypeError("ContextMenu requires an explicit fallback trigger.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const Dropdown = this._bootstrap("Dropdown");
|
|
52
|
+
if (!Dropdown) {
|
|
53
|
+
throw new TypeError("ContextMenu requires bootstrap.Dropdown (bootstrap.bundle.js).");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this._config = { ...config };
|
|
57
|
+
this._listeners = [];
|
|
58
|
+
this._invoker = null;
|
|
59
|
+
this._pointerRect = null;
|
|
60
|
+
this._openMethod = null;
|
|
61
|
+
this._reference = {
|
|
62
|
+
getBoundingClientRect: () => this._currentRect(),
|
|
63
|
+
contextElement: this._fallback,
|
|
64
|
+
};
|
|
65
|
+
this._dropdown = new Dropdown(this._fallback, {
|
|
66
|
+
reference: this._reference,
|
|
67
|
+
autoClose: "outside",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
instances.set(element, this);
|
|
71
|
+
this._bindEvents();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
dispose() {
|
|
75
|
+
this._listeners.forEach(({ target, type, handler, options }) => {
|
|
76
|
+
target.removeEventListener(type, handler, options);
|
|
77
|
+
});
|
|
78
|
+
this._listeners = [];
|
|
79
|
+
this._dropdown.dispose();
|
|
80
|
+
this._syncExpanded(false);
|
|
81
|
+
this._invoker = null;
|
|
82
|
+
this._pointerRect = null;
|
|
83
|
+
instances.delete(this._element);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
show(anchor = this._fallback) {
|
|
87
|
+
this._open(anchor, null);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
hide() {
|
|
91
|
+
this._dropdown.hide();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_listen(target, type, handler, options) {
|
|
95
|
+
target.addEventListener(type, handler, options);
|
|
96
|
+
this._listeners.push({ target, type, handler, options });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_bootstrap(name) {
|
|
100
|
+
return this._window.bootstrap?.[name] || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_currentRect() {
|
|
104
|
+
if (this._pointerRect) {
|
|
105
|
+
return this._pointerRect;
|
|
106
|
+
}
|
|
107
|
+
return (this._invoker || this._fallback).getBoundingClientRect();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_trigger(name, cancelable = false) {
|
|
111
|
+
return this._element.dispatchEvent(
|
|
112
|
+
new this._window.CustomEvent(`${name}.moo.context-menu`, {
|
|
113
|
+
bubbles: true,
|
|
114
|
+
cancelable,
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_triggers() {
|
|
120
|
+
return Array.from(this._element.querySelectorAll(":scope > .context-menu-trigger"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_syncExpanded(expanded) {
|
|
124
|
+
this._triggers().forEach((trigger) => {
|
|
125
|
+
trigger.setAttribute("aria-expanded", String(expanded));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_menuItems() {
|
|
130
|
+
return Array.from(this._menu.querySelectorAll(".dropdown-item")).filter(
|
|
131
|
+
(item) => !isDisabledTrigger(item) && !item.classList.contains("disabled")
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_open(trigger, pointerEvent, openMethod) {
|
|
136
|
+
if (isDisabledTrigger(trigger)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this._menu.classList.contains("show")) {
|
|
140
|
+
this._dropdown.hide();
|
|
141
|
+
}
|
|
142
|
+
this._invoker = trigger;
|
|
143
|
+
this._pointerRect = pointerEvent ? pointerRect(pointerEvent) : null;
|
|
144
|
+
this._openMethod = openMethod;
|
|
145
|
+
this._dropdown.show();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_handleContextMenu(event) {
|
|
149
|
+
const trigger = event.target.closest(".context-menu-trigger");
|
|
150
|
+
if (!trigger || !this._element.contains(trigger)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
this._open(trigger, event, "pointer");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_handleKeydown(event) {
|
|
158
|
+
const trigger = event.target.closest(".context-menu-trigger");
|
|
159
|
+
if (!trigger || !this._element.contains(trigger)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const isShiftF10 = event.shiftKey && event.key === "F10";
|
|
163
|
+
const isContextMenuKey = event.key === "ContextMenu";
|
|
164
|
+
if (!isShiftF10 && !isContextMenuKey) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
this._open(trigger, null, "keyboard");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_handleFallbackClick() {
|
|
172
|
+
this._invoker = this._fallback;
|
|
173
|
+
this._pointerRect = null;
|
|
174
|
+
this._openMethod = "pointer";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_handleMenuClick(event) {
|
|
178
|
+
const item = menuItemFromEvent(event);
|
|
179
|
+
if (!item) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const persistentItem = item.matches(".dropdown-item-check, [data-context-menu-persistent]");
|
|
183
|
+
const disabledItem =
|
|
184
|
+
item.matches('.dropdown-item.disabled, .dropdown-item[aria-disabled="true"], .dropdown-item:disabled') ||
|
|
185
|
+
isDisabledTrigger(item);
|
|
186
|
+
if (disabledItem) {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
event.stopPropagation();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Radio-grouped toggle buttons reuse the dropdown_toggle_item() button
|
|
192
|
+
// Bootstrap's own roving arrow-key focus already picks up (it only ever
|
|
193
|
+
// matches real .dropdown-item elements); Moo adds only the missing
|
|
194
|
+
// mutual-exclusivity Bootstrap's independent button-toggle plugin does
|
|
195
|
+
// not provide.
|
|
196
|
+
const radioButton = item.matches("[data-context-menu-radio-group]") ? item : null;
|
|
197
|
+
if (radioButton) {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
event.stopPropagation();
|
|
200
|
+
const group = radioButton.dataset.contextMenuRadioGroup;
|
|
201
|
+
this._menu
|
|
202
|
+
.querySelectorAll(`[data-context-menu-radio-group="${group}"]`)
|
|
203
|
+
.forEach((button) => {
|
|
204
|
+
const checked = button === radioButton;
|
|
205
|
+
button.classList.toggle("active", checked);
|
|
206
|
+
button.setAttribute("aria-pressed", String(checked));
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (persistentItem) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this._dropdown.hide();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_bindEvents() {
|
|
217
|
+
this._listen(this._element, "contextmenu", (event) => this._handleContextMenu(event));
|
|
218
|
+
this._listen(this._element, "keydown", (event) => this._handleKeydown(event));
|
|
219
|
+
this._listen(this._fallback, "click", () => this._handleFallbackClick());
|
|
220
|
+
this._listen(this._menu, "click", (event) => this._handleMenuClick(event));
|
|
221
|
+
this._listen(this._fallback, "show.bs.dropdown", (event) => {
|
|
222
|
+
if (!this._trigger("show", true)) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
this._listen(this._fallback, "shown.bs.dropdown", () => {
|
|
227
|
+
this._syncExpanded(true);
|
|
228
|
+
// Bootstrap's own show() never focuses an item either -- only its
|
|
229
|
+
// Up/Down-key handler does, since a pointer-driven open (right-click,
|
|
230
|
+
// or a click on the fallback trigger) should let :hover carry the
|
|
231
|
+
// highlight instead of a focus ring competing with it. A keyboard-
|
|
232
|
+
// driven open (Shift+F10/ContextMenu key) still lands on the first
|
|
233
|
+
// item, matching that same native arrow-key-open convention.
|
|
234
|
+
if (this._openMethod === "keyboard") {
|
|
235
|
+
this._menuItems()[0]?.focus();
|
|
236
|
+
} else {
|
|
237
|
+
this._menu.focus();
|
|
238
|
+
}
|
|
239
|
+
this._trigger("shown");
|
|
240
|
+
});
|
|
241
|
+
this._listen(this._fallback, "hide.bs.dropdown", (event) => {
|
|
242
|
+
if (!this._trigger("hide", true)) {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
this._listen(this._fallback, "hidden.bs.dropdown", () => {
|
|
247
|
+
this._syncExpanded(false);
|
|
248
|
+
const invoker = this._invoker;
|
|
249
|
+
this._pointerRect = null;
|
|
250
|
+
this._invoker = null;
|
|
251
|
+
// A native Bootstrap click on the fallback trigger bypasses _open()
|
|
252
|
+
// entirely (Bootstrap's own capture-phase click-to-toggle handler
|
|
253
|
+
// calls show() directly), so _handleFallbackClick's bubble-phase
|
|
254
|
+
// "click" listener always runs too late to set _openMethod before
|
|
255
|
+
// shown.bs.dropdown reads it. Resetting it here, back to the neutral
|
|
256
|
+
// pointer default, is what keeps that path correct rather than
|
|
257
|
+
// inheriting whatever a prior keyboard-triggered open last set.
|
|
258
|
+
this._openMethod = null;
|
|
259
|
+
this._trigger("hidden");
|
|
260
|
+
// Bootstrap's own dataApiKeydownHandler (js/src/dropdown.js) intercepts
|
|
261
|
+
// Escape in the document capture phase and hardcodes
|
|
262
|
+
// `getToggleButton.focus()` (our fallback trigger) after hide().
|
|
263
|
+
// Nothing downstream of a capture-phase stopPropagation() can observe
|
|
264
|
+
// that keydown to redirect focus, so correct it in a microtask, which
|
|
265
|
+
// still runs before the next paint: if focus landed on the fallback
|
|
266
|
+
// but a different trigger invoked this menu, return focus there.
|
|
267
|
+
queueMicrotask(() => {
|
|
268
|
+
if (invoker && invoker !== this._fallback && this._document.activeElement === this._fallback) {
|
|
269
|
+
invoker.focus();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|