From e9abf882ce98d80a9b58181df3e8279437652220 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Sat, 27 Jan 2024 11:35:32 +0100 Subject: [PATCH 1/4] Add flowchart for core navigation logic --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 626096d..ebacada 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,48 @@ The following attributes may be added in the markup to guide the moving of focus These limitations are intentional to keep the library simple and focused. +## Mechanism + +```mermaid +flowchart TB + Idle(((Idle))) == keypress ==> D_KP{Is arrow key?} + %% Terminology from https://github.com/whatwg/html/issues/897 + D_KP -- Yes --> A_BC[Get top blocking element] + D_KP -- No --> Idle + A_BC --> D_AE{Contains activeElement?} + D_AE -- No --> A_SI[Select initial focus] + A_SI --> Idle + D_AE -- Yes --> Find + + subgraph Find + direction TB + A_FC[Find candidates within next parent group] + A_FC --> D_CF{Candidates found?} + D_CF -- Yes --> A_SN[Stop with candidates] + D_CF -- No --> D_PB[Is parent the top blocking element?] + D_PB -- Yes --> A_NC[Stop with no candidates] + D_PB -- No --> A_FC + end + + Find --> D_HC[One or more candidates?] + D_HC -- Yes --> Activate + D_HC -- No --> Idle + + subgraph Activate + direction TB + D_DP[Direct projection along movement axis non-empty?] -- Yes --> A_FD[Reduce to only those candidates] + D_DP -- No --> A_CA[Continue with all candidates] + A_CA --> A_SC[Select candidate with lowest Euclidean distance] + A_FD --> A_SC + A_SC --> D_CG[Is selected candidate a group?] + D_CG -- Yes --> A_DG[Select new candidate based on group's strategy] + A_DG --> D_CG + D_CG -- No --> A_FS[Focus selected candidate] + end + + Activate --> Idle +``` + ## Contributing Contributions are welcome. Please fork the repository and submit a pull request with your proposed changes. From c1dbf81ab122727ad83af5de99d25d59aca57ad7 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Tue, 23 Jan 2024 11:45:02 +0100 Subject: [PATCH 2/4] Add initial implementation of library functionality Squashed into one commit as large refactors were interleaved with applying formatters with different settings during initial stabilization, so producing a nice readable history is unfortunately infeasible. --- .github/workflows/main.yml | 65 +- .gitignore | 1 + .prettierignore | 1 + .prettierrc | 7 + README.md | 84 +- cypress.config.ts | 10 + cypress/e2e/spec.cy.ts | 193 +++ cypress/fixtures/group-active.html | 16 + cypress/fixtures/group-first.html | 16 + cypress/fixtures/group-last.html | 16 + cypress/fixtures/group-linear.html | 20 + cypress/fixtures/group-memorize.html | 18 + cypress/fixtures/styles.css | 23 + cypress/support/commands.ts | 0 cypress/support/e2e.ts | 1 + examples/everything-bagel.html | 112 ++ examples/keyevent-viewer.js | 25 + index.js | 913 +++++++++++ package-lock.json | 2105 ++++++++++++++++++++++++++ package.json | 40 + tsconfig.json | 19 + 21 files changed, 3659 insertions(+), 26 deletions(-) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 cypress.config.ts create mode 100644 cypress/e2e/spec.cy.ts create mode 100644 cypress/fixtures/group-active.html create mode 100644 cypress/fixtures/group-first.html create mode 100644 cypress/fixtures/group-last.html create mode 100644 cypress/fixtures/group-linear.html create mode 100644 cypress/fixtures/group-memorize.html create mode 100644 cypress/fixtures/styles.css create mode 100644 cypress/support/commands.ts create mode 100644 cypress/support/e2e.ts create mode 100644 examples/everything-bagel.html create mode 100644 examples/keyevent-viewer.js create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 990b7c2..11c2b29 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Chrome +name: Test suite on: push: pull_request: @@ -6,11 +6,70 @@ on: workflow_dispatch: jobs: - chrome: - runs-on: ubuntu-22.04 + formatting: + runs-on: ubuntu-latest + name: Formatting + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v2 + with: + node-version: "20" + + - run: npm install + + - run: npx prettier --check . + + typechecks: + runs-on: ubuntu-latest + name: Typechecks + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v2 + with: + node-version: "20" + + - run: npm install + + - run: npx tsc + + e2e-chrome: + runs-on: ubuntu-latest name: E2E on Chrome steps: - uses: actions/checkout@v4 + - uses: cypress-io/github-action@v6 with: browser: chrome + + e2e-firefox: + runs-on: ubuntu-latest + name: E2E on Firefox + steps: + - uses: actions/checkout@v4 + + - uses: cypress-io/github-action@v6 + with: + browser: firefox + + e2e-edge: + runs-on: windows-latest + name: E2E on Edge + steps: + - uses: actions/checkout@v4 + + - uses: cypress-io/github-action@v6 + with: + browser: edge + + e2e-safari: + runs-on: macos-latest + name: E2E on WebKit + steps: + - uses: actions/checkout@v4 + + - uses: cypress-io/github-action@v6 + with: + browser: webkit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d87b1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fcacade --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +semi: false +trailingComma: "none" +tabWidth: 2 +overrides: + - files: "cypress/**/*" + options: + printWidth: 120 diff --git a/README.md b/README.md index ebacada..f9ceae7 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,75 @@ # focus-shift -## Introduction -focus-shift is a lightweight JavaScript library designed for keyboard-based navigation in web applications. It allows users to move focus between elements using the arrow keys. The behaviour of focus shifting can be guided by annotations in the HTML markup. - -The library is kept simple and assumes use in kiosk-like interfaces. +focus-shift is a lightweight, zero-dependency JavaScript library designed for keyboard-based navigation in web applications. It restricts itself to shifting focus between elements in response to arrow key events. The behaviour of focus shifting can be guided by annotations in the HTML markup. This allows the library to work well with technologies that prefer generating HTML over interacting with JavaScript directly. ## Features -- Move focus with arrow keys + +- Move focus with the arrow keys - Declare groups with custom focus strategies -- Mark subtrees of the DOM as muted +- Mark subtrees of the DOM that should trap focus +- Mark subtrees of the DOM that should be skipped ## Usage + Include the library in your HTML file: + ```html ``` ### Basic Example -Here's a simple example of how to use the library: + +Here's a simple example of annotating markup: ```html -
+
- +
+ + ``` ## Options -The following attributes may be added in the markup to guide the moving of focus" +The following attributes may be added in the markup to guide the moving of focus: -- `data-focus-group`: Defines a navigation group. -- `data-focus-group-select`: Determines the initial focus when focus moves to a group. +- `data-focus-group`: Defines a navigation group and the initial focus when focus moves to a group. Default is `linear`. - `first`: The first element in the DOM order receives focus. - `last`: The last element in the DOM order is focused initially. - `active`: Focuses on the element within the group marked as active. - `linear`: Focus is determined by the spatial direction of user navigation. -- `data-focus-group-active`: Marks an element as the currently active element within a group. -- `data-focus-mute`: Skips the element and its descendants in navigation. -- `data-focus-solo`: Focuses within this element only, ignoring others in the same group. + - `memorize`: The last focused element within the group receives focus again. +- `data-focus-active`: Marks an element as the currently active element within a group. +- `data-focus-skip`: Skips the element and its descendants in navigation. +- `data-focus-trap`: Only allows elements within the annotated layer to receive focus. + +## Principles and Scope + +- **It doesn't just work.** It would be nice if focus could automatically move to the intuitive element in each case, but this seems to require a sophisticated model of visual weight and Gestalt principles. This is out of scope for a simple library like this. +- **It should be easy to make it work.** With a little bit of annotation in the markup, one can express relationships to help the algorithm move focus in an adequate way. +- **Annotations should be logical, not spatial.** To be useful in responsive layouts, the annotations should express logical rather than spatial relationships. +- **Keep state to a minimum.** As much as possible, the library should treat each event in isolation and not maintain state representing the page layout. This may make the library less performant, but avoids complicated and error prone recomputation logic. + +### What the library doesn't do, but might + +- Dispatch cancelable events when descending into or out of groups +- Dispatch cancelable events before applying focus to an element +- Treat elements in open shadow DOM as focusable +- Allow defining custom selectors for focusables +- Use focus heuristics based on user agent's text direction +- Offer a JavaScript API + +### What the library probably shouldn't do -## Limitations and simplifying assumptions -- **Arrow key navigation only**: The library listens only to arrow key events for navigation. -- **No iframe or ShadowDOM support**: Does not handle navigation within iframes or shadow DOM elements. -- **Viewport-filling applications without scrollbars**: Optimized for applications that fill the viewport without scrolling. -- **Exclusion of conflicting elements**: Avoids navigation to inputs like radio buttons to simplify navigation logic. +- Handle keyboard events other than arrow keys -These limitations are intentional to keep the library simple and focused. +### What the library can not do -## Mechanism +- Handle focus in iframes or closed shadow DOM + +### Mechanism ```mermaid flowchart TB @@ -91,8 +111,26 @@ flowchart TB Activate --> Idle ``` +## Development + +The library is implemented in [withered](https://en.wikipedia.org/wiki/Gunpei_Yokoi#Lateral_Thinking_with_Withered_Technology) JavaScript, so it should work directly with most browsers and a development server is not needed. + +There is ample JSDoc documentation so that the TypeScript compiler may be used for typechecking in strict mode: + + npm test + +The code is formatted with slightly non-standard prettier: + + npm run format + +End-to-end tests are done using Cypress. + ## Contributing + Contributions are welcome. Please fork the repository and submit a pull request with your proposed changes. ## License -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details. + +(C) Copyright 2024 Dividat AG + +Published under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000..255421b --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "cypress" + +export default defineConfig({ + experimentalWebKitSupport: true, + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + } + } +}) diff --git a/cypress/e2e/spec.cy.ts b/cypress/e2e/spec.cy.ts new file mode 100644 index 0000000..6b45db5 --- /dev/null +++ b/cypress/e2e/spec.cy.ts @@ -0,0 +1,193 @@ +describe("focus-shift spec", () => { + function keyevent(opts) { + return { + key: opts.key, + ctrlKey: opts.ctrlKey || false, + metaKey: opts.metaKey || false, + altKey: opts.altKey || false, + getModifierState: function (name) { + return opts.modifierState === name + } + } + } + + function testFor(testPage, opts, sequence) { + return function () { + cy.visit(testPage) + + cy.get("body").then(($body) => { + $body[0].className = opts.className || "" + $body[0].focus() + }) + + for (let pair of sequence) { + switch (pair.eventType) { + case "focus": + cy.get(pair.selector).focus() + break + default: + cy.get("body") + .trigger(pair.eventType, pair.options) + .then(($body) => { + cy.wait(50) + + cy.document().then((doc) => { + if (pair.selector) { + expect(doc.querySelector(pair.selector)).to.equal(doc.activeElement) + } else { + expect(doc.activeElement).to.equal(doc.body) + } + }) + }) + } + } + } + } + + it( + "first-type group top-down", + testFor("./cypress/fixtures/group-first.html", { className: "rows" }, [ + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) } + ]) + ) + + it( + "first-type group left-right", + testFor("./cypress/fixtures/group-first.html", { className: "columns" }, [ + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) } + ]) + ) + + it( + "last-type group top-down", + testFor("./cypress/fixtures/group-last.html", { className: "rows" }, [ + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) } + ]) + ) + + it( + "last-type group left-right", + testFor("./cypress/fixtures/group-last.html", { className: "columns" }, [ + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) } + ]) + ) + + it( + "active-type group TD", + testFor("./cypress/fixtures/group-active.html", { className: "rows" }, [ + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) } + ]) + ) + + it( + "active-type group LR", + testFor("./cypress/fixtures/group-active.html", { className: "columns" }, [ + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) } + ]) + ) + + it( + "linear-type group TD", + testFor("./cypress/fixtures/group-linear.html", { className: "rows" }, [ + { eventType: "focus", selector: "#before" }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#after", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#before", options: keyevent({ key: "ArrowUp" }) } + ]) + ) + + it( + "linear-type group DT", + testFor("./cypress/fixtures/group-linear.html", { className: "rows" }, [ + { eventType: "focus", selector: "#after" }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#before", options: keyevent({ key: "ArrowUp" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#after", options: keyevent({ key: "ArrowDown" }) } + ]) + ) + + it( + "linear-type group LR", + testFor("./cypress/fixtures/group-linear.html", { className: "columns" }, [ + { eventType: "focus", selector: "#before" }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#after", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#before", options: keyevent({ key: "ArrowLeft" }) } + ]) + ) + + it( + "linear-type group RL", + testFor("./cypress/fixtures/group-linear.html", { className: "columns" }, [ + { eventType: "focus", selector: "#after" }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#before", options: keyevent({ key: "ArrowLeft" }) }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#button-3", options: keyevent({ key: "ArrowRight" }) }, + { eventType: "keydown", selector: "#after", options: keyevent({ key: "ArrowRight" }) } + ]) + ) + + it( + "memorize-type group TD", + testFor("./cypress/fixtures/group-memorize.html", { className: "rows" }, [ + { eventType: "focus", selector: "#before" }, + { eventType: "keydown", selector: "#button-1", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) }, + { eventType: "focus", selector: "#before" }, + { eventType: "keydown", selector: "#button-2", options: keyevent({ key: "ArrowDown" }) } + ]) + ) +}) diff --git a/cypress/fixtures/group-active.html b/cypress/fixtures/group-active.html new file mode 100644 index 0000000..99031e3 --- /dev/null +++ b/cypress/fixtures/group-active.html @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/cypress/fixtures/group-first.html b/cypress/fixtures/group-first.html new file mode 100644 index 0000000..85569b0 --- /dev/null +++ b/cypress/fixtures/group-first.html @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/cypress/fixtures/group-last.html b/cypress/fixtures/group-last.html new file mode 100644 index 0000000..993345c --- /dev/null +++ b/cypress/fixtures/group-last.html @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/cypress/fixtures/group-linear.html b/cypress/fixtures/group-linear.html new file mode 100644 index 0000000..3b51f99 --- /dev/null +++ b/cypress/fixtures/group-linear.html @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/cypress/fixtures/group-memorize.html b/cypress/fixtures/group-memorize.html new file mode 100644 index 0000000..4be1b17 --- /dev/null +++ b/cypress/fixtures/group-memorize.html @@ -0,0 +1,18 @@ + + + + + + + +
+ +
+ + + diff --git a/cypress/fixtures/styles.css b/cypress/fixtures/styles.css new file mode 100644 index 0000000..c632cee --- /dev/null +++ b/cypress/fixtures/styles.css @@ -0,0 +1,23 @@ +.nav-group { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 10px; +} +.rows .nav-group { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; +} +.columns .nav-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} +button { + margin: 5px; +} +:focus { + outline: 3px yellow solid; +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000..e69de29 diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000..b7cb303 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1 @@ +import "./commands" diff --git a/examples/everything-bagel.html b/examples/everything-bagel.html new file mode 100644 index 0000000..c8fbb35 --- /dev/null +++ b/examples/everything-bagel.html @@ -0,0 +1,112 @@ + + + + + Make Me One With Everything + + + + + + + + +
+ + + +
+ +
+ + + +
+ + + + + + + + + + + + + + + diff --git a/examples/keyevent-viewer.js b/examples/keyevent-viewer.js new file mode 100644 index 0000000..8dc68d6 --- /dev/null +++ b/examples/keyevent-viewer.js @@ -0,0 +1,25 @@ +document.addEventListener("keydown", function (event) { + // Clear previous overlay + const existingDisplay = document.getElementById("key-display") + if (existingDisplay) { + document.body.removeChild(existingDisplay) + } + + // Create a new overlay + const displayDiv = document.createElement("div") + displayDiv.id = "key-display" + displayDiv.style.position = "absolute" + displayDiv.style.bottom = "20px" + displayDiv.style.right = "20px" + displayDiv.style.padding = "10px" + displayDiv.style.backgroundColor = "#ddd" + displayDiv.style.border = "1px solid black" + displayDiv.textContent = `Key Pressed: ${event.key}` + + document.body.appendChild(displayDiv) + + // Remove overlay + setTimeout(() => { + if (displayDiv.parentNode) displayDiv.parentNode.removeChild(displayDiv) + }, 750) +}) diff --git a/index.js b/index.js new file mode 100644 index 0000000..8f2c91a --- /dev/null +++ b/index.js @@ -0,0 +1,913 @@ +/** + * @overview focus-shift, library for spatial navigation with arrow keys + * + * https://github.com/dividat/focus-shift + * + * @copyright Dividat AG, 2024 + * @license MIT + */ + +function init() { + document.addEventListener("keydown", handleKeyDown) +} + +/** + * Handle any keydown event and decide whether it should be used for navigation. + * + * @param {KeyboardEvent} event + * @returns {void} + */ +function handleKeyDown(event) { + const direction = KEY_TO_DIRECTION[event.key] + + // Ignore irrelevant inputs + if ( + direction == null || + hasModifiers(event) || + isInputInteraction(direction, event) + ) { + return + } else { + condsole.group(event.key) + event.preventDefault() + handleUserDirection(KEY_TO_DIRECTION[event.key]) + condsole.groupEnd() + } +} + +/** + * Handle a user's request for focus shift. + * + * @param {Direction} direction + * @returns {void} + */ +function handleUserDirection(direction) { + const container = getBlockingElement() + const activeElement = getActiveElement(container) + + if (activeElement == null) { + focusInitial(direction, container) + return + } + + const candidates = getFocusCandidates(direction, activeElement, container) + if (candidates.length > 0) { + performMove(direction, activeElement.getBoundingClientRect(), candidates) + } +} + +/** + * Apply the initial focus within the given container. + * + * Standard heuristics are used to determine which element should be the first to receive focus. + * + * 1. Look for elements with explicit tabindex attribute set, choose lowest index > 0 + * 2. If no tabindex was set, treat container as a 'first' group + * + * @param {Direction} direction + * @param {Element} container + * @returns {void} + */ +function focusInitial(direction, container) { + // 1. tabindex + /** @type {Array} */ + const tabindexed = Array.from(container.querySelectorAll("[tabindex]")) + .filter(hasTabIndex) + .filter((elem) => elem.tabIndex > 0) + const markedElement = getMinimumBy(tabindexed, (elem) => elem.tabIndex) + if (markedElement != null) { + applyFocus(direction, makeVirtualOrigin(direction), markedElement) + return + } + + // 2. 'linear' group + focusLinear(direction, makeVirtualOrigin(direction), container) +} + +/** + * Get all focusable elements within the container. + * + * Only the top-most elements are returned, any descendants of focusable elements are omitted. + * + * @param {Element} container + * @returns {Element[]} + */ +function getFocusableElements(container) { + const selector = + '[data-focus-group], [tabindex], a[href], button, input, textarea, select, [contenteditable="true"], summary' + + // Find the focusable elements within the container + const focusableElements = Array.from( + container.querySelectorAll(selector) + ).filter(isFocusable) + // Reduce to the focusable elements highest up the tree + const topMostElements = focusableElements.filter((elem) => { + return ( + elem.parentElement != null && + (elem.parentElement.closest(selector) === container || + elem.parentElement.closest(selector) == null) + ) + }) + + return topMostElements +} + +/** + * Tests whether an element may be focused using the keyboard. + * + * An element is inert for the purposes of this library if one or more of the following apply: + * + * - it has negative tabindex, + * - it has been marked with `data-focus-skip`, + * - it is a descendant of an element marked with `data-focus-skip`, + * - it is `disabled`, + * - it is `inert`. + * + * Otherwise it counts as focusable. + * + * Properties are tested for before access, as the function may receive non-HTML elements. + * + * @param {Element} element + * @returns {boolean} - True if the element may be focused using the keyboard + */ +function isFocusable(element) { + // Has negative tabindex attribute explicitly set + if (parseInt(element.getAttribute("tabindex") || "", 10) <= -1) return false + // Is or descends from skipped element + if ( + element.hasAttribute("data-focus-skip") || + element.closest("[data-focus-skip]") != null + ) + return false + // Is inert + if ("inert" in element && element.inert) return false + // Is disabled + if ("disabled" in element && element.disabled) return false + + return true +} + +/** + * Get all candidates for receiving focus when moving from the active element in the given direction. + * + * @param {Direction} direction + * @param {Element} activeElement + * @param {Element} container + * @returns {Array} - All elements that lie in the direction of movement from the active element + */ +function getFocusCandidates(direction, activeElement, container) { + const activeRect = activeElement.getBoundingClientRect() + + let nextParent = activeElement || container + let candidateElements = [] + + do { + nextParent = + (nextParent.parentElement && + nextParent.parentElement.closest("[data-focus-group]")) || + container + + const annotatedElements = getFocusableElements(nextParent).map((e) => + annotate(direction, activeRect, e) + ) + + candidateElements = annotatedElements.filter(({ rect }) => { + switch (direction) { + case "left": + return Math.floor(rect.right) <= activeRect.left + case "up": + return Math.floor(rect.bottom) <= activeRect.top + case "right": + return Math.ceil(rect.left) >= activeRect.right + case "down": + return Math.ceil(rect.top) >= activeRect.bottom + } + }) + } while (candidateElements.length === 0 && nextParent !== container) + + return candidateElements +} + +/** + * Perform a move, guaranteeing that focus is going to change if `candidates` is non-empty. + * + * This function only selects the "best" from the list of candidates it is given. + * + * @param {Direction} direction + * @param {DOMRect} originRect - The bounding box of the element that has focus at the time the move is initiated + * @param {Array} candidates - The candidates from which to pick + * @returns {void} + */ +function performMove(direction, originRect, candidates) { + condsole.debug("performMove", direction, originRect, candidates) + + const originPoint = makeOrigin(direction, originRect) + + const candidatesInDirectProjection = candidates.filter((candidate) => + isWithinProjection(direction, originRect, candidate.rect) + ) + + if (candidatesInDirectProjection.length > 0) { + candidates = candidatesInDirectProjection + } + + const bestCandidate = getMinimumBy(candidates, (candidate) => + euclidean(originPoint, candidate.point) + ) + if (bestCandidate != null) { + applyFocus(direction, originRect, bestCandidate.element) + } +} + +/** + * Apply focus to an element, descending into it if it is a group. + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} target + * @returns {void} + */ +function applyFocus(direction, origin, target) { + condsole.debug("applyFocus", direction, target) + + const parentGroup = target.closest("[data-focus-group]") + if ( + parentGroup != null && + parentGroup != target && + getGroupType(parentGroup) === "memorize" + ) { + const memorizingElement = /** @type {MemorizingElement} */ (parentGroup) + memorizingElement.lastFocused = target + } + + if (isGroup(target)) { + dispatchGroupFocus(direction, origin, target) + } else if ("focus" in target && typeof target.focus === "function") { + target.focus() + } +} + +// +// Containers and focus traps +// + +/** + * Get the top-most blocking element on the page. + * + * This returns `document.body` if no other blocking elements are found. + * + * You can give a trap index to your elements, higher indices block lower + * indices. Just `data-focus-trap` is equivalent to `data-focus-trap="0"`. + * + * NOTE Because the web APIs are lacking, we have to determine the order of + * blocking elements heuristically. See open spec issues: + * + * - https://github.com/whatwg/html/issues/897 + * - https://github.com/whatwg/html/issues/8783 + * - https://github.com/whatwg/html/issues/9075 + * + * To work around this limitation you can use explicit trap indices. + * + * @returns {Element} + */ +function getBlockingElement() { + // Try top-layer pseudo class (2022+ browsers) + let trapElements = document.querySelectorAll(":modal") + // If none, use fallback selector + if (trapElements.length === 0) { + trapElements = document.querySelectorAll("dialog[open], [data-focus-trap]") + } + + // If no explicit trap elements were found, body is the top element + return ( + getMinimumBy(Array.from(trapElements), (elem) => -getTrapIndex(elem)) || + document.body + ) +} + +/** + * Get the trap index for an element. + * + * - The numeric value of `data-focus-trap` if attribute is set + * - `0` if the element has a boolean `data-focus-trap` attribute + * - `0` if the element is an open dialog element + * - `-Infinity` otherwise + * + * @param {Element} element + * @returns {number} + */ +function getTrapIndex(element) { + const attribute = element.getAttribute("data-focus-trap") + if (typeof attribute === "string" && /\d+/.test(attribute)) { + return parseInt(attribute, 10) + } else if (element.hasAttribute("data-focus-trap")) { + return 0 + } else if ( + element.tagName === "DIALOG" && + "open" in element && + element.open + ) { + return 0 + } else { + return -Infinity + } +} + +/** + * Get the currently active element, only within the given container. + * + * It might be that the document element has an active element, but the + * container does not. In this case the function returns `null`. + * + * @param {Element} container + * @returns {Element | null} + */ +function getActiveElement(container) { + const activeElement = document.activeElement + if ( + // The activeElement may be `null` or `document.body` if no element has focus + // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement#value + activeElement == null || + activeElement === container || + // Ignore the activeElement if it is not within container + !container.contains(activeElement) + ) { + return null + } + + return activeElement +} + +// +// Groups +// + +/** + * @typedef {'first' | 'last' | 'linear' | 'active' | 'memorize'} GroupType + */ + +/** + * Tests whether the element is annotated to be a group. + * + * @param {Element} element + * @returns {boolean} - True if the element is a group + */ +function isGroup(element) { + return getGroupType(element) != null +} + +/** + * Get the group type for an element, if any. + * + * @param {Element} element + * @returns {GroupType | null} - The group type, or `null` if element is not a group + */ +function getGroupType(element) { + if (!element.hasAttribute("data-focus-group")) { + return null + } + + const str = element.getAttribute("data-focus-group") + switch (str) { + case "first": + case "last": + case "linear": + case "active": + case "memorize": + return str + case "": + case null: + return "linear" + default: + console.warn(`Invalid focus group type: ${str}`) + return null + } +} + +/** + * Dispatches focus within a group. + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} group + * @returns {void} + */ +function dispatchGroupFocus(direction, origin, group) { + const strategy = getGroupType(group) + switch (strategy) { + case "first": + focusFirstElement(direction, origin, group) + break + case "last": + focusLastElement(direction, origin, group) + break + case "active": + focusActiveElement(direction, origin, group) + break + case "linear": + focusLinear(direction, origin, group) + break + case "memorize": + focusMemorized(direction, origin, group) + break + } +} + +/** + * Focuses the first element in the given focus group. + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} group + */ +function focusFirstElement(direction, origin, group) { + const focusables = getFocusableElements(group) + if (focusables.length > 0) { + applyFocus(direction, origin, focusables[0]) + } +} + +/** + * Focuses the last element in the given navigation group. + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} group + */ +function focusLastElement(direction, origin, group) { + const focusables = getFocusableElements(group) + if (focusables.length > 0) { + applyFocus(direction, origin, focusables[focusables.length - 1]) + } +} + +/** + * Focuses the active element in the given navigation group. + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} group + */ +function focusActiveElement(direction, origin, group) { + const activeElement = getFocusableElements(group).find((elem) => + elem.hasAttribute("data-focus-active") + ) + if (activeElement) { + applyFocus(direction, origin, activeElement) + } else { + focusFirstElement(direction, origin, group) + } +} + +/** + * Moves focus linearly in the direction of "travel". + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} group + */ +function focusLinear(direction, origin, group) { + const originPoint = makeOrigin(opposite(direction), origin) + const candidates = getFocusableElements(group).map((candidate) => + annotate(opposite(direction), origin, candidate) + ) + const bestCandidate = getMinimumBy(candidates, (candidate) => + euclidean(originPoint, candidate.point) + ) + if (bestCandidate != null) { + applyFocus(direction, origin, bestCandidate.element) + } +} + +/** + * Moves focus to the last focused element in the group. + * + * If a previously memorized element can not be found, behave as 'linear'. + * + * @param {Direction} direction + * @param {DOMRect} origin + * @param {Element} group + */ +function focusMemorized(direction, origin, group) { + if (isMemorizing(group) && group.contains(group.lastFocused)) { + applyFocus(direction, origin, group.lastFocused) + } else { + focusLinear(direction, origin, group) + } +} + +/** + * @typedef {Element & { lastFocused: Element; }} MemorizingElement - An HTML element with an additional memorized element property + */ + +/** + * Type guard for memorizing elements. + * + * @param {Element} elem + * @returns {elem is MemorizingElement} + * */ +function isMemorizing(elem) { + return "lastFocused" in elem && elem.lastFocused instanceof Element +} + +// +// DOM and Events +// + +/** + * Tests whether the keyboard event announces any modifier keys. + * + * @param {KeyboardEvent} e + * @returns {boolean} + */ +function hasModifiers(e) { + return ( + e.shiftKey || + e.ctrlKey || + e.metaKey || + e.altKey || + e.getModifierState("CapsLock") + ) +} + +/** + * Tests whether the keyboard event is a form interaction that should not lead to focus shifts. + * + * Adapted from the Spatial Navigation Polyfill. + * + * Original Copyright (c) 2018-2019 LG Electronics Inc. + * Source: https://github.com/WICG/spatial-navigation/polyfill + * Licensed under the MIT license (MIT) + * + * @param {Direction} direction - The direction read from the keydown event + * @param {KeyboardEvent} event - The original keydown event + * @returns {boolean} + */ +function isInputInteraction(direction, event) { + const eventTarget = document.activeElement + + if ( + eventTarget instanceof HTMLInputElement || + eventTarget instanceof HTMLTextAreaElement + ) { + const targetType = eventTarget.getAttribute("type") + const isTextualInput = [ + "email", + "password", + "text", + "search", + "tel", + "url", + null + ].includes(targetType) + const isSpinnable = + targetType != null && + ["date", "month", "number", "time", "week"].includes(targetType) + + if (isTextualInput || isSpinnable || eventTarget.nodeName === "TEXTAREA") { + // If there is a selection, assume user action is an input interaction + if (eventTarget.selectionStart !== eventTarget.selectionEnd) { + return true + // If there is only the cursor, check if it is natural to leave the element in given direction + } else { + const cursorPosition = eventTarget.selectionStart + const isVerticalMove = direction === "up" || direction === "down" + + if (eventTarget.value.length === 0) { + // If field is empty, leave in any direction + return false + } else if (cursorPosition == null) { + // If cursor position was not given, we always exit unless we see a "spinning" input + return isSpinnable && isVerticalMove + } else if (cursorPosition === 0) { + // Cursor at beginning + return direction === "right" || (isSpinnable && isVerticalMove) + } else if (cursorPosition === eventTarget.value.length) { + // Cursor at end + return direction === "left" || (isSpinnable && isVerticalMove) + } else { + // Cursor in middle + return ( + direction === "left" || + direction === "right" || + (isSpinnable && isVerticalMove) + ) + } + } + } else { + return false + } + } else { + return false + } +} + +/** + * Type guard for tabindexed elements. + * + * @param {Element} elem + * @returns {elem is Element & { tabIndex: number; }} + * */ +function hasTabIndex(elem) { + return "tabIndex" in elem && typeof elem.tabIndex === "number" +} + +// +// Geometry +// + +/** + * @typedef {'up' | 'right' | 'down' | 'left'} Direction + */ + +/** + * Returns the opposite direction. + * + * @param {Direction} direction + * @returns {Direction} + */ +function opposite(direction) { + switch (direction) { + case "left": + return "right" + case "up": + return "down" + case "right": + return "left" + case "down": + return "up" + } +} + +/** + * Make the target point for a move between origin and target rect in given direction. + * + * @param {Direction} direction + * @param {DOMRect} originRect + * @param {DOMRect} targetRect + * @returns {Point} + */ +function makeTarget(direction, originRect, targetRect) { + switch (direction) { + case "left": + return { + x: targetRect.right, + y: closestTo( + (originRect.top + originRect.bottom) / 2, + targetRect.top, + targetRect.bottom + ) + } + case "up": + return { + x: closestTo( + (originRect.left + originRect.right) / 2, + targetRect.left, + targetRect.right + ), + y: targetRect.bottom + } + case "right": + return { + x: targetRect.left, + y: closestTo( + (originRect.top + originRect.bottom) / 2, + targetRect.top, + targetRect.bottom + ) + } + case "down": + return { + x: closestTo( + (originRect.left + originRect.right) / 2, + targetRect.left, + targetRect.right + ), + y: targetRect.top + } + } +} + +/** + * Make the origin point for a move between origin and target rect in given direction. + * + * @param {Direction} direction + * @param {DOMRect} originRect + * @returns {Point} + */ +function makeOrigin(direction, originRect) { + switch (direction) { + case "left": + return { x: originRect.left, y: originRect.top + originRect.height / 2 } + case "up": + return { x: originRect.left + originRect.width / 2, y: originRect.top } + case "right": + return { x: originRect.right, y: originRect.top + originRect.height / 2 } + case "down": + return { x: originRect.left + originRect.width / 2, y: originRect.bottom } + } +} + +/** + * Make the virtual origin a movement would be expected to come from. + * + * This allows us to jump into the viewport from any of the four directions. + * + * │ + * ▼ ArrowDown + * ArrowRight ──►┌────────────────────────┐◄─ + * │ │ ArrowLeft + * │ │ + * │ │ + * │ │ + * │ │ + * │ │ + * │ │ + * │ │ + * │ │ + * └────────────────────────┘ + * ▲ + * │ ArrowUp + * + * To keep it simple and based on own needs this assumes LTR text direction. + * It could try to determine the user agent's preferred direction instead. + * + * @param {Direction} direction + * @returns {DOMRect} - The region of the virtual origin + */ +function makeVirtualOrigin(direction) { + const width = window.innerWidth + const height = window.innerHeight + switch (direction) { + case "down": + case "right": + return DOMRect.fromRect({ x: 0, y: 0, width: 0, height: 0 }) + case "left": + return DOMRect.fromRect({ x: width, y: 0, width: 0, height: 0 }) + case "up": + return DOMRect.fromRect({ x: 0, y: height, width: 0, height: 0 }) + } +} + +/** + * Map the `key` property of a keyboard event to a `Direction`. + * + * @type {Object.} + */ +const KEY_TO_DIRECTION = { + ArrowUp: "up", + ArrowRight: "right", + ArrowDown: "down", + ArrowLeft: "left" +} + +/** + * @typedef {Object} AnnotatedElement - An HTML element annotated with spatial information, specific to a move + * @property {Element} element - The element + * @property {DOMRect} rect - The bounding box for the element + * @property {Point} point - The point defined as characteristic for the given move + */ + +/** + * Annotate an element with meta information for a given move. + * + * @param {Direction} direction + * @param {DOMRect} originRect + * @param {Element} element + * @returns {AnnotatedElement} + */ +function annotate(direction, originRect, element) { + const rect = element.getBoundingClientRect() + return { + element: element, + rect: rect, + point: makeTarget(direction, originRect, rect) + } +} + +/** + * @typedef {{ x: number; y: number; }} Point + */ + +/** + * Computes the Euclidean distance between two points. + * + * @param {Point} a + * @param {Point} b + * @returns {number} + */ +function euclidean(a, b) { + return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) +} + +/** + * Find the value closest to a given value that lies within the interval. + * + * @param {number} val - The value of interest + * @param {number} intervalLower - The lower boundary of the interval + * @param {number} intervalUpper - The upper boundary of the interval + * @returns {number} - The value within the interval that is closest to the value of interest + */ +function closestTo(val, intervalLower, intervalUpper) { + if (val >= intervalLower && val <= intervalUpper) { + return val + } else if (val > intervalUpper) { + return intervalUpper + } else { + return intervalLower + } +} + +/** + * Tests whether the candidate lies within the directed projection from the origin. + ii* + * @param {Direction} direction + * @param {DOMRect} origin + * @param {DOMRect} candidate + * @returns {boolean} - True if the candidate lies within the projection + */ +function isWithinProjection(direction, origin, candidate) { + switch (direction) { + case "left": + case "right": + return hasOverlap( + candidate.top, + candidate.bottom, + origin.top, + origin.bottom + ) + case "up": + case "down": + return hasOverlap( + candidate.left, + candidate.right, + origin.left, + origin.right + ) + default: + return false + } +} + +/** + * Tests whether two intervals overlap. + * + * @param {number} start1 - The start of the first interval + * @param {number} end1 - The end of the first interval + * @param {number} start2 - The start of the second interval + * @param {number} end2 - The end of the second interval + * @returns {boolean} + */ +function hasOverlap(start1, end1, start2, end2) { + return !(start1 > end2 || start2 > end1) +} + +// +// Generic utilities +// + +/** + * Returns the element in `array` for which `toNumeric` is minimal. + * + * @template T + * @param {Array} array + * @param {(item: T) => number} toNumber + * @returns {T | null} + */ +function getMinimumBy(array, toNumber) { + let minVal = Infinity + let min = null + let currentVal = Infinity + + for (let current of array) { + currentVal = toNumber(current) + + if (currentVal < minVal) { + minVal = currentVal + min = current + } + } + + return min +} + +const condsole = /** @type {Console} */ ( + new Proxy(console, { + get: /** @type {(target: any, level: any) => any} */ ( + function (target, level) { + //return function () {} + if (level in target && typeof target[level] === "function") { + return /** @type {(args: any[]) => void} */ ( + function (...args) { + target[level].apply(target, args) + } + ) + } else if (level in target) { + return target[level] + } + } + ) + }) +) + +init() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b3c12a3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2105 @@ +{ + "name": "focus-shift", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "focus-shift", + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "cypress": "^13.6.4", + "playwright-webkit": "1.34", + "prettier": "^3.2.4", + "typescript": "^5.3.3" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@types/node": { + "version": "20.11.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.15.tgz", + "integrity": "sha512-gscmuADZfvNULx1eyirVbr3kVOVZtpQtzKMCZpeSZcN6MfbkRXAR4s9/gsQ4CzxLHw6EStDtKLNtSDL3vbq05A==", + "dev": true, + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cypress": { + "version": "13.6.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz", + "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.6.0", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/playwright-core": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz", + "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/playwright-webkit": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/playwright-webkit/-/playwright-webkit-1.34.3.tgz", + "integrity": "sha512-F5JJlmq1VvRpblPgYId2/NZb/WycNgSBgv9siF8H1yFbcssohNLEwHyG3U7QWqr3FAa07XEr5EVj3pnzLnR2eQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.34.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/prettier": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c0e2d49 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "focus-shift", + "version": "0.0.0", + "description": "A simple JavaScript library to shift element focus with the arrow keys", + "keywords": [ + "spatial navigation", + "keyboard navigation", + "kiosk", + "focus" + ], + "files": [ + "index.js", + "README.md", + "LICENSE" + ], + "browser": "index.js", + "directories": { + "example": "examples" + }, + "scripts": { + "test": "tsc", + "format": "prettier --write ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dividat/focus-shift.git" + }, + "author": "Dividat AG", + "license": "MIT", + "bugs": { + "url": "https://github.com/dividat/focus-shift/issues" + }, + "homepage": "https://github.com/dividat/focus-shift", + "devDependencies": { + "cypress": "^13.6.4", + "playwright-webkit": "1.34", + "prettier": "^3.2.4", + "typescript": "^5.3.3" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b2f3514 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "files": ["index.js"], + "compilerOptions": { + /* Language and Environment */ + "target": "es2016", + "lib": ["es2016", "dom"], + "strict": true, + + /* JSDoc checking */ + "allowJs": true, + "checkJs": true, + "noEmit": true, + + /* Interop Constraints */ + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} From ac7c0f1fbafa8f90bffdb1f2822142dae9b5d7a2 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Fri, 2 Feb 2024 12:36:48 +0100 Subject: [PATCH 3/4] Add inspos --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f9ceae7..ebb152b 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,12 @@ End-to-end tests are done using Cypress. Contributions are welcome. Please fork the repository and submit a pull request with your proposed changes. +## Related Work and Inspiration + +- https://github.com/bbc/lrud-spatial, a nice and simple but more spatially oriented library +- https://github.com/luke-chang/js-spatial-navigation, spatial navigation library with good functionality but JavaScript-focused and stateful configuration +- https://github.com/WICG/spatial-navigation, a possibly abandoned proposal for a Web Platform API + ## License (C) Copyright 2024 Dividat AG From 1554e0f455f42d9bf90c757b31cdcc898c68c1d5 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Sat, 10 Feb 2024 13:04:14 +0100 Subject: [PATCH 4/4] Move debug logs behind global flag --- README.md | 2 ++ index.js | 31 +++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ebb152b..c622653 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ The following attributes may be added in the markup to guide the moving of focus - `data-focus-skip`: Skips the element and its descendants in navigation. - `data-focus-trap`: Only allows elements within the annotated layer to receive focus. +Setting `window.FOCUS_SHIFT_DEBUG = true` lets the library log processing steps to the browser's console. + ## Principles and Scope - **It doesn't just work.** It would be nice if focus could automatically move to the intuitive element in each case, but this seems to require a sophisticated model of visual weight and Gestalt principles. This is out of scope for a simple library like this. diff --git a/index.js b/index.js index 8f2c91a..e849923 100644 --- a/index.js +++ b/index.js @@ -28,10 +28,10 @@ function handleKeyDown(event) { ) { return } else { - condsole.group(event.key) + logging.group(`focus-shift: ${event.key}`) event.preventDefault() handleUserDirection(KEY_TO_DIRECTION[event.key]) - condsole.groupEnd() + logging.groupEnd() } } @@ -199,7 +199,7 @@ function getFocusCandidates(direction, activeElement, container) { * @returns {void} */ function performMove(direction, originRect, candidates) { - condsole.debug("performMove", direction, originRect, candidates) + logging.debug("performMove", direction, originRect, candidates) const originPoint = makeOrigin(direction, originRect) @@ -228,7 +228,7 @@ function performMove(direction, originRect, candidates) { * @returns {void} */ function applyFocus(direction, origin, target) { - condsole.debug("applyFocus", direction, target) + logging.debug("applyFocus", direction, target) const parentGroup = target.closest("[data-focus-group]") if ( @@ -891,19 +891,22 @@ function getMinimumBy(array, toNumber) { return min } -const condsole = /** @type {Console} */ ( +const logging = /** @type {Console} */ ( new Proxy(console, { get: /** @type {(target: any, level: any) => any} */ ( function (target, level) { - //return function () {} - if (level in target && typeof target[level] === "function") { - return /** @type {(args: any[]) => void} */ ( - function (...args) { - target[level].apply(target, args) - } - ) - } else if (level in target) { - return target[level] + if ("FOCUS_SHIFT_DEBUG" in window && window.FOCUS_SHIFT_DEBUG) { + if (level in target && typeof target[level] === "function") { + return /** @type {(args: any[]) => void} */ ( + function (...args) { + target[level].apply(target, args) + } + ) + } else if (level in target) { + return target[level] + } + } else { + return function () {} } } )