Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,8 @@
"name": "pnpm",
"onFail": "warn"
}
},
"dependencies": {
"@medv/finder": "^4.0.2"
}
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 6 additions & 46 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ScrollState,
Target,
} from "./defs.js";
import { finder } from "@medv/finder";

/** The logger and event prefix for the debug mode */
export const prefix = "restore-scroll";
Expand Down Expand Up @@ -80,54 +81,13 @@ export function isScrollState(value: unknown): value is ScrollState {

/**
* Create a unique CSS selector for a given DOM element.
* The selector is built from tag names, IDs, classes, and :nth-child where necessary.
* Uses @medv/finder library for robust selector generation.
*/
function createUniqueSelector(el: Element): string {
if (el.id) return `#${el.id}`;

const path: string[] = [];

// Traverse up the DOM tree from the element to the root <html> element
while (el && el.nodeType === Node.ELEMENT_NODE) {
// Start with the lowercase tag name (e.g., "div", "span")
let selector = el.nodeName.toLowerCase();

// If the element has an ID, use it as it's guaranteed to be unique in the document
if (el.id) {
selector += `#${el.id}`;
path.unshift(selector); // Add to the beginning of the path
break; // No need to go further up the tree
}

// If the element has class names, add them (dot-separated like in CSS)
if (el.className && typeof el.className === "string") {
// Clean up and convert class names to a valid CSS class selector
const classes = el.className.trim().split(/\s+/).join(".");
if (classes) {
selector += `.${classes}`;
}
}

// Use :nth-child() if the element is one of multiple siblings
const parent = el.parentNode as Element;
if (parent) {
const siblings = Array.from(parent.children);
if (siblings.length > 1) {
// Get the element's index among its siblings (1-based index for CSS)
const index = siblings.indexOf(el) + 1;
selector += `:nth-child(${index})`;
}
}

// Add the constructed selector for this level to the front of the path
path.unshift(selector);

// Move up to the parent element
el = el.parentElement!;
}

// Combine all parts of the path with `>` to form a full unique selector
return path.join(" > ");
// Use finder library to generate an optimal unique selector
return finder(el, {
root: document.body,
});
}

/**
Expand Down
25 changes: 14 additions & 11 deletions tests/unit/tests/storageSelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ describe("createContainerSelector", () => {
<div></div>
<div class="scroller"></div>
</main>
`)
);
expect(createContainerSelector(document.querySelector(".scroller")!)).toEqual(
"html > body:nth-child(2) > main > div.scroller:nth-child(3)"
`),
);
// The finder library generates optimized selectors
// In this case, .scroller is unique enough
expect(
createContainerSelector(document.querySelector(".scroller")!),
).toEqual(".scroller");
});

it("should use ':root' if not inside the body", () => {
Expand All @@ -38,12 +40,12 @@ describe("createContainerSelector", () => {
<div></div>
<div id="scroller"></div>
</main>
`)
`),
);

expect(createContainerSelector(document.querySelector("#scroller")!)).toEqual(
"#scroller"
);
expect(
createContainerSelector(document.querySelector("#scroller")!),
).toEqual("#scroller");
});

it("should inject the storage selector into the element", () => {
Expand All @@ -54,12 +56,13 @@ describe("createContainerSelector", () => {
<div></div>
<div class="scroller"></div>
</main>
`)
`),
);
restoreScroll(document.querySelector(".scroller"));
// The finder library generates optimized selectors
expect(
document.querySelector<ScrollContainer>(".scroller")?.__restore_scroll
?.selector
).toEqual("html > body:nth-child(2) > main > div.scroller:nth-child(3)");
?.selector,
).toEqual(".scroller");
});
});
79 changes: 78 additions & 1 deletion tests/unit/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,83 @@
import { vi } from 'vitest';
import { vi } from "vitest";

// Stub browser functions for vitest
// console.log = vi.fn();
// console.warn = vi.fn();
// console.error = vi.fn();

// Polyfill CSS.escape for jsdom
// Implementation based on: https://drafts.csswg.org/cssom/#the-css.escape()-method
declare global {
interface CSS {
escape(value: string): string;
}
}

if (!globalThis.CSS || !globalThis.CSS.escape) {
globalThis.CSS = globalThis.CSS || ({} as CSS);
globalThis.CSS.escape = (value: string): string => {
if (typeof value !== "string") {
throw new TypeError("CSS.escape requires a string argument");
}

const length = value.length;
let result = "";
let index = 0;

while (index < length) {
const character = value.charAt(index);
const charCode = character.charCodeAt(0);

// Handle null character
if (charCode === 0x0000) {
result += "\uFFFD";
}
// If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F
else if (
(charCode >= 0x0001 && charCode <= 0x001f) ||
charCode === 0x007f
) {
result += "\\" + charCode.toString(16) + " ";
}
// If the character is the first character and is a digit (U+0030 to U+0039)
else if (index === 0 && charCode >= 0x0030 && charCode <= 0x0039) {
result += "\\" + charCode.toString(16) + " ";
}
// If the character is the second character and is a digit (U+0030 to U+0039)
// and the first character is a `-` (U+002D)
else if (
index === 1 &&
charCode >= 0x0030 &&
charCode <= 0x0039 &&
value.charCodeAt(0) === 0x002d
) {
result += "\\" + charCode.toString(16) + " ";
}
// If the character is the first character and is a `-` (U+002D), and there is no second character
else if (index === 0 && length === 1 && charCode === 0x002d) {
result += "\\" + character;
}
// If the character is not handled above and is greater than or equal to U+0080,
// is `-` (U+002D) or `_` (U+005F), or is in one of the ranges [0-9] (U+0030 to U+0039),
// [A-Z] (U+0041 to U+005A), or [a-z] (U+0061 to U+007A)
else if (
charCode >= 0x0080 ||
charCode === 0x002d ||
charCode === 0x005f ||
(charCode >= 0x0030 && charCode <= 0x0039) ||
(charCode >= 0x0041 && charCode <= 0x005a) ||
(charCode >= 0x0061 && charCode <= 0x007a)
) {
result += character;
}
// Otherwise, the escaped character
else {
result += "\\" + character;
}

index++;
}

return result;
};
}
Loading