Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions frontend/__tests__/utils/numbers.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,4 +46,33 @@ describe("numbers", () => {
expect(Numbers.abbreviateNumber((number *= 1000))).toEqual("1.0d");
});
});
describe("parseIntOptional", () => {
it("should return a number when given a valid string", () => {
expect(Numbers.parseIntOptional("123")).toBe(123);
expect(Numbers.parseIntOptional("42")).toBe(42);
expect(Numbers.parseIntOptional("0")).toBe(0);
});

it("should return undefined when given null", () => {
expect(Numbers.parseIntOptional(null)).toBeUndefined();
});

it("should return undefined when given undefined", () => {
expect(Numbers.parseIntOptional(undefined)).toBeUndefined();
});

it("should handle non-numeric strings", () => {
expect(Numbers.parseIntOptional("abc")).toBeNaN();
expect(Numbers.parseIntOptional("12abc")).toBe(12); // parseInt stops at non-numeric chars
});

it("should handle leading and trailing spaces", () => {
expect(Numbers.parseIntOptional(" 42 ")).toBe(42);
});
it("should return a number when given a valid string and radix", () => {
expect(Numbers.parseIntOptional("1010", 2)).toBe(10);
expect(Numbers.parseIntOptional("CF", 16)).toBe(207);
expect(Numbers.parseIntOptional("C", 26)).toBe(12);
});
});
});
123 changes: 72 additions & 51 deletions frontend/src/ts/commandline/commandline.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,9 @@ import * as ActivePage from "../states/active-page";
import { focusWords } from "../test/test-ui";
import * as Loader from "../elements/loader";
import { Command, CommandsSubgroup } from "./types";
import { areSortedArraysEqual } from "../utils/arrays";
import { parseIntOptional } from "../utils/numbers";
import { debounce } from "throttle-debounce";

type CommandlineMode = "search" | "input";
type InputModeParams = {
Expand DownExpand Up@@ -325,6 +328,7 @@ function hideCommands(): void {
throw new Error("Commandline element not found");
}
element.innerHTML = "";
lastList = undefined;
}

let cachedSingleSubgroup: CommandsSubgroup | null = null;
Expand All@@ -349,18 +353,24 @@ async function getList(): Promise<Command[]> {
return (await getSubgroup()).list;
}

let lastList: Command[] | undefined;

