@library/dom 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@library/dom",
3
+ "version": "0.0.6",
4
+ "type": "module",
5
+ "main": "./dist/cjs/index.cjs",
6
+ "types": "./dist/cjs/index.d.cts",
7
+ "module": "./dist/esm/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "default": "./dist/esm/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/cjs/index.d.cts",
16
+ "default": "./dist/cjs/index.cjs"
17
+ }
18
+ }
19
+ },
20
+ "license": "MIT",
21
+ "scripts": {
22
+ "build": "rimraf dist && tsup --tsconfig tsconfig.build.json",
23
+ "test": "vitest run"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "gitHead": "e1d0caf47e9ca8305ad84b2e2284c30fbe6b754d"
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { uniqueSelectorOf } from "./uniqueSelectorOf";
@@ -0,0 +1,242 @@
1
+ import { uniqueSelectorOf } from "./uniqueSelectorOf";
2
+
3
+ describe("uniqueSelectorOf", () => {
4
+ beforeEach(() => {
5
+ document.body.innerHTML = "";
6
+ });
7
+
8
+ it("should return ID selector when element has an ID", () => {
9
+ document.body.innerHTML = '<div id="myId"></div>';
10
+ const el = document.getElementById("myId")!;
11
+
12
+ expect(uniqueSelectorOf(el)).toBe("#myId");
13
+ });
14
+
15
+ it("should return unique class selector", () => {
16
+ document.body.innerHTML = '<div class="unique-class"></div>';
17
+ const el = document.querySelector(".unique-class") as HTMLElement;
18
+
19
+ expect(uniqueSelectorOf(el)).toBe(".unique-class");
20
+ });
21
+
22
+ it("should return combined class selector when single classes are not unique", () => {
23
+ document.body.innerHTML = `
24
+ <div class="common another"></div>
25
+ <div class="common unique"></div>
26
+ `;
27
+ const el = document.querySelector(".unique") as HTMLElement;
28
+
29
+ // .unique is actually unique in this case, so it should return .unique
30
+ expect(uniqueSelectorOf(el)).toBe(".unique");
31
+ });
32
+
33
+ it("should prefer single class over combined classes when unique", () => {
34
+ document.body.innerHTML = '<div class="first second"></div>';
35
+ const el = document.querySelector(".first") as HTMLElement;
36
+
37
+ const result = uniqueSelectorOf(el);
38
+ expect(result === ".first" || result === ".second").toBe(true);
39
+ });
40
+
41
+ it("should return path with nth-of-type when classes are not unique", () => {
42
+ document.body.innerHTML = `
43
+ <div>
44
+ <p>First</p>
45
+ <p>Second</p>
46
+ <p>Third</p>
47
+ </div>
48
+ `;
49
+ const el = document.querySelectorAll("p")[1] as HTMLElement;
50
+
51
+ // Returns shortest unique path - p:nth-of-type(2) is already unique
52
+ expect(uniqueSelectorOf(el)).toBe("p:nth-of-type(2)");
53
+ });
54
+
55
+ it("should return shortest unique path", () => {
56
+ document.body.innerHTML = `
57
+ <div>
58
+ <section>
59
+ <p>First</p>
60
+ </section>
61
+ </div>
62
+ `;
63
+ const el = document.querySelector("p") as HTMLElement;
64
+
65
+ // Should return shortest unique path
66
+ expect(uniqueSelectorOf(el)).toBe("p");
67
+ });
68
+
69
+ it("should handle nested elements with same tag", () => {
70
+ document.body.innerHTML = `
71
+ <div>
72
+ <span>1</span>
73
+ <span>2</span>
74
+ </div>
75
+ <div>
76
+ <span>3</span>
77
+ <span>4</span>
78
+ </div>
79
+ `;
80
+ const el = document.querySelectorAll("span")[2] as HTMLElement;
81
+
82
+ expect(uniqueSelectorOf(el)).toBe(
83
+ "div:nth-of-type(2) > span:nth-of-type(1)",
84
+ );
85
+ });
86
+
87
+ it("should stop at parent with ID when needed", () => {
88
+ document.body.innerHTML = `
89
+ <div id="parent">
90
+ <p>Content</p>
91
+ </div>
92
+ <div>
93
+ <p>Another p</p>
94
+ </div>
95
+ `;
96
+ const el = document.querySelector("#parent p") as HTMLElement;
97
+
98
+ // Should use parent ID to make it unique
99
+ expect(uniqueSelectorOf(el)).toBe("#parent > p");
100
+ });
101
+
102
+ it("should return unique selector for deeply nested elements", () => {
103
+ document.body.innerHTML = `
104
+ <div>
105
+ <div>
106
+ <div>
107
+ <span>Target</span>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ `;
112
+ const el = document.querySelector("span") as HTMLElement;
113
+
114
+ const selector = uniqueSelectorOf(el)!;
115
+ expect(document.querySelectorAll(selector).length).toBe(1);
116
+ expect(document.querySelector(selector)).toBe(el);
117
+ });
118
+
119
+ it("should handle elements with multiple non-unique classes", () => {
120
+ document.body.innerHTML = `
121
+ <div class="common shared">1</div>
122
+ <div class="common shared">2</div>
123
+ <div class="common shared">3</div>
124
+ `;
125
+ const el = document.querySelectorAll("div")[1] as HTMLElement;
126
+
127
+ const selector = uniqueSelectorOf(el)!;
128
+ expect(document.querySelectorAll(selector).length).toBe(1);
129
+ expect(document.querySelector(selector)).toBe(el);
130
+ });
131
+
132
+ it("should handle body element", () => {
133
+ const selector = uniqueSelectorOf(document.body)!;
134
+
135
+ expect(selector).toBe("body");
136
+ expect(document.querySelector(selector)).toBe(document.body);
137
+ });
138
+
139
+ it("should return unique selector even for complex DOM", () => {
140
+ document.body.innerHTML = `
141
+ <div>
142
+ <div>
143
+ <span class="common">1</span>
144
+ <span class="common">2</span>
145
+ </div>
146
+ <div>
147
+ <span class="common">3</span>
148
+ <span class="common">4</span>
149
+ </div>
150
+ </div>
151
+ `;
152
+ const el = document.querySelectorAll(".common")[2] as HTMLElement;
153
+
154
+ const selector = uniqueSelectorOf(el)!;
155
+ expect(document.querySelectorAll(selector).length).toBe(1);
156
+ expect(document.querySelector(selector)).toBe(el);
157
+ });
158
+
159
+ it("should handle elements with empty class attribute", () => {
160
+ document.body.innerHTML = `
161
+ <div class="">
162
+ <p></p>
163
+ </div>
164
+ `;
165
+ const el = document.querySelector("p") as HTMLElement;
166
+
167
+ expect(uniqueSelectorOf(el)).toBe("p");
168
+ });
169
+
170
+ it("should verify returned selector actually queries the correct element", () => {
171
+ document.body.innerHTML = `
172
+ <div id="container">
173
+ <div class="row">
174
+ <span>A</span>
175
+ <span>B</span>
176
+ </div>
177
+ <div class="row">
178
+ <span>C</span>
179
+ <span>D</span>
180
+ </div>
181
+ </div>
182
+ `;
183
+
184
+ const allSpans = document.querySelectorAll("span");
185
+
186
+ allSpans.forEach((span) => {
187
+ const selector = uniqueSelectorOf(span as HTMLElement)!;
188
+ const queried = document.querySelector(selector);
189
+
190
+ expect(queried).toBe(span);
191
+ expect(document.querySelectorAll(selector).length).toBe(1);
192
+ });
193
+ });
194
+
195
+ it("should return combined class selector when unique", () => {
196
+ document.body.innerHTML = `
197
+ <div class="foo">1</div>
198
+ <div class="bar">2</div>
199
+ <div class="foo bar">3</div>
200
+ `;
201
+ const el = document.querySelector(".foo.bar") as HTMLElement;
202
+
203
+ expect(uniqueSelectorOf(el)).toBe(".foo.bar");
204
+ });
205
+
206
+ it("should handle elements without parent", () => {
207
+ const html = document.documentElement;
208
+
209
+ const selector = uniqueSelectorOf(html);
210
+ expect(selector).toBe("html");
211
+ });
212
+
213
+ it("should handle case where path never becomes unique until root", () => {
214
+ // Create a scenario where we traverse all the way to root
215
+ // This tests the final return path.join(' > ') at line 79
216
+ document.body.innerHTML = `
217
+ <div>
218
+ <div>
219
+ <div>
220
+ <div>
221
+ <span></span>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ `;
227
+
228
+ const span = document.querySelector("span") as HTMLElement;
229
+ const selector = uniqueSelectorOf(span)!;
230
+
231
+ // Should return the full path and be unique
232
+ expect(document.querySelectorAll(selector).length).toBe(1);
233
+ expect(document.querySelector(selector)).toBe(span);
234
+ });
235
+
236
+ it("should return null when unable to generate unique selector", () => {
237
+ // Create a detached element that's not in the document
238
+ const detachedElement = document.createElement("div");
239
+
240
+ expect(uniqueSelectorOf(detachedElement)).toBeNull();
241
+ });
242
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Generate the shortest unique querySelector string that identifies the given element.
3
+ * Priority: id > unique class > path with nth-of-type
4
+ * @param {HTMLElement} element - The HTML element to generate a query selector for.
5
+ * @returns {string | null} - A unique query selector string that can be used with document.querySelector.
6
+ * @example
7
+ * ```
8
+ * import { queryKeyOf } from "@library/dom";
9
+ *
10
+ * const el = document.getElementById('myId');
11
+ * queryKeyOf(el); // "#myId"
12
+ *
13
+ * const el2 = document.querySelector('.unique-class');
14
+ * queryKeyOf(el2); // ".unique-class"
15
+ * ```
16
+ */
17
+ export function uniqueSelectorOf(element: HTMLElement): string | null {
18
+ // 1. ID is the shortest and always unique
19
+ if (element.id) {
20
+ return `#${element.id}`;
21
+ }
22
+
23
+ // 2. Check for unique class combinations
24
+ if (element.className) {
25
+ const classes = element.className.trim().split(/\s+/).filter(Boolean);
26
+
27
+ // Try single classes first
28
+ for (const cls of classes) {
29
+ const selector = `.${cls}`;
30
+ if (document.querySelectorAll(selector).length === 1) {
31
+ return selector;
32
+ }
33
+ }
34
+
35
+ // Try class combinations
36
+ if (classes.length > 1) {
37
+ const selector = classes.map((c) => `.${c}`).join("");
38
+ if (document.querySelectorAll(selector).length === 1) {
39
+ return selector;
40
+ }
41
+ }
42
+ }
43
+
44
+ // 3. Build path from root until unique
45
+ const path: string[] = [];
46
+ let current: HTMLElement | null = element;
47
+
48
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
49
+ let selector = current.tagName.toLowerCase();
50
+
51
+ // If parent exists, add nth-of-type
52
+ if (current.parentElement) {
53
+ const siblings = Array.from(current.parentElement.children).filter(
54
+ (el) => el.tagName === current!.tagName,
55
+ );
56
+
57
+ if (siblings.length > 1) {
58
+ const index = siblings.indexOf(current) + 1;
59
+ selector += `:nth-of-type(${index})`;
60
+ }
61
+ }
62
+
63
+ path.unshift(selector);
64
+
65
+ // Check if current path is unique
66
+ const currentPath = path.join(" > ");
67
+ if (document.querySelectorAll(currentPath).length === 1) {
68
+ return currentPath;
69
+ }
70
+
71
+ // Move to parent, but stop at elements with ID
72
+ current = current.parentElement;
73
+ if (current?.id) {
74
+ path.unshift(`#${current.id}`);
75
+ return path.join(" > ");
76
+ }
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src/**/*"],
7
+ "exclude": ["**/*.spec.ts"]
8
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ const common = {
4
+ dts: true,
5
+ clean: true,
6
+ sourcemap: false,
7
+ skipNodeModulesBundle: true,
8
+ minify: true,
9
+ } as const;
10
+
11
+ export default defineConfig([
12
+ {
13
+ ...common,
14
+ format: ["cjs"],
15
+ target: ["es2016"],
16
+ outDir: "dist/cjs",
17
+ entry: ["src/index.ts"],
18
+ },
19
+ {
20
+ ...common,
21
+ format: ["esm"],
22
+ target: ["esnext"],
23
+ outDir: "dist/esm",
24
+ entry: ["src/**/*.ts", "!src/**/*.spec.ts"],
25
+ },
26
+ ]);
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { baseConfig } from "../../vitest.config.base";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ ...baseConfig,
7
+ environment: "jsdom",
8
+ },
9
+ });