diff options
-rw-r--r-- | src/app/css/launcher.css | 9 | ||||
-rw-r--r-- | src/app/css/selection.css | 19 | ||||
-rw-r--r-- | src/app/css/theming.css | 2 | ||||
-rw-r--r-- | src/app/index.html | 8 | ||||
-rw-r--r-- | src/app/js/gamepad.js | 301 | ||||
-rw-r--r-- | src/app/js/navigate.js | 688 | ||||
-rw-r--r-- | src/app/js/tooltip.js | 2 | ||||
-rw-r--r-- | src/app/main.css | 1 | ||||
-rw-r--r-- | src/app/main.js | 3 |
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")(); |