mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-11 04:52:09 +10:00
feat: finish big picture navigation
This commit is contained in:
@ -43,9 +43,7 @@ router.beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const rootNode = ref<HTMLElement>();
|
const rootNode = ref<HTMLElement>();
|
||||||
onMounted(() => {
|
const navigator = createTVNavigator(rootNode);
|
||||||
const navigator = createTVNavigator(rootNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
setupHooks();
|
setupHooks();
|
||||||
initialNavigation(state);
|
initialNavigation(state);
|
||||||
|
|||||||
@ -2,9 +2,7 @@
|
|||||||
<div class="h-16 bg-zinc-950 flex flex-row justify-between">
|
<div class="h-16 bg-zinc-950 flex flex-row justify-between">
|
||||||
<div class="flex flex-row grow items-center pl-5 pr-2 py-3">
|
<div class="flex flex-row grow items-center pl-5 pr-2 py-3">
|
||||||
<div class="inline-flex items-center gap-x-10">
|
<div class="inline-flex items-center gap-x-10">
|
||||||
<NuxtLink to="/store">
|
<Wordmark class="h-8 mb-0.5" />
|
||||||
<Wordmark class="h-8 mb-0.5" />
|
|
||||||
</NuxtLink>
|
|
||||||
<nav class="inline-flex items-center mt-0.5">
|
<nav class="inline-flex items-center mt-0.5">
|
||||||
<ol class="inline-flex items-center gap-x-6">
|
<ol class="inline-flex items-center gap-x-6">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
@ -42,7 +40,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WindowControl />
|
<WindowControl />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,11 @@ interface NavigationJump {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Position = [number, number, number, number];
|
||||||
|
|
||||||
class TVModeNavigator {
|
class TVModeNavigator {
|
||||||
private rootNode: Ref<HTMLElement | undefined>;
|
private rootNode: Ref<HTMLElement | undefined>;
|
||||||
private navigationNodes: Map<string, Array<Element>> = new Map();
|
private navigationNodes: Map<string, Array<HTMLElement>> = new Map();
|
||||||
|
|
||||||
constructor(rootNode: Ref<HTMLElement | undefined>) {
|
constructor(rootNode: Ref<HTMLElement | undefined>) {
|
||||||
this.rootNode = rootNode;
|
this.rootNode = rootNode;
|
||||||
@ -29,9 +31,110 @@ class TVModeNavigator {
|
|||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (ev) => {
|
||||||
|
switch (ev.code) {
|
||||||
|
case "KeyW":
|
||||||
|
return this.moveUp();
|
||||||
|
case "KeyS":
|
||||||
|
return this.moveDown();
|
||||||
|
case "KeyD":
|
||||||
|
return this.moveRight();
|
||||||
|
case "KeyA":
|
||||||
|
return this.moveLeft();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
recursivelyFindInteractable(element: Element): Array<Element> {
|
private getCurrentPosition(element?: HTMLElement): Position {
|
||||||
|
const el = element || document.activeElement;
|
||||||
|
if (!el) throw "No active position";
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return [rect.left, rect.right, rect.top, rect.bottom];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSamePosition(a: Position, b: Position) {
|
||||||
|
return a.map((v, i) => v === b[i]).every((v) => v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findElementWithPredicate(
|
||||||
|
distanceCalculator: (current: Position, target: Position) => number,
|
||||||
|
check: (current: Position, target: Position) => boolean
|
||||||
|
) {
|
||||||
|
const current = this.getCurrentPosition();
|
||||||
|
// We want things in the x direction, with a limit on the y
|
||||||
|
let distance = Math.max(window.innerWidth, window.innerHeight);
|
||||||
|
let element = null;
|
||||||
|
for (const newElement of this.navigationNodes.values().toArray().flat()) {
|
||||||
|
const target = this.getCurrentPosition(newElement);
|
||||||
|
if(this.isSamePosition(current, target)) continue;
|
||||||
|
const newDistance = distanceCalculator(current, target);
|
||||||
|
// If we're the wrong way, or further than the current option
|
||||||
|
if (newDistance < 0 || newDistance > distance) continue;
|
||||||
|
if (check(current, target)) continue;
|
||||||
|
distance = newDistance;
|
||||||
|
element = newElement;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveUp() {
|
||||||
|
const leeway = 20; // 20px
|
||||||
|
const element = this.findElementWithPredicate(
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
ytop - ebottom,
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
xleft - leeway > eright && xright + leeway < eleft
|
||||||
|
);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveDown() {
|
||||||
|
const leeway = 20; // 20px
|
||||||
|
const element = this.findElementWithPredicate(
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
etop - ybottom,
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
xleft - leeway > eright && xright + leeway < eleft
|
||||||
|
);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveRight() {
|
||||||
|
const leeway = 20; // 20px
|
||||||
|
const element = this.findElementWithPredicate(
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
eleft - xright,
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
etop - leeway > ytop && ebottom + leeway < etop
|
||||||
|
);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLeft() {
|
||||||
|
const leeway = 20; // 20px
|
||||||
|
const element = this.findElementWithPredicate(
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
xleft - eright,
|
||||||
|
([xleft, xright, ytop, ybottom], [eleft, eright, etop, ebottom]) =>
|
||||||
|
etop - leeway > ytop && ebottom + leeway < etop
|
||||||
|
);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recursivelyFindInteractable(element: Element): Array<HTMLElement> {
|
||||||
const elements = [];
|
const elements = [];
|
||||||
for (const child of element.children) {
|
for (const child of element.children) {
|
||||||
if (!child) continue;
|
if (!child) continue;
|
||||||
@ -43,6 +146,10 @@ class TVModeNavigator {
|
|||||||
elements.push(child);
|
elements.push(child);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if(child instanceof HTMLInputElement) {
|
||||||
|
elements.push(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Save ourselves a function call
|
// Save ourselves a function call
|
||||||
if (child.children.length > 0) {
|
if (child.children.length > 0) {
|
||||||
@ -124,49 +231,12 @@ class TVModeNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const interactiveElements = this.navigationNodes.values().toArray().flat();
|
const interactiveElements = this.navigationNodes.values().toArray().flat();
|
||||||
for (const element of interactiveElements) {
|
|
||||||
const currentId = self.getInteractionId(element);
|
|
||||||
const directionJumps: Map<Direction, NavigationJump> = new Map();
|
|
||||||
for (const direction of Directions) {
|
|
||||||
const jump = self.getNavJump(element, direction);
|
|
||||||
if (jump) {
|
|
||||||
directionJumps.set(direction, jump);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerX = element.clientLeft;
|
// Set focus so we aren't confused
|
||||||
const upperX = element.clientLeft + element.clientWidth;
|
if (!document.activeElement || document.activeElement.tagName === "BODY") {
|
||||||
const lowerY = element.clientTop;
|
const active = interactiveElements.at(0);
|
||||||
const upperY = element.clientTop + element.clientHeight;
|
if (active) {
|
||||||
|
active.focus();
|
||||||
for (const otherElement of interactiveElements) {
|
|
||||||
const otherId = self.getInteractionId(otherElement);
|
|
||||||
if (otherId == currentId) continue; // Skip us
|
|
||||||
|
|
||||||
const otherLowerX = otherElement.clientLeft;
|
|
||||||
const otherUpperX = otherElement.clientLeft + otherElement.clientWidth;
|
|
||||||
const otherLowerY = otherElement.clientTop;
|
|
||||||
const otherUpperY = otherElement.clientTop + otherElement.clientHeight;
|
|
||||||
|
|
||||||
for (const direction of Directions) {
|
|
||||||
let jump;
|
|
||||||
switch (direction) {
|
|
||||||
case "down":
|
|
||||||
const leeway =
|
|
||||||
0.1 * (element.clientWidth + otherElement.clientWidth);
|
|
||||||
if (
|
|
||||||
lowerX - leeway < otherUpperX ||
|
|
||||||
upperX + leeway > otherLowerX
|
|
||||||
)
|
|
||||||
break;
|
|
||||||
jump = {
|
|
||||||
id: otherId,
|
|
||||||
distance: upperY - otherUpperY,
|
|
||||||
} satisfies NavigationJump;
|
|
||||||
console.log(jump);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,5 +17,9 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
app: {
|
app: {
|
||||||
baseURL: "/main",
|
baseURL: "/main",
|
||||||
}
|
},
|
||||||
|
|
||||||
|
devtools: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user