diff --git a/shared/js/main.ts b/shared/js/main.ts index e37c4dbe..969769a4 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -397,6 +397,57 @@ function main() { } } + $(document).on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + event.preventDefault(); + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + name: 'Test item 1', + type: contextmenu.MenuEntryType.ENTRY, + callback: () => console.log("Test 1 item clicked!"), + + disabled: true + }, { + name: 'Test item 2', + type: contextmenu.MenuEntryType.ENTRY, + callback: () => console.log("Test 2 item clicked!") + }, { + name: 'Test item 3', + type: contextmenu.MenuEntryType.SUB_MENU, + callback: () => console.log("Test 3 item clicked!"), + sub_menu: [ + { + name: 'Test sub item 1', + type: contextmenu.MenuEntryType.ENTRY, + callback: () => console.log("Test sub 1 item clicked!") + }, { + name: 'Test sub item 2', + type: contextmenu.MenuEntryType.ENTRY, + callback: () => console.log("Test sub 2 item clicked!") + }, { + name: 'Test sub item 4', + type: contextmenu.MenuEntryType.ENTRY, + callback: () => console.log("Test sub 3 item clicked!") + } + ] + }, + contextmenu.Entry.HR(), + { + name: 'Test item 4', + type: contextmenu.MenuEntryType.ENTRY, + callback: () => console.log("Test 4 item clicked!") + }, { + name: 'Test item 5', + type: contextmenu.MenuEntryType.CHECKBOX, + callback: () => console.log("Test 5 item clicked!") + }, { + name: 'Test item 5', + type: contextmenu.MenuEntryType.CHECKBOX, + callback: () => console.log("Test 5 item clicked!"), + checkbox_checked: true + }, contextmenu.Entry.CLOSE(() => console.log("Menu closed!"))) + }) } const task_teaweb_starter: loader.Task = { diff --git a/shared/js/ui/channel.ts b/shared/js/ui/channel.ts index f2fd0602..e15ea472 100644 --- a/shared/js/ui/channel.ts +++ b/shared/js/ui/channel.ts @@ -452,58 +452,60 @@ class ChannelEntry { } let trigger_close = true; - spawn_context_menu(x, y, { - type: MenuEntryType.ENTRY, + + const bold = text => contextmenu.get_provider().html_format_enabled() ? "" + text + "" : text; + contextmenu.spawn_context_menu(x, y, { + type: contextmenu.MenuEntryType.ENTRY, name: tr("Show channel info"), callback: () => { trigger_close = false; this.channelTree.client.select_info.open_popover() }, - icon: "client-about", + icon_class: "client-about", visible: this.channelTree.client.select_info.is_popover() }, { - type: MenuEntryType.HR, + type: contextmenu.MenuEntryType.HR, visible: this.channelTree.client.select_info.is_popover(), name: '' }, { - type: MenuEntryType.ENTRY, - icon: "client-channel_switch", - name: tr("Switch to channel"), + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_switch", + name: bold(tr("Switch to channel")), callback: () => this.joinChannel() }, ...(() => { const local_client = this.channelTree.client.getClient(); if (!local_client || local_client.currentChannel() !== this) return [ - MenuEntry.HR(), + contextmenu.Entry.HR(), { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, icon: "client-subscribe_to_channel", - name: tr("Subscribe to channel"), + name: bold(tr("Subscribe to channel")), callback: () => this.subscribe(), visible: !this.flag_subscribed }, { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, icon: "client-channel_unsubscribed", - name: tr("Unsubscribe from channel"), + name: bold(tr("Unsubscribe from channel")), callback: () => this.unsubscribe(), visible: this.flag_subscribed }, { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, icon: "client-subscribe_mode", - name: tr("Use inherited subscribe mode"), + name: bold(tr("Use inherited subscribe mode")), callback: () => this.unsubscribe(true), visible: this.subscribe_mode != ChannelSubscribeMode.INHERITED } ]; return []; })(), - MenuEntry.HR(), + contextmenu.Entry.HR(), { - type: MenuEntryType.ENTRY, - icon: "client-channel_edit", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_edit", name: tr("Edit channel"), invalidPermission: !channelModify, callback: () => { @@ -536,8 +538,8 @@ class ChannelEntry { } }, { - type: MenuEntryType.ENTRY, - icon: "client-channel_delete", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_delete", name: tr("Delete channel"), invalidPermission: !flagDelete, callback: () => { @@ -546,10 +548,10 @@ class ChannelEntry { }) } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), { - type: MenuEntryType.ENTRY, - icon: "client-addon-collection", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-addon-collection", name: tr("Create music bot"), callback: () => { this.channelTree.client.serverConnection.send_command("musicbotcreate", {cid: this.channelId}).then(() => { @@ -563,21 +565,21 @@ class ChannelEntry { }); } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), { - type: MenuEntryType.ENTRY, - icon: "client-channel_create_sub", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_create_sub", name: tr("Create sub channel"), invalidPermission: !(channelCreate && this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_CHILD).granted(1)), callback: () => this.channelTree.spawnCreateChannel(this) }, { - type: MenuEntryType.ENTRY, - icon: "client-channel_create", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_create", name: tr("Create channel"), invalidPermission: !channelCreate, callback: () => this.channelTree.spawnCreateChannel() }, - MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) ); } diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index ed21918a..6a14ef6d 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -169,20 +169,15 @@ class ClientEntry { }); } - protected assignment_context() : ContextMenuEntry[] { - let server_groups: ContextMenuEntry[] = []; + protected assignment_context() : contextmenu.MenuEntry[] { + let server_groups: contextmenu.MenuEntry[] = []; for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { if(group.type != GroupType.NORMAL) continue; - let entry: ContextMenuEntry = {} as any; - - { - let tag = $.spawn("label").addClass("checkbox"); - $.spawn("input").attr("type", "checkbox").prop("checked", this.groupAssigned(group)).appendTo(tag); - $.spawn("span").addClass("checkmark").appendTo(tag); - entry.icon = tag; - } + let entry: contextmenu.MenuEntry = {} as any; + //TODO: May add the server group icon? + entry.checkbox_checked = this.groupAssigned(group); entry.name = group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]"; if(this.groupAssigned(group)) { entry.callback = () => { @@ -201,21 +196,19 @@ class ClientEntry { }; entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberAddPower); } - entry.type = MenuEntryType.ENTRY; + entry.type = contextmenu.MenuEntryType.CHECKBOX; + server_groups.push(entry); } - let channel_groups: ContextMenuEntry[] = []; + let channel_groups: contextmenu.MenuEntry[] = []; for(let group of this.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { if(group.type != GroupType.NORMAL) continue; - let entry: ContextMenuEntry = {} as any; - { - let tag = $.spawn("label").addClass("checkbox"); - $.spawn("input").attr("type", "checkbox").prop("checked", this.assignedChannelGroup() == group.id).appendTo(tag); - $.spawn("span").addClass("checkmark").appendTo(tag); - entry.icon = tag; - } + let entry: contextmenu.MenuEntry = {} as any; + + //TODO: May add the channel group icon? + entry.checkbox_checked = this.assignedChannelGroup() == group.id; entry.name = group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]"; entry.callback = () => { this.channelTree.client.serverConnection.send_command("setclientchannelgroup", { @@ -225,17 +218,17 @@ class ClientEntry { }); }; entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower); - entry.type = MenuEntryType.ENTRY; + entry.type = contextmenu.MenuEntryType.CHECKBOX; channel_groups.push(entry); } return [{ - type: MenuEntryType.SUB_MENU, - icon: "client-permission_server_groups", + type: contextmenu.MenuEntryType.SUB_MENU, + icon_class: "client-permission_server_groups", name: tr("Set server group"), sub_menu: [ { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, icon: "client-permission_server_groups", name: "Server groups dialog", callback: () => { @@ -253,19 +246,19 @@ class ClientEntry { }); } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), ...server_groups ] },{ - type: MenuEntryType.SUB_MENU, - icon: "client-permission_channel", + type: contextmenu.MenuEntryType.SUB_MENU, + icon_class: "client-permission_channel", name: tr("Set channel group"), sub_menu: [ ...channel_groups ] },{ - type: MenuEntryType.SUB_MENU, - icon: "client-permission_client", + type: contextmenu.MenuEntryType.SUB_MENU, + icon_class: "client-permission_client", name: tr("Permissions"), disabled: true, sub_menu: [ ] @@ -274,31 +267,33 @@ class ClientEntry { showContextMenu(x: number, y: number, on_close: () => void = undefined) { let trigger_close = true; - spawn_context_menu(x, y, + contextmenu.spawn_context_menu(x, y, { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, name: tr("Show client info"), callback: () => { trigger_close = false; this.channelTree.client.select_info.open_popover() }, - icon: "client-about", + icon_class: "client-about", visible: this.channelTree.client.select_info.is_popover() }, { - type: MenuEntryType.HR, + type: contextmenu.MenuEntryType.HR, visible: this.channelTree.client.select_info.is_popover(), name: '' }, { - type: MenuEntryType.ENTRY, - icon: "client-change_nickname", - name: tr("Open text chat"), + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-change_nickname", + name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + + tr("Open text chat") + + (contextmenu.get_provider().html_format_enabled() ? "" : ""), callback: () => { this.channelTree.client.chat.activeChat = this.chat(true); this.channelTree.client.chat.focus(); } }, { - type: MenuEntryType.ENTRY, - icon: "client-poke", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-poke", name: tr("Poke client"), callback: () => { createInputModal(tr("Poke client"), tr("Poke message:
"), text => true, result => { @@ -314,8 +309,8 @@ class ClientEntry { }, { width: 400, maxLength: 512 }).open(); } }, { - type: MenuEntryType.ENTRY, - icon: "client-edit", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-edit", name: tr("Change description"), callback: () => { createInputModal(tr("Change client description"), tr("New description:
"), text => true, result => { @@ -331,11 +326,11 @@ class ClientEntry { }, { width: 400, maxLength: 1024 }).open(); } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), ...this.assignment_context(), - MenuEntry.HR(), { - type: MenuEntryType.ENTRY, - icon: "client-move_client_to_own_channel", + contextmenu.Entry.HR(), { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-move_client_to_own_channel", name: tr("Move client to your channel"), callback: () => { this.channelTree.client.serverConnection.send_command("clientmove", { @@ -344,8 +339,8 @@ class ClientEntry { }); } }, { - type: MenuEntryType.ENTRY, - icon: "client-kick_channel", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-kick_channel", name: tr("Kick client from channel"), callback: () => { createInputModal(tr("Kick client from channel"), tr("Kick reason:
"), text => true, result => { @@ -362,8 +357,8 @@ class ClientEntry { }, { width: 400, maxLength: 255 }).open(); } }, { - type: MenuEntryType.ENTRY, - icon: "client-kick_server", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-kick_server", name: tr("Kick client fom server"), callback: () => { createInputModal(tr("Kick client from server"), tr("Kick reason:
"), text => true, result => { @@ -380,8 +375,8 @@ class ClientEntry { }, { width: 400, maxLength: 255 }).open(); } }, { - type: MenuEntryType.ENTRY, - icon: "client-ban_client", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-ban_client", name: tr("Ban client"), invalidPermission: !this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { @@ -398,7 +393,7 @@ class ClientEntry { }); } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), /* { type: MenuEntryType.ENTRY, @@ -418,8 +413,8 @@ class ClientEntry { MenuEntry.HR(), */ { - type: MenuEntryType.ENTRY, - icon: "client-volume", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-volume", name: tr("Change Volume"), callback: () => { Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => { @@ -430,7 +425,7 @@ class ClientEntry { }); } }, - MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) ); } @@ -839,15 +834,18 @@ class LocalClientEntry extends ClientEntry { showContextMenu(x: number, y: number, on_close: () => void = undefined): void { const _self = this; - spawn_context_menu(x, y, + contextmenu.spawn_context_menu(x, y, { - name: tr("Change name"), - icon: "client-change_nickname", + + name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + + tr("Change name") + + (contextmenu.get_provider().html_format_enabled() ? "" : ""), + icon_class: "client-change_nickname", callback: () =>_self.openRename(), - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Change description"), - icon: "client-edit", + icon_class: "client-edit", callback: () => { createInputModal(tr("Change own description"), tr("New description:
"), text => true, result => { if(result) { @@ -860,11 +858,11 @@ class LocalClientEntry extends ClientEntry { } }, { width: 400, maxLength: 1024 }).open(); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, - MenuEntry.HR(), + contextmenu.Entry.HR(), ...this.assignment_context(), - MenuEntry.CLOSE(on_close) + contextmenu.Entry.CLOSE(on_close) ); } @@ -968,23 +966,23 @@ class MusicClientEntry extends ClientEntry { showContextMenu(x: number, y: number, on_close: () => void = undefined): void { let trigger_close = true; - spawn_context_menu(x, y, + contextmenu.spawn_context_menu(x, y, { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, name: tr("Show bot info"), callback: () => { trigger_close = false; this.channelTree.client.select_info.open_popover() }, - icon: "client-about", + icon_class: "client-about", visible: this.channelTree.client.select_info.is_popover() }, { - type: MenuEntryType.HR, + type: contextmenu.MenuEntryType.HR, visible: this.channelTree.client.select_info.is_popover(), name: '' }, { name: tr("Change bot name"), - icon: "client-change_nickname", + icon_class: "client-change_nickname", disabled: false, callback: () => { createInputModal(tr("Change music bots nickname"), tr("New nickname:
"), text => text.length >= 3 && text.length <= 31, result => { @@ -997,10 +995,10 @@ class MusicClientEntry extends ClientEntry { } }, { width: 400, maxLength: 255 }).open(); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Change bot description"), - icon: "client-edit", + icon_class: "client-edit", disabled: false, callback: () => { createInputModal(tr("Change music bots description"), tr("New description:
"), text => true, result => { @@ -1013,7 +1011,7 @@ class MusicClientEntry extends ClientEntry { } }, { width: 400, maxLength: 255 }).open(); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, /* { @@ -1026,7 +1024,7 @@ class MusicClientEntry extends ClientEntry { */ { name: tr("Open bot's playlist"), - icon: "client-edit", + icon_class: "client-edit", disabled: false, callback: () => { this.channelTree.client.serverConnection.command_helper.request_playlist_list().then(lists => { @@ -1041,11 +1039,11 @@ class MusicClientEntry extends ClientEntry { createErrorModal(tr("Failed to query playlist."), tr("Failed to query playlist info.")).open(); }); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Quick url replay"), - icon: "client-edit", + icon_class: "client-edit", disabled: false, callback: () => { createInputModal(tr("Please enter the URL"), tr("URL:"), text => true, result => { @@ -1064,13 +1062,13 @@ class MusicClientEntry extends ClientEntry { } }, { width: 400, maxLength: 255 }).open(); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, - MenuEntry.HR(), + contextmenu.Entry.HR(), ...super.assignment_context(), - MenuEntry.HR(),{ - type: MenuEntryType.ENTRY, - icon: "client-move_client_to_own_channel", + contextmenu.Entry.HR(),{ + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-move_client_to_own_channel", name: tr("Move client to your channel"), callback: () => { this.channelTree.client.serverConnection.send_command("clientmove", { @@ -1079,8 +1077,8 @@ class MusicClientEntry extends ClientEntry { }); } }, { - type: MenuEntryType.ENTRY, - icon: "client-kick_channel", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-kick_channel", name: tr("Kick client from channel"), callback: () => { createInputModal(tr("Kick client from channel"), tr("Kick reason:
"), text => true, result => { @@ -1095,10 +1093,10 @@ class MusicClientEntry extends ClientEntry { }, { width: 400, maxLength: 255 }).open(); } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), { - type: MenuEntryType.ENTRY, - icon: "client-volume", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-volume", name: tr("Change local volume"), callback: () => { Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => { @@ -1110,8 +1108,8 @@ class MusicClientEntry extends ClientEntry { } }, { - type: MenuEntryType.ENTRY, - icon: "client-volume", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-volume", name: tr("Change remote volume"), callback: () => { let max_volume = this.channelTree.client.permissions.neededPermission(PermissionType.I_CLIENT_MUSIC_CREATE_MODIFY_MAX_VOLUME).value; @@ -1132,10 +1130,10 @@ class MusicClientEntry extends ClientEntry { }); } }, - MenuEntry.HR(), + contextmenu.Entry.HR(), { name: tr("Delete bot"), - icon: "client-delete", + icon_class: "client-delete", disabled: false, callback: () => { const tag = $.spawn("div").append(MessageHelper.formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false))); @@ -1147,9 +1145,9 @@ class MusicClientEntry extends ClientEntry { } }); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }, - MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) ); } diff --git a/shared/js/ui/elements/context_menu.ts b/shared/js/ui/elements/context_menu.ts index c8f89f81..ce5f0488 100644 --- a/shared/js/ui/elements/context_menu.ts +++ b/shared/js/ui/elements/context_menu.ts @@ -1,142 +1,220 @@ -let context_menu: JQuery; +namespace contextmenu { + export interface MenuEntry { + callback?: () => void; + type: MenuEntryType; + name: (() => string) | string; + icon_class?: string; + icon_path?: string; + disabled?: boolean; + visible?: boolean; -$(document).bind("click", function (e) { - let menu = context_menu || (context_menu = $(".context-menu")); + checkbox_checked?: boolean; - if(!menu.is(":visible")) return; + invalidPermission?: boolean; + sub_menu?: MenuEntry[]; + } - if ($(e.target).parents(".context-menu").length == 0) { + export enum MenuEntryType { + CLOSE, + ENTRY, + CHECKBOX, + HR, + SUB_MENU + } + + export class Entry { + static HR() { + return { + callback: () => {}, + type: MenuEntryType.HR, + name: "", + icon: "" + }; + }; + + static CLOSE(callback: () => void) { + return { + callback: callback, + type: MenuEntryType.CLOSE, + name: "", + icon: "" + }; + } + } + + export interface ContextMenuProvider { despawn_context_menu(); - e.preventDefault(); + spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]); + + initialize(); + finalize(); + + html_format_enabled() : boolean; } -}); -let contextMenuCloseFn = undefined; -function despawn_context_menu() { - let menu = context_menu || (context_menu = $(".context-menu")); + let provider: ContextMenuProvider; + export function spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) { + if(!provider) { + console.error(tr("Failed to spawn context menu! Missing provider!")); + return; + } - if(!menu.is(":visible")) return; - menu.animate({opacity: 0}, 100, () => menu.css("display", "none")); - if(contextMenuCloseFn) contextMenuCloseFn(); -} + provider.spawn_context_menu(x, y, ...entries); + } -enum MenuEntryType { - CLOSE, - ENTRY, - HR, - SUB_MENU -} + export function despawn_context_menu() { + if(!provider) + return; -class MenuEntry { - static HR() { - return { - callback: () => {}, - type: MenuEntryType.HR, - name: "", - icon: "" - }; - }; + provider.despawn_context_menu(); + } - static CLOSE(callback: () => void) { - return { - callback: callback, - type: MenuEntryType.CLOSE, - name: "", - icon: "" - }; + export function get_provider() : ContextMenuProvider { return provider; } + export function set_provider(_provider: ContextMenuProvider) { + provider = _provider; + provider.initialize(); } } -interface ContextMenuEntry { - callback?: () => void; - type: MenuEntryType; - name: (() => string) | string; - icon?: (() => string) | string | JQuery; - disabled?: boolean; - visible?: boolean; +class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { + private _global_click_listener: (event) => any; + private _context_menu: JQuery; + private _close_callbacks: (() => any)[] = []; - invalidPermission?: boolean; - sub_menu?: ContextMenuEntry[]; -} + despawn_context_menu() { + let menu = this._context_menu || (this._context_menu = $(".context-menu")); -function generate_tag(entry: ContextMenuEntry) : JQuery { - if(entry.type == MenuEntryType.HR) { - return $.spawn("hr"); - } else if(entry.type == MenuEntryType.ENTRY) { - console.log(entry.icon); - let icon = $.isFunction(entry.icon) ? entry.icon() : entry.icon; - if(typeof(icon) === "string") { + if(!menu.is(":visible")) + return; + + menu.animate({opacity: 0}, 100, () => menu.css("display", "none")); + for(const callback of this._close_callbacks) + callback(); + this._close_callbacks = []; + } + + finalize() { + $(document).unbind('click', this._global_click_listener); + } + + initialize() { + this._global_click_listener = this.on_global_click.bind(this); + $(document).bind('click', this._global_click_listener); + } + + private on_global_click(event) { + let menu = this._context_menu || (this._context_menu = $(".context-menu")); + + if(!menu.is(":visible")) return; + + if ($(event.target).parents(".context-menu").length == 0) { + this.despawn_context_menu(); + event.preventDefault(); + } + } + + private generate_tag(entry: contextmenu.MenuEntry) : JQuery { + if(entry.type == contextmenu.MenuEntryType.HR) { + return $.spawn("hr"); + } else if(entry.type == contextmenu.MenuEntryType.ENTRY) { + let icon = entry.icon_class; if(!icon || icon.length == 0) icon = "icon_empty"; else icon = "icon " + icon; - } - let tag = $.spawn("div").addClass("entry"); - tag.append(typeof(icon) === "string" ? $.spawn("div").addClass(icon) : icon); - tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); + let tag = $.spawn("div").addClass("entry"); + tag.append($.spawn("div").addClass(icon)); + tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); - if(entry.disabled || entry.invalidPermission) tag.addClass("disabled"); - else { - tag.click(function () { - if($.isFunction(entry.callback)) entry.callback(); - despawn_context_menu(); - }); - } - return tag; - } else if(entry.type == MenuEntryType.SUB_MENU) { - let icon = $.isFunction(entry.icon) ? entry.icon() : entry.icon; - if(typeof(icon) === "string") { - if(!icon || icon.length == 0) icon = "icon_empty"; - else icon = "icon " + icon; - } - - let tag = $.spawn("div").addClass("entry").addClass("sub-container"); - tag.append(typeof(icon) === "string" ? $.spawn("div").addClass(icon) : icon); - tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); - - tag.append($.spawn("div").addClass("arrow right")); - - if(entry.disabled || entry.invalidPermission) tag.addClass("disabled"); - else { - let menu = $.spawn("div").addClass("sub-menu").addClass("context-menu-container"); - for(const e of entry.sub_menu) { - if(typeof(entry.visible) === 'boolean' && !entry.visible) - continue; - menu.append(generate_tag(e)); + if(entry.disabled || entry.invalidPermission) tag.addClass("disabled"); + else { + tag.click( () => { + if($.isFunction(entry.callback)) + entry.callback(); + this.despawn_context_menu(); + }); } - menu.appendTo(tag); + return tag; + } else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) { + let checkbox = $.spawn("label").addClass("checkbox"); + $.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox); + $.spawn("span").addClass("checkmark").appendTo(checkbox); + + let tag = $.spawn("div").addClass("entry"); + tag.append(checkbox); + tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); + + if(entry.disabled || entry.invalidPermission) + tag.addClass("disabled"); + else { + tag.click( () => { + if($.isFunction(entry.callback)) + entry.callback(); + this.despawn_context_menu(); + }); + } + return tag; + } else if(entry.type == contextmenu.MenuEntryType.SUB_MENU) { + let icon = entry.icon_class; + if(!icon || icon.length == 0) icon = "icon_empty"; + else icon = "icon " + icon; + + let tag = $.spawn("div").addClass("entry").addClass("sub-container"); + tag.append($.spawn("div").addClass(icon)); + tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); + + tag.append($.spawn("div").addClass("arrow right")); + + if(entry.disabled || entry.invalidPermission) tag.addClass("disabled"); + else { + let menu = $.spawn("div").addClass("sub-menu").addClass("context-menu-container"); + for(const e of entry.sub_menu) { + if(typeof(entry.visible) === 'boolean' && !entry.visible) + continue; + menu.append(this.generate_tag(e)); + } + menu.appendTo(tag); + } + return tag; } - return tag; + return $.spawn("div").text("undefined"); + } + + spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) { + let menu_tag = this._context_menu || (this._context_menu = $(".context-menu")); + menu_tag.finish().empty().css("opacity", "0"); + + const menu_container = $.spawn("div").addClass("context-menu-container"); + this._close_callbacks = []; + + for(const entry of entries){ + if(typeof(entry.visible) === 'boolean' && !entry.visible) + continue; + + if(entry.type == contextmenu.MenuEntryType.CLOSE) { + this._close_callbacks.push(entry.callback); + } else + menu_container.append(this.generate_tag(entry)); + } + + menu_tag.append(menu_container); + menu_tag.animate({opacity: 1}, 100).css("display", "block"); + + const width = menu_container.visible_width(); + if(x + width + 5 > window.innerWidth) + menu_container.addClass("left"); + + // In the right position (the mouse) + menu_tag.css({ + "top": y + "px", + "left": x + "px" + }); + } + + html_format_enabled(): boolean { + return true; } - return $.spawn("div").text("undefined"); } -function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) { - let menu_tag = context_menu || (context_menu = $(".context-menu")); - menu_tag.finish().empty().css("opacity", "0"); - - const menu_container = $.spawn("div").addClass("context-menu-container"); - contextMenuCloseFn = undefined; - - for(const entry of entries){ - if(typeof(entry.visible) === 'boolean' && !entry.visible) - continue; - - if(entry.type == MenuEntryType.CLOSE) { - contextMenuCloseFn = entry.callback; - } else - menu_container.append(generate_tag(entry)); - } - - menu_tag.append(menu_container); - menu_tag.animate({opacity: 1}, 100).css("display", "block"); - - const width = menu_container.visible_width(); - if(x + width + 5 > window.innerWidth) - menu_container.addClass("left"); - - // In the right position (the mouse) - menu_tag.css({ - "top": y + "px", - "left": x + "px" - }); -} \ No newline at end of file +//TODO: Improve +if(!window.require) + contextmenu.set_provider(new HTMLContextMenuProvider()); \ No newline at end of file diff --git a/shared/js/ui/frames/ControlBar.ts b/shared/js/ui/frames/ControlBar.ts index d00c3aaa..46aa3186 100644 --- a/shared/js/ui/frames/ControlBar.ts +++ b/shared/js/ui/frames/ControlBar.ts @@ -541,17 +541,17 @@ class ControlBar { return; event.preventDefault(); - spawn_context_menu(event.pageX, event.pageY, { - type: MenuEntryType.ENTRY, + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, name: tr("Connect"), - icon: 'client-connect', + icon_class: 'client-connect', callback: () => bookmark_connect(false) }, { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, name: tr("Connect in a new tab"), - icon: 'client-connect', + icon_class: 'client-connect', callback: () => bookmark_connect(true) - }, MenuEntry.CLOSE(() => { + }, contextmenu.Entry.CLOSE(() => { setTimeout(() => { this.htmlTag.find(".btn_bookmark.button-dropdown").removeClass("force-show") }, 250); diff --git a/shared/js/ui/frames/SelectedItemInfo.ts b/shared/js/ui/frames/SelectedItemInfo.ts index c2c08ed8..9121c657 100644 --- a/shared/js/ui/frames/SelectedItemInfo.ts +++ b/shared/js/ui/frames/SelectedItemInfo.ts @@ -455,7 +455,7 @@ class ChannelInfoManager extends InfoManager { properties["channel_name"] = channel.generate_tag(false); properties["channel_type"] = ChannelType.normalize(channel.channelType()); properties["channel_clients"] = channel.channelTree.clientsByChannel(channel).length; - properties["channel_subscribed"] = true; //TODO + properties["channel_subscribed"] = channel.flag_subscribed; properties["server_encryption"] = channel.channelTree.server.properties.virtualserver_codec_encryption_mode; for(let key in channel.properties) @@ -465,16 +465,15 @@ class ChannelInfoManager extends InfoManager { properties["bbcode_channel_description"] = tag_channel_description; channel.getChannelDescription().then(description => { - let result = XBBCODE.process({ - text: description, - escapeHtml: true, - addInLineBreaks: true - }); + const result = xbbcode.parse(description, {}); + /* if(result.error) { console.log("BBCode parse error: %o", result.errorQueue); } + */ - tag_channel_description.html(result.html) + tag_channel_description.empty() + .append($.spawn("div").html(result.build_html()).contents()) .css("overflow-y", "auto") .css("flex-grow", "1"); }); diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index 9cc6d614..b76de264 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -245,10 +245,10 @@ class ChatEntry { tag.on("contextmenu", (e) => { e.preventDefault(); - let actions: ContextMenuEntry[] = []; + let actions: contextmenu.MenuEntry[] = []; actions.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "", name: tr("Clear"), callback: () => { this.history = []; @@ -257,23 +257,23 @@ class ChatEntry { }); if(this.flag_closeable) { actions.push({ - type: MenuEntryType.ENTRY, - icon: "client-tab_close_button", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-tab_close_button", name: tr("Close"), callback: () => this.handle.deleteChat(this) }); } actions.push({ - type: MenuEntryType.ENTRY, - icon: "client-tab_close_button", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-tab_close_button", name: tr("Close all private tabs"), callback: () => { //TODO Implement this? }, visible: false }); - spawn_context_menu(e.pageX, e.pageY, ...actions); + contextmenu.spawn_context_menu(e.pageX, e.pageY, ...actions); }); tag_close.click(() => { diff --git a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts index bb3c95c8..c7409ae1 100644 --- a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts +++ b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts @@ -181,24 +181,20 @@ namespace unused { if(event.isDefaultPrevented()) return; event.preventDefault(); - spawn_context_menu(event.pageX, event.pageY, { - type: MenuEntryType.ENTRY, - icon: "", + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend group"), callback: () => update_collapse_status(true, false) }, { - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), callback: () => update_collapse_status(true, true) }, { - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse group"), callback: () => update_collapse_status(false, false) }, { - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse all"), callback: () => update_collapse_status(false, true) }); diff --git a/shared/js/ui/modal/permission/ModalPermissionEdit.ts b/shared/js/ui/modal/permission/ModalPermissionEdit.ts index 43b5307d..7ecb1324 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEdit.ts +++ b/shared/js/ui/modal/permission/ModalPermissionEdit.ts @@ -130,14 +130,12 @@ namespace Modals { if(event.isDefaultPrevented()) return; event.preventDefault(); - spawn_context_menu(event.pageX, event.pageY, { - type: MenuEntryType.ENTRY, - icon: "", + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), callback: () => this.entry_editor.expend_all() }, { - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse all"), callback: () => this.entry_editor.collapse_all() }); @@ -221,18 +219,16 @@ namespace Modals { }; entry.on_context_menu = (x, y) => { - let entries: ContextMenuEntry[] = []; + let entries: contextmenu.MenuEntry[] = []; if(typeof(entry.value) === "undefined") { entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Add permission"), callback: () => entry.trigger_value_assign() }); } else { entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove permission"), callback: () => { entry.value = undefined; @@ -243,15 +239,13 @@ namespace Modals { if(typeof(entry.granted) === "undefined") { entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Add grant permission"), callback: () => entry.trigger_grant_assign() }); } else { entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove grant permission"), callback: () => { entry.granted = undefined; @@ -259,23 +253,20 @@ namespace Modals { } }); } - entries.push(MenuEntry.HR()); + entries.push(contextmenu.Entry.HR()); entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), callback: () => this.entry_editor.expend_all() }); entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse all"), callback: () => this.entry_editor.collapse_all() }); - entries.push(MenuEntry.HR()); + entries.push(contextmenu.Entry.HR()); entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Show permission description"), callback: () => { createInfoModal( @@ -285,15 +276,14 @@ namespace Modals { } }); entries.push({ - type: MenuEntryType.ENTRY, - icon: "", + type: contextmenu.MenuEntryType.ENTRY, name: tr("Copy permission name"), callback: () => { copy_to_clipboard(permission.name); } }); - spawn_context_menu(x, y, ...entries); + contextmenu.spawn_context_menu(x, y, ...entries); } } } @@ -1220,10 +1210,10 @@ namespace Modals { return; event.preventDefault(); - spawn_context_menu(event.pageX, event.pageY, { - type: MenuEntryType.ENTRY, + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove client"), - icon: 'client-delete', + icon_class: 'client-delete', callback: () => { connection.serverConnection.send_command("servergroupdelclient", { sgid: current_group.id, @@ -1233,9 +1223,9 @@ namespace Modals { }); } }, { - type: MenuEntryType.ENTRY, + type: contextmenu.MenuEntryType.ENTRY, name: tr("Copy unique id"), - icon: 'client-copy', + icon_class: 'client-copy', callback: () => copy_to_clipboard(client.client_unique_identifier) }) }); diff --git a/shared/js/ui/server.ts b/shared/js/ui/server.ts index 398dc8dc..9ed774da 100644 --- a/shared/js/ui/server.ts +++ b/shared/js/ui/server.ts @@ -136,22 +136,22 @@ class ServerEntry { spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) { let trigger_close = true; - spawn_context_menu(x, y, { - type: MenuEntryType.ENTRY, + contextmenu.spawn_context_menu(x, y, { + type: contextmenu.MenuEntryType.ENTRY, name: tr("Show server info"), callback: () => { trigger_close = false; this.channelTree.client.select_info.open_popover() }, - icon: "client-about", + icon_class: "client-about", visible: this.channelTree.client.select_info.is_popover() }, { - type: MenuEntryType.HR, + type: contextmenu.MenuEntryType.HR, visible: this.channelTree.client.select_info.is_popover(), name: '' }, { - type: MenuEntryType.ENTRY, - icon: "client-virtualserver_edit", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-virtualserver_edit", name: tr("Edit"), callback: () => { Modals.createServerModal(this, properties => { @@ -164,22 +164,22 @@ class ServerEntry { }); } }, { - type: MenuEntryType.ENTRY, - icon: "client-iconviewer", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-iconviewer", name: tr("View icons"), callback: () => Modals.spawnIconSelect(this.channelTree.client) }, { - type: MenuEntryType.ENTRY, - icon: 'client-iconsview', + type: contextmenu.MenuEntryType.ENTRY, + icon_class: 'client-iconsview', name: tr("View avatars"), callback: () => Modals.spawnAvatarList(this.channelTree.client) }, { - type: MenuEntryType.ENTRY, - icon: "client-invite_buddy", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-invite_buddy", name: tr("Invite buddy"), callback: () => Modals.spawnInviteEditor(this.channelTree.client) }, - MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) ); } diff --git a/shared/js/ui/view.ts b/shared/js/ui/view.ts index a55f0832..c94ebee8 100644 --- a/shared/js/ui/view.ts +++ b/shared/js/ui/view.ts @@ -90,15 +90,15 @@ class ChannelTree { this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT).granted(1) || this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_PERMANENT).granted(1); - spawn_context_menu(x, y, + contextmenu.spawn_context_menu(x, y, { - type: MenuEntryType.ENTRY, - icon: "client-channel_create", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_create", name: tr("Create channel"), invalidPermission: !channelCreate, callback: () => this.spawnCreateChannel() }, - MenuEntry.CLOSE(on_close) + contextmenu.Entry.CLOSE(on_close) ); } @@ -461,11 +461,11 @@ class ChannelTree { const music_entry = clients.map(e => e instanceof MusicClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; const local_client = clients.map(e => e instanceof LocalClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; console.log(tr("Music only: %o | Container music: %o | Container local: %o"), music_entry, music_entry, local_client); - let entries: ContextMenuEntry[] = []; + let entries: contextmenu.MenuEntry[] = []; if (!music_entry && !local_client) { //Music bots or local client cant be poked entries.push({ - type: MenuEntryType.ENTRY, - icon: "client-poke", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-poke", name: tr("Poke clients"), callback: () => { createInputModal(tr("Poke clients"), tr("Poke message:
"), text => true, result => { @@ -482,8 +482,8 @@ class ChannelTree { }); } entries.push({ - type: MenuEntryType.ENTRY, - icon: "client-move_client_to_own_channel", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-move_client_to_own_channel", name: tr("Move clients to your channel"), callback: () => { const target = this.client.getClient().currentChannel().getChannelId(); @@ -495,10 +495,10 @@ class ChannelTree { } }); if (!local_client) {//local client cant be kicked and/or banned or kicked - entries.push(MenuEntry.HR()); + entries.push(contextmenu.Entry.HR()); entries.push({ - type: MenuEntryType.ENTRY, - icon: "client-kick_channel", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-kick_channel", name: tr("Kick clients from channel"), callback: () => { createInputModal(tr("Kick clients from channel"), tr("Kick reason:
"), text => true, result => { @@ -517,8 +517,8 @@ class ChannelTree { if (!music_entry) { //Music bots cant be banned or kicked entries.push({ - type: MenuEntryType.ENTRY, - icon: "client-kick_server", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-kick_server", name: tr("Kick clients fom server"), callback: () => { createInputModal(tr("Kick clients from server"), tr("Kick reason:
"), text => true, result => { @@ -534,8 +534,8 @@ class ChannelTree { }, {width: 400, maxLength: 255}).open(); } }, { - type: MenuEntryType.ENTRY, - icon: "client-ban_client", + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-ban_client", name: tr("Ban clients"), invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { @@ -555,10 +555,10 @@ class ChannelTree { }); } if(music_only) { - entries.push(MenuEntry.HR()); + entries.push(contextmenu.Entry.HR()); entries.push({ name: tr("Delete bots"), - icon: "client-delete", + icon_class: "client-delete", disabled: false, callback: () => { const param_string = clients.map((_, index) => "{" + index + "}").join(', '); @@ -574,11 +574,11 @@ class ChannelTree { } }); }, - type: MenuEntryType.ENTRY + type: contextmenu.MenuEntryType.ENTRY }); } } - spawn_context_menu(event.pageX, event.pageY, ...entries); + contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); } clientsByGroup(group: Group) : ClientEntry[] { diff --git a/vendor/xbbcode b/vendor/xbbcode index d9a47d05..8092afb5 160000 --- a/vendor/xbbcode +++ b/vendor/xbbcode @@ -1 +1 @@ -Subproject commit d9a47d059ae9cce559d7a75553a25ba342d36229 +Subproject commit 8092afb59615aa19cff372689cd6985c96e3f2ba