Skip to content

Commit b097c3b

Browse files
committed
🙌 whuddup
0 parents  commit b097c3b

File tree

5 files changed

+468
-0
lines changed

5 files changed

+468
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.DS_Store
2+
node_modules

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lts/boron

index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title></title>
5+
<link rel=stylesheet type=text/css href=nwjs-menu-browser.css>
6+
</head>
7+
<body>
8+
<script src=index.js></script>
9+
</body>
10+
</html>

index.js

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
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

Comments
 (0)