226 lines
6.4 KiB
JavaScript
226 lines
6.4 KiB
JavaScript
export class CommandLine extends HTMLElement {
|
|
static commands = new Map();
|
|
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.suggestions = this.appendChild(document.createElement("ul"));
|
|
this.suggestions.classList.add("suggestions");
|
|
|
|
let inputWrapper = this.appendChild(document.createElement("div"));
|
|
inputWrapper.classList.add("input-wrapper");
|
|
this.input = inputWrapper.appendChild(document.createElement("input"));
|
|
this.input.type = "text";
|
|
|
|
window.addEventListener("keydown", (event) => {
|
|
if (event.key == ":" && !this.visible) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
this.show();
|
|
}
|
|
|
|
if (event.key == "Escape") {
|
|
this.hide();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("click", (event) => {
|
|
if (!this.contains(event.target)) {
|
|
this.hide();
|
|
}
|
|
});
|
|
|
|
this.input.addEventListener("keydown", (event) => {
|
|
if (event.key == "Enter") {
|
|
event.preventDefault();
|
|
this.hide();
|
|
this.runCommand(this.input.value);
|
|
}
|
|
|
|
if (event.key == "Tab") {
|
|
event.preventDefault();
|
|
if (event.shiftKey) this.tabToPreviousSuggestion();
|
|
else this.tabToNextSuggestion();
|
|
}
|
|
});
|
|
|
|
this.input.addEventListener("input", () => {
|
|
this.updateSuggestions();
|
|
});
|
|
}
|
|
|
|
get visible() {
|
|
return this.classList.contains("visible");
|
|
}
|
|
|
|
show() {
|
|
this.classList.add("visible");
|
|
this.input.focus();
|
|
this.input.value = "";
|
|
this.updateSuggestions();
|
|
}
|
|
|
|
hide() {
|
|
this.classList.remove("visible");
|
|
}
|
|
|
|
tab(current, next) {
|
|
current?.classList?.remove("tabbed");
|
|
next.classList.add("tabbed");
|
|
next.scrollIntoView();
|
|
|
|
this.input.value = next.name;
|
|
// NOTE: Do NOT update suggestions here.
|
|
// This would cause the tabbing to break.
|
|
}
|
|
|
|
tabToNextSuggestion() {
|
|
let current = this.suggestions.querySelector(".tabbed");
|
|
let next = current?.nextSibling ?? this.suggestions.childNodes[0];
|
|
this.tab(current, next);
|
|
}
|
|
|
|
tabToPreviousSuggestion() {
|
|
let current = this.suggestions.querySelector(".tabbed");
|
|
let previous =
|
|
current?.previousSibling ??
|
|
this.suggestions.childNodes[this.suggestions.childNodes.length - 1];
|
|
this.tab(current, previous);
|
|
}
|
|
|
|
updateSuggestions() {
|
|
let search = parseCommand(this.input.value)?.command ?? "";
|
|
let suggestions = Array.from(CommandLine.commands.entries()).filter(
|
|
([name, def]) =>
|
|
!def.isAlias &&
|
|
(def.showInSuggestions?.(this.input.value) ?? true) &&
|
|
fuzzyMatch(search, name),
|
|
);
|
|
suggestions.sort();
|
|
|
|
this.suggestions.replaceChildren();
|
|
for (let [name, def] of suggestions) {
|
|
let suggestion = this.suggestions.appendChild(document.createElement("li"));
|
|
if (def.immediate) {
|
|
suggestion.classList.add("immediate");
|
|
}
|
|
|
|
let commandName = suggestion.appendChild(document.createElement("dfn"));
|
|
commandName.textContent = name;
|
|
let commandDescription = suggestion.appendChild(document.createElement("span"));
|
|
commandDescription.classList.add("description");
|
|
commandDescription.textContent = def.description;
|
|
|
|
suggestion.name = name;
|
|
suggestion.def = def;
|
|
|
|
suggestion.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
|
|
if (def.immediate) {
|
|
this.hide();
|
|
this.runCommand(name);
|
|
} else {
|
|
this.input.value = name + " ";
|
|
this.updateSuggestions();
|
|
this.input.focus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
runCommand(commandLine) {
|
|
let { command, args } = parseCommand(commandLine);
|
|
let commandDef = CommandLine.commands.get(command);
|
|
if (CommandLine.commands.has(command)) {
|
|
commandDef.run(args);
|
|
} else {
|
|
console.log(`unknown command`);
|
|
}
|
|
}
|
|
|
|
static registerCommand({ aliases, description, immediate, showInSuggestions, run }) {
|
|
for (let i = 0; i < aliases.length; ++i) {
|
|
CommandLine.commands.set(aliases[i], {
|
|
isAlias: i != 0,
|
|
description,
|
|
immediate,
|
|
showInSuggestions,
|
|
run,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define("th-command-line", CommandLine);
|
|
|
|
function parseCommand(commandLine) {
|
|
let result = /^([^ ]+) *(.*)$/.exec(commandLine);
|
|
if (result == null) return null;
|
|
|
|
let [_, command, args] = result;
|
|
return { command, args };
|
|
}
|
|
|
|
// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/
|
|
function fuzzyMatch(pattern, string) {
|
|
let iPattern = 0;
|
|
let iString = 0;
|
|
|
|
while (iPattern < pattern.length && iString < string.length) {
|
|
if (pattern.charAt(iPattern).toLowerCase() == string.charAt(iString).toLowerCase()) {
|
|
iPattern += 1;
|
|
}
|
|
|
|
iString += 1;
|
|
}
|
|
|
|
return iPattern == pattern.length;
|
|
}
|
|
|
|
CommandLine.registerCommand({
|
|
aliases: ["help", "h"],
|
|
description: '"OwO, what is this?"',
|
|
immediate: true,
|
|
|
|
run() {
|
|
window.location = `${TREEHOUSE_SITE}/treehouse/cmd`;
|
|
},
|
|
});
|
|
|
|
CommandLine.registerCommand({
|
|
aliases: ["new", "n"],
|
|
description: "go to news feed",
|
|
immediate: true,
|
|
|
|
run() {
|
|
window.location = `${TREEHOUSE_SITE}/treehouse/new`;
|
|
},
|
|
});
|
|
|
|
CommandLine.registerCommand({
|
|
aliases: ["index", "i", "-w-"],
|
|
description: "go home",
|
|
immediate: true,
|
|
|
|
run() {
|
|
window.location = `${TREEHOUSE_SITE}/`;
|
|
},
|
|
});
|
|
|
|
CommandLine.registerCommand({
|
|
aliases: ["quit", "exit", "q", "q!", "wq", "wq!", "wqa", "wqa!", "bc", "bc!", "bca", "bca!"],
|
|
description: "quit liquidex's treehouse (congration!)",
|
|
// non-immediate because this is a destructive action
|
|
|
|
showInSuggestions(commandLine) {
|
|
return commandLine.length >= 1;
|
|
},
|
|
|
|
run() {
|
|
window.location = `${TREEHOUSE_SITE}/treehouse/quit`;
|
|
},
|
|
});
|