├── xpath-finder.zip ├── icons ├── active-64.png └── default-64.png ├── README.md ├── manifest.json ├── LICENSE ├── options.html ├── background.js ├── options.js └── inspect.js /xpath-finder.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trembacz/xpath-finder/HEAD/xpath-finder.zip -------------------------------------------------------------------------------- /icons/active-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trembacz/xpath-finder/HEAD/icons/active-64.png -------------------------------------------------------------------------------- /icons/default-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trembacz/xpath-finder/HEAD/icons/default-64.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xPath Finder 2 | 3 | Firefox & Chrome plugin to get the elements xPath. 4 | 5 | https://addons.mozilla.org/addon/xpath_finder/ 6 | 7 | https://chrome.google.com/webstore/detail/xpath-finder/ihnknokegkbpmofmafnkoadfjkhlogph 8 | 9 | ## Options 10 | ![](https://i.imgur.com/4rJqlpp.png) 11 | 12 | ## Usage 13 | 1. Click on the plugin icon, cursor will be changed to the **crosshair** 14 | 2. Hover over the desired element **(elements are highlighted on hover)** 15 | 3. Click on the element and his xPath will display in the panel at the bottom of the page 16 | 17 | 18 | ![](http://i.imgur.com/dPQwezY.png) 19 | 20 | ![](https://i.imgur.com/o9cQvp0.png) 21 | 22 | ![](https://i.imgur.com/Lj0YW63.png) 23 | 24 | ![](https://i.imgur.com/8RsV8fw.png) 25 | 26 | ![](https://i.imgur.com/HJl6hFj.png) 27 | 28 | ![](https://i.imgur.com/1wOuAJq.png) 29 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "xPath Finder", 4 | "description": "Click on any element to get the xPath", 5 | "homepage_url": "https://github.com/trembacz/xpath-finder", 6 | "version": "1.0.3", 7 | "icons": { 8 | "64": "icons/default-64.png" 9 | }, 10 | "background": { 11 | "service_worker": "background.js" 12 | }, 13 | "action": { 14 | "default_icon": { 15 | "64": "icons/default-64.png" 16 | }, 17 | "default_title": "Click on any element to get the xPath" 18 | }, 19 | 20 | "content_scripts": [ 21 | { 22 | "all_frames": true, 23 | "matches": [""], 24 | "js": ["inspect.js"] 25 | } 26 | ], 27 | "commands": { 28 | "toggle-xpath": { 29 | "suggested_key": { 30 | "default": "Ctrl+Shift+U", 31 | "mac": "Command+Shift+U" 32 | }, 33 | "description": "Toggle plugin" 34 | } 35 | }, 36 | "options_ui": { 37 | "page": "options.html" 38 | }, 39 | "permissions": ["activeTab", "scripting", "storage"] 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tomasz Rembacz 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 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 38 |
39 |
40 | 41 | CTRL+SHIFT+ 42 | 43 | 44 | Shortcut is editable only in Firefox version of extension. Only A-Z capital letters allowed, please remember that you can't override browser default shortcuts. 45 |
46 |
47 | 48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const browserAppData = this.browser || this.chrome; 2 | const tabs = {}; 3 | const inspectFile = "inspect.js"; 4 | const activeIcon = "active-64.png"; 5 | const defaultIcon = "default-64.png"; 6 | 7 | const inspect = { 8 | toggleActivate: (id, type, icon) => { 9 | this.id = id; 10 | browserAppData.scripting.executeScript( 11 | { 12 | target: { tabId: id }, 13 | files: [inspectFile] 14 | }, 15 | () => { 16 | browserAppData.tabs.sendMessage(id, { action: type }); 17 | } 18 | ); 19 | 20 | browserAppData.action.setIcon({ tabId: id, path: { 19: "icons/" + icon } }); 21 | } 22 | }; 23 | 24 | function isSupportedProtocolAndFileType(urlString) { 25 | if (!urlString) { 26 | return false; 27 | } 28 | const url = new URL(urlString); 29 | const supportedProtocols = ["https:", "http:", "file:"]; 30 | const notSupportedFiles = ["xml", "pdf", "rss"]; 31 | const extension = url.href.split(".").pop().split(/\#|\?/)[0]; 32 | 33 | return supportedProtocols.indexOf(url.protocol) !== -1 && notSupportedFiles.indexOf(extension) === -1; 34 | } 35 | 36 | function toggle(tab) { 37 | if (isSupportedProtocolAndFileType(tab.url)) { 38 | if (!tabs[tab.id]) { 39 | tabs[tab.id] = Object.create(inspect); 40 | inspect.toggleActivate(tab.id, "activate", activeIcon); 41 | } else { 42 | inspect.toggleActivate(tab.id, "deactivate", defaultIcon); 43 | for (const tabId in tabs) { 44 | if (tabId == tab.id) delete tabs[tabId]; 45 | } 46 | } 47 | } 48 | } 49 | 50 | function deactivateItem(tab) { 51 | if (tab[0]) { 52 | if (isSupportedProtocolAndFileType(tab[0].url)) { 53 | for (const tabId in tabs) { 54 | if (tabId == tab[0].id) { 55 | delete tabs[tabId]; 56 | inspect.toggleActivate(tab[0].id, "deactivate", defaultIcon); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | function getActiveTab() { 64 | browserAppData.tabs.query({ active: true, currentWindow: true }, tab => { 65 | deactivateItem(tab); 66 | }); 67 | } 68 | 69 | browserAppData.commands.onCommand.addListener(command => { 70 | if (command === "toggle-xpath") { 71 | browserAppData.tabs.query({ active: true, currentWindow: true }, tab => { 72 | toggle(tab[0]); 73 | }); 74 | } 75 | }); 76 | 77 | browserAppData.tabs.onUpdated.addListener(getActiveTab); 78 | browserAppData.action.onClicked.addListener(toggle); 79 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | const browserAppData = this.browser || this.chrome; 2 | const shortcutCommand = "toggle-xpath"; 3 | const updateAvailable = typeof browserAppData.commands.update !== "undefined" ? true : false; 4 | const isMac = navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) ? true : false; 5 | const shortCutKeys = isMac ? "Command+Shift+" : "Ctrl+Shift+"; 6 | const shortCutLabels = isMac ? "CMD+SHIFT+" : "CTRL+SHIFT+"; 7 | 8 | async function updateShortcut() { 9 | updateAvailable && 10 | (await browserAppData.commands.update({ 11 | name: shortcutCommand, 12 | shortcut: shortCutKeys + document.querySelector("#shortcut").value 13 | })); 14 | } 15 | 16 | async function resetShortcut() { 17 | if (updateAvailable) { 18 | await browserAppData.commands.reset(shortcutCommand); 19 | const commands = await browserAppData.commands.getAll(); 20 | for (const command of commands) { 21 | if (command.name === shortcutCommand) { 22 | document.querySelector("#shortcut").value = command.shortcut.substr(-1); 23 | } 24 | } 25 | saveOptions(); 26 | } 27 | } 28 | 29 | function shortcutKeyField(event) { 30 | event.target.value = event.target.value.toUpperCase(); 31 | } 32 | 33 | function saveOptions(e) { 34 | browserAppData.storage.local.set( 35 | { 36 | inspector: document.querySelector("#inspector").checked, 37 | clipboard: document.querySelector("#copy").checked, 38 | shortid: document.querySelector("#shortid").checked, 39 | position: document.querySelector("#position").value, 40 | shortcut: document.querySelector("#shortcut").value 41 | }, 42 | () => { 43 | const status = document.querySelector(".status"); 44 | status.textContent = "Options saved."; 45 | updateAvailable && updateShortcut(); 46 | setTimeout(() => { 47 | status.textContent = ""; 48 | }, 1000); 49 | } 50 | ); 51 | e && e.preventDefault(); 52 | } 53 | 54 | function restoreOptions() { 55 | browserAppData.storage.local.get( 56 | { 57 | inspector: true, 58 | clipboard: true, 59 | shortid: true, 60 | position: "bl", 61 | shortcut: "U" 62 | }, 63 | items => { 64 | document.querySelector("#inspector").checked = items.inspector; 65 | document.querySelector("#copy").checked = items.clipboard; 66 | document.querySelector("#shortid").checked = items.shortid; 67 | document.querySelector("#position").value = items.position; 68 | document.querySelector("#shortcut").value = items.shortcut; 69 | } 70 | ); 71 | } 72 | 73 | // update shortcut string in options box 74 | document.querySelector(".command").textContent = shortCutLabels; 75 | 76 | // check if browser support updating shortcuts 77 | if (updateAvailable) { 78 | document.querySelector("#reset").addEventListener("click", resetShortcut); 79 | document.querySelector("#shortcut").addEventListener("keyup", shortcutKeyField); 80 | } else { 81 | // remove button and disable input field 82 | document.querySelector("#reset").remove(); 83 | document.querySelector("#shortcut").setAttribute("disabled", "true"); 84 | } 85 | 86 | document.addEventListener("DOMContentLoaded", restoreOptions); 87 | document.querySelector("form").addEventListener("submit", saveOptions); 88 | -------------------------------------------------------------------------------- /inspect.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | var xPathFinder = xPathFinder || (() => { 3 | class Inspector { 4 | constructor() { 5 | this.win = window; 6 | this.doc = window.document; 7 | 8 | this.draw = this.draw.bind(this); 9 | this.getData = this.getData.bind(this); 10 | this.setOptions = this.setOptions.bind(this); 11 | 12 | this.cssNode = 'xpath-css'; 13 | this.contentNode = 'xpath-content'; 14 | this.overlayElement = 'xpath-overlay'; 15 | } 16 | 17 | getData(e, iframe) { 18 | e.stopImmediatePropagation(); 19 | e.preventDefault && e.preventDefault(); 20 | e.stopPropagation && e.stopPropagation(); 21 | 22 | if (e.target.id !== this.contentNode) { 23 | this.XPath = this.getXPath(e.target); 24 | const contentNode = document.getElementById(this.contentNode); 25 | const iframeNode = window.frameElement || iframe; 26 | const contentString = iframeNode ? `Iframe: ${this.getXPath(iframeNode)}
XPath: ${this.XPath}` : this.XPath; 27 | 28 | if (contentNode) { 29 | contentNode.innerHTML = contentString; 30 | } else { 31 | const contentHtml = document.createElement('div'); 32 | contentHtml.innerHTML = contentString; 33 | contentHtml.id = this.contentNode; 34 | document.body.appendChild(contentHtml); 35 | } 36 | this.options.clipboard && ( this.copyText(this.XPath) ); 37 | } 38 | } 39 | 40 | getOptions() { 41 | const storage = chrome.storage && (chrome.storage.local); 42 | const promise = storage.get({ 43 | inspector: true, 44 | clipboard: true, 45 | shortid: true, 46 | position: 'bl' 47 | }, this.setOptions); 48 | (promise && promise.then) && (promise.then(this.setOptions())); 49 | } 50 | 51 | setOptions(options) { 52 | this.options = options; 53 | let position = 'bottom:0;left:0'; 54 | switch (options.position) { 55 | case 'tl': position = 'top:0;left:0'; break; 56 | case 'tr': position = 'top:0;right:0'; break; 57 | case 'br': position = 'bottom:0;right:0'; break; 58 | default: break; 59 | } 60 | this.styles = `body *{cursor:crosshair!important;}#xpath-content{${position};cursor:initial!important;padding:10px;background:gray;color:white;position:fixed;font-size:14px;z-index:10000001;}`; 61 | this.activate(); 62 | } 63 | 64 | createOverlayElements() { 65 | const overlayStyles = { 66 | background: 'rgba(120, 170, 210, 0.7)', 67 | padding: 'rgba(77, 200, 0, 0.3)', 68 | margin: 'rgba(255, 155, 0, 0.3)', 69 | border: 'rgba(255, 200, 50, 0.3)' 70 | }; 71 | 72 | this.container = this.doc.createElement('div'); 73 | this.node = this.doc.createElement('div'); 74 | this.border = this.doc.createElement('div'); 75 | this.padding = this.doc.createElement('div'); 76 | this.content = this.doc.createElement('div'); 77 | 78 | this.border.style.borderColor = overlayStyles.border; 79 | this.padding.style.borderColor = overlayStyles.padding; 80 | this.content.style.backgroundColor = overlayStyles.background; 81 | 82 | Object.assign(this.node.style, { 83 | borderColor: overlayStyles.margin, 84 | pointerEvents: 'none', 85 | position: 'fixed' 86 | }); 87 | 88 | this.container.id = this.overlayElement; 89 | this.container.style.zIndex = 10000000; 90 | this.node.style.zIndex = 10000000; 91 | 92 | this.container.appendChild(this.node); 93 | this.node.appendChild(this.border); 94 | this.border.appendChild(this.padding); 95 | this.padding.appendChild(this.content); 96 | } 97 | 98 | removeOverlay() { 99 | const overlayHtml = document.getElementById(this.overlayElement); 100 | overlayHtml && overlayHtml.remove(); 101 | } 102 | 103 | copyText(XPath) { 104 | const hdInp = document.createElement('textarea'); 105 | hdInp.textContent = XPath; 106 | document.body.appendChild(hdInp); 107 | hdInp.select(); 108 | document.execCommand('copy'); 109 | hdInp.remove(); 110 | } 111 | 112 | draw(e) { 113 | const node = e.target; 114 | if (node.id !== this.contentNode) { 115 | this.removeOverlay(); 116 | 117 | const box = this.getNestedBoundingClientRect(node, this.win); 118 | const dimensions = this.getElementDimensions(node); 119 | 120 | this.boxWrap(dimensions, 'margin', this.node); 121 | this.boxWrap(dimensions, 'border', this.border); 122 | this.boxWrap(dimensions, 'padding', this.padding); 123 | 124 | Object.assign(this.content.style, { 125 | height: box.height - dimensions.borderTop - dimensions.borderBottom - dimensions.paddingTop - dimensions.paddingBottom + 'px', 126 | width: box.width - dimensions.borderLeft - dimensions.borderRight - dimensions.paddingLeft - dimensions.paddingRight + 'px', 127 | }); 128 | 129 | Object.assign(this.node.style, { 130 | top: box.top - dimensions.marginTop + 'px', 131 | left: box.left - dimensions.marginLeft + 'px', 132 | }); 133 | 134 | this.doc.body.appendChild(this.container); 135 | } 136 | } 137 | 138 | activate() { 139 | this.createOverlayElements(); 140 | // add styles 141 | if (!document.getElementById(this.cssNode)) { 142 | const styles = document.createElement('style'); 143 | styles.innerText = this.styles; 144 | styles.id = this.cssNode; 145 | document.getElementsByTagName('head')[0].appendChild(styles); 146 | } 147 | // add listeners for all frames and root 148 | document.addEventListener('click', this.getData, true); 149 | this.options.inspector && ( document.addEventListener('mouseover', this.draw) ); 150 | const frameLength = window.parent.frames.length 151 | for (let i = 0 ; i < frameLength; i++) { 152 | let frame = window.parent.frames[i]; 153 | frame.document.addEventListener('click', e => this.getData(e, frame.frameElement), true); 154 | this.options.inspector && (frame.document.addEventListener('mouseover', this.draw) ); 155 | } 156 | 157 | } 158 | 159 | deactivate() { 160 | // remove styles 161 | const cssNode = document.getElementById(this.cssNode); 162 | cssNode && cssNode.remove(); 163 | // remove overlay 164 | this.removeOverlay(); 165 | // remove xpath html 166 | const contentNode = document.getElementById(this.contentNode); 167 | contentNode && contentNode.remove(); 168 | // remove listeners for all frames and root 169 | document.removeEventListener('click', this.getData, true); 170 | this.options && this.options.inspector && ( document.removeEventListener('mouseover', this.draw) ); 171 | const frameLength = window.parent.frames.length 172 | for (let i = 0 ; i < frameLength; i++) { 173 | let frameDocument = window.parent.frames[i].document 174 | frameDocument.removeEventListener('click', this.getData, true); 175 | this.options && this.options.inspector && ( frameDocument.removeEventListener('mouseover', this.draw) ); 176 | } 177 | 178 | } 179 | 180 | getXPath(el) { 181 | let nodeElem = el; 182 | if (nodeElem.id && this.options.shortid) { 183 | return `//*[@id="${nodeElem.id}"]`; 184 | } 185 | const parts = []; 186 | while (nodeElem && nodeElem.nodeType === Node.ELEMENT_NODE) { 187 | let nbOfPreviousSiblings = 0; 188 | let hasNextSiblings = false; 189 | let sibling = nodeElem.previousSibling; 190 | while (sibling) { 191 | if (sibling.nodeType !== Node.DOCUMENT_TYPE_NODE && sibling.nodeName === nodeElem.nodeName) { 192 | nbOfPreviousSiblings++; 193 | } 194 | sibling = sibling.previousSibling; 195 | } 196 | sibling = nodeElem.nextSibling; 197 | while (sibling) { 198 | if (sibling.nodeName === nodeElem.nodeName) { 199 | hasNextSiblings = true; 200 | break; 201 | } 202 | sibling = sibling.nextSibling; 203 | } 204 | const prefix = nodeElem.prefix ? nodeElem.prefix + ':' : ''; 205 | const nth = nbOfPreviousSiblings || hasNextSiblings ? `[${nbOfPreviousSiblings + 1}]` : ''; 206 | parts.push(prefix + nodeElem.localName + nth); 207 | nodeElem = nodeElem.parentNode; 208 | } 209 | return parts.length ? '/' + parts.reverse().join('/') : ''; 210 | } 211 | 212 | getElementDimensions(domElement) { 213 | const calculatedStyle = window.getComputedStyle(domElement); 214 | return { 215 | borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0], 216 | borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0], 217 | borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0], 218 | borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0], 219 | marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0], 220 | marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0], 221 | marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0], 222 | marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0], 223 | paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0], 224 | paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0], 225 | paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0], 226 | paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0] 227 | }; 228 | } 229 | 230 | getOwnerWindow(node) { 231 | if (!node.ownerDocument) { return null; } 232 | return node.ownerDocument.defaultView; 233 | } 234 | 235 | getOwnerIframe(node) { 236 | const nodeWindow = this.getOwnerWindow(node); 237 | if (nodeWindow) { 238 | return nodeWindow.frameElement; 239 | } 240 | return null; 241 | } 242 | 243 | getBoundingClientRectWithBorderOffset(node) { 244 | const dimensions = this.getElementDimensions(node); 245 | return this.mergeRectOffsets([ 246 | node.getBoundingClientRect(), 247 | { 248 | top: dimensions.borderTop, 249 | left: dimensions.borderLeft, 250 | bottom: dimensions.borderBottom, 251 | right: dimensions.borderRight, 252 | width: 0, 253 | height: 0 254 | } 255 | ]); 256 | } 257 | 258 | mergeRectOffsets(rects) { 259 | return rects.reduce((previousRect, rect) => { 260 | if (previousRect === null) { return rect; } 261 | return { 262 | top: previousRect.top + rect.top, 263 | left: previousRect.left + rect.left, 264 | width: previousRect.width, 265 | height: previousRect.height, 266 | bottom: previousRect.bottom + rect.bottom, 267 | right: previousRect.right + rect.right 268 | }; 269 | }); 270 | } 271 | 272 | getNestedBoundingClientRect(node, boundaryWindow) { 273 | const ownerIframe = this.getOwnerIframe(node); 274 | if (ownerIframe && ownerIframe !== boundaryWindow) { 275 | const rects = [node.getBoundingClientRect()]; 276 | let currentIframe = ownerIframe; 277 | let onlyOneMore = false; 278 | while (currentIframe) { 279 | const rect = this.getBoundingClientRectWithBorderOffset(currentIframe); 280 | rects.push(rect); 281 | currentIframe = this.getOwnerIframe(currentIframe); 282 | if (onlyOneMore) { break; } 283 | if (currentIframe && this.getOwnerWindow(currentIframe) === boundaryWindow) { 284 | onlyOneMore = true; 285 | } 286 | } 287 | return this.mergeRectOffsets(rects); 288 | } 289 | return node.getBoundingClientRect(); 290 | } 291 | 292 | boxWrap(dimensions, parameter, node) { 293 | Object.assign(node.style, { 294 | borderTopWidth: dimensions[parameter + 'Top'] + 'px', 295 | borderLeftWidth: dimensions[parameter + 'Left'] + 'px', 296 | borderRightWidth: dimensions[parameter + 'Right'] + 'px', 297 | borderBottomWidth: dimensions[parameter + 'Bottom'] + 'px', 298 | borderStyle: 'solid' 299 | }); 300 | } 301 | } 302 | 303 | const inspect = new Inspector(); 304 | 305 | chrome.runtime.onMessage.addListener(request => { 306 | if (request.action === 'activate') { 307 | return inspect.getOptions(); 308 | } 309 | return inspect.deactivate(); 310 | }); 311 | 312 | return true; 313 | })(); 314 | --------------------------------------------------------------------------------