2018-03-26 08:08:43 +00:00
|
|
|
"use strict";
|
2018-04-22 10:59:01 +00:00
|
|
|
|
2018-03-26 08:08:43 +00:00
|
|
|
const $ = require("jquery");
|
2018-04-22 10:59:01 +00:00
|
|
|
const Mousetrap = require("mousetrap");
|
2018-03-26 08:08:43 +00:00
|
|
|
const templates = require("../views");
|
2018-04-22 10:59:01 +00:00
|
|
|
|
|
|
|
const contextMenuContainer = $("#context-menu-container");
|
2018-03-26 08:08:43 +00:00
|
|
|
|
|
|
|
module.exports = class ContextMenu {
|
|
|
|
constructor(contextMenuItems, contextMenuActions, selectedElement, event) {
|
2018-04-22 10:59:01 +00:00
|
|
|
this.previousActiveElement = document.activeElement;
|
2018-03-26 08:08:43 +00:00
|
|
|
this.contextMenuItems = contextMenuItems;
|
|
|
|
this.contextMenuActions = contextMenuActions;
|
|
|
|
this.selectedElement = selectedElement;
|
|
|
|
this.event = event;
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
2019-07-17 09:33:59 +00:00
|
|
|
const contextMenu = showContextMenu(
|
|
|
|
this.contextMenuItems,
|
|
|
|
this.selectedElement,
|
|
|
|
this.event
|
|
|
|
);
|
2018-04-22 10:59:01 +00:00
|
|
|
this.bindEvents(contextMenu);
|
2018-03-26 08:08:43 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-04-22 10:59:01 +00:00
|
|
|
hide() {
|
|
|
|
contextMenuContainer
|
|
|
|
.hide()
|
|
|
|
.empty()
|
|
|
|
.off(".contextMenu");
|
|
|
|
|
|
|
|
Mousetrap.unbind("escape");
|
|
|
|
}
|
|
|
|
|
|
|
|
bindEvents(contextMenu) {
|
2018-03-26 08:08:43 +00:00
|
|
|
const contextMenuActions = this.contextMenuActions;
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
contextMenuActions.execute = (id, ...args) =>
|
|
|
|
contextMenuActions[id] && contextMenuActions[id](...args);
|
2018-03-26 08:08:43 +00:00
|
|
|
|
2018-04-22 10:59:01 +00:00
|
|
|
const clickItem = (item) => {
|
|
|
|
const itemData = item.attr("data-data");
|
|
|
|
const contextAction = item.attr("data-action");
|
|
|
|
|
|
|
|
this.hide();
|
|
|
|
|
2018-03-26 08:08:43 +00:00
|
|
|
contextMenuActions.execute(contextAction, itemData);
|
2018-04-22 10:59:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
contextMenu.on("click", ".context-menu-item", function() {
|
|
|
|
clickItem($(this));
|
|
|
|
});
|
|
|
|
|
|
|
|
const trap = Mousetrap(contextMenu.get(0));
|
|
|
|
|
|
|
|
trap.bind(["up", "down"], (e, key) => {
|
|
|
|
const items = contextMenu.find(".context-menu-item");
|
|
|
|
|
|
|
|
let index = items.toArray().findIndex((item) => $(item).is(":focus"));
|
|
|
|
|
|
|
|
if (key === "down") {
|
|
|
|
index = (index + 1) % items.length;
|
|
|
|
} else {
|
|
|
|
index = Math.max(index, 0) - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
items.eq(index).trigger("focus");
|
|
|
|
});
|
|
|
|
|
|
|
|
trap.bind("enter", () => {
|
|
|
|
const item = contextMenu.find(".context-menu-item:focus");
|
|
|
|
|
|
|
|
if (item.length) {
|
|
|
|
clickItem(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Hide context menu when clicking or right clicking outside of it
|
|
|
|
contextMenuContainer.on("click.contextMenu contextmenu.contextMenu", (e) => {
|
|
|
|
// Do not close the menu when clicking inside of the context menu (e.g. on a divider)
|
|
|
|
if ($(e.target).prop("id") === "context-menu") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.hide();
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Hide the context menu when pressing escape within the context menu container
|
|
|
|
Mousetrap.bind("escape", () => {
|
|
|
|
this.hide();
|
|
|
|
|
|
|
|
// Return focus to the previously focused element
|
|
|
|
$(this.previousActiveElement).trigger("focus");
|
|
|
|
|
|
|
|
return false;
|
2018-03-26 08:08:43 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function showContextMenu(contextMenuItems, selectedElement, event) {
|
|
|
|
const target = $(event.currentTarget);
|
2018-04-24 07:29:14 +00:00
|
|
|
const contextMenu = $("<ul>", {
|
|
|
|
id: "context-menu",
|
|
|
|
role: "menu",
|
|
|
|
});
|
2018-03-26 08:08:43 +00:00
|
|
|
|
|
|
|
for (const item of contextMenuItems) {
|
|
|
|
if (item.check(target)) {
|
|
|
|
if (item.divider) {
|
2018-04-22 10:59:01 +00:00
|
|
|
contextMenu.append(templates.contextmenu_divider());
|
2018-03-26 08:08:43 +00:00
|
|
|
} else {
|
2019-07-17 09:33:59 +00:00
|
|
|
contextMenu.append(
|
|
|
|
templates.contextmenu_item({
|
|
|
|
class:
|
|
|
|
typeof item.className === "function"
|
|
|
|
? item.className(target)
|
|
|
|
: item.className,
|
|
|
|
action: item.actionId,
|
|
|
|
text:
|
|
|
|
typeof item.displayName === "function"
|
|
|
|
? item.displayName(target)
|
|
|
|
: item.displayName,
|
|
|
|
data: typeof item.data === "function" ? item.data(target) : item.data,
|
|
|
|
})
|
|
|
|
);
|
2018-03-26 08:08:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
contextMenuContainer.html(contextMenu).show();
|
2018-04-22 10:59:01 +00:00
|
|
|
|
2018-03-26 08:08:43 +00:00
|
|
|
contextMenu
|
2018-04-22 10:59:01 +00:00
|
|
|
.css(positionContextMenu(contextMenu, selectedElement, event))
|
|
|
|
.find(".context-menu-item:first-child")
|
|
|
|
.trigger("focus");
|
|
|
|
|
|
|
|
return contextMenu;
|
2018-03-26 08:08:43 +00:00
|
|
|
}
|
|
|
|
|
2018-04-22 10:59:01 +00:00
|
|
|
function positionContextMenu(contextMenu, selectedElement, e) {
|
2018-03-26 08:08:43 +00:00
|
|
|
let offset;
|
|
|
|
const menuWidth = contextMenu.outerWidth();
|
|
|
|
const menuHeight = contextMenu.outerHeight();
|
|
|
|
|
|
|
|
if (selectedElement.hasClass("menu")) {
|
|
|
|
offset = selectedElement.offset();
|
|
|
|
offset.left -= menuWidth - selectedElement.outerWidth();
|
|
|
|
offset.top += selectedElement.outerHeight();
|
|
|
|
return offset;
|
|
|
|
}
|
|
|
|
|
|
|
|
offset = {left: e.pageX, top: e.pageY};
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
if (window.innerWidth - offset.left < menuWidth) {
|
2018-03-26 08:08:43 +00:00
|
|
|
offset.left = window.innerWidth - menuWidth;
|
|
|
|
}
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
if (window.innerHeight - offset.top < menuHeight) {
|
2018-03-26 08:08:43 +00:00
|
|
|
offset.top = window.innerHeight - menuHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
return offset;
|
|
|
|
}
|