Blame Raw
Cohee · e3f41666 · · 126 lines (4.7 KB)
1 contributor
1import { QuickReply } from '../../QuickReply.js';
2import { QuickReplySet } from '../../QuickReplySet.js';
3import { MenuHeader } from './MenuHeader.js';
4import { MenuItem } from './MenuItem.js';
5
6export class ContextMenu {
7 /**@type {MenuItem[]}*/ itemList = [];
8 /**@type {Boolean}*/ isActive = false;
9
10 /**@type {HTMLElement}*/ root;
11 /**@type {HTMLElement}*/ menu;
12
13
14 constructor(/**@type {QuickReply}*/qr) {
15 // this.itemList = items;
16 this.itemList = this.build(qr).children;
17 this.itemList.forEach(item => {
18 item.onExpand = () => {
19 this.itemList.filter(it => it !== item)
20 .forEach(it => it.collapse());
21 };
22 });
23 }
24
25 /**
26 * @param {QuickReply} qr
27 * @param {String} chainedMessage
28 * @param {QuickReplySet[]} hierarchy
29 * @param {String[]} labelHierarchy
30 */
31 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
32 const tree = {
33 icon: qr.icon,
34 showLabel: qr.showLabel,
35 label: qr.label,
36 title: qr.title,
37 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
38 children: [],
39 };
40 qr.contextList.forEach((cl) => {
41 if (!cl.set) return;
42 if (!hierarchy.includes(cl.set)) {
43 const nextHierarchy = [...hierarchy, cl.set];
44 const nextLabelHierarchy = [...labelHierarchy, tree.label];
45 tree.children.push(new MenuHeader(cl.set.name));
46
47 // If the Quick Reply's own set is added as a context menu,
48 // show only the sub-QRs that are Invisible but have an icon
49 // intent: allow a QR set to be assigned to one of its own QR buttons for a "burger" menu
50 // with "UI" QRs either in the bar or in the menu, and "library function" QRs still hidden.
51 // - QRs already visible on the bar are filtered out,
52 // - hidden QRs without an icon are filtered out,
53 // - hidden QRs **with an icon** are shown in the menu
54 // so everybody is happy
55 const qrsOwnSetAddedAsContextMenu = cl.set.qrList.includes(qr);
56 const visible = (subQr) => {
57 return qrsOwnSetAddedAsContextMenu
58 ? subQr.isHidden && !!subQr.icon // yes .isHidden gets inverted here
59 : !subQr.isHidden;
60 };
61
62 cl.set.qrList.filter(visible).forEach(subQr => {
63 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
64 tree.children.push(new MenuItem(
65 subTree.icon,
66 subTree.showLabel,
67 subTree.label,
68 subTree.title,
69 subTree.message,
70 (evt) => {
71 evt.stopPropagation();
72 const finalQr = Object.assign(new QuickReply(), subQr);
73 finalQr.message = subTree.message.replace(/%%parent(-\d+)?%%/g, (_, index) => {
74 return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0];
75 });
76 cl.set.execute(finalQr);
77 },
78 subTree.children,
79 ));
80 });
81 }
82 });
83 return tree;
84 }
85
86 render() {
87 if (!this.root) {
88 const blocker = document.createElement('div'); {
89 this.root = blocker;
90 blocker.classList.add('ctx-blocker');
91 blocker.addEventListener('click', () => this.hide());
92 const menu = document.createElement('ul'); {
93 this.menu = menu;
94 menu.classList.add('list-group');
95 menu.classList.add('ctx-menu');
96 this.itemList.forEach(it => menu.append(it.render()));
97 blocker.append(menu);
98 }
99 }
100 }
101 return this.root;
102 }
103
104
105 show({ clientX, clientY }) {
106 if (this.isActive) return;
107 this.isActive = true;
108 this.render();
109 this.menu.style.bottom = `${window.innerHeight - clientY}px`;
110 this.menu.style.left = `${clientX}px`;
111 document.body.append(this.root);
112 }
113 hide() {
114 if (this.root) {
115 this.root.remove();
116 }
117 this.isActive = false;
118 }
119 toggle(/**@type {PointerEvent}*/evt) {
120 if (this.isActive) {
121 this.hide();
122 } else {
123 this.show(evt);
124 }
125 }
126}