Blame Raw
Cohee · e3f41666 · · 147 lines (8.1 KB)
1 contributor
1import { escapeRegex } from '../utils.js';
2import { SlashCommandParser } from './SlashCommandParser.js';
3
4export 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}