| 1 | import { QuickReply } from '../../QuickReply.js'; |
| 2 | import { QuickReplySet } from '../../QuickReplySet.js'; |
| 3 | import { MenuHeader } from './MenuHeader.js'; |
| 4 | import { MenuItem } from './MenuItem.js'; |
| 5 | |
| 6 | export 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 | } |