894 lines
22 KiB
JavaScript
894 lines
22 KiB
JavaScript
class Menu {
|
|
constructor(settings = {}, itemArgs = []) {
|
|
const typeEnum = ['contextmenu', 'menubar'];
|
|
let items = [];
|
|
let type = isValidType(settings.type) ? settings.type : 'contextmenu';
|
|
let beforeShow = settings.beforeShow;
|
|
Object.defineProperty(this, 'items', {
|
|
get: () => {
|
|
return items;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'beforeShow', {
|
|
get: () => {
|
|
return beforeShow;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'type', {
|
|
get: () => {
|
|
return type;
|
|
},
|
|
set: (typeIn) => {
|
|
type = isValidType(typeIn) ? typeIn : type;
|
|
}
|
|
});
|
|
|
|
this.append = item => {
|
|
if(!(item instanceof MenuItem)) {
|
|
console.error('appended item must be an instance of MenuItem');
|
|
return false;
|
|
}
|
|
let index = items.push(item);
|
|
return index;
|
|
};
|
|
|
|
this.insert = (item, index) => {
|
|
if(!(item instanceof MenuItem)) {
|
|
console.error('inserted item must be an instance of MenuItem');
|
|
return false;
|
|
}
|
|
|
|
items.splice(index, 0, item);
|
|
return true;
|
|
};
|
|
|
|
this.remove = item => {
|
|
if(!(item instanceof MenuItem)) {
|
|
console.error('item to be removed is not an instance of MenuItem');
|
|
return false;
|
|
}
|
|
|
|
let index = items.indexOf(item);
|
|
if(index < 0) {
|
|
console.error('item to be removed was not found in this.items');
|
|
return false;
|
|
} else {
|
|
items.splice(index, 0);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
this.removeAt = index => {
|
|
items.splice(index, 0);
|
|
return true;
|
|
};
|
|
|
|
this.node = null;
|
|
let nitems = itemArgs.length;
|
|
for(let i = 0; i < nitems; i++) {
|
|
let item = itemArgs[i];
|
|
if (item instanceof MenuItem)
|
|
items.push(item);
|
|
else
|
|
items.push(new MenuItem(item));
|
|
}
|
|
|
|
function isValidType(typeIn = '', debug = false) {
|
|
if(typeEnum.indexOf(typeIn) < 0) {
|
|
if(debug) console.error(`${typeIn} is not a valid type`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
createMacBuiltin() {
|
|
console.error('This method is not available in browser :(');
|
|
return false;
|
|
}
|
|
|
|
popup(x, y, itemNode = null, menubarSubmenu = false) {
|
|
Menu._keydownListen(true);
|
|
|
|
let setRight = false;
|
|
|
|
let submenu = itemNode != null || this.submenu;
|
|
this.submenu = menubarSubmenu;
|
|
|
|
menubarSubmenu = menubarSubmenu || this.menubarSubmenu;
|
|
this.menubarSubmenu = menubarSubmenu;
|
|
let top = Menu.contextMenuParent || document.body;
|
|
if (! Menu._topSheet && Menu.topsheetZindex > 0) {
|
|
let topSheet = document.createElement("div");
|
|
topSheet.setAttribute("style",
|
|
"position: fixed; top: 0px; bottom: 0px; left: 0px; right: 0px; z-index: "+Menu.topsheetZindex);
|
|
top.appendChild(topSheet);
|
|
Menu._topSheet = topSheet;
|
|
top = topSheet;
|
|
}
|
|
if (! Menu._topmostMenu) {
|
|
Menu._topmostMenu = this;
|
|
Menu._listenerElement = top;
|
|
top.addEventListener('mouseup', Menu._mouseHandler, false);
|
|
top.addEventListener('mousedown', Menu._mouseHandler, false);
|
|
}
|
|
|
|
let menuNode = this.buildMenu(submenu, menubarSubmenu);
|
|
menuNode.jsMenu = this;
|
|
this.node = menuNode;
|
|
Menu._currentMenuNode = menuNode;
|
|
|
|
if(this.node.parentNode) {
|
|
if(menuNode === this.node) return;
|
|
this.node.parentNode.replaceChild(menuNode, this.node);
|
|
} else {
|
|
((! (menubarSubmenu && Menu._topSheet) && itemNode)
|
|
|| top).appendChild(this.node);
|
|
}
|
|
|
|
let width = menuNode.clientWidth;
|
|
let height = menuNode.clientHeight;
|
|
let wwidth = top.offsetWidth;
|
|
const base_x = x, base_y = y;
|
|
if ((x + width) > wwidth) {
|
|
setRight = true;
|
|
if(submenu && ! menubarSubmenu) {
|
|
x = wwidth - itemNode.parentNode.offsetLeft + 2;
|
|
if (width + x > wwidth) {
|
|
x = 0;
|
|
setRight = false;
|
|
}
|
|
} else {
|
|
x = 0;
|
|
}
|
|
}
|
|
|
|
let wheight = top.offsetHeight;
|
|
if((y + height) > wheight) {
|
|
y = wheight - height;
|
|
if (y < -0.5)
|
|
y = wheight - height;
|
|
}
|
|
|
|
if(!setRight) {
|
|
menuNode.style.left = x + 'px';
|
|
menuNode.style.right = 'auto';
|
|
} else {
|
|
menuNode.style.right = x + 'px';
|
|
menuNode.style.left = 'auto';
|
|
}
|
|
// Don't have topSheet cover menubar, so we can catch mouseenter
|
|
if (Menu._menubarNode && Menu._topSheet)
|
|
Menu._topSheet.style.top = `${Menu._menubarNode.offsetHeight}px`;
|
|
|
|
menuNode.style.top = y + 'px';
|
|
if (! Menu.showMenuNode
|
|
|| ! Menu.showMenuNode(this, menuNode, width, height, base_x, base_y, x, y)) {
|
|
menuNode.classList.add('show');
|
|
}
|
|
}
|
|
|
|
popdown() {
|
|
this.items.forEach(item => {
|
|
if(item.submenu) {
|
|
item.submenu.popdown();
|
|
} else {
|
|
item.node = null;
|
|
}
|
|
});
|
|
if(this.node && this.type !== 'menubar') {
|
|
Menu._currentMenuNode = this.node.parentMenuNode;
|
|
if (this.menubarSubmenu)
|
|
Menu.showSubmenuActive(this.node.menuItem, false);
|
|
if (Menu.hideMenuNode)
|
|
Menu.hideMenuNode(this, this.node);
|
|
this.node.parentNode.removeChild(this.node);
|
|
if (Menu._topSheet && Menu._topSheet.firstChild == null) {
|
|
Menu._topSheet.parentNode.removeChild(Menu._topSheet);
|
|
Menu._topSheet = undefined;
|
|
}
|
|
this.node = null;
|
|
}
|
|
if (this == Menu._topmostMenu) {
|
|
Menu._topmostMenu = null;
|
|
let el = Menu._listenerElement;
|
|
if (el) {
|
|
el.removeEventListener('mouseup', Menu._mouseHandler, false);
|
|
el.removeEventListener('mousedown', Menu._mouseHandler, false);
|
|
Menu._listenerElement = null;
|
|
}
|
|
}
|
|
|
|
if(this.type === 'menubar') {
|
|
this.clearActiveSubmenuStyling();
|
|
}
|
|
}
|
|
|
|
static showSubmenuActive(node, active) {
|
|
if (active)
|
|
node.classList.add('submenu-active');
|
|
else
|
|
node.classList.remove('submenu-active');
|
|
if (node.firstChild instanceof Element)
|
|
node.firstChild.setAttribute('aria-expanded',
|
|
active?'true':'false');
|
|
}
|
|
|
|
static popdownAll() {
|
|
Menu._topmostMenu.popdown();
|
|
return;
|
|
}
|
|
|
|
buildMenu(submenu = false, menubarSubmenu = false) {
|
|
if (this.beforeShow)
|
|
(this.beforeShow)(this);
|
|
let menuNode = document.createElement('ul');
|
|
menuNode.classList.add('nwjs-menu', this.type);
|
|
menuNode.spellcheck = false;
|
|
// make focusable
|
|
menuNode.setAttribute('contenteditable', 'true');
|
|
menuNode.setAttribute('role',
|
|
this.type === 'menubar' ? 'menubar' : 'menu'); // ARIA recommended
|
|
if(submenu) menuNode.classList.add('submenu');
|
|
if(menubarSubmenu) menuNode.classList.add('menubar-submenu');
|
|
|
|
menuNode.jsMenu = this;
|
|
menuNode.parentMenuNode = Menu._currentMenuNode;
|
|
this.items.forEach(item => {
|
|
if (item.beforeShow)
|
|
(item.beforeShow)(item);
|
|
if (item.visible) {
|
|
item.buildItem(menuNode,
|
|
this.type === 'menubar');
|
|
}
|
|
});
|
|
return menuNode;
|
|
}
|
|
|
|
static isDescendant(parent, child) {
|
|
let node = child.parentNode;
|
|
while(node !== null) {
|
|
if(node === parent) {
|
|
return true;
|
|
}
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static _inMenubar(node) {
|
|
if (Menu._menubarNode === null)
|
|
return false;
|
|
while(node instanceof Element
|
|
&& ! node.classList.contains('submenu')) {
|
|
if(node === Menu._menubarNode)
|
|
return true;
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static _activateSubmenu(miNode) {
|
|
let item = miNode.jsMenuItem;
|
|
let wasActive = item.node.classList.contains('submenu-active');
|
|
Menu.showSubmenuActive(item.node, !wasActive);
|
|
// FIXME use select method
|
|
if(item.submenu) {
|
|
if(! wasActive) {
|
|
miNode.jsMenu.node.activeItemNode = item.node;
|
|
let rect = item.node.getBoundingClientRect();
|
|
item.popupSubmenu(rect.left, rect.bottom, true);
|
|
} else {
|
|
item.submenu.popdown();
|
|
miNode.jsMenu.node.currentSubmenu = null;
|
|
miNode.jsMenu.node.activeItemNode = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
static _mouseHandler(e) {
|
|
e.preventDefault(); // prevent focus change on mousedown
|
|
let inMenubar = Menu._inMenubar(e.target);
|
|
let menubarHandler = e.currentTarget == Menu._menubarNode;
|
|
let miNode = e.target;
|
|
while (miNode && ! miNode.jsMenuItem)
|
|
miNode = miNode.parentNode;
|
|
/* mouseenter:
|
|
if selected sibling: unhighlight (and popdown if submenu)
|
|
select item and if submenu popup
|
|
mouseout (or mouseleave):
|
|
if (! submenu) unhighlight
|
|
mousedown:
|
|
if (miNode) select
|
|
else popdownAll
|
|
*/
|
|
if (e.type=="mousedown" && inMenubar == menubarHandler
|
|
&& (!miNode || miNode.jsMenuItem.menuBarTopLevel)) {
|
|
if (Menu._topmostMenu) {
|
|
Menu.popdownAll();
|
|
if (Menu.menuDone)
|
|
Menu.menuDone(null);
|
|
}
|
|
}
|
|
if ((inMenubar == menubarHandler) && miNode) {
|
|
if (e.type=="mousedown") {
|
|
Menu._activateSubmenu(miNode);
|
|
}
|
|
if (e.type=="mouseup") {
|
|
miNode.jsMenuItem.doit(miNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
static focusMenubar() {
|
|
const items = Menu._menubar?.items;
|
|
if (items && items.length > 0 && items[0].node
|
|
&& Menu._menubarNode) {
|
|
Menu._activateSubmenu(items[0].node);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static setApplicationMenu(menubar, parent=document.body, before=undefined) {
|
|
let oldNode = Menu._menubarNode;
|
|
if (oldNode) {
|
|
let oldParent = oldNode.parentNode;
|
|
if (oldParent != null)
|
|
oldParent.removeChild(oldNode);
|
|
oldNode.removeEventListener('mousedown', Menu._mouseHandler, false);
|
|
Menu._menubarNode = null;
|
|
}
|
|
if (before==undefined) {
|
|
before = parent.firstChild
|
|
}
|
|
if (menubar != null) {
|
|
let newNode = menubar.buildMenu();
|
|
newNode.jsMenuItem = null;
|
|
parent.insertBefore(newNode, before);
|
|
newNode.addEventListener('mousedown', Menu._mouseHandler, false);
|
|
Menu._menubarNode = newNode;
|
|
menubar.node = newNode;
|
|
}
|
|
Menu._menubar = menubar;
|
|
}
|
|
|
|
clearActiveSubmenuStyling(notThisNode) {
|
|
if (! this.node)
|
|
return;
|
|
let submenuActive = this.node.querySelectorAll('.submenu-active');
|
|
for(let node of submenuActive) {
|
|
if(node === notThisNode) continue;
|
|
Menu.showSubmenuActive(node, false);
|
|
}
|
|
}
|
|
|
|
static recursiveNodeFind(menu, node) {
|
|
if(menu.node === node) {
|
|
return true;
|
|
} else if(Menu.isDescendant(menu.node, node)) {
|
|
return true;
|
|
} else if(menu.items.length > 0) {
|
|
for(var i=0; i < menu.items.length; i++) {
|
|
let menuItem = menu.items[i];
|
|
if(!menuItem.node) continue;
|
|
|
|
if(menuItem.node === node) {
|
|
return true;
|
|
} else if(Menu.isDescendant(menuItem.node, node)) {
|
|
return true;
|
|
} else {
|
|
if(menuItem.submenu) {
|
|
if(recursiveNodeFind(menuItem.submenu, node)) {
|
|
return true;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isNodeInChildMenuTree(node = false) {
|
|
if(!node) return false;
|
|
return recursiveNodeFind(this, node);
|
|
}
|
|
}
|
|
|
|
// Parent node for context menu popup. If null, document.body is the default.
|
|
Menu.contextMenuParent = null;
|
|
|
|
Menu._currentMenuNode = null;
|
|
|
|
Menu._keydownListener = function(e) {
|
|
function nextItem(menuNode, curNode, forwards) {
|
|
let nullSeen = false;
|
|
let next = curNode;
|
|
for (;;) {
|
|
next = !next ? null
|
|
: forwards ? next.nextSibling
|
|
: next.previousSibling;
|
|
if (! next) {
|
|
next = forwards ? menuNode.firstChild
|
|
: menuNode.lastChild;
|
|
if (nullSeen || !next)
|
|
return null;
|
|
nullSeen = true;
|
|
}
|
|
if (next instanceof Element
|
|
&& next.classList.contains("menu-item")
|
|
&& next.jsMenuItem.type != 'separator'
|
|
&& ! (next.classList.contains("disabled")))
|
|
return next;
|
|
}
|
|
}
|
|
function nextMenu(menuNode, forwards) {
|
|
let menubarNode = menuNode.menuItem.parentNode;
|
|
let next = nextItem(menubarNode,
|
|
menubarNode.activeItemNode,
|
|
forwards);
|
|
if (next)
|
|
next.jsMenuItem.select(next, true, true, true);
|
|
return next;
|
|
|
|
}
|
|
function openSubmenu(active) {
|
|
active.jsMenuItem.selectSubmenu(active);
|
|
menuNode = Menu._currentMenuNode;
|
|
let next = nextItem(menuNode, null, true);
|
|
if (next)
|
|
next.jsMenuItem.select(next, true, false);
|
|
}
|
|
let menuNode = Menu._currentMenuNode
|
|
if (menuNode) {
|
|
let active = menuNode.activeItemNode;
|
|
switch (e.keyCode) {
|
|
case 27: // Escape
|
|
case 37: // Left
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (e.keyCode == 37
|
|
&& menuNode.jsMenu.menubarSubmenu
|
|
&& nextMenu(menuNode, false))
|
|
return;
|
|
menuNode.jsMenu.popdown();
|
|
if (! Menu._topmostMenu && Menu.menuDone)
|
|
Menu.menuDone(null);
|
|
break;
|
|
case 32: // Space
|
|
case 13: // Enter
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (active) {
|
|
if (active.jsMenuItem.submenu)
|
|
openSubmenu(active);
|
|
else
|
|
active.jsMenuItem.doit(active);
|
|
}
|
|
break;
|
|
case 39: // Right
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (active && active.jsMenuItem.submenu)
|
|
openSubmenu(active);
|
|
else if (Menu._topmostMenu.menubarSubmenu)
|
|
nextMenu(menuNode, true);
|
|
break;
|
|
case 38: // Up
|
|
case 40: // Down
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
let next = nextItem(menuNode,
|
|
menuNode.activeItemNode,
|
|
e.keyCode == 40);
|
|
if (next)
|
|
next.jsMenuItem.select(next, true, false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Menu._keydownListening = false;
|
|
Menu._keydownListen = function(value) {
|
|
if (value != Menu._keydownListening) {
|
|
if (value)
|
|
document.addEventListener('keydown', Menu._keydownListener, true);
|
|
else
|
|
document.removeEventListener('keydown', Menu._keydownListener, true);
|
|
}
|
|
Menu._keydownListening = value;
|
|
}
|
|
|
|
Menu._isMac = typeof navigator != "undefined" ? /Mac/.test(navigator.platform)
|
|
: typeof os != "undefined" ? os.platform() == "darwin" : false
|
|
|
|
// If positive, create a "sheet" above the Menu.contextMenuParent
|
|
// at the given z-index.
|
|
// Used to capture mouse-clicks - needed if there are iframes involved.
|
|
Menu.topsheetZindex = 5;
|
|
|
|
class MenuItem {
|
|
constructor(settings = {}) {
|
|
|
|
const typeEnum = ['separator', 'checkbox', 'radio', 'normal'];
|
|
let type = isValidType(settings.type) ? settings.type : 'normal';
|
|
let submenu = settings.submenu || null;
|
|
if (submenu && ! (submenu instanceof Menu))
|
|
submenu = new Menu({}, submenu);
|
|
let click = settings.click || null;
|
|
this.modifiers = settings.modifiers;
|
|
let label = settings.label || '';
|
|
let enabled = settings.enabled;
|
|
if(typeof settings.enabled === 'undefined') enabled = true;
|
|
let visible = settings.visible;
|
|
if(typeof settings.visible === 'undefined') visible = true;
|
|
let beforeShow = settings.beforeShow;
|
|
|
|
Object.defineProperty(this, 'type', {
|
|
get: () => {
|
|
return type;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'beforeShow', {
|
|
get: () => {
|
|
return beforeShow;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'submenu', {
|
|
get: () => {
|
|
return submenu;
|
|
},
|
|
set: (inputMenu) => {
|
|
console.warn('submenu should be set on initialisation, changing this at runtime could be slow on some platforms.');
|
|
if(!(inputMenu instanceof Menu)) {
|
|
console.error('submenu must be an instance of Menu');
|
|
return;
|
|
} else {
|
|
submenu = inputMenu;
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'click', {
|
|
get: () => {
|
|
return click;
|
|
},
|
|
set: (inputCallback) => {
|
|
if(typeof inputCallback !== 'function') {
|
|
console.error('click must be a function');
|
|
return;
|
|
} else {
|
|
click = inputCallback;
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'enabled', {
|
|
get: () => {
|
|
return enabled;
|
|
},
|
|
set: (inputEnabled) => {
|
|
enabled = inputEnabled;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'visible', {
|
|
get: () => {
|
|
return visible;
|
|
},
|
|
set: (inputVisible) => {
|
|
visible = inputVisible;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'label', {
|
|
get: () => {
|
|
return label;
|
|
},
|
|
set: (inputLabel) => {
|
|
label = inputLabel;
|
|
}
|
|
});
|
|
|
|
this.icon = settings.icon || null;
|
|
this.iconIsTemplate = settings.iconIsTemplate || false;
|
|
this.tooltip = settings.tooltip || '';
|
|
this.checked = settings.checked || false;
|
|
|
|
this.key = settings.key || null;
|
|
let accelerator = settings.accelerator;
|
|
if (! accelerator && settings.key) {
|
|
accelerator = (settings.modifiers ? (settings.modifiers + "+") : "") + settings.key;
|
|
}
|
|
if (accelerator instanceof Array)
|
|
accelerator = accelerator.join(" ");
|
|
if (accelerator) {
|
|
accelerator = accelerator
|
|
.replace(/Command[+]/i, "Cmd+")
|
|
.replace(/Control[+]/i, "Ctrl+")
|
|
.replace(/(Mod|((Command|Cmd)OrCtrl))[+]/i,
|
|
Menu._isMac ? "Cmd+" : "Ctrl+");
|
|
}
|
|
this.accelerator = accelerator;
|
|
if (accelerator && ! settings.key) {
|
|
const plus =
|
|
accelerator.lastIndexOf("+", accelerator.length-2);
|
|
if (plus > 0) {
|
|
this.modifiers = accelerator.substring(0, plus);
|
|
this.key = accelerator.substring(plus+1);
|
|
} else {
|
|
settings.key = accelerator;
|
|
}
|
|
}
|
|
this.node = null;
|
|
|
|
if(this.key) {
|
|
this.key = this.key.toUpperCase();
|
|
}
|
|
|
|
function isValidType(typeIn = '', debug = false) {
|
|
if(typeEnum.indexOf(typeIn) < 0) {
|
|
if(debug) console.error(`${typeIn} is not a valid type`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
toString() {
|
|
return this.type+"["+this.label+"]";
|
|
}
|
|
|
|
_mouseoverHandle_menubarTop() {
|
|
let pmenu = this.node.jsMenuNode;
|
|
if (pmenu.activeItemNode) {
|
|
pmenu.activeItemNode.classList.remove('active');
|
|
pmenu.activeItemNode = null;
|
|
}
|
|
if (pmenu && pmenu.querySelector('.submenu-active')) {
|
|
if(this.node.classList.contains('submenu-active')) return;
|
|
Menu.showSubmenuActive(this.node, true);
|
|
this.select(this.node, true, true, true);
|
|
}
|
|
}
|
|
|
|
doit(node) {
|
|
if (! this.submenu) {
|
|
Menu.popdownAll();
|
|
if(this.type === 'checkbox')
|
|
this.checked = !this.checked;
|
|
else if (this.type === 'radio') {
|
|
this.checked = true;
|
|
for (let dir = 0; dir <= 1; dir++) {
|
|
for (let n = node; ; ) {
|
|
n = dir ? n.nextSibling
|
|
: n.previousSibling;
|
|
if (! (n instanceof Element
|
|
&& n.classList.contains("radio")))
|
|
break;
|
|
n.jsMenuItem.checked = false;
|
|
}
|
|
}
|
|
}
|
|
if(this.click) this.click(this);
|
|
if (Menu.menuDone)
|
|
Menu.menuDone(this);
|
|
}
|
|
}
|
|
|
|
select(node, turnOn, popupSubmenu, menubarSubmenu = false) {
|
|
let pmenu = node.jsMenuNode;
|
|
if (pmenu.activeItemNode) {
|
|
pmenu.activeItemNode.classList.remove('active');
|
|
Menu.showSubmenuActive(pmenu.activeItemNode, false);
|
|
pmenu.activeItemNode = null;
|
|
}
|
|
if(pmenu.currentSubmenu) {
|
|
pmenu.currentSubmenu.popdown();
|
|
pmenu.currentSubmenu = null;
|
|
}
|
|
if(this.submenu && popupSubmenu)
|
|
this.selectSubmenu(node, menubarSubmenu);
|
|
else
|
|
node.classList.add('active');
|
|
node.jsMenuNode.activeItemNode = node;
|
|
}
|
|
|
|
selectSubmenu(node, menubarSubmenu) {
|
|
node.jsMenuNode.currentSubmenu = this.submenu;
|
|
if(this.submenu.node)
|
|
return;
|
|
|
|
let parentNode = node.parentNode;
|
|
let x, y;
|
|
if (menubarSubmenu) {
|
|
let rect = node.getBoundingClientRect();
|
|
x = rect.left;
|
|
y = rect.bottom;
|
|
} else {
|
|
x = parentNode.offsetWidth + parentNode.offsetLeft - 2;
|
|
y = parentNode.offsetTop + node.offsetTop - 4;
|
|
}
|
|
this.popupSubmenu(x, y, menubarSubmenu);
|
|
Menu.showSubmenuActive(node, true);
|
|
}
|
|
|
|
buildItem(menuNode, menuBarTopLevel = false) {
|
|
let node = document.createElement('li');
|
|
node.setAttribute('role', this.type === 'separator' ? 'separator' : 'menuitem');
|
|
node.jsMenuNode = menuNode;
|
|
node.jsMenu = menuNode.jsMenu;
|
|
node.jsMenuItem = this;
|
|
node.classList.add('menu-item', this.type);
|
|
|
|
menuBarTopLevel = menuBarTopLevel || this.menuBarTopLevel || false;
|
|
this.menuBarTopLevel = menuBarTopLevel;
|
|
|
|
if(menuBarTopLevel) {
|
|
node.addEventListener('mouseenter', this._mouseoverHandle_menubarTop.bind(this));
|
|
}
|
|
|
|
let iconWrapNode = document.createElement('div');
|
|
iconWrapNode.classList.add('icon-wrap');
|
|
|
|
if(this.icon) {
|
|
let iconNode = new Image();
|
|
iconNode.src = this.icon;
|
|
iconNode.classList.add('icon');
|
|
iconWrapNode.appendChild(iconNode);
|
|
}
|
|
|
|
let labelNode = document.createElement('span');
|
|
labelNode.classList.add('label');
|
|
|
|
let checkmarkNode = document.createElement('span');
|
|
checkmarkNode.classList.add('checkmark');
|
|
|
|
if(this.checked && !menuBarTopLevel)
|
|
node.classList.add('checked');
|
|
|
|
if(this.submenu)
|
|
node.setAttribute('aria-haspopup', 'true');
|
|
|
|
if(this.submenu && !menuBarTopLevel) {
|
|
node.addEventListener('mouseleave', (e) => {
|
|
if(node !== e.target) {
|
|
if(!Menu.isDescendant(node, e.target))
|
|
this.submenu.popdown();
|
|
}
|
|
});
|
|
}
|
|
|
|
if(!this.enabled) {
|
|
node.classList.add('disabled');
|
|
}
|
|
|
|
if(this.icon) labelNode.appendChild(iconWrapNode);
|
|
|
|
let buttonNode;
|
|
if (this.type !== 'separator') {
|
|
buttonNode = document.createElement('span');
|
|
buttonNode.setAttribute('role', 'button');
|
|
node.appendChild(buttonNode);
|
|
if (this.submenu)
|
|
buttonNode.setAttribute('aria-expanded',
|
|
'false');
|
|
if(!menuBarTopLevel) {
|
|
buttonNode.addEventListener('mouseenter', () => {
|
|
this.select(node, true, true);
|
|
});
|
|
}
|
|
} else
|
|
buttonNode = node;
|
|
|
|
let textLabelNode = document.createElement('span');
|
|
textLabelNode.textContent = this.label;
|
|
textLabelNode.classList.add('label-text');
|
|
|
|
buttonNode.appendChild(checkmarkNode);
|
|
|
|
labelNode.appendChild(textLabelNode);
|
|
buttonNode.appendChild(labelNode);
|
|
|
|
if(this.submenu && !menuBarTopLevel) {
|
|
const n = document.createElement('span');
|
|
n.classList.add('modifiers');
|
|
n.append(MenuItem.submenuSymbol);
|
|
buttonNode.appendChild(n);
|
|
}
|
|
let accelerator = this.accelerator;
|
|
if (accelerator) {
|
|
let keyNode = document.createElement('span');
|
|
keyNode.classList.add('keys');
|
|
let i = 0;
|
|
const len = accelerator.length;
|
|
for (;;) {
|
|
if (i > 0) {
|
|
keyNode.append(" ");
|
|
}
|
|
let sp = accelerator.indexOf(' ', i);
|
|
let key = accelerator.substring(i, sp < 0 ? len : sp);
|
|
let pl = key.lastIndexOf('+', key.length-2);
|
|
if (pl > 0) {
|
|
let mod = key.substring(0, pl);
|
|
let modNode = document.createElement('span');
|
|
modNode.classList.add('modifiers');
|
|
if (MenuItem.useModifierSymbols) {
|
|
let mods = mod.toLowerCase().split('+');
|
|
mod = "";
|
|
// Looping this way to keep order of symbols - required by macOS
|
|
for(let symbol in MenuItem.modifierSymbols) {
|
|
if(mods.indexOf(symbol) >= 0) {
|
|
mod += MenuItem.modifierSymbols[symbol];
|
|
}
|
|
}
|
|
} else
|
|
mod += "+";
|
|
modNode.append(mod);
|
|
keyNode.append(modNode);
|
|
key = key.substring(pl+1);
|
|
}
|
|
keyNode.append(key);
|
|
if (sp < 0)
|
|
break;
|
|
i = sp + 1;
|
|
}
|
|
keyNode.normalize();
|
|
buttonNode.appendChild(keyNode);
|
|
}
|
|
|
|
node.title = this.tooltip;
|
|
this.node = node;
|
|
menuNode.appendChild(node);
|
|
}
|
|
|
|
popupSubmenu(x, y, menubarSubmenu = false) {
|
|
this.submenu.popup(x, y, this.node, menubarSubmenu);
|
|
this.submenu.node.menuItem = this.node;
|
|
this.node.jsMenuNode.currentSubmenu = this.submenu;
|
|
}
|
|
}
|
|
|
|
MenuItem.submenuSymbol = '\u27a7'; // '➧' Squat Black Rightwards Arrow[
|
|
|
|
MenuItem.modifierSymbols = {
|
|
shift: '⇧',
|
|
ctrl: '⌃',
|
|
alt: '⌥',
|
|
cmd: '⌘',
|
|
super: '⌘',
|
|
command: '⌘'
|
|
};
|
|
|
|
MenuItem.keySymbols = {
|
|
up: '↑',
|
|
esc: '⎋',
|
|
tab: '⇥',
|
|
left: '←',
|
|
down: '↓',
|
|
right: '→',
|
|
pageUp: '⇞',
|
|
escape: '⎋',
|
|
pageDown: '⇟',
|
|
backspace: '⌫',
|
|
space: 'Space'
|
|
};
|
|
MenuItem.useModifierSymbols = Menu._isMac;
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = { Menu: Menu, MenuItem: MenuItem };
|
|
}
|
|
|
|
// Local Variables:
|
|
// js-indent-level: 8
|
|
// indent-tabs-mode: t
|
|
// End:
|