1
+ //jshint esnext:true
2
+
3
+ function isDescendant ( parent , child ) {
4
+ var node = child . parentNode ;
5
+ while ( node !== null ) {
6
+ if ( node == parent ) {
7
+ return true ;
8
+ }
9
+ node = node . parentNode ;
10
+ }
11
+ return false ;
12
+ }
13
+
14
+ class Menu {
15
+ constructor ( settings = { } ) {
16
+ const typeEnum = [ 'contextmenu' , 'menubar' ] ;
17
+ let items = [ ] ;
18
+ let type = isValidType ( settings . type ) ? settings . type : 'contextmenu' ;
19
+
20
+ Object . defineProperty ( this , 'items' , {
21
+ get : ( ) => {
22
+ return items ;
23
+ }
24
+ } ) ;
25
+
26
+ Object . defineProperty ( this , 'type' , {
27
+ get : ( ) => {
28
+ return type ;
29
+ } ,
30
+ set : ( typeIn ) => {
31
+ type = isValidType ( typeIn ) ? typeIn : type ;
32
+ }
33
+ } ) ;
34
+
35
+ this . append = item => {
36
+ if ( ! ( item instanceof MenuItem ) ) {
37
+ console . error ( 'appended item must be an instance of MenuItem' ) ;
38
+ return false ;
39
+ }
40
+ item . parentMenu = this ;
41
+ return items . push ( item ) ;
42
+ } ;
43
+
44
+ this . insert = ( item , index ) => {
45
+ if ( ! ( item instanceof MenuItem ) ) {
46
+ console . error ( 'inserted item must be an instance of MenuItem' ) ;
47
+ return false ;
48
+ }
49
+
50
+ items . splice ( index , 0 , item ) ;
51
+ item . parentMenu = this ;
52
+ return true ;
53
+ } ;
54
+
55
+ this . remove = item => {
56
+ if ( ! ( item instanceof MenuItem ) ) {
57
+ console . error ( 'item to be removed is not an instance of MenuItem' ) ;
58
+ return false ;
59
+ }
60
+
61
+ let index = items . indexOf ( item ) ;
62
+ if ( index < 0 ) {
63
+ console . error ( 'item to be removed was not found in this.items' ) ;
64
+ return false ;
65
+ } else {
66
+ items . splice ( index , 0 ) ;
67
+ return true ;
68
+ }
69
+ } ;
70
+
71
+ this . removeAt = index => {
72
+ items . splice ( index , 0 ) ;
73
+ return true ;
74
+ } ;
75
+
76
+ this . node = null ;
77
+ this . clickHandler = this . _clickHandle_hideMenu . bind ( this ) ;
78
+ this . currentSubmenuNode = null ;
79
+ this . parentMenuItem = null ;
80
+
81
+ function isValidType ( typeIn = '' , debug = false ) {
82
+ if ( typeEnum . indexOf ( typeIn ) < 0 ) {
83
+ if ( debug ) console . error ( `${ typeIn } is not a valid type` ) ;
84
+ return false ;
85
+ }
86
+ return true ;
87
+ }
88
+ }
89
+
90
+ _clickHandle_hideMenu ( e ) {
91
+ if ( e . target !== this . node && ! isDescendant ( this . node , e . target ) ) {
92
+ this . node . classList . remove ( 'show' ) ;
93
+ }
94
+ }
95
+
96
+ createMacBuiltin ( ) {
97
+ console . error ( 'This method is not available in browser :(' ) ;
98
+ return false ;
99
+ }
100
+
101
+ popup ( x , y , submenu = false ) {
102
+ let menuNode ;
103
+
104
+ if ( this . node ) {
105
+ menuNode = this . node ;
106
+ } else {
107
+ menuNode = this . buildMenu ( submenu ) ;
108
+ this . node = menuNode ;
109
+ document . body . appendChild ( menuNode ) ;
110
+ }
111
+
112
+ this . items . forEach ( item => {
113
+ if ( item . submenu ) {
114
+ item . node . classList . remove ( 'submenu-active' ) ;
115
+ item . submenu . popdown ( ) ;
116
+ }
117
+ } ) ;
118
+
119
+ let width = menuNode . clientWidth ;
120
+ let height = menuNode . clientHeight ;
121
+
122
+ if ( ( x + width ) > window . innerWidth ) {
123
+ x = window . innerWidth - width ;
124
+ }
125
+
126
+ if ( ( y + height ) > window . innerHeight ) {
127
+ y = window . innerHeight - height ;
128
+ }
129
+
130
+ menuNode . style . left = x + 'px' ;
131
+ menuNode . style . top = y + 'px' ;
132
+ menuNode . classList . add ( 'show' ) ;
133
+
134
+ document . addEventListener ( 'click' , this . clickHandler ) ;
135
+ }
136
+
137
+ popdown ( ) {
138
+ if ( this . node ) this . node . classList . remove ( 'show' ) ;
139
+
140
+ this . items . forEach ( item => {
141
+ if ( item . submenu ) {
142
+ item . node . classList . remove ( 'submenu-active' ) ;
143
+ item . submenu . popdown ( ) ;
144
+ }
145
+ } ) ;
146
+ }
147
+
148
+ buildMenu ( submenu = false ) {
149
+ let menuNode = this . menuNode ;
150
+ if ( submenu ) menuNode . classList . add ( 'submenu' ) ;
151
+
152
+ this . items . forEach ( item => {
153
+ let itemNode = item . buildItem ( ) ;
154
+ if ( item . submenu ) {
155
+ let submenuNode = item . submenu . buildMenu ( true ) ;
156
+ }
157
+ menuNode . appendChild ( itemNode ) ;
158
+ } ) ;
159
+
160
+ return menuNode ;
161
+ }
162
+
163
+ get menuNode ( ) {
164
+ let node = document . createElement ( 'ul' ) ;
165
+ node . classList . add ( this . type ) ;
166
+ return node ;
167
+ }
168
+ }
169
+
170
+ class MenuItem {
171
+ constructor ( settings = { } ) {
172
+ const modifiersEnum = [ 'cmd' , 'command' , 'super' , 'shift' , 'ctrl' , 'alt' ] ;
173
+ const typeEnum = [ 'separator' , 'checkbox' , 'normal' ] ;
174
+ let type = isValidType ( settings . type ) ? settings . type : 'normal' ;
175
+ let submenu = settings . submenu || null ;
176
+ let click = settings . click || null ;
177
+ let modifiers = validModifiers ( settings . modifiers ) ? settings . modifiers : null ;
178
+
179
+ if ( submenu ) {
180
+ submenu . parentMenuItem = this ;
181
+ }
182
+
183
+ Object . defineProperty ( this , 'type' , {
184
+ get : ( ) => {
185
+ return type ;
186
+ }
187
+ } ) ;
188
+
189
+ Object . defineProperty ( this , 'submenu' , {
190
+ get : ( ) => {
191
+ return submenu ;
192
+ } ,
193
+ set : ( inputMenu ) => {
194
+ console . warn ( 'submenu should be set on initialisation, changing this at runtime could be slow on some platforms.' ) ;
195
+ if ( ! ( inputMenu instanceof Menu ) ) {
196
+ console . error ( 'submenu must be an instance of Menu' ) ;
197
+ return ;
198
+ } else {
199
+ submenu = inputMenu ;
200
+ submenu . parentMenuItem = this ;
201
+ }
202
+ }
203
+ } ) ;
204
+
205
+ Object . defineProperty ( this , 'click' , {
206
+ get : ( ) => {
207
+ return click ;
208
+ } ,
209
+ set : ( inputCallback ) => {
210
+ if ( typeof inputCallback !== 'function' ) {
211
+ console . error ( 'click must be a function' ) ;
212
+ return ;
213
+ } else {
214
+ click = inputCallback ;
215
+ }
216
+ }
217
+ } ) ;
218
+
219
+ Object . defineProperty ( this , 'modifiers' , {
220
+ get : ( ) => {
221
+ return modifiers ;
222
+ } ,
223
+ set : ( inputModifiers ) => {
224
+ modifiers = validModifiers ( inputModifiers ) ? inputModifiers : modifiers ;
225
+ }
226
+ } ) ;
227
+
228
+ this . label = settings . label || '' ;
229
+ this . icon = settings . icon || null ;
230
+ this . iconIsTemplate = settings . iconIsTemplate || false ;
231
+ this . tooltip = settings . tooltip || '' ;
232
+ this . checked = settings . checked || false ;
233
+ this . enabled = settings . enabled || true ;
234
+ this . key = settings . key || null ;
235
+ this . node = null ;
236
+
237
+ function validModifiers ( modifiersIn = '' ) {
238
+ let modsArr = modifiersIn . split ( '+' ) ;
239
+ for ( let i = 0 ; i < modsArr ; i ++ ) {
240
+ let mod = modsArr [ i ] . trim ( ) ;
241
+ if ( modifiersEnum . indexOf ( mod ) < 0 ) {
242
+ console . error ( `${ mod } is not a valid modifier` ) ;
243
+ return false ;
244
+ }
245
+ }
246
+ return true ;
247
+ }
248
+
249
+ function isValidType ( typeIn = '' , debug = false ) {
250
+ if ( typeEnum . indexOf ( typeIn ) < 0 ) {
251
+ if ( debug ) console . error ( `${ typeIn } is not a valid type` ) ;
252
+ return false ;
253
+ }
254
+ return true ;
255
+ }
256
+ }
257
+
258
+ buildItem ( ) {
259
+ let node = document . createElement ( 'li' ) ;
260
+ node . classList . add ( 'menu-item' , this . type ) ;
261
+
262
+ node . addEventListener ( 'click' , ( ) => {
263
+ document . removeEventListener ( 'click' , this . parentMenu . clickHandler ) ;
264
+ this . parentMenu . popdown ( ) ;
265
+ if ( this . click ) this . click ( ) ;
266
+ } ) ;
267
+
268
+ let iconWrapNode = document . createElement ( 'div' ) ;
269
+ iconWrapNode . classList . add ( 'icon-wrap' ) ;
270
+
271
+ if ( this . icon ) {
272
+ let iconNode = new Image ( ) ;
273
+ iconNode . src = this . icon ;
274
+ iconNode . classList . add ( 'icon' ) ;
275
+ iconWrapNode . appendChild ( iconNode ) ;
276
+ }
277
+
278
+ let labelNode = document . createElement ( 'div' ) ;
279
+ labelNode . classList . add ( 'label' ) ;
280
+ labelNode . textContent = this . label ;
281
+
282
+ let modifierNode = document . createElement ( 'div' ) ;
283
+ modifierNode . classList . add ( 'modifiers' ) ;
284
+ modifierNode . textContent = this . modifiers ;
285
+
286
+ if ( this . submenu ) {
287
+ modifierNode . textContent = '▶︎' ;
288
+
289
+ node . addEventListener ( 'mouseout' , ( e ) => {
290
+ if ( ! isDescendant ( node , e . target ) ) this . submenu . popdown ( ) ;
291
+
292
+ node . classList . add ( 'submenu-active' ) ;
293
+ } ) ;
294
+ }
295
+
296
+ node . addEventListener ( 'mouseover' , ( e ) => {
297
+ if ( this . submenu ) {
298
+ let parentNode = node . parentNode ;
299
+
300
+ let x = parentNode . offsetWidth + parentNode . offsetLeft - 2 ;
301
+ let y = parentNode . offsetTop + node . offsetHeight ;
302
+ this . submenu . popup ( x , y , true ) ;
303
+ this . parentMenu . currentSubmenu = this . submenu ;
304
+ } else {
305
+ if ( this . parentMenu . currentSubmenu ) {
306
+ this . parentMenu . currentSubmenu . popdown ( ) ;
307
+ this . parentMenu . currentSubmenu . parentMenuItem . node . classList . remove ( 'submenu-active' ) ;
308
+ this . parentMenu . currentSubmenu = null ;
309
+ }
310
+ }
311
+ } ) ;
312
+
313
+ node . appendChild ( iconWrapNode ) ;
314
+ node . appendChild ( labelNode ) ;
315
+ node . appendChild ( modifierNode ) ;
316
+
317
+ this . node = node ;
318
+ return node ;
319
+ }
320
+ }
321
+
322
+ let m = new Menu ( ) ;
323
+ for ( let i = 0 ; i < 10 ; i ++ ) {
324
+ let mi = new MenuItem ( {
325
+ label : 'Item ' + i
326
+ } ) ;
327
+ m . append ( mi ) ;
328
+ }
329
+
330
+ let sm = new Menu ( ) ;
331
+ for ( let i = 10 ; i < 20 ; i ++ ) {
332
+ let mi = new MenuItem ( {
333
+ label : 'Item ' + i ,
334
+ click : function ( ) { //jshint ignore:line
335
+ alert ( 'hello m8 - ' + i ) ;
336
+ }
337
+ } ) ;
338
+ sm . append ( mi ) ;
339
+ }
340
+
341
+ let mi = new MenuItem ( {
342
+ label : 'Item with sub' ,
343
+ submenu : sm
344
+ } ) ;
345
+
346
+ m . insert ( mi , 1 ) ;
347
+
348
+ let sm2 = new Menu ( ) ;
349
+ for ( let i = 20 ; i < 30 ; i ++ ) {
350
+ let mi = new MenuItem ( {
351
+ label : 'Item ' + i
352
+ } ) ;
353
+ sm2 . append ( mi ) ;
354
+ }
355
+
356
+ let mi2 = new MenuItem ( {
357
+ label : 'Item with sub 2' ,
358
+ submenu : sm2
359
+ } ) ;
360
+
361
+ let mi3 = new MenuItem ( {
362
+ type : 'separator'
363
+ } ) ;
364
+
365
+ sm . insert ( mi2 , 1 ) ;
366
+ sm . insert ( mi3 , 2 ) ;
367
+
368
+ console . log ( m , sm , sm2 ) ;
369
+
370
+ document . addEventListener ( 'contextmenu' , ( e ) => {
371
+ e . preventDefault ( ) ;
372
+ m . popup ( e . clientX , e . clientY ) ;
373
+ return false ;
374
+ } ) ;
0 commit comments