Skip to content

Instantly share code, notes, and snippets.

@prantlf
Last active February 18, 2025 13:07
Show Gist options
  • Save prantlf/1f70855b388d87b4dff31dbdf309921a to your computer and use it in GitHub Desktop.
Save prantlf/1f70855b388d87b4dff31dbdf309921a to your computer and use it in GitHub Desktop.
getShadyElementByXPath: returns an element by XPath, recognising elements in shadow DOM too.
// 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