hardlounge/client/js/contextMenuFactory.js
2019-11-25 20:12:54 +02:00

437 lines
11 KiB
JavaScript

"use strict";
const $ = require("jquery");
const socket = require("./socket");
const utils = require("./utils");
const ContextMenu = require("./contextMenu");
const contextMenuActions = [];
const contextMenuItems = [];
const {vueApp} = require("./vue");
const store = require("./store").default;
addDefaultItems();
registerEvents();
/**
* Used for adding context menu items. eg:
*
* addContextMenuItem({
* check: (target) => target.hasClass("user"),
* className: "customItemName",
* data: (target) => target.attr("data-name"),
* displayName: "Do something",
* callback: (name) => console.log(name), // print the name of the user to console
* });
*
* @param opts
* @param {function(Object)} [opts.check] - Function to check whether item should show on the context menu, called with the target jquery element, shows if return is truthy
* @param {string|function(Object)} opts.className - class name for the menu item, should be prefixed for non-default menu items (if function, called with jquery element, and uses return value)
* @param {string|function(Object)} opts.data - data that will be sent to the callback function (if function, called with jquery element, and uses return value)
* @param {string|function(Object)} opts.displayName - text to display on the menu item (if function, called with jquery element, and uses return value)
* @param {function(Object)} opts.callback - Function to call when the context menu item is clicked, called with the data requested in opts.data
*/
function addContextMenuItem(opts) {
opts.check = opts.check || (() => true);
opts.actionId = contextMenuActions.push(opts.callback) - 1;
contextMenuItems.push(opts);
}
function addContextDivider(opts) {
opts.check = opts.check || (() => true);
opts.divider = true;
contextMenuItems.push(opts);
}
function createContextMenu(that, event) {
return new ContextMenu(contextMenuItems, contextMenuActions, that, event);
}
function addWhoisItem() {
function whois(itemData) {
const chan = store.getters.findChannelOnCurrentNetwork(itemData);
if (chan) {
vueApp.switchToChannel(chan);
}
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/whois " + itemData,
});
}
addContextMenuItem({
check: (target) => target.hasClass("user"),
className: "user",
displayName: (target) => target.attr("data-name"),
data: (target) => target.attr("data-name"),
callback: whois,
});
addContextDivider({
check: (target) => target.hasClass("user"),
});
addContextMenuItem({
check: (target) => target.hasClass("user") || target.hasClass("query"),
className: "action-whois",
displayName: "User information",
data: (target) => target.attr("data-name") || target.attr("aria-label"),
callback: whois,
});
}
function addQueryItem() {
function query(itemData) {
const chan = store.getters.findChannelOnCurrentNetwork(itemData);
if (chan) {
vueApp.switchToChannel(chan);
}
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/query " + itemData,
});
}
addContextMenuItem({
check: (target) => target.hasClass("user"),
className: "action-query",
displayName: "Direct messages",
data: (target) => target.attr("data-name"),
callback: query,
});
}
function addCloseItem() {
function getCloseDisplay(target) {
if (target.hasClass("lobby")) {
return "Remove";
} else if (target.hasClass("channel")) {
return "Leave";
}
return "Close";
}
addContextMenuItem({
check: (target) => target.hasClass("chan"),
className: "close",
displayName: getCloseDisplay,
data: (target) => target.attr("data-target"),
callback(itemData) {
const close = document.querySelector(
`.networks .chan[data-target="${itemData}"] .close`
);
if (close) {
// TODO: After context menus are ported to Vue, figure out a direct api
close.click();
}
},
});
}
function addConnectItem() {
function connect(itemData) {
socket.emit("input", {
target: Number(itemData),
text: "/connect",
});
}
addContextMenuItem({
check: (target) => target.hasClass("lobby") && target.parent().hasClass("not-connected"),
className: "connect",
displayName: "Connect",
data: (target) => target.attr("data-id"),
callback: connect,
});
}
function addDisconnectItem() {
function disconnect(itemData) {
socket.emit("input", {
target: Number(itemData),
text: "/disconnect",
});
}
addContextMenuItem({
check: (target) => target.hasClass("lobby") && !target.parent().hasClass("not-connected"),
className: "disconnect",
displayName: "Disconnect",
data: (target) => target.attr("data-id"),
callback: disconnect,
});
}
function addKickItem() {
function kick(itemData) {
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/kick " + itemData,
});
}
addContextMenuItem({
check: (target) =>
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
target.closest(".chan").attr("data-type") === "channel",
className: "action-kick",
displayName: "Kick",
data: (target) => target.attr("data-name"),
callback: kick,
});
}
function addOpItem() {
function op(itemData) {
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/op " + itemData,
});
}
addContextMenuItem({
check: (target) =>
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
!utils.hasRoleInChannel(target.closest(".chan"), ["op"], target.attr("data-name")),
className: "action-op",
displayName: "Give operator (+o)",
data: (target) => target.attr("data-name"),
callback: op,
});
}
function addDeopItem() {
function deop(itemData) {
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/deop " + itemData,
});
}
addContextMenuItem({
check: (target) =>
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
utils.hasRoleInChannel(target.closest(".chan"), ["op"], target.attr("data-name")),
className: "action-op",
displayName: "Revoke operator (-o)",
data: (target) => target.attr("data-name"),
callback: deop,
});
}
function addVoiceItem() {
function voice(itemData) {
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/voice " + itemData,
});
}
addContextMenuItem({
check: (target) =>
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
!utils.hasRoleInChannel(target.closest(".chan"), ["voice"], target.attr("data-name")),
className: "action-voice",
displayName: "Give voice (+v)",
data: (target) => target.attr("data-name"),
callback: voice,
});
}
function addDevoiceItem() {
function devoice(itemData) {
socket.emit("input", {
target: Number($("#chat").attr("data-id")),
text: "/devoice " + itemData,
});
}
addContextMenuItem({
check: (target) =>
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
utils.hasRoleInChannel(target.closest(".chan"), ["voice"], target.attr("data-name")),
className: "action-voice",
displayName: "Revoke voice (-v)",
data: (target) => target.attr("data-name"),
callback: devoice,
});
}
function addFocusItem() {
function focusChan(itemData) {
$(`.networks .chan[data-target="${itemData}"]`).click();
}
const getClass = (target) => {
if (target.hasClass("lobby")) {
return "network";
} else if (target.hasClass("query")) {
return "query";
}
return "chan";
};
addContextMenuItem({
check: (target) => target.hasClass("chan"),
className: getClass,
displayName: (target) => target.attr("data-name") || target.attr("aria-label"),
data: (target) => target.attr("data-target"),
callback: focusChan,
});
addContextDivider({
check: (target) => target.hasClass("chan"),
});
}
function addEditNetworkItem() {
function edit(networkUuid) {
vueApp.$router.push("/edit-network/" + networkUuid);
}
addContextMenuItem({
check: (target) => target.hasClass("lobby"),
className: "edit",
displayName: "Edit this network…",
data: (target) => target.closest(".network").attr("data-uuid"),
callback: edit,
});
}
function addChannelListItem() {
function list(itemData) {
socket.emit("input", {
target: parseInt(itemData, 10),
text: "/list",
});
}
addContextMenuItem({
check: (target) => target.hasClass("lobby"),
className: "list",
displayName: "List all channels",
data: (target) => target.attr("data-id"),
callback: list,
});
}
function addEditTopicItem() {
function setEditTopic(itemData) {
store.getters.findChannel(Number(itemData)).channel.editTopic = true;
document.querySelector(`#sidebar .chan[data-id="${Number(itemData)}"]`).click();
vueApp.$nextTick(() => {
document.querySelector(`#chan-${Number(itemData)} .topic-input`).focus();
});
}
addContextMenuItem({
check: (target) => target.hasClass("channel"),
className: "edit",
displayName: "Edit topic",
data: (target) => target.attr("data-id"),
callback: setEditTopic,
});
}
function addBanListItem() {
function banlist(itemData) {
socket.emit("input", {
target: parseInt(itemData, 10),
text: "/banlist",
});
}
addContextMenuItem({
check: (target) => target.hasClass("channel"),
className: "list",
displayName: "List banned users",
data: (target) => target.attr("data-id"),
callback: banlist,
});
}
function addJoinItem() {
function openJoinForm(itemData) {
store.getters.findChannel(Number(itemData)).network.isJoinChannelShown = true;
}
addContextMenuItem({
check: (target) => target.hasClass("lobby"),
className: "join",
displayName: "Join a channel…",
data: (target) => target.attr("data-id"),
callback: openJoinForm,
});
}
function addIgnoreListItem() {
function ignorelist(itemData) {
socket.emit("input", {
target: parseInt(itemData, 10),
text: "/ignorelist",
});
}
addContextMenuItem({
check: (target) => target.hasClass("lobby"),
className: "list",
displayName: "List ignored users",
data: (target) => target.attr("data-id"),
callback: ignorelist,
});
}
function addDefaultItems() {
addFocusItem();
addWhoisItem();
addQueryItem();
addKickItem();
addOpItem();
addDeopItem();
addVoiceItem();
addDevoiceItem();
addEditNetworkItem();
addJoinItem();
addChannelListItem();
addEditTopicItem();
addBanListItem();
addIgnoreListItem();
addConnectItem();
addDisconnectItem();
addCloseItem();
}
function registerEvents() {
const viewport = $("#viewport");
viewport.on("contextmenu", ".network .chan", function(e) {
return createContextMenu($(this), e).show();
});
viewport.on("click contextmenu", ".user", function(e) {
// If user is selecting text, do not open context menu
// This primarily only targets mobile devices where selection is performed with touch
if (!window.getSelection().isCollapsed) {
return true;
}
return createContextMenu($(this), e).show();
});
viewport.on("click", "#chat .menu", function(e) {
e.currentTarget = $(
`#sidebar .chan[data-id="${$(this)
.closest(".chan")
.attr("data-id")}"]`
)[0];
return createContextMenu($(this), e).show();
});
}