feat(palette): press any button, link or field on the page by name
This commit is contained in:
1 parent
a1e7489ad4
commit
0ace2919aa
1 file changed
+69
-1
@@ -17,9 +17,75 @@
|
||||
run: () => void;
|
||||
}
|
||||
|
||||
const CLICKABLE =
|
||||
"a[href], button, summary, select, textarea, input:not([type='hidden']), [role='button'], [role='tab'], [role='link'], [role='menuitem']";
|
||||
const FIELD =
|
||||
"select, textarea, input:not([type='checkbox'], [type='radio'])";
|
||||
const SKIP = "tbody, [aria-hidden='true'], [inert]";
|
||||
const MAX_LABEL = 80;
|
||||
const WHITESPACE = /\s+/g;
|
||||
|
||||
let dialog = $state<HTMLDialogElement>();
|
||||
let input = $state<HTMLInputElement>();
|
||||
let cursor = $state(0);
|
||||
let pageEntries = $state.raw<Entry[]>([]);
|
||||
|
||||
function labelOf(element: HTMLElement): string {
|
||||
const field = element as HTMLInputElement;
|
||||
const text =
|
||||
element.getAttribute("aria-label") ||
|
||||
field.labels?.[0]?.innerText ||
|
||||
element.innerText ||
|
||||
element.getAttribute("title") ||
|
||||
field.placeholder ||
|
||||
"";
|
||||
return text.replace(WHITESPACE, " ").trim().slice(0, MAX_LABEL);
|
||||
}
|
||||
|
||||
function press(element: HTMLElement, field: boolean) {
|
||||
requestAnimationFrame(() => {
|
||||
element.focus();
|
||||
if (!field) {
|
||||
element.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scanPage(): Entry[] {
|
||||
const seen = new Set<string>();
|
||||
const found: Entry[] = [];
|
||||
const elements =
|
||||
document
|
||||
.querySelector("main")
|
||||
?.querySelectorAll<HTMLElement>(CLICKABLE) ?? [];
|
||||
for (const element of elements) {
|
||||
const hidden =
|
||||
element.closest(SKIP) !== null ||
|
||||
element.matches(":disabled") ||
|
||||
element.getClientRects().length === 0;
|
||||
const label = hidden ? "" : labelOf(element);
|
||||
const id = `${label}|${element.getAttribute("href") ?? ""}`;
|
||||
if (!label || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
const field = element.matches(FIELD);
|
||||
let hint = "button";
|
||||
if (field) {
|
||||
hint = "field";
|
||||
} else if (element.matches("a")) {
|
||||
hint = "link";
|
||||
}
|
||||
found.push({
|
||||
group: "On this page",
|
||||
hint,
|
||||
id: `dom:${found.length}`,
|
||||
label,
|
||||
run: () => press(element, field),
|
||||
});
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
const accounts = createQuery(() => ({
|
||||
...lists.accounts("all"),
|
||||
@@ -149,6 +215,7 @@
|
||||
]);
|
||||
|
||||
const entries = $derived([
|
||||
...pageEntries,
|
||||
...staticEntries,
|
||||
...commandEntries,
|
||||
...entityEntries,
|
||||
@@ -175,6 +242,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (ui.palette && dialog && !dialog.open) {
|
||||
pageEntries = scanPage();
|
||||
cursor = 0;
|
||||
dialog.showModal();
|
||||
input?.select();
|
||||
@@ -244,7 +312,7 @@
|
||||
aria-expanded="true"
|
||||
class="h-12 flex-1 bg-transparent text-[15px] outline-none placeholder:text-cladd-fg-softer"
|
||||
{onkeydown}
|
||||
placeholder="Go to page, account, budget, category, tag, or run a command"
|
||||
placeholder="Press anything on this page, go to a page, account, budget, tag, or run a command"
|
||||
role="combobox"
|
||||
bind:this={input}
|
||||
bind:value={ui.paletteQuery}
|
||||
|
||||
Reference in new issue
Block a user