├── .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 | [![npm version](https://badge.fury.io/js/pick-dom-element.svg)](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 | 25 | 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 | --------------------------------------------------------------------------------