| 1 | import { escapeRegex } from '../utils.js'; |
| 2 | import { SlashCommandParser } from './SlashCommandParser.js'; |
| 3 | |
| 4 | export class SlashCommandBrowser { |
| 5 | /**@type {SlashCommand[]}*/ cmdList; |
| 6 | /**@type {HTMLElement}*/ dom; |
| 7 | /**@type {HTMLElement}*/ search; |
| 8 | /**@type {HTMLElement}*/ details; |
| 9 | /**@type {Object.<string,HTMLElement>}*/ itemMap = {}; |
| 10 | /**@type {MutationObserver}*/ mo; |
| 11 | |
| 12 | renderInto(parent) { |
| 13 | if (!this.dom) { |
| 14 | const queryRegex = /(?:(?:^|\s+)([^\s"][^\s]*?)(?:\s+|$))|(?:(?:^|\s+)"(.*?)(?:"|$)(?:\s+|$))/; |
| 15 | const root = document.createElement('div'); { |
| 16 | this.dom = root; |
| 17 | const search = document.createElement('div'); { |
| 18 | search.classList.add('search'); |
| 19 | const lbl = document.createElement('label'); { |
| 20 | lbl.classList.add('searchLabel'); |
| 21 | lbl.textContent = 'Search: '; |
| 22 | const inp = document.createElement('input'); { |
| 23 | this.search = inp; |
| 24 | inp.classList.add('searchInput'); |
| 25 | inp.classList.add('text_pole'); |
| 26 | inp.type = 'search'; |
| 27 | inp.placeholder = 'Search slash commands - use quotes to search "literal" instead of fuzzy'; |
| 28 | inp.addEventListener('input', () => { |
| 29 | this.details?.remove(); |
| 30 | this.details = null; |
| 31 | let query = inp.value.trim(); |
| 32 | if (query.slice(-1) === '"' && !/(?:^|\s+)"/.test(query)) { |
| 33 | query = `"${query}`; |
| 34 | } |
| 35 | let fuzzyList = []; |
| 36 | let quotedList = []; |
| 37 | while (query.length > 0) { |
| 38 | const match = queryRegex.exec(query); |
| 39 | if (!match) break; |
| 40 | if (match[1] !== undefined) { |
| 41 | fuzzyList.push(new RegExp(`^(.*?)${match[1].split('').map(char => `(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i')); |
| 42 | } else if (match[2] !== undefined) { |
| 43 | quotedList.push(match[2]); |
| 44 | } |
| 45 | query = query.slice(match.index + match[0].length); |
| 46 | } |
| 47 | for (const cmd of this.cmdList) { |
| 48 | const targets = [ |
| 49 | cmd.name, |
| 50 | ...cmd.namedArgumentList.map(it => it.name), |
| 51 | ...cmd.namedArgumentList.map(it => it.description), |
| 52 | ...cmd.namedArgumentList.map(it => it.enumList.map(e => e.value)).flat(), |
| 53 | ...cmd.namedArgumentList.map(it => it.typeList).flat(), |
| 54 | ...cmd.unnamedArgumentList.map(it => it.description), |
| 55 | ...cmd.unnamedArgumentList.map(it => it.enumList.map(e => e.value)).flat(), |
| 56 | ...cmd.unnamedArgumentList.map(it => it.typeList).flat(), |
| 57 | ...cmd.aliases, |
| 58 | cmd.helpString, |
| 59 | ]; |
| 60 | const find = () => targets.find(t => (fuzzyList.find(f => f.test(t)) ?? quotedList.find(q => t.includes(q))) !== undefined) !== undefined; |
| 61 | if (fuzzyList.length + quotedList.length === 0 || find()) { |
| 62 | this.itemMap[cmd.name].classList.remove('isFiltered'); |
| 63 | } else { |
| 64 | this.itemMap[cmd.name].classList.add('isFiltered'); |
| 65 | } |
| 66 | } |
| 67 | }); |
| 68 | lbl.append(inp); |
| 69 | } |
| 70 | search.append(lbl); |
| 71 | } |
| 72 | root.append(search); |
| 73 | } |
| 74 | const container = document.createElement('div'); { |
| 75 | container.classList.add('commandContainer'); |
| 76 | const list = document.createElement('div'); { |
| 77 | list.classList.add('autoComplete'); |
| 78 | this.cmdList = Object |
| 79 | .keys(SlashCommandParser.commands) |
| 80 | .filter(key => SlashCommandParser.commands[key].name === key) // exclude aliases |
| 81 | .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) |
| 82 | .map(key => SlashCommandParser.commands[key]) |
| 83 | ; |
| 84 | for (const cmd of this.cmdList) { |
| 85 | const item = cmd.renderHelpItem(); |
| 86 | this.itemMap[cmd.name] = item; |
| 87 | let details; |
| 88 | item.addEventListener('click', () => { |
| 89 | if (!details) { |
| 90 | details = document.createElement('div'); { |
| 91 | details.classList.add('autoComplete-detailsWrap'); |
| 92 | const inner = document.createElement('div'); { |
| 93 | inner.classList.add('autoComplete-details'); |
| 94 | inner.append(cmd.renderHelpDetails()); |
| 95 | details.append(inner); |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | if (this.details !== details) { |
| 100 | Array.from(list.querySelectorAll('.selected')).forEach(it => it.classList.remove('selected')); |
| 101 | item.classList.add('selected'); |
| 102 | this.details?.remove(); |
| 103 | container.append(details); |
| 104 | this.details = details; |
| 105 | const pRect = list.getBoundingClientRect(); |
| 106 | const rect = item.children[0].getBoundingClientRect(); |
| 107 | details.style.setProperty('--targetOffset', rect.top - pRect.top); |
| 108 | } else { |
| 109 | item.classList.remove('selected'); |
| 110 | details.remove(); |
| 111 | this.details = null; |
| 112 | } |
| 113 | }); |
| 114 | list.append(item); |
| 115 | } |
| 116 | container.append(list); |
| 117 | } |
| 118 | root.append(container); |
| 119 | } |
| 120 | root.classList.add('slashCommandBrowser'); |
| 121 | } |
| 122 | } |
| 123 | parent.append(this.dom); |
| 124 | |
| 125 | this.mo = new MutationObserver(muts => { |
| 126 | if (muts.find(mut => Array.from(mut.removedNodes).find(it => it === this.dom || it.contains(this.dom)))) { |
| 127 | this.mo.disconnect(); |
| 128 | window.removeEventListener('keydown', boundHandler); |
| 129 | } |
| 130 | }); |
| 131 | this.mo.observe(document.querySelector('#chat'), { childList: true, subtree: true }); |
| 132 | const boundHandler = this.handleKeyDown.bind(this); |
| 133 | window.addEventListener('keydown', boundHandler); |
| 134 | return this.dom; |
| 135 | } |
| 136 | |
| 137 | handleKeyDown(evt) { |
| 138 | if (!evt.shiftKey && !evt.altKey && evt.ctrlKey && evt.key.toLowerCase() === 'f') { |
| 139 | if (!this.dom.closest('body')) return; |
| 140 | if (this.dom.closest('.mes') && !this.dom.closest('.last_mes')) return; |
| 141 | evt.preventDefault(); |
| 142 | evt.stopPropagation(); |
| 143 | evt.stopImmediatePropagation(); |
| 144 | this.search.focus(); |
| 145 | } |
| 146 | } |
| 147 | } |