add a vim-like command line under :
This commit is contained in:
parent
0ce7f50285
commit
9cac6c3c3e
9 changed files with 332 additions and 69 deletions
174
static/js/command-line.js
Normal file
174
static/js/command-line.js
Normal file
|
@ -0,0 +1,174 @@
|
|||
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", () => {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
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 && fuzzyMatch(search, name),
|
||||
);
|
||||
suggestions.sort();
|
||||
|
||||
this.suggestions.replaceChildren();
|
||||
for (let [name, def] of suggestions) {
|
||||
let suggestion = this.suggestions.appendChild(document.createElement("li"));
|
||||
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", () => {
|
||||
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, run }) {
|
||||
for (let i = 0; i < aliases.length; ++i) {
|
||||
CommandLine.commands.set(aliases[i], {
|
||||
isAlias: i != 0,
|
||||
description,
|
||||
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?"',
|
||||
run() {
|
||||
window.location = `${TREEHOUSE_SITE}/treehouse/cmd`;
|
||||
},
|
||||
});
|
|
@ -1,10 +1,11 @@
|
|||
import { CommandLine } from "treehouse/command-line.js";
|
||||
|
||||
class PictureUpload extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.tabIndex = 0;
|
||||
this.gotoInit();
|
||||
|
||||
this.preview = this.querySelector("img[name='preview']");
|
||||
|
@ -30,16 +31,17 @@ class PictureUpload extends HTMLElement {
|
|||
gotoInit() {
|
||||
this.setState("init");
|
||||
this.innerHTML = `
|
||||
<div class="nothing-pasted">
|
||||
<div class="nothing-pasted" tabindex="0">
|
||||
paste or drop an image here to make a picture out of it
|
||||
</div>
|
||||
`;
|
||||
this.querySelector(".nothing-pasted").focus();
|
||||
}
|
||||
|
||||
async gotoHavePicture(imageType, imageFile) {
|
||||
this.setState("have-picture");
|
||||
this.innerHTML = `
|
||||
<div class="have-picture">
|
||||
<form name="upload" class="have-picture">
|
||||
<img name="preview" class="pic" alt="preview">
|
||||
<p>
|
||||
<span name="preview-width"></span> × <span name="preview-height"></span> px (<span name="file-size"></span>)
|
||||
|
@ -57,19 +59,20 @@ class PictureUpload extends HTMLElement {
|
|||
</select>
|
||||
</p>
|
||||
|
||||
<button name="upload">upload</button>
|
||||
<button type="submit" name="upload">upload</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let uploadForm = this.querySelector("form[name='upload']");
|
||||
let preview = this.querySelector("img[name='preview']");
|
||||
let previewWidth = this.querySelector("[name='preview-width']");
|
||||
let previewHeight = this.querySelector("[name='preview-height']");
|
||||
let fileSize = this.querySelector("[name='file-size']");
|
||||
let label = this.querySelector("[name='label']");
|
||||
let compression = this.querySelector("[name='compression']");
|
||||
let upload = this.querySelector("button[name='upload']");
|
||||
|
||||
fileSize.textContent = formatSizeSI(imageFile.size);
|
||||
label.focus();
|
||||
|
||||
let url = URL.createObjectURL(imageFile);
|
||||
preview.src = url;
|
||||
|
@ -80,7 +83,9 @@ class PictureUpload extends HTMLElement {
|
|||
previewHeight.textContent = bitmap.height.toString();
|
||||
});
|
||||
|
||||
upload.addEventListener("click", async () => {
|
||||
uploadForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
let params = new URLSearchParams({
|
||||
label: label.value,
|
||||
format: imageType,
|
||||
|
@ -147,3 +152,21 @@ function formatSizeSI(bytes) {
|
|||
unitDisplay: "narrow",
|
||||
}).format(bytes);
|
||||
}
|
||||
|
||||
if (TREEHOUSE_DEV) {
|
||||
CommandLine.registerCommand({
|
||||
aliases: ["addpic"],
|
||||
description: "add a picture interactively and copy its ulid",
|
||||
run() {
|
||||
let dialog = document.body.appendChild(document.createElement("dialog"));
|
||||
dialog.addEventListener("keydown", (event) => {
|
||||
if (event.key == "Escape") dialog.close();
|
||||
});
|
||||
dialog.addEventListener("close", () => {
|
||||
dialog.remove();
|
||||
});
|
||||
dialog.appendChild(new PictureUpload());
|
||||
dialog.show();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue