├── .github
└── workflows
│ ├── ci.yml
│ └── npm-publish.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── example
├── example.js
├── index.html
├── package-lock.json
└── package.json
├── package-lock.json
├── package.json
├── src
├── element-overlay.ts
├── element-picker.ts
├── index.ts
└── utils.ts
└── tsconfig.json
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: 14
22 | - run: npm ci
23 | - run: npm run build
24 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: Node.js Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 14
15 | - run: npm ci
16 | - run: npm run build
17 |
18 | publish-npm:
19 | needs: build
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v2
23 | - uses: actions/setup-node@v1
24 | with:
25 | node-version: 14
26 | registry-url: https://registry.npmjs.org/
27 | - run: npm ci
28 | - run: npm publish
29 | env:
30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Harry Marr
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pick-dom-element
2 |
3 | [](https://badge.fury.io/js/pick-dom-element)
4 |
5 | A JavaScript library (written in TypeScript) for interactively picking DOM elements.
6 |
7 |
8 |
9 |
10 |
11 | ## Usage
12 |
13 | Create an instance of the `ElementPicker` class, and call its `start()` method to start picking. Provide an `onHover` or `onClick` callback to get the picked element(s). Call `stop()` to stop picking and remove the overlay from the DOM.
14 |
15 | ```javascript
16 | import { ElementPicker } from "pick-dom-element";
17 |
18 | const style = { borderColor: "#0000ff" };
19 | const picker = new ElementPicker({ style });
20 | picker.start({
21 | onHover: (el) => console.log(`Hover: ${el}`),
22 | onClick: (el) => {
23 | picker.stop();
24 | console.log(`Picked: ${el}`);
25 | },
26 | });
27 | ```
28 |
29 | See the [example](example/) directory for a more complete example of how to use the library.
30 |
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | import { ElementPicker } from "pick-dom-element";
2 |
3 | function main() {
4 | const status = document.getElementById("status");
5 | const startButton = document.getElementById("start");
6 | const onlyEmphasisCheckbox = document.getElementById("only-emphasis");
7 |
8 | const setElement = (el) => {
9 | const tags = [];
10 | while (el.parentNode) {
11 | tags.push(el.tagName);
12 | el = el.parentNode;
13 | }
14 | status.innerText = tags
15 | .reverse()
16 | .map((t) => t.toLowerCase())
17 | .join(" > ");
18 | };
19 |
20 | const picker = new ElementPicker({
21 | style: {
22 | background: "rgba(153, 235, 255, 0.5)",
23 | borderColor: "yellow"
24 | },
25 | });
26 | let onlyEmphasis = onlyEmphasisCheckbox.checked;
27 | onlyEmphasisCheckbox.onchange = (ev) => {
28 | onlyEmphasis = ev.target.checked;
29 | }
30 | const start = () => {
31 | startButton.disabled = true;
32 | picker.start({
33 | onHover: setElement,
34 | onClick: () => {
35 | picker.stop();
36 | startButton.disabled = false;
37 | },
38 | elementFilter: (el) => {
39 | if (!onlyEmphasis) {
40 | return true;
41 | }
42 | return ['I', 'B'].includes(el.tagName);
43 | }
44 | });
45 | };
46 |
47 | startButton.addEventListener("click", start);
48 | }
49 |
50 | document.addEventListener("DOMContentLoaded", main);
51 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 | Element Picker example
19 |
20 | This is an example page . Try hovering over some elements.
22 |
23 | Click "start" to activate the picker.
24 | Start
25 | Only emphasis tags
26 |
27 |
28 |
--------------------------------------------------------------------------------
/example/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pick-dom-element-example",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "pick-dom-element-example",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "pick-dom-element": "file:.."
13 | },
14 | "devDependencies": {
15 | "vite": "^2.9.13"
16 | }
17 | },
18 | "..": {
19 | "version": "0.2.1",
20 | "license": "MIT",
21 | "devDependencies": {
22 | "typescript": "^4.7.4"
23 | }
24 | },
25 | "../node_modules/typescript": {
26 | "version": "4.5.4",
27 | "dev": true,
28 | "license": "Apache-2.0",
29 | "bin": {
30 | "tsc": "bin/tsc",
31 | "tsserver": "bin/tsserver"
32 | },
33 | "engines": {
34 | "node": ">=4.2.0"
35 | }
36 | },
37 | "node_modules/esbuild-darwin-arm64": {
38 | "version": "0.14.48",
39 | "cpu": [
40 | "arm64"
41 | ],
42 | "dev": true,
43 | "license": "MIT",
44 | "optional": true,
45 | "os": [
46 | "darwin"
47 | ],
48 | "engines": {
49 | "node": ">=12"
50 | }
51 | },
52 | "node_modules/function-bind": {
53 | "version": "1.1.1",
54 | "dev": true,
55 | "license": "MIT"
56 | },
57 | "node_modules/has": {
58 | "version": "1.0.3",
59 | "dev": true,
60 | "license": "MIT",
61 | "dependencies": {
62 | "function-bind": "^1.1.1"
63 | },
64 | "engines": {
65 | "node": ">= 0.4.0"
66 | }
67 | },
68 | "node_modules/is-core-module": {
69 | "version": "2.9.0",
70 | "dev": true,
71 | "license": "MIT",
72 | "dependencies": {
73 | "has": "^1.0.3"
74 | },
75 | "funding": {
76 | "url": "https://github.com/sponsors/ljharb"
77 | }
78 | },
79 | "node_modules/nanoid": {
80 | "version": "3.3.4",
81 | "dev": true,
82 | "license": "MIT",
83 | "bin": {
84 | "nanoid": "bin/nanoid.cjs"
85 | },
86 | "engines": {
87 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
88 | }
89 | },
90 | "node_modules/path-parse": {
91 | "version": "1.0.7",
92 | "dev": true,
93 | "license": "MIT"
94 | },
95 | "node_modules/pick-dom-element": {
96 | "resolved": "..",
97 | "link": true
98 | },
99 | "node_modules/picocolors": {
100 | "version": "1.0.0",
101 | "dev": true,
102 | "license": "ISC"
103 | },
104 | "node_modules/postcss": {
105 | "version": "8.4.14",
106 | "dev": true,
107 | "funding": [
108 | {
109 | "type": "opencollective",
110 | "url": "https://opencollective.com/postcss/"
111 | },
112 | {
113 | "type": "tidelift",
114 | "url": "https://tidelift.com/funding/github/npm/postcss"
115 | }
116 | ],
117 | "license": "MIT",
118 | "dependencies": {
119 | "nanoid": "^3.3.4",
120 | "picocolors": "^1.0.0",
121 | "source-map-js": "^1.0.2"
122 | },
123 | "engines": {
124 | "node": "^10 || ^12 || >=14"
125 | }
126 | },
127 | "node_modules/resolve": {
128 | "version": "1.22.1",
129 | "dev": true,
130 | "license": "MIT",
131 | "dependencies": {
132 | "is-core-module": "^2.9.0",
133 | "path-parse": "^1.0.7",
134 | "supports-preserve-symlinks-flag": "^1.0.0"
135 | },
136 | "bin": {
137 | "resolve": "bin/resolve"
138 | },
139 | "funding": {
140 | "url": "https://github.com/sponsors/ljharb"
141 | }
142 | },
143 | "node_modules/source-map-js": {
144 | "version": "1.0.2",
145 | "dev": true,
146 | "license": "BSD-3-Clause",
147 | "engines": {
148 | "node": ">=0.10.0"
149 | }
150 | },
151 | "node_modules/supports-preserve-symlinks-flag": {
152 | "version": "1.0.0",
153 | "dev": true,
154 | "license": "MIT",
155 | "engines": {
156 | "node": ">= 0.4"
157 | },
158 | "funding": {
159 | "url": "https://github.com/sponsors/ljharb"
160 | }
161 | },
162 | "node_modules/vite": {
163 | "version": "2.9.13",
164 | "dev": true,
165 | "license": "MIT",
166 | "dependencies": {
167 | "esbuild": "^0.14.27",
168 | "postcss": "^8.4.13",
169 | "resolve": "^1.22.0",
170 | "rollup": "^2.59.0"
171 | },
172 | "bin": {
173 | "vite": "bin/vite.js"
174 | },
175 | "engines": {
176 | "node": ">=12.2.0"
177 | },
178 | "optionalDependencies": {
179 | "fsevents": "~2.3.2"
180 | },
181 | "peerDependencies": {
182 | "less": "*",
183 | "sass": "*",
184 | "stylus": "*"
185 | },
186 | "peerDependenciesMeta": {
187 | "less": {
188 | "optional": true
189 | },
190 | "sass": {
191 | "optional": true
192 | },
193 | "stylus": {
194 | "optional": true
195 | }
196 | }
197 | },
198 | "node_modules/vite/node_modules/esbuild": {
199 | "version": "0.14.48",
200 | "dev": true,
201 | "hasInstallScript": true,
202 | "license": "MIT",
203 | "bin": {
204 | "esbuild": "bin/esbuild"
205 | },
206 | "engines": {
207 | "node": ">=12"
208 | },
209 | "optionalDependencies": {
210 | "esbuild-android-64": "0.14.48",
211 | "esbuild-android-arm64": "0.14.48",
212 | "esbuild-darwin-64": "0.14.48",
213 | "esbuild-darwin-arm64": "0.14.48",
214 | "esbuild-freebsd-64": "0.14.48",
215 | "esbuild-freebsd-arm64": "0.14.48",
216 | "esbuild-linux-32": "0.14.48",
217 | "esbuild-linux-64": "0.14.48",
218 | "esbuild-linux-arm": "0.14.48",
219 | "esbuild-linux-arm64": "0.14.48",
220 | "esbuild-linux-mips64le": "0.14.48",
221 | "esbuild-linux-ppc64le": "0.14.48",
222 | "esbuild-linux-riscv64": "0.14.48",
223 | "esbuild-linux-s390x": "0.14.48",
224 | "esbuild-netbsd-64": "0.14.48",
225 | "esbuild-openbsd-64": "0.14.48",
226 | "esbuild-sunos-64": "0.14.48",
227 | "esbuild-windows-32": "0.14.48",
228 | "esbuild-windows-64": "0.14.48",
229 | "esbuild-windows-arm64": "0.14.48"
230 | }
231 | },
232 | "node_modules/vite/node_modules/fsevents": {
233 | "version": "2.3.2",
234 | "dev": true,
235 | "license": "MIT",
236 | "optional": true,
237 | "os": [
238 | "darwin"
239 | ],
240 | "engines": {
241 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
242 | }
243 | },
244 | "node_modules/vite/node_modules/rollup": {
245 | "version": "2.75.7",
246 | "dev": true,
247 | "license": "MIT",
248 | "bin": {
249 | "rollup": "dist/bin/rollup"
250 | },
251 | "engines": {
252 | "node": ">=10.0.0"
253 | },
254 | "optionalDependencies": {
255 | "fsevents": "~2.3.2"
256 | }
257 | }
258 | },
259 | "dependencies": {
260 | "esbuild-darwin-arm64": {
261 | "version": "0.14.48",
262 | "dev": true,
263 | "optional": true
264 | },
265 | "function-bind": {
266 | "version": "1.1.1",
267 | "dev": true
268 | },
269 | "has": {
270 | "version": "1.0.3",
271 | "dev": true,
272 | "requires": {
273 | "function-bind": "^1.1.1"
274 | }
275 | },
276 | "is-core-module": {
277 | "version": "2.9.0",
278 | "dev": true,
279 | "requires": {
280 | "has": "^1.0.3"
281 | }
282 | },
283 | "nanoid": {
284 | "version": "3.3.4",
285 | "dev": true
286 | },
287 | "path-parse": {
288 | "version": "1.0.7",
289 | "dev": true
290 | },
291 | "pick-dom-element": {
292 | "version": "file:..",
293 | "requires": {
294 | "typescript": "^4.7.4"
295 | },
296 | "dependencies": {
297 | "typescript": {
298 | "version": "4.5.4",
299 | "dev": true
300 | }
301 | }
302 | },
303 | "picocolors": {
304 | "version": "1.0.0",
305 | "dev": true
306 | },
307 | "postcss": {
308 | "version": "8.4.14",
309 | "dev": true,
310 | "requires": {
311 | "nanoid": "^3.3.4",
312 | "picocolors": "^1.0.0",
313 | "source-map-js": "^1.0.2"
314 | }
315 | },
316 | "resolve": {
317 | "version": "1.22.1",
318 | "dev": true,
319 | "requires": {
320 | "is-core-module": "^2.9.0",
321 | "path-parse": "^1.0.7",
322 | "supports-preserve-symlinks-flag": "^1.0.0"
323 | }
324 | },
325 | "source-map-js": {
326 | "version": "1.0.2",
327 | "dev": true
328 | },
329 | "supports-preserve-symlinks-flag": {
330 | "version": "1.0.0",
331 | "dev": true
332 | },
333 | "vite": {
334 | "version": "2.9.13",
335 | "dev": true,
336 | "requires": {
337 | "esbuild": "^0.14.27",
338 | "fsevents": "~2.3.2",
339 | "postcss": "^8.4.13",
340 | "resolve": "^1.22.0",
341 | "rollup": "^2.59.0"
342 | },
343 | "dependencies": {
344 | "esbuild": {
345 | "version": "0.14.48",
346 | "dev": true,
347 | "requires": {
348 | "esbuild-android-64": "0.14.48",
349 | "esbuild-android-arm64": "0.14.48",
350 | "esbuild-darwin-64": "0.14.48",
351 | "esbuild-darwin-arm64": "0.14.48",
352 | "esbuild-freebsd-64": "0.14.48",
353 | "esbuild-freebsd-arm64": "0.14.48",
354 | "esbuild-linux-32": "0.14.48",
355 | "esbuild-linux-64": "0.14.48",
356 | "esbuild-linux-arm": "0.14.48",
357 | "esbuild-linux-arm64": "0.14.48",
358 | "esbuild-linux-mips64le": "0.14.48",
359 | "esbuild-linux-ppc64le": "0.14.48",
360 | "esbuild-linux-riscv64": "0.14.48",
361 | "esbuild-linux-s390x": "0.14.48",
362 | "esbuild-netbsd-64": "0.14.48",
363 | "esbuild-openbsd-64": "0.14.48",
364 | "esbuild-sunos-64": "0.14.48",
365 | "esbuild-windows-32": "0.14.48",
366 | "esbuild-windows-64": "0.14.48",
367 | "esbuild-windows-arm64": "0.14.48"
368 | }
369 | },
370 | "fsevents": {
371 | "version": "2.3.2",
372 | "dev": true,
373 | "optional": true
374 | },
375 | "rollup": {
376 | "version": "2.75.7",
377 | "dev": true,
378 | "requires": {
379 | "fsevents": "~2.3.2"
380 | }
381 | }
382 | }
383 | }
384 | }
385 | }
386 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pick-dom-element-example",
3 | "description": "",
4 | "version": "1.0.0",
5 | "main": "example.js",
6 | "scripts": {
7 | "start": "vite --open"
8 | },
9 | "author": "Harry Marr",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "vite": "^2.9.13"
13 | },
14 | "dependencies": {
15 | "pick-dom-element": "file:.."
16 | }
17 | }
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pick-dom-element",
3 | "version": "0.2.3",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "pick-dom-element",
9 | "version": "0.2.3",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "typescript": "^4.7.4"
13 | }
14 | },
15 | "node_modules/typescript": {
16 | "version": "4.7.4",
17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
18 | "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
19 | "dev": true,
20 | "bin": {
21 | "tsc": "bin/tsc",
22 | "tsserver": "bin/tsserver"
23 | },
24 | "engines": {
25 | "node": ">=4.2.0"
26 | }
27 | }
28 | },
29 | "dependencies": {
30 | "typescript": {
31 | "version": "4.7.4",
32 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
33 | "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
34 | "dev": true
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pick-dom-element",
3 | "version": "0.2.3",
4 | "description": "Interactively pick elements in the DOM",
5 | "author": "Harry Marr",
6 | "license": "MIT",
7 | "homepage": "https://github.com/hmarr/pick-dom-element",
8 | "repository": "github:hmarr/pick-dom-element",
9 | "main": "dist/index.js",
10 | "types": "dist/index.d.ts",
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "prepare": "npm run build",
16 | "build": "tsc",
17 | "watch": "tsc --watch"
18 | },
19 | "devDependencies": {
20 | "typescript": "^4.7.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/element-overlay.ts:
--------------------------------------------------------------------------------
1 | import { BoundingBox, ElementOverlayOptions } from "./utils";
2 |
3 | export default class ElementOverlay {
4 | overlay: HTMLDivElement;
5 | shadowContainer: HTMLDivElement;
6 | shadowRoot: ShadowRoot;
7 | usingShadowDOM?: boolean;
8 |
9 | constructor(options: ElementOverlayOptions) {
10 | this.overlay = document.createElement("div");
11 | this.overlay.className = options.className || "_ext-element-overlay";
12 | this.overlay.style.background = options.style?.background || "rgba(250, 240, 202, 0.2)";
13 | this.overlay.style.borderColor = options.style?.borderColor || "#F95738";
14 | this.overlay.style.borderStyle = options.style?.borderStyle || "solid";
15 | this.overlay.style.borderRadius = options.style?.borderRadius || "1px";
16 | this.overlay.style.borderWidth = options.style?.borderWidth || "1px";
17 | this.overlay.style.boxSizing = options.style?.boxSizing || "border-box";
18 | this.overlay.style.cursor = options.style?.cursor || "crosshair";
19 | this.overlay.style.position = options.style?.position || "absolute";
20 | this.overlay.style.zIndex = options.style?.zIndex || "2147483647";
21 | this.overlay.style.margin = options.style?.margin || "0px";
22 | this.overlay.style.padding = options.style?.padding || "0px";
23 |
24 | this.shadowContainer = document.createElement("div");
25 | this.shadowContainer.className = "_ext-element-overlay-container";
26 | this.shadowContainer.style.position = "absolute";
27 | this.shadowContainer.style.top = "0px";
28 | this.shadowContainer.style.left = "0px";
29 | this.shadowContainer.style.margin = "0px";
30 | this.shadowContainer.style.padding = "0px";
31 | this.shadowRoot = this.shadowContainer.attachShadow({ mode: "open" });
32 | }
33 |
34 | addToDOM(parent: Node, useShadowDOM: boolean) {
35 | this.usingShadowDOM = useShadowDOM;
36 | if (useShadowDOM) {
37 | parent.insertBefore(this.shadowContainer, parent.firstChild);
38 | this.shadowRoot.appendChild(this.overlay);
39 | } else {
40 | parent.appendChild(this.overlay);
41 | }
42 | }
43 |
44 | removeFromDOM() {
45 | this.setBounds({ x: 0, y: 0, width: 0, height: 0 });
46 | this.overlay.remove();
47 | if (this.usingShadowDOM) {
48 | this.shadowContainer.remove();
49 | }
50 | }
51 |
52 | captureCursor() {
53 | this.overlay.style.pointerEvents = "auto";
54 | }
55 |
56 | ignoreCursor() {
57 | this.overlay.style.pointerEvents = "none";
58 | }
59 |
60 | setBounds({ x, y, width, height }: BoundingBox) {
61 | this.overlay.style.left = x + "px";
62 | this.overlay.style.top = y + "px";
63 | this.overlay.style.width = width + "px";
64 | this.overlay.style.height = height + "px";
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/element-picker.ts:
--------------------------------------------------------------------------------
1 | import ElementOverlay from "./element-overlay";
2 | import { getElementBounds, ElementOverlayOptions } from "./utils";
3 |
4 | type ElementCallback = (el: HTMLElement) => T;
5 | type ElementPickerOptions = {
6 | parentElement?: Node;
7 | useShadowDOM?: boolean;
8 | onClick?: ElementCallback;
9 | onHover?: ElementCallback;
10 | elementFilter?: ElementCallback;
11 | };
12 |
13 | export default class ElementPicker {
14 | private overlay: ElementOverlay;
15 | private active: boolean;
16 | private options?: ElementPickerOptions;
17 | private target?: HTMLElement;
18 | private mouseX?: number;
19 | private mouseY?: number;
20 | private tickReq?: number;
21 |
22 | constructor(overlayOptions?: ElementOverlayOptions) {
23 | this.active = false;
24 | this.overlay = new ElementOverlay(overlayOptions ?? {});
25 | }
26 |
27 | start(options: ElementPickerOptions): boolean {
28 | if (this.active) {
29 | return false;
30 | }
31 |
32 | this.active = true;
33 | this.options = options;
34 | document.addEventListener("mousemove", this.handleMouseMove, true);
35 | document.addEventListener("click", this.handleClick, true);
36 |
37 | this.overlay.addToDOM(
38 | options.parentElement ?? document.body,
39 | options.useShadowDOM ?? true
40 | );
41 |
42 | this.tick();
43 |
44 | return true;
45 | }
46 |
47 | stop() {
48 | this.active = false;
49 | this.options = undefined;
50 | document.removeEventListener("mousemove", this.handleMouseMove, true);
51 | document.removeEventListener("click", this.handleClick, true);
52 |
53 | this.overlay.removeFromDOM();
54 | this.target = undefined;
55 | this.mouseX = undefined;
56 | this.mouseY = undefined;
57 |
58 | if (this.tickReq) {
59 | window.cancelAnimationFrame(this.tickReq);
60 | }
61 | }
62 |
63 | private handleMouseMove = (event: MouseEvent) => {
64 | this.mouseX = event.clientX;
65 | this.mouseY = event.clientY;
66 | };
67 |
68 | private handleClick = (event: MouseEvent) => {
69 | if (this.target && this.options?.onClick) {
70 | this.options.onClick(this.target);
71 | }
72 | event.preventDefault();
73 | };
74 |
75 | private tick = () => {
76 | this.updateTarget();
77 | this.tickReq = window.requestAnimationFrame(this.tick);
78 | };
79 |
80 | private updateTarget() {
81 | if (this.mouseX === undefined || this.mouseY === undefined) {
82 | return;
83 | }
84 |
85 | // Peek through the overlay to find the new target
86 | this.overlay.ignoreCursor();
87 | const elAtCursor = document.elementFromPoint(this.mouseX, this.mouseY);
88 | const newTarget = elAtCursor as HTMLElement;
89 | this.overlay.captureCursor();
90 |
91 | // If the target hasn't changed, there's nothing to do
92 | if (!newTarget || newTarget === this.target) {
93 | return;
94 | }
95 |
96 | // If we have an element filter and the new target doesn't match,
97 | // clear out the target
98 | if (this.options?.elementFilter) {
99 | if (!this.options.elementFilter(newTarget)) {
100 | this.target = undefined;
101 | this.overlay.setBounds({ x: 0, y: 0, width: 0, height: 0 });
102 | return;
103 | }
104 | }
105 |
106 | this.target = newTarget;
107 |
108 | const bounds = getElementBounds(newTarget);
109 | this.overlay.setBounds(bounds);
110 |
111 | if (this.options?.onHover) {
112 | this.options.onHover(newTarget);
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import ElementPicker from "./element-picker";
2 |
3 | export { ElementPicker };
4 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export interface BoundingBox {
2 | x: number;
3 | y: number;
4 | width: number;
5 | height: number;
6 | }
7 |
8 | export interface ElementOverlayStyleOptions {
9 | background?: string;
10 | borderColor?: string;
11 | borderStyle?: string;
12 | borderRadius?: string;
13 | borderWidth?: string;
14 | boxSizing?: string;
15 | cursor?: string;
16 | position?: string;
17 | zIndex?: string;
18 | margin?: string;
19 | padding?: string;
20 | };
21 |
22 | export type ElementOverlayOptions = {
23 | className?: string;
24 | style?: ElementOverlayStyleOptions;
25 | };
26 |
27 | export const getElementBounds = (el: HTMLElement): BoundingBox => {
28 | const rect = el.getBoundingClientRect();
29 | return {
30 | x: window.pageXOffset + rect.left,
31 | y: window.pageYOffset + rect.top,
32 | width: el.offsetWidth,
33 | height: el.offsetHeight,
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noImplicitAny": true,
5 | "target": "es2016",
6 | "sourceMap": false,
7 | "module": "ES6",
8 | "declaration": true,
9 | "outDir": "./dist"
10 | },
11 | "include": ["./src/"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------