From 9ed56f94987ff00fdd768c1bb9c65c0d08ecb838 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Mon, 4 Aug 2025 18:38:35 +0200 Subject: [PATCH] feat(locked-navigation): add navigation arrows when lock mode is enabled --- i18n/english.js | 4 + i18n/french.js | 4 + public/components/legend/legend.js | 4 +- .../locked-navigation/locked-navigation.js | 102 ++++++++++++++++++ public/components/locker/locker.js | 3 + public/components/package/package.js | 3 +- public/components/views/settings/settings.js | 3 +- public/core/events.js | 9 ++ public/core/network-navigation.js | 98 ++++++++++++----- public/main.js | 6 +- views/index.html | 4 +- 11 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 public/components/locked-navigation/locked-navigation.js create mode 100644 public/core/events.js diff --git a/i18n/english.js b/i18n/english.js index 53022da8..0d8f4a1a 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -227,6 +227,10 @@ const ui = { default: "The package is fine.", warn: "The package has warnings.", friendly: "The package is maintained by the same authors as the root package." + }, + lockedNavigation: { + next: "Next", + prev: "Prev" } }; diff --git a/i18n/french.js b/i18n/french.js index eaca3eaf..44f30e62 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -227,6 +227,10 @@ const ui = { default: "Rien à signaler.", warn: "La dépendance contient des menaces.", friendly: "La dépendance est maintenu par des auteurs du package principal." + }, + lockedNavigation: { + next: "Suivant", + prev: "Précédent" } }; diff --git a/public/components/legend/legend.js b/public/components/legend/legend.js index 4470d14f..46cb9b1d 100644 --- a/public/components/legend/legend.js +++ b/public/components/legend/legend.js @@ -1,5 +1,5 @@ // Import Third-party Dependencies -import { LitElement, html, css } from "lit"; +import { LitElement, html, css, nothing } from "lit"; // Import Internal Dependencies import { COLORS } from "../../../workspaces/vis-network/src/constants.js"; @@ -79,7 +79,7 @@ class Legend extends LitElement { render() { if (!this.isVisible) { - return html``; + return nothing; } const colors = COLORS.LIGHT; diff --git a/public/components/locked-navigation/locked-navigation.js b/public/components/locked-navigation/locked-navigation.js new file mode 100644 index 00000000..89f4b9b8 --- /dev/null +++ b/public/components/locked-navigation/locked-navigation.js @@ -0,0 +1,102 @@ +// Import Third-party Dependencies +import { LitElement, html, css, nothing } from "lit"; + +// Import Internal Dependencies +import { EVENTS } from "../../core/events"; + +export class LockedNavigation extends LitElement { + static styles = css` + :host { + position: absolute; + right: 132px; + bottom: 10px; + z-index: 100; + display: flex; + align-items: center; + gap: 10px; +} + +.btn{ + width: 0; + height: 0; + background: none; + border: none; + cursor: pointer; + transition: all 0.3s ease; + border-radius: 5px; +} + +.next{ + border-top: 12px solid transparent; + border-bottom: 12px solid transparent; + border-left: 16px solid #af2222; +} + +.prev{ + border-top: 12px solid transparent; + border-bottom: 12px solid transparent; + border-right: 16px solid #af2222; +} + +.next:hover{ + border-left-color: #cb3d3d; +} + +.prev:hover{ + border-right-color: #cb3d3d; +} +`; + + static properties = { + isLocked: { type: Boolean }, + nextLabel: { type: String }, + prevLabel: { type: String } + }; + + constructor() { + super(); + this.isLocked = false; + + this.lock = () => { + this.isLocked = true; + }; + + this.unlock = () => { + this.isLocked = false; + }; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener(EVENTS.LOCKED, this.lock); + window.addEventListener(EVENTS.UNLOCKED, this.unlock); + } + + disconnectedCallback() { + window.removeEventListener(EVENTS.LOCKED, this.lock); + window.removeEventListener(EVENTS.UNLOCKED, this.unlock); + super.disconnectedCallback(); + } + + render() { + if (!this.isLocked) { + return nothing; + } + + return html` + + + `; + } + + moveToNextLockedNode() { + window.dispatchEvent(new CustomEvent(EVENTS.MOVED_TO_NEXT_LOCKED_NODE, { composed: true })); + } + moveToPreviousLockedNode() { + window.dispatchEvent(new CustomEvent(EVENTS.MOVED_TO_PREVIOUS_LOCKED_NODE, { composed: true })); + } +} + +customElements.define("locked-navigation", LockedNavigation); diff --git a/public/components/locker/locker.js b/public/components/locker/locker.js index e7ef8543..41780227 100644 --- a/public/components/locker/locker.js +++ b/public/components/locker/locker.js @@ -1,5 +1,6 @@ // Import Internal Dependencies import * as utils from "../../common/utils.js"; +import { EVENTS } from "../../core/events.js"; export class Locker { constructor(nsn) { @@ -58,6 +59,7 @@ export class Locker { console.log("[LOCKER] lock triggered"); this.renderLock(); this.locked = true; + window.dispatchEvent(new CustomEvent(EVENTS.LOCKED, { composed: true })); } } @@ -69,6 +71,7 @@ export class Locker { console.log("[LOCKER] unlock triggered"); this.renderUnlock(); this.locked = false; + window.dispatchEvent(new CustomEvent(EVENTS.UNLOCKED, { composed: true })); // No node selected, so we reset highlight const selectedNode = window.networkNav.currentNodeParams; diff --git a/public/components/package/package.js b/public/components/package/package.js index 499dfb29..cbdc44d0 100644 --- a/public/components/package/package.js +++ b/public/components/package/package.js @@ -3,6 +3,7 @@ import "../bundlephobia/bundlephobia.js"; import { PackageHeader } from "./header/header.js"; import * as Pannels from "./pannels/index.js"; import * as utils from "../../common/utils.js"; +import { EVENTS } from "../../core/events.js"; export class PackageInfo { static DOMElementName = "package-info"; @@ -13,7 +14,7 @@ export class PackageInfo { domElement.setAttribute("class", "slide-out"); } - window.dispatchEvent(new CustomEvent("package-info-closed", { detail: null })); + window.dispatchEvent(new CustomEvent(EVENTS.PACKAGE_INFO_CLOSED, { detail: null })); } /** diff --git a/public/components/views/settings/settings.js b/public/components/views/settings/settings.js index f0139a0d..ed8ce20f 100644 --- a/public/components/views/settings/settings.js +++ b/public/components/views/settings/settings.js @@ -4,6 +4,7 @@ import { warnings } from "@nodesecure/js-x-ray/warnings"; // Import Internal Dependencies import * as utils from "../../../common/utils.js"; +import { EVENTS } from "../../../core/events.js"; // CONSTANTS const kAllowedHotKeys = new Set([ @@ -231,7 +232,7 @@ export class Settings { this.config = newConfig; this.saveButton.classList.add("disabled"); - window.dispatchEvent(new CustomEvent("settings-saved", { detail: this.config })); + window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, { detail: this.config })); } updateSettings() { diff --git a/public/core/events.js b/public/core/events.js new file mode 100644 index 00000000..d7a4fb81 --- /dev/null +++ b/public/core/events.js @@ -0,0 +1,9 @@ +// Constants +export const EVENTS = { + LOCKED: "locked", + UNLOCKED: "unlocked", + PACKAGE_INFO_CLOSED: "package-info-closed", + SETTINGS_SAVED: "settings-saved", + MOVED_TO_NEXT_LOCKED_NODE: "moved-to-next-locked-node", + MOVED_TO_PREVIOUS_LOCKED_NODE: "moved-to-previous-locked-node" +}; diff --git a/public/core/network-navigation.js b/public/core/network-navigation.js index 30584ed3..c2ef4f5a 100644 --- a/public/core/network-navigation.js +++ b/public/core/network-navigation.js @@ -1,5 +1,6 @@ // Import Internal Dependencies import { PackageInfo } from "../components/package/package.js"; +import { EVENTS } from "./events.js"; export class NetworkNavigation { /** @@ -118,6 +119,13 @@ export class NetworkNavigation { this.#dependenciesMapByLevel.set(0, this.rootNodeParams); + window.addEventListener(EVENTS.MOVED_TO_NEXT_LOCKED_NODE, () => { + this.#moveToNextLockedNode(); + }); + window.addEventListener(EVENTS.MOVED_TO_PREVIOUS_LOCKED_NODE, () => { + this.#moveToPreviousLockedNode(); + }); + document.addEventListener("keydown", (event) => { const isNetworkViewHidden = document.getElementById("network--view").classList.contains("hidden"); const isWikiOpen = document.getElementById("documentation-root-element").classList.contains("slide-in"); @@ -147,22 +155,9 @@ export class NetworkNavigation { const nodeParam = this.#currentNodeParams ?? this.rootNodeParams; - if (this.#nsn.lastHighlightedIds === null) { - this.#lockedNodes = []; - } - else { - this.#lockedNodes = this.#sortByAngle( - [...this.#nsn.lastHighlightedIds].map( - (id) => [id, { - ...this.#secureDataSet.linker.get(id), - position: nsn.network.getPosition(id) - }] - ), - { ...nsn.network.getPosition(this.rootNodeParams.nodes[0]) } - ); - } + this.#updateLockedNodes(); - if (this.#lockedNodes.length > 0) { + if (this.#hasLockedNodes()) { this.#navigateBetweenLockedNodes(event); return; @@ -361,32 +356,81 @@ export class NetworkNavigation { this.#navigateTreeLevel(nearthestNode); } + #updateLockedNodes() { + if (this.#nsn.lastHighlightedIds === null) { + this.#lockedNodes = []; + } + else { + this.#lockedNodes = this.#sortByAngle( + [...this.#nsn.lastHighlightedIds].map( + (id) => [id, { + ...this.#secureDataSet.linker.get(id), + position: this.#nsn.network.getPosition(id) + }] + ), + { ...this.#nsn.network.getPosition(this.rootNodeParams.nodes[0]) } + ); + } + } + + #moveToPreviousLockedNode() { + this.#updateLockedNodes(); + if (this.#hasLockedNodes()) { + this.#selectPreviousLockedNode(); + this.#focusOnActiveLockedNode(); + } + } + + #moveToNextLockedNode() { + this.#updateLockedNodes(); + if (this.#hasLockedNodes()) { + this.#selectNextLockedNode(); + this.#focusOnActiveLockedNode(); + } + } + #navigateBetweenLockedNodes(event) { switch (event.code) { case "ArrowLeft": - if (this.#lockedNodesActiveIndex === 0) { - this.#lockedNodesActiveIndex = this.#lockedNodes.length - 1; - } - else { - this.#lockedNodesActiveIndex--; - } + this.#selectPreviousLockedNode(); break; case "ArrowRight": - if (this.#lockedNodesActiveIndex === this.#lockedNodes.length - 1) { - this.#lockedNodesActiveIndex = 0; - } - else { - this.#lockedNodesActiveIndex++; - } + this.#selectNextLockedNode(); break; default: return; } + this.#focusOnActiveLockedNode(); + } + + #selectPreviousLockedNode() { + if (this.#lockedNodesActiveIndex === 0) { + this.#lockedNodesActiveIndex = this.#lockedNodes.length - 1; + } + else { + this.#lockedNodesActiveIndex--; + } + } + + #selectNextLockedNode() { + if (this.#lockedNodesActiveIndex === this.#lockedNodes.length - 1) { + this.#lockedNodesActiveIndex = 0; + } + else { + this.#lockedNodesActiveIndex++; + } + } + + #focusOnActiveLockedNode() { this.#nsn.network.focus(this.#lockedNodes[this.#lockedNodesActiveIndex][0], { animation: true, scale: 0.35, offset: { x: 150, y: 0 } }); } + + #hasLockedNodes() { + return this.#lockedNodes.length > 0; + } } diff --git a/public/main.js b/public/main.js index 7551cd1b..a2b41f6a 100644 --- a/public/main.js +++ b/public/main.js @@ -8,6 +8,7 @@ import { Wiki } from "./components/wiki/wiki.js"; import { Popup } from "./components/popup/popup.js"; import { Locker } from "./components/locker/locker.js"; import "./components/legend/legend.js"; +import "./components/locked-navigation/locked-navigation.js"; import { Settings } from "./components/views/settings/settings.js"; import { HomeView } from "./components/views/home/home.js"; import { SearchView } from "./components/views/search/search.js"; @@ -15,6 +16,7 @@ import { NetworkNavigation } from "./core/network-navigation.js"; import { i18n } from "./core/i18n.js"; import { initSearchNav } from "./core/search-nav.js"; import * as utils from "./common/utils.js"; +import { EVENTS } from "./core/events.js"; let secureDataSet; let nsn; @@ -131,7 +133,7 @@ async function init(options = {}) { homeView ??= new HomeView(secureDataSet, nsn); searchview ??= new SearchView(secureDataSet, nsn); - window.addEventListener("package-info-closed", () => { + window.addEventListener(EVENTS.PACKAGE_INFO_CLOSED, () => { window.networkNav.currentNodeParams = null; packageInfoOpened = false; }); @@ -261,7 +263,7 @@ function onSettingsSaved(defaultConfig = null) { updateSettings(defaultConfig); } - window.addEventListener("settings-saved", async(event) => { + window.addEventListener(EVENTS.SETTINGS_SAVED, async(event) => { updateSettings(event.detail); }); } diff --git a/views/index.html b/views/index.html index 1a397222..9a24cd0c 100644 --- a/views/index.html +++ b/views/index.html @@ -84,7 +84,9 @@

[[=z.token('network.unlocked')]]

- + + +