@lemonadejs/contextmenu 5.2.3 → 5.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/dist/index.js +179 -75
- package/dist/style.css +14 -4
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -12,6 +12,36 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
12
12
|
global.Contextmenu = factory();
|
|
13
13
|
}(this, (function () {
|
|
14
14
|
|
|
15
|
+
class CustomEvents extends Event {
|
|
16
|
+
constructor(type, props, options) {
|
|
17
|
+
super(type, {
|
|
18
|
+
bubbles: true,
|
|
19
|
+
composed: true,
|
|
20
|
+
...options,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (props) {
|
|
24
|
+
for (const key in props) {
|
|
25
|
+
// Avoid assigning if property already exists anywhere on `this`
|
|
26
|
+
if (! (key in this)) {
|
|
27
|
+
this[key] = props[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Dispatcher
|
|
35
|
+
const Dispatch = function(method, type, options) {
|
|
36
|
+
// Try calling the method directly if provided
|
|
37
|
+
if (typeof method === 'function') {
|
|
38
|
+
let a = Object.values(options);
|
|
39
|
+
return method(...a);
|
|
40
|
+
} else if (this.tagName) {
|
|
41
|
+
this.dispatchEvent(new CustomEvents(type, options));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
// Get the coordinates of the action
|
|
16
46
|
const getCoords = function(e) {
|
|
17
47
|
let x;
|
|
@@ -25,11 +55,6 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
25
55
|
y = e.clientY;
|
|
26
56
|
}
|
|
27
57
|
|
|
28
|
-
// Adjust for any scrollable parent element
|
|
29
|
-
let b = document.body;
|
|
30
|
-
x -= b.scrollLeft;
|
|
31
|
-
y -= b.scrollTop;
|
|
32
|
-
|
|
33
58
|
return [x,y];
|
|
34
59
|
}
|
|
35
60
|
|
|
@@ -42,13 +67,16 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
42
67
|
}
|
|
43
68
|
}
|
|
44
69
|
|
|
70
|
+
// Initialize expanded state
|
|
71
|
+
self.expanded = false;
|
|
72
|
+
|
|
45
73
|
if (self.type === 'line') {
|
|
46
|
-
return `<hr />`;
|
|
74
|
+
return `<hr role="separator" />`;
|
|
47
75
|
} else if (self.type === 'inline') {
|
|
48
76
|
return `<div></div>`;
|
|
49
77
|
} else {
|
|
50
|
-
return `<div class="lm-menu-item" data-disabled="{{self.disabled}}" data-cursor="{{self.cursor}}" data-icon="{{self.icon}}" title="{{self.tooltip}}" data-submenu="${!!self.submenu}" onmouseup="self.parent.mouseUp" onmouseenter="self.parent.mouseEnter" onmouseleave="self.parent.mouseLeave">
|
|
51
|
-
<
|
|
78
|
+
return `<div class="lm-menu-item" role="menuitem" data-disabled="{{self.disabled}}" data-cursor="{{self.cursor}}" data-icon="{{self.icon}}" title="{{self.tooltip}}" data-submenu="${!!self.submenu}" aria-haspopup="${!!self.submenu}" aria-expanded="{{self.expanded}}" aria-label="{{self.title}}" tabindex="-1" onmouseup="self.parent.mouseUp" onmouseenter="self.parent.mouseEnter" onmouseleave="self.parent.mouseLeave">
|
|
79
|
+
<span>{{self.title}}</span> <div>{{self.shortcut}}</div>
|
|
52
80
|
</div>`;
|
|
53
81
|
}
|
|
54
82
|
}
|
|
@@ -100,7 +128,7 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
100
128
|
let current = self.parent.modals[index+1];
|
|
101
129
|
// Do not exist yet, create it.
|
|
102
130
|
if (! current) {
|
|
103
|
-
// Modal
|
|
131
|
+
// Modal needs to be created
|
|
104
132
|
current = self.parent.create();
|
|
105
133
|
}
|
|
106
134
|
// Get the parent from this one
|
|
@@ -112,13 +140,17 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
112
140
|
// Close other modals
|
|
113
141
|
self.parent.close(index+1);
|
|
114
142
|
}
|
|
115
|
-
// Update selected modal
|
|
143
|
+
// Update the selected modal
|
|
116
144
|
self.parent.modalIndex = index+1;
|
|
117
145
|
let rect = parent.modal.el.getBoundingClientRect();
|
|
118
146
|
// Update modal
|
|
119
|
-
current.modal.
|
|
147
|
+
current.modal.open();
|
|
148
|
+
// Aria indication
|
|
120
149
|
current.modal.top = rect.y + s.el.offsetTop + 2;
|
|
121
150
|
current.modal.left = rect.x + 248;
|
|
151
|
+
// Keep current item for each modal
|
|
152
|
+
current.item = s;
|
|
153
|
+
s.expanded = true;
|
|
122
154
|
|
|
123
155
|
// Activate the cursor
|
|
124
156
|
if (cursor === true) {
|
|
@@ -161,7 +193,7 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
161
193
|
}
|
|
162
194
|
|
|
163
195
|
let template = `<lm-modal :overflow="true" :closed="true" :ref="self.modal" :responsive="false" :auto-adjust="true" :focus="false" :layers="false" :onopen="self.onopen" :onclose="self.onclose">
|
|
164
|
-
<div class="lm-menu-submenu">
|
|
196
|
+
<div class="lm-menu-submenu" role="menu" aria-orientation="vertical">
|
|
165
197
|
<Item :loop="self.options" />
|
|
166
198
|
</div>
|
|
167
199
|
</lm-modal>`;
|
|
@@ -169,46 +201,71 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
169
201
|
return lemonade.element(template, self, { Item: Item });
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
const findNextEnabledCursor = function(startIndex, direction) {
|
|
205
|
+
if (!this.options || this.options.length === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let cursor = startIndex;
|
|
210
|
+
let attempts = 0;
|
|
211
|
+
const maxAttempts = this.options.length;
|
|
212
|
+
|
|
213
|
+
while (attempts < maxAttempts) {
|
|
214
|
+
if (direction) {
|
|
215
|
+
// Down
|
|
216
|
+
if (cursor >= this.options.length) {
|
|
217
|
+
cursor = 0;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Up
|
|
221
|
+
if (cursor < 0) {
|
|
222
|
+
cursor = this.options.length - 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let item = this.options[cursor];
|
|
227
|
+
if (item && !item.disabled && item.type !== 'line') {
|
|
228
|
+
return cursor;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
cursor = direction ? cursor + 1 : cursor - 1;
|
|
232
|
+
attempts++;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
};
|
|
236
|
+
|
|
172
237
|
const setCursor = function(direction) {
|
|
173
238
|
let cursor = null;
|
|
174
239
|
|
|
175
240
|
if (typeof(this.cursor) !== 'undefined') {
|
|
176
241
|
if (! direction) {
|
|
177
242
|
// Up
|
|
178
|
-
cursor = this.cursor - 1;
|
|
179
|
-
if (cursor < 0) {
|
|
180
|
-
cursor = this.options.length - 1;
|
|
181
|
-
}
|
|
243
|
+
cursor = findNextEnabledCursor.call(this, this.cursor - 1, false);
|
|
182
244
|
} else {
|
|
183
245
|
// Down
|
|
184
|
-
cursor = this.cursor + 1;
|
|
185
|
-
if (cursor >= this.options.length) {
|
|
186
|
-
cursor = 0;
|
|
187
|
-
}
|
|
246
|
+
cursor = findNextEnabledCursor.call(this, this.cursor + 1, true);
|
|
188
247
|
}
|
|
189
248
|
}
|
|
190
249
|
|
|
191
250
|
// Remove the cursor
|
|
192
251
|
if (cursor === null) {
|
|
193
252
|
if (direction) {
|
|
194
|
-
cursor = 0;
|
|
253
|
+
cursor = findNextEnabledCursor.call(this, 0, true);
|
|
195
254
|
} else {
|
|
196
|
-
cursor = this.options.length - 1;
|
|
255
|
+
cursor = findNextEnabledCursor.call(this, this.options.length - 1, false);
|
|
197
256
|
}
|
|
198
|
-
} else {
|
|
257
|
+
} else if (typeof(this.cursor) !== 'undefined') {
|
|
199
258
|
this.options[this.cursor].cursor = false;
|
|
200
259
|
}
|
|
201
260
|
|
|
202
|
-
// Add the cursor
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (this.options[cursor].type === 'line') {
|
|
208
|
-
setCursor.call(this, direction);
|
|
261
|
+
// Add the cursor if found
|
|
262
|
+
if (cursor !== null) {
|
|
263
|
+
this.options[cursor].cursor = true;
|
|
264
|
+
this.cursor = cursor;
|
|
265
|
+
return true;
|
|
209
266
|
}
|
|
210
267
|
|
|
211
|
-
return
|
|
268
|
+
return false;
|
|
212
269
|
}
|
|
213
270
|
|
|
214
271
|
/**
|
|
@@ -260,68 +317,112 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
260
317
|
return s;
|
|
261
318
|
}
|
|
262
319
|
|
|
263
|
-
self.
|
|
320
|
+
self.isClosed = function() {
|
|
321
|
+
return self.modals[0].modal.closed === true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
self.open = function(options, x, y, adjust) {
|
|
264
325
|
// Get the main modal
|
|
265
326
|
let menu = self.modals[0];
|
|
266
327
|
// Reset cursor
|
|
267
328
|
resetCursor.call(menu);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
329
|
+
// Define new position
|
|
330
|
+
menu.modal.top = y;
|
|
331
|
+
menu.modal.left = x;
|
|
332
|
+
// Open
|
|
333
|
+
menu.modal.open();
|
|
334
|
+
// If the modal is open and the content is different from what is shown. Close modals with higher level
|
|
335
|
+
self.close(1);
|
|
336
|
+
// Update the data
|
|
337
|
+
if (options && menu.options !== options) {
|
|
338
|
+
// Refresh content
|
|
339
|
+
menu.options = options;
|
|
274
340
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
341
|
+
onopen(self, options);
|
|
342
|
+
|
|
343
|
+
// Adjust position to respect mouse cursor after auto-adjust
|
|
344
|
+
// Use queueMicrotask to ensure it runs after the modal's auto-adjust
|
|
345
|
+
if (adjust === true) {
|
|
346
|
+
queueMicrotask(() => {
|
|
347
|
+
let modalEl = menu.modal.el;
|
|
348
|
+
let rect = modalEl.getBoundingClientRect();
|
|
349
|
+
let marginLeft = parseFloat(modalEl.style.marginLeft) || 0;
|
|
350
|
+
let marginTop = parseFloat(modalEl.style.marginTop) || 0;
|
|
351
|
+
|
|
352
|
+
// Check if horizontal adjustment was applied (margin is non-zero)
|
|
353
|
+
if (marginLeft !== 0) {
|
|
354
|
+
// Position modal so its right edge is at x - 1 (cursor 1px to the right of modal)
|
|
355
|
+
// Formula: left + margin + width = x - 1, where left = x
|
|
356
|
+
// Therefore: margin = -width - 1
|
|
357
|
+
let newMarginLeft = -rect.width - 1;
|
|
358
|
+
// Check if this would push modal off the left edge
|
|
359
|
+
let newLeft = x + newMarginLeft;
|
|
360
|
+
if (newLeft < 10) {
|
|
361
|
+
// Keep a 10px margin from the left edge
|
|
362
|
+
newMarginLeft = 10 - x;
|
|
363
|
+
}
|
|
364
|
+
modalEl.style.marginLeft = newMarginLeft + 'px';
|
|
365
|
+
}
|
|
287
366
|
|
|
288
|
-
|
|
367
|
+
// Check if vertical adjustment was applied (margin is non-zero)
|
|
368
|
+
if (marginTop !== 0) {
|
|
369
|
+
// Position modal so its bottom edge is at y - 1 (cursor 1px below modal)
|
|
370
|
+
// Formula: top + margin + height = y - 1, where top = y
|
|
371
|
+
// Therefore: margin = -height - 1
|
|
372
|
+
let newMarginTop = -rect.height - 1;
|
|
373
|
+
// Check if this would push modal off the top edge
|
|
374
|
+
let newTop = y + newMarginTop;
|
|
375
|
+
if (newTop < 10) {
|
|
376
|
+
// Keep a 10px margin from the top edge
|
|
377
|
+
newMarginTop = 10 - y;
|
|
378
|
+
}
|
|
379
|
+
modalEl.style.marginTop = newMarginTop + 'px';
|
|
380
|
+
}
|
|
381
|
+
});
|
|
289
382
|
}
|
|
383
|
+
// Focus
|
|
384
|
+
self.el.classList.add('lm-menu-focus');
|
|
385
|
+
// Focus on the contextmenu
|
|
386
|
+
self.el.focus();
|
|
290
387
|
}
|
|
291
388
|
|
|
292
389
|
self.close = function(level) {
|
|
293
390
|
// Close all modals from the level specified
|
|
294
391
|
self.modals.forEach(function(menu, k) {
|
|
295
392
|
if (k >= level) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
393
|
+
if (menu.item) {
|
|
394
|
+
menu.item.expanded = false;
|
|
395
|
+
menu.item = null;
|
|
396
|
+
}
|
|
397
|
+
menu.modal.close();
|
|
300
398
|
}
|
|
301
399
|
});
|
|
302
400
|
// Keep the index of the modal that is opened
|
|
303
401
|
self.modalIndex = level ? level - 1 : 0;
|
|
304
|
-
}
|
|
305
402
|
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
self.
|
|
309
|
-
}
|
|
403
|
+
// Close event
|
|
404
|
+
if (level === 0) {
|
|
405
|
+
self.el.classList.remove('lm-menu-focus');
|
|
310
406
|
|
|
311
|
-
|
|
407
|
+
Dispatch.call(self, self.onclose, 'close', {
|
|
408
|
+
instance: self,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
312
412
|
|
|
413
|
+
onload(() => {
|
|
313
414
|
// Create first menu
|
|
314
415
|
self.create();
|
|
315
416
|
|
|
316
|
-
//
|
|
317
|
-
self.
|
|
318
|
-
if (e.
|
|
417
|
+
// Create event for focus out
|
|
418
|
+
self.el.addEventListener("focusout", (e) => {
|
|
419
|
+
if (! (e.relatedTarget && (self.el.contains(e.relatedTarget) || self.root?.contains(e.relatedTarget)))) {
|
|
319
420
|
self.close(0);
|
|
320
421
|
}
|
|
321
422
|
});
|
|
322
423
|
|
|
323
424
|
// Keyboard event
|
|
324
|
-
self.
|
|
425
|
+
self.el.addEventListener("keydown", function(e) {
|
|
325
426
|
// Menu object
|
|
326
427
|
let menu = self.modals[self.modalIndex];
|
|
327
428
|
// Modal must be opened
|
|
@@ -387,23 +488,26 @@ if (! Modal && typeof (require) === 'function') {
|
|
|
387
488
|
}
|
|
388
489
|
});
|
|
389
490
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
491
|
+
if (! self.root) {
|
|
492
|
+
if (self.tagName) {
|
|
493
|
+
self.root = self.el.parentNode.parentNode;
|
|
494
|
+
} else {
|
|
495
|
+
self.root = self.el.parentNode;
|
|
394
496
|
}
|
|
395
|
-
}
|
|
497
|
+
}
|
|
396
498
|
|
|
397
499
|
// Parent
|
|
398
500
|
self.root.addEventListener("contextmenu", function(e) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
501
|
+
if (Array.isArray(self.options) && self.options.length) {
|
|
502
|
+
let [x, y] = getCoords(e);
|
|
503
|
+
self.open(self.options, x, y, true);
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
e.stopImmediatePropagation();
|
|
506
|
+
}
|
|
403
507
|
});
|
|
404
508
|
});
|
|
405
509
|
|
|
406
|
-
return `<div class="lm-menu"></div>`;
|
|
510
|
+
return `<div class="lm-menu" role="menu" aria-orientation="vertical" tabindex="0"></div>`;
|
|
407
511
|
}
|
|
408
512
|
|
|
409
513
|
lemonade.setComponents({ Contextmenu: Contextmenu });
|
package/dist/style.css
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
.lm-menu {
|
|
2
|
+
display: none;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.lm-menu-focus {
|
|
6
|
+
display: unset;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
.lm-menu .lm-modal {
|
|
2
10
|
color: #555;
|
|
3
11
|
user-select: none;
|
|
4
12
|
border: 1px solid var(--lm-border-color-light, #e9e9e9);
|
|
5
13
|
border-radius: 4px;
|
|
6
14
|
box-shadow: 0 2px 4px 2px rgba(60,64,67,.2);
|
|
7
|
-
max-height:
|
|
15
|
+
max-height: 600px;
|
|
8
16
|
width: initial;
|
|
9
17
|
height: initial;
|
|
10
18
|
min-width: 250px;
|
|
@@ -29,9 +37,10 @@
|
|
|
29
37
|
font-family:sans-serif;
|
|
30
38
|
text-align: left;
|
|
31
39
|
align-items: center;
|
|
40
|
+
position: relative;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
.lm-menu-submenu > div.lm-menu-item
|
|
43
|
+
.lm-menu-submenu > div.lm-menu-item span {
|
|
35
44
|
text-decoration: none;
|
|
36
45
|
flex: 1;
|
|
37
46
|
cursor: pointer;
|
|
@@ -57,7 +66,7 @@
|
|
|
57
66
|
|
|
58
67
|
.lm-menu-submenu > div.lm-menu-item:hover,
|
|
59
68
|
.lm-menu-submenu > div.lm-menu-item[data-cursor="true"] {
|
|
60
|
-
background-color: var(--lm-background-color-
|
|
69
|
+
background-color: var(--lm-background-color-highlight, #ebebeb);
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
.lm-menu-submenu hr {
|
|
@@ -76,6 +85,7 @@
|
|
|
76
85
|
line-height: 24px;
|
|
77
86
|
position: absolute;
|
|
78
87
|
left: 11px;
|
|
88
|
+
transform: rotate(0.03deg);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
.lm-dark-mode .lm-menu .lm-modal {
|
|
@@ -86,5 +96,5 @@
|
|
|
86
96
|
|
|
87
97
|
.lm-dark-mode .lm-menu-submenu > div.lm-menu-item:hover,
|
|
88
98
|
.lm-dark-mode .lm-menu-submenu > div.lm-menu-item[data-cursor="true"] {
|
|
89
|
-
background-color: var(--lm-background-color-
|
|
99
|
+
background-color: var(--lm-background-color-highlight, #2d2d2d);
|
|
90
100
|
}
|
package/package.json
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"build": "webpack --config webpack.config.js"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"lemonadejs": "^5.2
|
|
18
|
-
"@lemonadejs/modal": "^5.
|
|
17
|
+
"lemonadejs": "^5.3.2",
|
|
18
|
+
"@lemonadejs/modal": "^5.8.0"
|
|
19
19
|
},
|
|
20
20
|
"main": "dist/index.js",
|
|
21
21
|
"types": "dist/index.d.ts",
|
|
22
|
-
"version": "5.
|
|
22
|
+
"version": "5.8.0"
|
|
23
23
|
}
|