mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-15 10:48:36 +01:00
149 lines
8.1 KiB
JavaScript
149 lines
8.1 KiB
JavaScript
|
import { escapeRegex } from '../utils.js';
|
||
|
import { SlashCommand } from './SlashCommand.js';
|
||
|
import { SlashCommandParser } from './SlashCommandParser.js';
|
||
|
|
||
|
export class SlashCommandBrowser {
|
||
|
/**@type {SlashCommand[]}*/ cmdList;
|
||
|
/**@type {HTMLElement}*/ dom;
|
||
|
/**@type {HTMLElement}*/ search;
|
||
|
/**@type {HTMLElement}*/ details;
|
||
|
/**@type {Object.<string,HTMLElement>}*/ itemMap = {};
|
||
|
/**@type {MutationObserver}*/ mo;
|
||
|
|
||
|
renderInto(parent) {
|
||
|
if (!this.dom) {
|
||
|
const queryRegex = /(?:(?:^|\s+)([^\s"][^\s]*?)(?:\s+|$))|(?:(?:^|\s+)"(.*?)(?:"|$)(?:\s+|$))/;
|
||
|
const root = document.createElement('div'); {
|
||
|
this.dom = root;
|
||
|
const search = document.createElement('div'); {
|
||
|
search.classList.add('search');
|
||
|
const lbl = document.createElement('label'); {
|
||
|
lbl.classList.add('searchLabel');
|
||
|
lbl.textContent = 'Search: ';
|
||
|
const inp = document.createElement('input'); {
|
||
|
this.search = inp;
|
||
|
inp.classList.add('searchInput');
|
||
|
inp.classList.add('text_pole');
|
||
|
inp.type = 'search';
|
||
|
inp.placeholder = 'Search slash commands - use quotes to search "literal" instead of fuzzy';
|
||
|
inp.addEventListener('input', ()=>{
|
||
|
this.details?.remove();
|
||
|
this.details = null;
|
||
|
let query = inp.value.trim();
|
||
|
if (query.slice(-1) == '"' && !/(?:^|\s+)"/.test(query)) {
|
||
|
query = `"${query}`;
|
||
|
}
|
||
|
let fuzzyList = [];
|
||
|
let quotedList = [];
|
||
|
while (query.length > 0) {
|
||
|
const match = queryRegex.exec(query);
|
||
|
if (!match) break;
|
||
|
if (match[1] !== undefined) {
|
||
|
fuzzyList.push(new RegExp(`^(.*?)${match[1].split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i'));
|
||
|
} else if (match[2] !== undefined) {
|
||
|
quotedList.push(match[2]);
|
||
|
}
|
||
|
query = query.slice(match.index + match[0].length);
|
||
|
}
|
||
|
for (const cmd of this.cmdList) {
|
||
|
const targets = [
|
||
|
cmd.name,
|
||
|
...cmd.namedArgumentList.map(it=>it.name),
|
||
|
...cmd.namedArgumentList.map(it=>it.description),
|
||
|
...cmd.namedArgumentList.map(it=>it.enumList.map(e=>e.value)).flat(),
|
||
|
...cmd.namedArgumentList.map(it=>it.typeList).flat(),
|
||
|
...cmd.unnamedArgumentList.map(it=>it.description),
|
||
|
...cmd.unnamedArgumentList.map(it=>it.enumList.map(e=>e.value)).flat(),
|
||
|
...cmd.unnamedArgumentList.map(it=>it.typeList).flat(),
|
||
|
...cmd.aliases,
|
||
|
cmd.helpString,
|
||
|
];
|
||
|
const find = ()=>targets.find(t=>(fuzzyList.find(f=>f.test(t)) ?? quotedList.find(q=>t.includes(q))) !== undefined) !== undefined;
|
||
|
if (fuzzyList.length + quotedList.length == 0 || find()) {
|
||
|
this.itemMap[cmd.name].classList.remove('isFiltered');
|
||
|
} else {
|
||
|
this.itemMap[cmd.name].classList.add('isFiltered');
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
lbl.append(inp);
|
||
|
}
|
||
|
search.append(lbl);
|
||
|
}
|
||
|
root.append(search);
|
||
|
}
|
||
|
const container = document.createElement('div'); {
|
||
|
container.classList.add('commandContainer');
|
||
|
const list = document.createElement('div'); {
|
||
|
list.classList.add('autoComplete');
|
||
|
this.cmdList = Object
|
||
|
.keys(SlashCommandParser.commands)
|
||
|
.filter(key => SlashCommandParser.commands[key].name == key) // exclude aliases
|
||
|
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||
|
.map(key => SlashCommandParser.commands[key])
|
||
|
;
|
||
|
for (const cmd of this.cmdList) {
|
||
|
const item = cmd.renderHelpItem();
|
||
|
this.itemMap[cmd.name] = item;
|
||
|
let details;
|
||
|
item.addEventListener('click', ()=>{
|
||
|
if (!details) {
|
||
|
details = document.createElement('div'); {
|
||
|
details.classList.add('autoComplete-detailsWrap');
|
||
|
const inner = document.createElement('div'); {
|
||
|
inner.classList.add('autoComplete-details');
|
||
|
inner.append(cmd.renderHelpDetails());
|
||
|
details.append(inner);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (this.details != details) {
|
||
|
Array.from(list.querySelectorAll('.selected')).forEach(it=>it.classList.remove('selected'));
|
||
|
item.classList.add('selected');
|
||
|
this.details?.remove();
|
||
|
container.append(details);
|
||
|
this.details = details;
|
||
|
const pRect = list.getBoundingClientRect();
|
||
|
const rect = item.children[0].getBoundingClientRect();
|
||
|
details.style.setProperty('--targetOffset', rect.top - pRect.top);
|
||
|
} else {
|
||
|
item.classList.remove('selected');
|
||
|
details.remove();
|
||
|
this.details = null;
|
||
|
}
|
||
|
});
|
||
|
list.append(item);
|
||
|
}
|
||
|
container.append(list);
|
||
|
}
|
||
|
root.append(container);
|
||
|
}
|
||
|
root.classList.add('slashCommandBrowser');
|
||
|
}
|
||
|
}
|
||
|
parent.append(this.dom);
|
||
|
|
||
|
this.mo = new MutationObserver(muts=>{
|
||
|
if (muts.find(mut=>Array.from(mut.removedNodes).find(it=>it == this.dom || it.contains(this.dom)))) {
|
||
|
this.mo.disconnect();
|
||
|
window.removeEventListener('keydown', boundHandler);
|
||
|
}
|
||
|
});
|
||
|
this.mo.observe(document.querySelector('#chat'), { childList:true, subtree:true });
|
||
|
const boundHandler = this.handleKeyDown.bind(this);
|
||
|
window.addEventListener('keydown', boundHandler);
|
||
|
return this.dom;
|
||
|
}
|
||
|
|
||
|
handleKeyDown(evt) {
|
||
|
if (!evt.shiftKey && !evt.altKey && evt.ctrlKey && evt.key.toLowerCase() == 'f') {
|
||
|
if (!this.dom.closest('body')) return;
|
||
|
if (this.dom.closest('.mes') && !this.dom.closest('.last_mes')) return;
|
||
|
evt.preventDefault();
|
||
|
evt.stopPropagation();
|
||
|
evt.stopImmediatePropagation();
|
||
|
this.search.focus();
|
||
|
}
|
||
|
}
|
||
|
}
|