Last active
February 18, 2025 13:07
-
-
Save prantlf/1f70855b388d87b4dff31dbdf309921a to your computer and use it in GitHub Desktop.
getShadyElementByXPath: returns an element by XPath, recognising elements in shadow DOM too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Returns an element by XPath, recognising elements in shadow DOM too. | |
// | |
// For example, a value returned by "Copy full XPath" in developer tools: | |
// "/html/body/div[1]/main/form/piwo-label[2]/piwo-input//input". | |
// The part "//" represents the shadow root divider. | |
// | |
// The input XPath has to: | |
// * be an absolute XPath starting with "/html". | |
// * include segments with tag names only, with an optional element index. | |
// | |
// A different shadow root divider can be specified by the optinal | |
// second parameter. For example, "#document-fragment" for an XPath: | |
// "/html[1]/body/form/piwo-input/#document-fragment/input". | |
// | |
getShadyElementByXPath = function (xpath, shadowRootDivider = '') { | |
// Splits XPath by the first slash to the first segment and the rest path. | |
function splitAfterFirstXPathSegment(xpath) { | |
let start, rest | |
const slash = xpath.indexOf('/') | |
if (slash >= 0) { | |
start = xpath.slice(0, slash) | |
rest = xpath.slice(slash + 1) | |
} else { | |
start = xpath | |
rest = null | |
} | |
return [start, rest] | |
} | |
// Gets a child specified by an XPath segment "<tag>" or "<tag>[<index>]". | |
// For example, "div" or "div[2]". | |
function getChildOnIndex({ children }, segment) { | |
const match = /^([^\[]+)(?:\[([^\]]+)\])?$/.exec(segment) | |
if (!match) throw new Error(`XPath segment, not just a tag: ${segment}`) | |
const tag = match[1].toUpperCase() | |
let tagIndex = +(match[2] ?? 1) | |
// console.log('child:', children, tag, tagIndex) | |
const element = Array | |
.from(children) | |
.find(({ tagName }) => tag === tagName && --tagIndex === 0) | |
return element | |
} | |
if (!xpath.startsWith('/html')) throw new Error(`XPath not absolute: ${xpath}`) | |
shadowRootDivider = `/${shadowRootDivider}/` | |
const segments = xpath | |
.slice(1) | |
.split(shadowRootDivider) | |
let rootScope = document | |
for (let i = 0, l = segments.length - 1; ; ++i) { | |
const segment = segments[i] | |
const [startSegment, restPath] = splitAfterFirstXPathSegment(segment) | |
// console.log(`${i}:`, segment, startSegment, restPath) | |
const startElement = getChildOnIndex(rootScope, startSegment) | |
if (!startElement || !restPath) return startElement | |
const result = document.evaluate(restPath, startElement, null, | |
XPathResult.FIRST_ORDERED_NODE_TYPE, null) | |
const element = result.singleNodeValue | |
if (!element || i === l) return element | |
// console.log('host:', element) | |
rootScope = element.shadowRoot | |
if (!rootScope) { | |
const xpath = segments | |
.slice(0, i) | |
.concat(segment) | |
.join(shadowRootDivider) | |
throw new Error(`No shadow DOM at ${xpath}`) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment