aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/css/launcher.css9
-rw-r--r--src/app/css/selection.css19
-rw-r--r--src/app/css/theming.css2
-rw-r--r--src/app/index.html8
-rw-r--r--src/app/js/gamepad.js301
-rw-r--r--src/app/js/navigate.js688
-rw-r--r--src/app/js/tooltip.js2
-rw-r--r--src/app/main.css1
-rw-r--r--src/app/main.js3
9 files changed, 1024 insertions, 9 deletions
diff --git a/src/app/css/launcher.css b/src/app/css/launcher.css
index c43bce1..694acf1 100644
--- a/src/app/css/launcher.css
+++ b/src/app/css/launcher.css
@@ -111,7 +111,8 @@
.contentMenu li:last-child {margin-right: 0px}
.contentMenu li:first-child {margin-left: 0px}
-.contentMenu li:hover {opacity: 0.7}
+.contentMenu li:hover,
+.contentMenu li.active-selection {opacity: 0.7}
.contentMenu li[active] {
opacity: 1.0;
@@ -305,7 +306,8 @@ button:has(img):has(span) img {
transition: opacity 0.2s ease-in-out;
}
-button:has(img):has(span):hover img {
+button:has(img):has(span):hover img,
+button:has(img):has(span).active-selection img {
opacity: 1.0;
}
@@ -315,7 +317,8 @@ button:has(img):has(span) span {
transition: right 0.2s ease-in-out;
}
-button:has(img):has(span):hover span {
+button:has(img):has(span):hover span,
+button:has(img):has(span).active-selection span {
right: 0.1em;
}
diff --git a/src/app/css/selection.css b/src/app/css/selection.css
new file mode 100644
index 0000000..04e1d5d
--- /dev/null
+++ b/src/app/css/selection.css
@@ -0,0 +1,19 @@
+#selection {
+ z-index: 99999999999999;
+
+ transition: 0.15s ease-in-out;
+ transition-property:
+ opacity, border-radius,
+ transform, width, height, top, left;
+
+ position: fixed;
+ pointer-events: none;
+ transform: scale(1.0);
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: calc(var(--padding) / 2);
+}
+
+#selection.keyboard-selecting,
+#selection.controller-selecting {
+ transform: scale(1.1);
+}
diff --git a/src/app/css/theming.css b/src/app/css/theming.css
index fb8fbf7..c0b6497 100644
--- a/src/app/css/theming.css
+++ b/src/app/css/theming.css
@@ -42,7 +42,7 @@ a.disabled:not("[onclick='kill('game')']") {
pointer-events: none;
}
-a:hover {
+a:hover, a.active-selection {
filter: brightness(80%) !important;
}
diff --git a/src/app/index.html b/src/app/index.html
index ffc13de..40b64a0 100644
--- a/src/app/index.html
+++ b/src/app/index.html
@@ -6,6 +6,8 @@
<meta charset="utf-8">
</head>
<body>
+ <div id="selection"></div>
+
<div id="tooltip">Test</div>
<div id="bgHolder"></div>
@@ -33,7 +35,7 @@
<div id="overlay" onclick="popups.set_all(false)"></div>
<div class="popup" id="options">
<div class="misc">
- <input class="search" placeholder="%%gui.search%%">
+ <input class="search default-selection" placeholder="%%gui.search%%">
<button id="apply" onclick="settings.popup.apply();settings.popup.toggle(false)">
<img src="icons/apply.png">
%%gui.settings.save%%
@@ -204,7 +206,7 @@
</div>
<div class="misc">
- <input class="search" placeholder="%%gui.search%%">
+ <input class="search default-selection" placeholder="%%gui.search%%">
<button id="filter" onclick="browser.filters.toggle()">
<img src="icons/filter.png">
</button>
@@ -217,7 +219,7 @@
</div>
</div>
<div class="popup small blur" id="preview">
- <div class="misc fixed vertical">
+ <div class="misc fixed vertical default-selection">
<button id="close" onclick="browser.preview.hide()">
<img src="icons/close.png">
</button>
diff --git a/src/app/js/gamepad.js b/src/app/js/gamepad.js
new file mode 100644
index 0000000..abe3db5
--- /dev/null
+++ b/src/app/js/gamepad.js
@@ -0,0 +1,301 @@
+const navigate = require("./navigate");
+
+window.addEventListener("gamepadconnected", (e) => {
+ console.log("Gamepad connected:", e.gamepad.id);
+}, false)
+
+window.addEventListener("gamepaddisconnected", (e) => {
+ console.log("Gamepad disconnected:", e.gamepad.id);
+}, false)
+
+// this contains the names/directions of axes and IDs that have
+// previously been pressed, if it is found that these were recently
+// pressed in the next iteration of the `setInterval()` below than the
+// iteration is skipped
+//
+// the value of each item is equivalent to the amount of iterations to
+// wait, so `up: 3` will cause it to wait 3 iterations, before `up` can
+// be pressed again
+let delay_press = {};
+
+setInterval(() => {
+ let gamepads = navigator.getGamepads();
+
+ // this has a list of all the directions that the `.axes[]` are
+ // pointing in, letting us navigate in that direction
+ let directions = {}
+
+ // keeps track of which buttons `delay_press` that have already been
+ // lowered, that way we can lower the ones that haven't been lowered
+ // through a button press
+ let lowered_delay = [];
+
+ // is the select/accept button being held
+ let selecting = false;
+
+ for (let i in gamepads) {
+ if (! gamepads[i]) {continue}
+ // every other `.axes[]` element is a different coordinate, each
+ // analog stick has 2 elements in `.axes[]`, the first one is
+ // the x coordinate, second is the y coordinate
+ //
+ // so we use this to keep track of which coordinate we're
+ // currently on, and thereby the direction of the float inside
+ // `.axes[i]`
+ let coord = "x";
+ let deadzone = 0.5;
+
+ for (let ii = 0; ii < gamepads[i].axes.length; ii++) {
+ let value = gamepads[i].axes[ii];
+
+ // check if we're beyond the deadzone in both the negative
+ // and positive direction, and then using `coord` add a
+ // direction to `directions`
+ if (value < -deadzone) {
+ if (coord == "y") {
+ directions.up = true;
+ } else {
+ directions.left = true;
+ }
+ } else if (value > deadzone) {
+ if (coord == "y") {
+ directions.down = true;
+ } else {
+ directions.right = true;
+ }
+ }
+
+ // flip `coord`
+ if (coord == "x") {
+ coord = "y";
+ } else {
+ coord = "x";
+ }
+ }
+
+ // only support "standard" button layouts/mappings
+ //
+ // TODO: for anybody reading this in the future, the support
+ // for other mappings is something that's on the table,
+ // however, due to not having all the hardware in the world,
+ // this will have to be up to someone else
+ if (gamepads[i].mapping != "standard") {
+ continue;
+ }
+
+ for (let ii = 0; ii < gamepads[i].buttons.length; ii++) {
+ if (! gamepads[i].buttons[ii].pressed) {
+ continue;
+ }
+
+ // a list of known combinations of buttons for the most
+ // common brands out there, more should possibly be added
+ let brands = {
+ "Xbox": {
+ accept: 0,
+ cancel: 1
+ },
+ "Nintendo": {
+ accept: 1,
+ cancel: 0
+ },
+ "PlayStation": {
+ accept: 0,
+ cancel: 1
+ }
+ }
+
+ // this is the most common setup, to my understanding, with
+ // the exception of third party Nintendo controller, may
+ // need to be adjusted in the future
+ let buttons = {
+ accept: 0,
+ cancel: 1
+ }
+
+ // set `cancel` and `accept` accordingly to the ID of the
+ // gamepad, if its a known brand
+ for (let brand in brands) {
+ // unknown brand
+ if (! gamepads[i].id.includes(brand)) {
+ continue;
+ }
+
+ // set buttons according to brand
+ buttons = brands[brand];
+ break;
+ }
+
+ // if the button that's being pressed is the "accept"
+ // button, then we set `selecting` to `true`, this is done
+ // before we check for the button delay so that holding the
+ // button keeps the selection in place, until the button is
+ // no longer pressed
+ if (ii == buttons.accept) {
+ selecting = true;
+ }
+
+ // if this button is still delayed, we lower the delay and
+ // then go to the next button
+ if (delay_press[ii]) {
+ delay_press[ii]--;
+ lowered_delay.push(ii);
+ continue;
+ }
+
+ // add delay to this button, so it doesn't get clicked
+ // immediately again after this
+ delay_press[ii] = 3;
+
+ // interpret `ii` as a specific button/action, using the
+ // standard IDs: https://w3c.github.io/gamepad/#remapping
+ switch(ii) {
+ case 4: // switch tab (prev)
+ case 5: // switch tab (next)
+
+ case 12: navigate.move("up"); break;
+ case 13: navigate.move("down"); break;
+ case 14: navigate.move("left"); break;
+ case 15: navigate.move("right"); break;
+
+ case buttons.accept: navigate.select(); break;
+
+ case buttons.cancel: popups.set_all(false); break;
+ }
+ }
+ }
+
+ for (let i in directions) {
+ if (directions[i] === true) {
+ // if this direction is still delayed, we lower the delay,
+ // and then go to the next direction
+ if (delay_press[i]) {
+ delay_press[i]--;
+ lowered_delay.push(i);
+ continue;
+ }
+
+ // move in the direction
+ navigate.move(i);
+
+ // add delay to this direction, to prevent it from being
+ // triggered immediately again
+ delay_press[i] = 3;
+ }
+ }
+
+ // run through buttons that have or have had a delay
+ for (let i in delay_press) {
+ // if a button has a delay, and it hasn't already been lowered,
+ // then we lower it
+ if (delay_press[i] && ! lowered_delay.includes(i)) {
+ delay_press[i]--;
+ }
+ }
+
+ let selection_el = document.getElementById("selection");
+
+ // add `.selecting` to `#selection` depending on whether
+ // `selecting`, is set or not
+ if (selecting) {
+ selection_el.classList.add("controller-selecting");
+ } else {
+ selection_el.classList.remove("controller-selecting");
+ }
+}, 50)
+
+
+let can_keyboard_navigate = (e) => {
+ // quite empty right now, might add more in the future, these are
+ // just element selectors where movement with the keyboard is off
+ let ignore_on_focus = [
+ "input",
+ "select"
+ ]
+
+ // check for whether the active element is one that matches
+ // something in `ignore_on_focus`
+ for (let i = 0; i < ignore_on_focus.length; i++) {
+ if (! document.activeElement.matches(ignore_on_focus)) {
+ // active element does not match to `ignore_on_focus[i]`
+ continue;
+ }
+
+ // if the key that's being pressed is "Escape" then we unfocus
+ // to the currently focused active element, this lets you go
+ // into an input, and then exit it as well
+ if (e.key == "Escape") {
+ document.activeElement.blur();
+ }
+
+ return false;
+ }
+
+ // check if there's already an active selection
+ if (document.querySelector(".active-selection")) {
+ // this is a list of keys where this keyboard event will be
+ // cancelled on, this prevents key events from being sent to
+ // element, but still lets you type
+ let cancel_keys = [
+ "Space", "Enter",
+ "ArrowUp", "ArrowDown",
+ "ArrowLeft", "ArrowRight"
+ ]
+
+ // cancel this keyboard event if `e.key` is inside `cancel_keys`
+ if (cancel_keys.includes(e.code)) {
+ e.preventDefault();
+ }
+ }
+
+ return true;
+}
+
+window.addEventListener("keydown", (e) => {
+ // do nothing if we cant navigate
+ if (! can_keyboard_navigate(e)) {
+ return;
+ }
+
+ let select = () => {
+ // do nothing if this is a repeat key press
+ if (e.repeat) {return}
+
+ // select `.active-selection`
+ navigate.select();
+
+ // add `.keyboard-selecting` to `#selection`
+ document.getElementById("selection")
+ .classList.add("keyboard-selecting");
+ }
+
+ // perform the relevant action for the key that was pressed
+ switch(e.code) {
+ // select
+ case "Space": return select();
+ case "Enter": return select();
+
+ // move selection
+ case "ArrowUp": return navigate.move("up")
+ case "ArrowDown": return navigate.move("down")
+ case "ArrowLeft": return navigate.move("left")
+ case "ArrowRight": return navigate.move("right")
+ }
+})
+
+window.addEventListener("keyup", (e) => {
+ if (! can_keyboard_navigate(e)) {
+ return;
+ }
+
+ let selection_el = document.getElementById("selection");
+
+ // perform the relevant action for the key that was pressed
+ switch(e.code) {
+ case "Space": return selection_el
+ .classList.remove("keyboard-selecting");
+
+ case "Enter": return selection_el
+ .classList.remove("keyboard-selecting");
+ }
+})
diff --git a/src/app/js/navigate.js b/src/app/js/navigate.js
new file mode 100644
index 0000000..d9744fa
--- /dev/null
+++ b/src/app/js/navigate.js
@@ -0,0 +1,688 @@
+const events = require("./events");
+const popups = require("./popups");
+const settings = require("./settings");
+
+let navigate = {};
+
+// sets `#selection` to the correct position, size and border radius,
+// according to what is currently the `.active-selection`, if none is
+// found, it'll instead be hidden
+navigate.selection = () => {
+ // shorthands
+ let selection_el = document.getElementById("selection");
+ let active_el = document.querySelector(".active-selection");
+
+ // make sure there's an `active_el`, and hide the `selection_el` if
+ // that isn't the case
+ if (! active_el) {
+ selection_el.style.opacity = "0.0";
+ return;
+ }
+
+
+ // this adds space between the `selection_el` and ``
+ let padding = 8;
+
+
+ // attempt to get the border radius of `active_el`
+ let radius = getComputedStyle(active_el).borderRadius;
+
+ // if there's no radius set, we default to the default of
+ // `selection_el` through using `null`
+ if (! radius || radius == "0px") {
+ radius = null;
+ }
+
+ // set visibility and radius
+ selection_el.style.opacity = "1.0";
+ selection_el.style.borderRadius = radius;
+
+ // get bounds for position and size calculations of `selection_el`
+ let active_bounds = active_el.getBoundingClientRect();
+
+ // set top and left side coordinate subtracting the padding
+ selection_el.style.top = active_bounds.top - padding + "px";
+ selection_el.style.left = active_bounds.left - padding + "px";
+
+ // set width of `selection_el` with the padding
+ selection_el.style.width =
+ active_bounds.width + (padding * 2) + "px";
+
+ // set height of `selection_el` with the padding
+ selection_el.style.height =
+ active_bounds.height + (padding * 2) + "px";
+}
+
+// these events cause the `#selection` element to reposition itself
+window.addEventListener("resize", () => {navigate.selection()}, true);
+window.addEventListener("scroll", () => {navigate.selection()}, true);
+
+// listen for click events, and hide the `#selection` element, when
+// emitting a mouse event we will want to hide, as it then isn't needed
+window.addEventListener("click", (e) => {
+ // make sure its a trusted click event, and therefore actually a
+ // mouse, and not anything else
+ if (! e.isTrusted) {
+ return;
+ }
+
+ // get the `.active-selection`
+ let active_el = document.querySelector(".active-selection");
+
+ // if there's an `active_el` then we unselect it, and update the
+ // `#selection` element, hiding it
+ if (active_el) {
+ active_el.classList.remove("active-selection");
+ navigate.selection();
+ }
+})
+
+// returns a list of valid elements that should be possible to navigate
+// to/select with the `#selection` element
+//
+// setting `div` makes it limit itself to elements inside that, without
+// it, it'll use `document.body` or the active popup, if one is found
+navigate.get_els = (div) => {
+ let els = [];
+
+ // is `div` not set, and is there a popup shown
+ if (! div && document.body.querySelector(".popup.shown")) {
+ // the spread operator is to convert from a `NodeList` to an
+ // `Array`, and then we need to reverse this to get the ones
+ // that are layered on top first.
+ let popups_list = [...popups.list()].reverse();
+
+ // run through the list of popups
+ for (let i = 0; i < popups_list.length; i++) {
+ // if this popup is shown, we make it the current `div`
+ if (popups_list[i].classList.contains("shown")) {
+ div = popups_list[i];
+ break;
+ }
+ }
+
+ // get buttons inside `#winbtns`
+ els = [...document.body.querySelectorAll("#winbtns [onclick]")];
+ } if (! div) { // default
+ div = document.body;
+ }
+
+ // this gets the list of all the elements we should be able to
+ // select inside `div`, on top of anything that's already in `els`
+ els = [...els, ...div.querySelectorAll([
+ "a",
+ "input",
+ "button",
+ "select",
+ "textarea",
+ "[onclick]"
+ ])]
+
+ // this'll contain a filtered list of `els`
+ let new_els = [];
+
+ // filter out elements we don't care about
+ filter: for (let i = 0; i < els.length; i++) {
+ // elements that match on `els.closest()` with any of these will
+ // be stripped away, as we dont want them
+ let ignore_closest = [
+ "#overlay",
+ ".no-navigate",
+ "button.visual",
+ ".popup:not(.shown)"
+ ]
+
+ // check if `els[i].closest()` matches on any of the elements
+ // inside of `ignore_closest`
+ for (let ii = 0; ii < ignore_closest.length; ii++) {
+ if (els[i].closest(ignore_closest[ii])) {
+ // it matches
+ continue filter;
+ }
+ }
+
+ // make sure `els[i]` is visible on screen
+ let visible = els[i].checkVisibility({
+ checkOpacity: true,
+ visibilityProperty: true,
+ checkVisibilityCSS: true,
+ contentVisibilityAuto: true
+ })
+
+ // filter out if not visible
+ if (! visible) {continue}
+
+ // add to filtered list
+ new_els.push(els[i])
+ }
+
+ // return the filtered list of elements
+ return new_els;
+}
+
+// attempts to select the currently default selection, if inside a popup
+// we'll look for a `.default-selection`, if it doesn't exist we'll
+// simply use the first selectable element in it
+//
+// if not inside a popup we'll just use the currently selected tab in
+// the `.gamesContainer` sidebar
+navigate.default_selection = () => {
+ // the spread operator is to convert from a `NodeList` to an
+ // `Array`, and then we need to reverse this to get the ones
+ // that are layered on top first.
+ let popups_list = [...popups.list()].reverse();
+
+ let active_popup;
+
+ // run through the list of popups
+ for (let i = 0; i < popups_list.length; i++) {
+ // if this popup is shown, set set `active_popup` to it
+ if (popups_list[i].classList.contains("shown")) {
+ active_popup = popups_list[i];
+ break;
+ }
+ }
+
+ // is there no active popup?
+ if (! active_popup) {
+ // select the currently selected page in `.gamesContainer`
+ document.querySelector(
+ ".gamesContainer :not(.inactive)"
+ ).classList.add("active-selection");
+
+ // update the `#selection` element
+ navigate.selection();
+
+ return;
+ }
+
+ // get the default element inside the active popup
+ let popup_default = active_popup.querySelector("default-selection");
+
+ // did we not find a default selection element?
+ if (! popup_default) {
+ // select the first selectable element in the popup
+ navigate.get_els(active_popup)[0].classList.add(
+ "active-selection"
+ )
+
+ // update the `#selection` element
+ navigate.selection();
+
+ return;
+ }
+
+ // select the default selection
+ popup_default.classList.add("active-selection");
+
+ // update the `#selection` element
+ navigate.selection();
+}
+
+// this navigates `#selection` in the direction of `direction`
+// this can be: up, down, left and right
+navigate.move = (direction) => {
+ // get the `.active-selection` if there is one
+ let active = document.querySelector(".active-selection");
+
+ // if there is no active selection, then attempt to select the
+ // default selection
+ if (! active) {
+ navigate.default_selection()
+
+ active = document.querySelector(".active-selection");
+
+ // if there is somehow still no active selection we stop here
+ if (! active) {
+ return;
+ }
+ }
+
+ // attempt to get the element in the `direction` requested
+ let move_to_el = navigate.get_relative_el(active, direction);
+
+ // if no element is found, do nothing
+ if (! move_to_el) {return}
+
+ // switch `.active-selection` from `active` to `move_to_el`
+ active.classList.remove("active-selection");
+ move_to_el.classList.add("active-selection");
+
+ // update the `#selection` element
+ navigate.selection();
+
+ // make sure the selecting classes are removed, and thereby the
+ // scale/pressed effect with it
+ document.getElementById("selection").classList.remove(
+ "keyboard-selecting",
+ "controller-selecting"
+ )
+
+ // stop here if `move_to_el` is a child to `document.body`
+ if (move_to_el.parentElement == document.body) {
+ return;
+ }
+
+ // this element will be scrolled in view later
+ let scroll_el = move_to_el;
+
+ // these elements cant be scrolled
+ let no_scroll_parents = [
+ ".el .text",
+ ".gamesContainer"
+ ]
+
+ // run through unscrollable parent elements
+ for (let i = 0; i < no_scroll_parents.length; i++) {
+ // check if `move_to_el.closest()` matches on anything in
+ // `no_scroll_parents`
+ let no_scroll_parent = move_to_el.closest(
+ no_scroll_parents[i]
+ )
+
+ if (! no_scroll_parent) {
+ // it does not match
+ continue;
+ }
+
+ // it matches, so we make the new `scroll_el` the parent
+ scroll_el = no_scroll_parent;
+ }
+
+ // refuse to scroll to begin with, if any of these are a parent
+ let ignore_parents = [
+ ".contentMenu",
+ ".gamesContainer",
+ ]
+
+ // check if `ignore_parents` match on `move_to_el`, and if so, stop
+ for (let i = 0; i < ignore_parents.length; i++) {
+ if (move_to_el.closest(ignore_parents[i])) {
+ return;
+ }
+ }
+
+ // scroll `scroll_el` smoothly into view, centered
+ scroll_el.scrollIntoView({
+ block: "center",
+ inline: "center",
+ behavior: "smooth",
+ })
+}
+
+// selects the currently selected element, by clicking or focusing it
+navigate.select = () => {
+ // get the current selection
+ let active = document.querySelector(".active-selection");
+
+ // make sure there is a selection
+ if (! active) {return}
+
+ // slight delay to prevent some timing issues
+ setTimeout(() => {
+ // if `active` is a switch, use `settings.popup.switch()` on it,
+ // to be able to toggle it
+ if (active.closest(".switch")) {
+ settings.popup.switch(active.closest(".switch"));
+ return;
+ }
+
+ // click and focus `active`
+ active.click();
+ active.focus();
+ }, 150)
+}
+
+// selects the closest and hopefully most correct element to select next
+// to `relative_el` in the direction of `direction`
+//
+// the direction can be: up, down, left and right
+navigate.get_relative_el = (relative_el, direction) => {
+ // get selectable elements
+ let els = navigate.get_els();
+
+ // get bounds of `relative_el`
+ let bounds = relative_el.getBoundingClientRect();
+
+ // get the centered coordinates of `relative_el`
+ let relative = {
+ x: bounds.left + (bounds.width / 2),
+ y: bounds.top + (bounds.height / 2)
+ }
+
+ // update the coordinates on the element itself
+ relative_el.coords = relative;
+
+ // attempt to return the element in the correct direction
+ // if `x` or `y` is a number that's greater or less than 0 then
+ // we'll go in the direction of the coordinate that is as such
+ //
+ // meaning `get_el(1, 0)` will go to the right
+ let get_el = (x = 0, y = 0) => {
+ // `coord` is the coordinate that we're trying to get an element
+ // on, and `rev_coord` is just the opposite coord
+ let coord, rev_coord;
+
+ // set `coord` and `rev_coord` according to `x` and `y`
+ if (x > 0 || x < 0) {
+ coord = "x";
+ rev_coord = "y";
+ } else if (y > 0 || y < 0) {
+ coord = "y";
+ rev_coord = "x";
+ } else { // something unexpected was given
+ return false;
+ }
+
+ // this is the distance between each point which we check for
+ // selectable elements, increasing this improves performance,
+ // but lowers accuracy, and likewise in reverse
+ let jump_distance = 5;
+
+ // this is the coordinates to check in the direct coord check
+ let check = {
+ x: relative.x,
+ y: relative.y
+ }
+
+ // this will contain the element that directly next to
+ // `relative_el` from checking every point from `relative_el`
+ // into `direction`
+ let direct_el;
+
+ // this is the amount of pixels inbetween `relative_el` and
+ // `direct_el`, this means it doesn't have the distance from the
+ // center of `direct_el` or anything included, just the raw
+ // distance between them, this number could vary in accuracy
+ // depending on how big or small `jump_distance` is
+ let direct_distance = 0;
+
+ // attempt to find an element from a straight line from
+ // `relative_el`, by checking whether there's an element at each
+ // point in the `direction` specified
+ while (! direct_el) {
+ // add `jump_distance` to the coordinates we're checking
+ check.x += x * jump_distance;
+ check.y += y * jump_distance;
+
+ // get the elements at the coordinates we're checking
+ let els_at = document.elementsFromPoint(check.x, check.y);
+
+ // run through all the elements we found
+ for (let i = 0; i < els_at.length; i++) {
+ // make sure `els_at[i]` isn't `relative_el` and that
+ // its a selectable element
+ if (els_at[i] == relative_el
+ || ! els.includes(els_at[i])) {
+
+ // not selectable or is `relative_el`
+ continue;
+ }
+
+ // set `direct_el`
+ direct_el = els_at[i];
+
+ // get the bounds of `direct_el`
+ let direct_bounds = direct_el.getBoundingClientRect();
+
+ // get the centered coordinates for `direct_el`
+ let direct_coords = {
+ x: direct_bounds.left + (direct_bounds.width / 2),
+ y: direct_bounds.top + (direct_bounds.height / 2)
+ }
+
+ // update the coordinates on the element itself
+ direct_el.coords = direct_coords;
+
+ // get the difference between `relative_el` and
+ // `direct_el`'s coordinates, effectively their distance
+ let diff_x = direct_coords.x - relative.x;
+ let diff_y = direct_coords.y - relative.y;
+
+ // make sure this element is marked as the element that
+ // was found directly
+ direct_el.is_direct_el = true;
+
+ // update the distance on the element itself
+ direct_el.distance = Math.sqrt(
+ diff_x*diff_x + diff_y*diff_y
+ )
+
+ // set the distance on the coord we're checking on the
+ // element itself
+ direct_el.coord_distance = direct_distance;
+
+ break; // we found the `direct_el` we can stop now
+ }
+
+ // if `els_at` has `relative_el` then we reset
+ // `direct_distance`
+ if (els_at.includes(relative_el)) {
+ direct_distance = 0;
+ } else {
+ // add `jump_distance` to `direct_distance`, because
+ // we're no longer on the `relative_el` nor the
+ // `direct_el`
+ direct_distance += jump_distance;
+ }
+
+ // are we beyond the edges of the window
+ if (check.x < 0 || check.y < 0 ||
+ check.x > innerWidth || check.y > innerHeight) {
+
+ // did we find no elements?
+ if (! els_at.length) {
+ break; // stop searching
+ }
+ }
+ }
+
+ // this contains elements in the respective directions
+ let positions = {
+ up: [], down: [],
+ left: [], right: []
+ }
+
+ // gets the nearest elements from the selectable elements
+ for (let i = 0; i < els.length; i++) {
+ // get bounds
+ let el_bounds = els[i].getBoundingClientRect();
+
+ // get centered coordinates
+ let el_coords = {
+ x: el_bounds.left + (el_bounds.width / 2),
+ y: el_bounds.top + (el_bounds.height / 2)
+ }
+
+ // get the difference between `el_coords` and `direct_el`'s
+ // coordinates, effectively their distance
+ let diff_x = el_coords.x - relative.x;
+ let diff_y = el_coords.y - relative.y;
+
+ // is this element not an element that was previously a
+ // `direct_el`?
+ if (! els[i].is_direct_el) {
+ // update centerd coordinates on the element itself
+ els[i].coords = el_coords;
+
+ // set the distance on the element itself
+ els[i].distance = Math.sqrt(
+ diff_x*diff_x + diff_y*diff_y
+ )
+
+ // get and set the distance on the coord we're checking
+ // on the element itself
+ els[i].coord_distance = Math.abs(
+ relative[coord] - el_coords[coord]
+ )
+ } else {
+ els[i].is_direct_el = false;
+ continue;
+ }
+
+ // put `els[i]` in the correct place in `positions`
+ if (el_coords.x < relative.x) {
+ positions.left.push(els[i]);
+ } if (el_coords.x > relative.x) {
+ positions.right.push(els[i]);
+ } if (el_coords.y < relative.y) {
+ positions.up.push(els[i]);
+ } if (el_coords.y > relative.y) {
+ positions.down.push(els[i]);
+ }
+ }
+
+ // this will contain the element closest to `relative_el` in the
+ // correct direction, but not necessarily at the same
+ // coordinates
+ let closest_el;
+
+ // set `closest_el` to the elements closest element in the
+ // respective position using `direction`
+ for (let i = 0; i < positions[direction].length; i++) {
+ // get the element
+ let el = positions[direction][i];
+
+ // is this the first check, or is it closer than the
+ // previous `closest_el`, then update `closest_el`
+ if (! closest_el || closest_el.distance > el.distance) {
+ closest_el = el;
+ }
+ }
+
+ // was there found a `closest_el` and `direct_el`
+ if (closest_el && direct_el) {
+ // simply return `direct_el` if its the same as `closest_el`
+ if (closest_el == direct_el) {
+ return direct_el;
+ }
+
+ // if the parent element of `closest_el` and `direct_el` is
+ // the same, then we prefer the `direct_el`
+ //
+ // unless the parent is `document.body`
+ if (closest_el.parentElement == direct_el.parentElement &&
+ direct_el.parentElement !== document.body) {
+ return direct_el;
+ }
+
+ // get the difference between `relative_el` and `direct_el`
+ // on the coordinate of `direction`
+ let same_coord_diff = Math.abs(
+ direct_el.coords[rev_coord] -
+ relative_el.coords[rev_coord]
+ )
+
+ // if the difference is less than 3 then we just return the
+ // `direct_el` as its only a couple pixels off being on the
+ // same coordinate as `relative_el`
+ if (same_coord_diff < 3) {
+ return direct_el;
+ }
+
+ // get the difference is distance on `direct_el` and
+ // `closest_el`
+ let difference = Math.abs(
+ direct_el.distance - closest_el.distance
+ )
+
+ // is the distance les than 50?
+ if (difference < 50) {
+ // get the difference between `direct_el` and
+ // `relative_el`
+ let direct_diff = Math.abs(
+ direct_el.coords[rev_coord] -
+ relative_el.coords[rev_coord]
+ )
+
+ // get the difference between `closest_el` and
+ // `relative_el`
+ let closest_diff = Math.abs(
+ closest_el.coords[rev_coord] -
+ relative_el.coords[rev_coord]
+ )
+
+ // if the `direct_el` is closer to `relative_el`, return
+ // that, otherwise return `closet_el`
+ if (direct_diff < closest_diff) {
+ return direct_el;
+ } else if (closest_diff < direct_diff) {
+ return closest_el;
+ }
+ }
+
+ // is `direct_el` closer than `closest_el` in either
+ // `.coord_distance` or `.distance`
+ //
+ // if not just return `closest_el`
+ if (direct_el.coord_distance <= closest_el.coord_distance ||
+ direct_el.distance <= closest_el.distance) {
+
+ // if `direct_el` is closer in `.coord_distance` then
+ // return `direct_el`
+ if (direct_el.coord_distance <=
+ closest_el.coord_distance) {
+
+ return direct_el;
+ }
+
+ // if the difference in `.distance` is less than 50,
+ // then return `direct_el`
+ if (difference < 50) {
+ return direct_el;
+ }
+
+ // if the `.distance` is overall closer on `direct_el`
+ // than `closest_el` then we return `direct_el`,
+ // otherwise we return `closest_el`
+ if (direct_el.distance < closest_el.distance) {
+ return direct_el;
+ } else {
+ return closest_el;
+ }
+ } else { // `direct_el` is unarguably too far away
+ return closest_el;
+ }
+ } else if (! direct_el && ! closest_el) {
+ // do nothing if no element at all was found
+ return false;
+ }
+
+ // return whichever element we did find
+ return direct_el || closest_el;
+ }
+
+ // translate `direction` into `get_el()` args
+ switch(direction) {
+ case "up": return get_el(0, -1);
+ case "down": return get_el(0, 1);
+ case "left": return get_el(-1, 0);
+ case "right": return get_el(1, 0);
+ }
+}
+
+// attempt to reselect the default selection when a popup is either
+// closed or opened
+events.on("popup-changed", () => {
+ // get the active selection
+ let active_el = document.querySelector(".active-selection");
+
+ // make sure there is a selection
+ if (! active_el) {
+ return;
+ }
+
+ // remove the currently active selection
+ active_el.classList.remove("active-selection");
+
+ // update the `#selection` element
+ navigate.selection();
+
+ // wait a moment to allow the popup to open or close completely
+ setTimeout(() => {
+ // select the default selection
+ navigate.default_selection();
+ }, 300)
+})
+
+module.exports = navigate;
diff --git a/src/app/js/tooltip.js b/src/app/js/tooltip.js
index f2d97a0..13dcd67 100644
--- a/src/app/js/tooltip.js
+++ b/src/app/js/tooltip.js
@@ -128,5 +128,3 @@ document.addEventListener("mousedown", tooltip_event);
document.addEventListener("mousemove", tooltip_event);
document.addEventListener("mouseenter", tooltip_event);
document.addEventListener("mouseleave", tooltip_event);
-
-module.exports = tooltip;
diff --git a/src/app/main.css b/src/app/main.css
index 3f77ac4..9ade423 100644
--- a/src/app/main.css
+++ b/src/app/main.css
@@ -5,6 +5,7 @@
@import "css/tooltip.css";
@import "css/theming.css";
@import "css/launcher.css";
+@import "css/selection.css";
body {
margin: 0;
diff --git a/src/app/main.js b/src/app/main.js
index 3adb96d..0a84e66 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -44,5 +44,8 @@ const launcher = require("./js/launcher");
const is_running = require("./js/is_running");
const set_buttons = require("./js/set_buttons");
+const navigate = require("./js/navigate");
+const gamepad = require("./js/gamepad");
+
require("./js/dom_events");
require("./js/localize")();