@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 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
- <a>{{self.title}}</a> <div>{{self.shortcut}}</div>
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 need to be created
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.closed = false;
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
- this.options[cursor].cursor = true;
204
- // Cursor
205
- this.cursor = cursor;
206
- // If is line move to the next one
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 true;
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.open = function(options, x, y, e) {
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
- // Current state
270
- if (! e || e.type === 'contextmenu') {
271
- menu.modal.closed = false;
272
- } else if (e.type === 'click') {
273
- menu.modal.closed = ! menu.modal.closed;
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
- // If the modal is open and the content is different from what is shown
276
- if (menu.modal.closed === false) {
277
- // Close modals with higher level
278
- self.close(1);
279
- // Update the data
280
- if (menu.options !== options) {
281
- // Refresh content
282
- menu.options = options;
283
- }
284
- // Define new position
285
- menu.modal.top = y;
286
- menu.modal.left = x;
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
- onopen(self, options);
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
- // Reset cursor
297
- resetCursor.call(menu);
298
- // Close the modal
299
- menu.modal.closed = true;
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
- onload(() => {
307
- if (! self.root) {
308
- self.root = self.el.parentNode;
309
- }
403
+ // Close event
404
+ if (level === 0) {
405
+ self.el.classList.remove('lm-menu-focus');
310
406
 
311
- self.root.setAttribute('tabindex', 0);
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
- // Normal click
317
- self.root.addEventListener("click", function(e) {
318
- if (e.target === self.root) {
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.root.addEventListener("keydown", function(e) {
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
- // Create event for focus out
391
- self.root.addEventListener("focusout", (e) => {
392
- if (! self.root.contains(e.relatedTarget)) {
393
- self.close(0);
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
- let [x, y] = getCoords(e);
400
- self.open(self.options, x, y, e);
401
- e.preventDefault();
402
- e.stopImmediatePropagation();
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: 350px;
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 a {
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-hover, #ebebeb);
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-hover, #2d2d2d);
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.0",
18
- "@lemonadejs/modal": "^5.2.0"
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.2.3"
22
+ "version": "5.8.0"
23
23
  }