@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/.turbo/turbo-test.log +19 -0
- package/README.md +60 -0
- package/coverage/lcov-report/base.css +362 -0
- package/coverage/lcov-report/block-navigation.js +82 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/index.ts.html +88 -0
- package/coverage/lcov-report/prettify.css +101 -0
- package/coverage/lcov-report/prettify.js +937 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +205 -0
- package/coverage/lcov-report/uniqueSelectorOf.ts.html +322 -0
- package/coverage/lcov.info +75 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.d.cts +19 -0
- package/dist/esm/chunk-GHJADCBS.js +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/uniqueSelectorOf.d.ts +19 -0
- package/dist/esm/uniqueSelectorOf.js +1 -0
- package/package.json +29 -0
- package/src/index.ts +1 -0
- package/src/uniqueSelectorOf.spec.ts +242 -0
- package/src/uniqueSelectorOf.ts +79 -0
- package/tsconfig.build.json +8 -0
- package/tsup.config.ts +26 -0
- package/vitest.config.ts +9 -0
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
|
+
}
|
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
|
+
]);
|