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;
|
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 dialog = $state<HTMLDialogElement>();
|
||||||
let input = $state<HTMLInputElement>();
|
let input = $state<HTMLInputElement>();
|
||||||
let cursor = $state(0);
|
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(() => ({
|
const accounts = createQuery(() => ({
|
||||||
...lists.accounts("all"),
|
...lists.accounts("all"),
|
||||||
@@ -149,6 +215,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const entries = $derived([
|
const entries = $derived([
|
||||||
|
...pageEntries,
|
||||||
...staticEntries,
|
...staticEntries,
|
||||||
...commandEntries,
|
...commandEntries,
|
||||||
...entityEntries,
|
...entityEntries,
|
||||||
@@ -175,6 +242,7 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (ui.palette && dialog && !dialog.open) {
|
if (ui.palette && dialog && !dialog.open) {
|
||||||
|
pageEntries = scanPage();
|
||||||
cursor = 0;
|
cursor = 0;
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
input?.select();
|
input?.select();
|
||||||
@@ -244,7 +312,7 @@
|
|||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
class="h-12 flex-1 bg-transparent text-[15px] outline-none placeholder:text-cladd-fg-softer"
|
class="h-12 flex-1 bg-transparent text-[15px] outline-none placeholder:text-cladd-fg-softer"
|
||||||
{onkeydown}
|
{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"
|
role="combobox"
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
bind:value={ui.paletteQuery}
|
bind:value={ui.paletteQuery}
|
||||||
|
|||||||
Reference in new issue
Block a user