async function showCommands(): Promise<void> {
const element = document.querySelector("#commandLine .suggestions");
if (element === null) {
throw new Error("Commandline element not found");
}

if (inputValue === "" && usingSingleList) {
element.innerHTML = "";
hideCommands();
return;
}

const list = (await getList()).filter((c) => c.found === true);
if (lastList && areSortedArraysEqual(list, lastList)) {
Comment thread
fehmer marked this conversation as resolved.
return;
}
lastList = list;

let html = "";
let index = 0;
Expand DownExpand Up@@ -458,28 +468,8 @@ async function showCommands(): Promise<void> {
if (firstActive !== null && !usingSingleList) {
activeIndex = firstActive;
}
element.innerHTML = html;

for (const command of element.querySelectorAll(".command")) {
Comment thread
fehmer marked this conversation as resolved.
command.addEventListener("mouseenter", async () => {
if (!mouseMode) return;
activeIndex = parseInt(command.getAttribute("data-index") ?? "0");
await updateActiveCommand();
});
command.addEventListener("mouseleave", async () => {
if (!mouseMode) return;
activeIndex = parseInt(command.getAttribute("data-index") ?? "0");
await updateActiveCommand();
});
command.addEventListener("click", async () => {
const previous = activeIndex;
activeIndex = parseInt(command.getAttribute("data-index") ?? "0");
if (previous !== activeIndex) {
await updateActiveCommand();
}
await runActiveCommand();
});
}
element.innerHTML = html;
}

async function updateActiveCommand(): Promise<void> {
Expand DownExpand Up@@ -573,23 +563,20 @@ async function runActiveCommand(): Promise<void> {
}
}

let lastActiveIndex: string | undefined;
function keepActiveCommandInView(): void {
if (mouseMode) return;
try {
const scroll =
Math.abs(
($(".suggestions").offset()?.top as number) -
($(".command.active").offset()?.top as number) -
($(".suggestions").scrollTop() as number)
) -
($(".suggestions").outerHeight() as number) / 2 +
($($(".command")[0] as HTMLElement).outerHeight() as number);
$(".suggestions").scrollTop(scroll);
} catch (e) {
if (e instanceof Error) {
console.log("could not scroll suggestions: " + e.message);
}

const active: HTMLElement | null = document.querySelector(
".suggestions .command.active"
);

if (active === null || active.dataset["index"] === lastActiveIndex) {
Comment thread
fehmer marked this conversation as resolved.
return;
}

active.scrollIntoView({ behavior: "auto", block: "center" });
lastActiveIndex = active.dataset["index"];
}

function updateInput(setInput?: string): void {
Expand DownExpand Up@@ -665,22 +652,25 @@ const modal = new AnimatedModal({
setup: async (modalEl): Promise<void> => {
const input = modalEl.querySelector("input") as HTMLInputElement;

input.addEventListener("input", async (e) => {
inputValue = (e.target as HTMLInputElement).value;
if (subgroupOverride === null) {
if (Config.singleListCommandLine === "on") {
usingSingleList = true;
} else {
usingSingleList = inputValue.startsWith(">");
input.addEventListener(
"input",
debounce(50, async (e) => {
inputValue = (e.target as HTMLInputElement).value;
if (subgroupOverride === null) {
if (Config.singleListCommandLine === "on") {
usingSingleList = true;
} else {
usingSingleList = inputValue.startsWith(">");
}
}
}
if (mode !== "search") return;
mouseMode = false;
activeIndex = 0;
await filterSubgroup();
await showCommands();
await updateActiveCommand();
});
if (mode !== "search") return;
mouseMode = false;
activeIndex = 0;
await filterSubgroup();
await showCommands();
await updateActiveCommand();
})
);

input.addEventListener("keydown", async (e) => {
mouseMode = false;
Expand DownExpand Up@@ -740,5 +730,36 @@ const modal = new AnimatedModal({
modalEl.addEventListener("mousemove", (_e) => {
mouseMode = true;
});

const suggestions = document.querySelector(".suggestions") as HTMLElement;
let lastHover: HTMLElement | undefined;

suggestions.addEventListener("mousemove", async (e) => {
const target = e.target as HTMLElement | null;
if (target === lastHover) return;

const dataIndex = parseIntOptional(target?.getAttribute("data-index"));

if (!dataIndex) return;

lastHover = e.target as HTMLElement;
activeIndex = dataIndex;
await updateActiveCommand();
});

suggestions.addEventListener("click", async (e) => {
const target = e.target as HTMLElement | null;

const dataIndex = parseIntOptional(target?.getAttribute("data-index"));

if (!dataIndex) return;

const previous = activeIndex;
activeIndex = dataIndex;
if (previous !== activeIndex) {
await updateActiveCommand();
}
await runActiveCommand();
});
},
});
16 changes: 16 additions & 0 deletions frontend/src/ts/utils/numbers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,3 +133,19 @@ export function findLineByLeastSquares(
];
return [returnpoint1, returnpoint2];
}

/**
* Parses a string into an integer if it is not null or undefined, otherwise returns undefined.
*
* @param The string to parse or null or undefined.
* @param radix A value between 2 and 36 that specifies the base of the number in `string`.
* @returns A number if a string is provided, otherwise undefined.
*/
export function parseIntOptional<T extends string | null | undefined>(
value: T,
radix: number = 10
): T extends string ? number : undefined {
return (
value !== null && value !== undefined ? parseInt(value, radix) : undefined
) as T extends string ? number : undefined;
}