@lemonadejs/contextmenu 5.2.3 → 5.2.4

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,12 +67,15 @@ 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">
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">
51
79
  <a>{{self.title}}</a> <div>{{self.shortcut}}</div>
52
80
  </div>`;
53
81
  }
@@ -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,73 @@ if (! Modal && typeof (require) === 'function') {
260
317
  return s;
261
318
  }
262
319
 
320
+ self.isClosed = function() {
321
+ return self.modals[0].modal.closed === true;
322
+ }
323
+
263
324
  self.open = function(options, x, y, e) {
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
+ // Open
330
+ menu.modal.open();
331
+ // If the modal is open and the content is different from what is shown. Close modals with higher level
332
+ self.close(1);
333
+ // Update the data
334
+ if (options && menu.options !== options) {
335
+ // Refresh content
336
+ menu.options = options;
274
337
  }
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;
338
+ // Define new position
339
+ menu.modal.top = y;
340
+ menu.modal.left = x;
287
341
 
288
- onopen(self, options);
289
- }
342
+ onopen(self, options);
343
+
344
+ // Focus
345
+ self.el.classList.add('lm-menu-focus');
346
+ // Focus on the contextmenu
347
+ self.el.focus();
290
348
  }
291
349
 
292
350
  self.close = function(level) {
293
351
  // Close all modals from the level specified
294
352
  self.modals.forEach(function(menu, k) {
295
353
  if (k >= level) {
296
- // Reset cursor
297
- resetCursor.call(menu);
298
- // Close the modal
299
- menu.modal.closed = true;
354
+ if (menu.item) {
355
+ menu.item.expanded = false;
356
+ menu.item = null;
357
+ }
358
+ menu.modal.close();
300
359
  }
301
360
  });
302
361
  // Keep the index of the modal that is opened
303
362
  self.modalIndex = level ? level - 1 : 0;
304
- }
305
363
 
306
- onload(() => {
307
- if (! self.root) {
308
- self.root = self.el.parentNode;
309
- }
364
+ // Close event
365
+ if (level === 0) {
366
+ self.el.classList.remove('lm-menu-focus');
310
367
 
311
- self.root.setAttribute('tabindex', 0);
368
+ Dispatch.call(self, self.onclose, 'close', {
369
+ instance: self,
370
+ });
371
+ }
372
+ }
312
373
 
374
+ onload(() => {
313
375
  // Create first menu
314
376
  self.create();
315
377
 
316
- // Normal click
317
- self.root.addEventListener("click", function(e) {
318
- if (e.target === self.root) {
378
+ // Create event for focus out
379
+ self.el.addEventListener("focusout", (e) => {
380
+ if (! (e.relatedTarget && (self.el.contains(e.relatedTarget) || self.root?.contains(e.relatedTarget)))) {
319
381
  self.close(0);
320
382
  }
321
383
  });
322
384
 
323
385
  // Keyboard event
324
- self.root.addEventListener("keydown", function(e) {
386
+ self.el.addEventListener("keydown", function(e) {
325
387
  // Menu object
326
388
  let menu = self.modals[self.modalIndex];
327
389
  // Modal must be opened
@@ -387,23 +449,22 @@ if (! Modal && typeof (require) === 'function') {
387
449
  }
388
450
  });
389
451
 
390
- // Create event for focus out
391
- self.root.addEventListener("focusout", (e) => {
392
- if (! self.root.contains(e.relatedTarget)) {
393
- self.close(0);
394
- }
395
- });
452
+ if (! self.root) {
453
+ self.root = self.el.parentNode;
454
+ }
396
455
 
397
456
  // Parent
398
457
  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();
458
+ if (Array.isArray(self.options) && self.options.length) {
459
+ let [x, y] = getCoords(e);
460
+ self.open(self.options, x, y, e);
461
+ e.preventDefault();
462
+ e.stopImmediatePropagation();
463
+ }
403
464
  });
404
465
  });
405
466
 
406
- return `<div class="lm-menu"></div>`;
467
+ return `<div class="lm-menu" role="menu" aria-orientation="vertical" tabindex="0"></div>`;
407
468
  }
408
469
 
409
470
  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,6 +37,7 @@
29
37
  font-family:sans-serif;
30
38
  text-align: left;
31
39
  align-items: center;
40
+ position: relative;
32
41
  }
33
42
 
34
43
  .lm-menu-submenu > div.lm-menu-item a {
@@ -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 {
package/package.json CHANGED
@@ -15,9 +15,9 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "lemonadejs": "^5.2.0",
18
- "@lemonadejs/modal": "^5.2.0"
18
+ "@lemonadejs/modal": "^5.2.1"
19
19
  },
20
20
  "main": "dist/index.js",
21
21
  "types": "dist/index.d.ts",
22
- "version": "5.2.3"
22
+ "version": "5.2.4"
23
23
  }