Last active
May 18, 2022 03:03
-
-
Save daniellizik/d72b1c8ac0f335afdd6526a6e5eab7e5 to your computer and use it in GitHub Desktop.
Generate browser query selector string from a given dom element
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
'use strict'; | |
(() => { | |
class QuerySelector { | |
constructor(origin) { | |
/** | |
* :nth-of-type is only ie9+ https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type | |
* But would we rather have nested query selectors? | |
* The former is better for pushing basket items because they will have a common parent element. | |
* ex: | |
* document.querySelector('.my-class')[2].querySelector('.some-element')[1] | |
* document.querySelector('.my-class:nth-of-type(2) .some-element:nth-of-type(1)') | |
*/ | |
this.constants = { | |
nthType: ':nth-of-type', | |
nthChild: ':nth-child' | |
}; | |
this.origin = origin; | |
this.originPath = this.walkUp(this.origin); | |
this.trees = this.findTrees(); | |
this.needle = this.diffTreePaths(); | |
} | |
/** | |
* Diffs tree paths of this.trees to find which selectors we need to add to this.originPath | |
*/ | |
diffTreePaths(curr = 0) { | |
let a = this.trees.map(tree => this.stringify(tree)); | |
console.log(JSON.stringify(a, null, 2)) | |
} | |
findTrees() { | |
let maxPath = this.stringify(this.originPath); | |
let minPath = this.maxLeftTrim(this.leftTrimId(maxPath)); | |
return [].slice.call(document.querySelectorAll(minPath.join(' '))).map(node => this.walkUp(node)); | |
} | |
/** | |
* Takes original target node and walks up tree and collects path | |
* @param {dom object} origin - needle in the haystack (dom tree) we start at | |
*/ | |
walkUp(origin, el, path = []) { | |
let target = el || origin; | |
let position = this.walkLeft(target); | |
let node = { | |
name: target.nodeName.toLowerCase(), | |
nthType: position.type, | |
nthChild: position.child | |
}; | |
if (target.attributes) { | |
if (target.attributes.length > 0) { | |
node.attrs = {}; | |
[].slice.call(target.attributes).forEach(attr => { | |
node.attrs[attr.name] = attr.value; | |
}); | |
} | |
} | |
if (target.parentNode) { | |
path.unshift(node); | |
return this.walkUp(origin, target.parentNode, path); | |
} else { | |
return path; | |
} | |
} | |
/** | |
* Takes node and walks left in list of sibling nodes to get nth-type-of and nth-child positions, which starts at 1, not 0. | |
* @param {dom object} node | |
*/ | |
walkLeft(node) { | |
return !node.parentNode ? 0 : [].slice.call(node.parentNode.childNodes || []).reduce((acc, el) => { | |
if (el === node) { | |
acc.hasPosition = true; | |
} | |
if (acc.hasPosition === false) { | |
acc.position.child++; | |
if (el.nodeName === node.nodeName) { | |
acc.position.type++; | |
} | |
} | |
return acc; | |
}, {position: {type: 1, child: 1}, hasPosition: false}).position; | |
} | |
/** | |
* Takes query selector array path and left trims to the last #id selector | |
* @param {array} path - array of selector strings | |
* | |
* ['.foo', '.bar', '.fizz', '.buzz', '#id', '.blah'] | |
* => ['#id', '.blah'] | |
*/ | |
leftTrimId(path) { | |
return path.reverse().reduce((acc, item) => { | |
if (acc.join(' ').indexOf('#') < 0) { | |
acc.push(item); | |
} | |
return acc; | |
}, []).reverse(); | |
} | |
/** | |
* Takes query selector path array and recursively left trims it until the original node is found | |
* @param {array} path - not a collection, just array of strings | |
* | |
* 1st iteration: ['.fizz' '.foo', '.bar']; | |
* next iteration: ['.foo', '.bar']; | |
* next iteration: ['.bar']; | |
* etc... | |
*/ | |
maxLeftTrim(path, origin = document.querySelector(path.join(' ')), trim = 0, previous) { | |
let branch = path.slice(trim); | |
let selector = branch.join(' '); | |
let target; | |
if (branch.length === 1) { | |
return branch; | |
} | |
if (branch.length === 0) { | |
return ['body']; | |
} | |
target = document.querySelector(selector); | |
if (origin === target) { | |
return this.maxLeftTrim(path, origin, trim + 1, branch); | |
} else if (origin !== target || target === null) { | |
return previous; | |
} | |
} | |
/** | |
* Takes collection of nodes and returns concatenated strings | |
* @param {array} path | |
* @property {string} name - node name | |
* @property {object} attrs - html attributes, each key/value is a string | |
* [ | |
* { | |
* name: 'div', | |
* attrs: { | |
* id: 'blah', | |
* class: 'foo bar what' | |
* } | |
* }, | |
* { | |
* name: 'p', | |
* nthChild: 5 | |
* } | |
* ] | |
* | |
* ... => ['#blah', 'p:nth-child(5)'] | |
* | |
*/ | |
stringify(path) { | |
return path.map(node => { | |
let name = node.name; | |
let nthType = node.nthType < 2 ? '' : `${this.constants.nthType}(${node.nthType})`; | |
let nthChild = node.nthChild < 2 ? '' : `${this.constants.nthChild}(${node.nthChild})`; | |
let id; | |
let classlist; | |
let blank = /^\s+$/; | |
let spaces = /\s{2,}/g | |
let selector = '' | |
if (node.attrs) { | |
if (node.attrs.id) { | |
id = '#' + node.attrs.id.trim(); | |
} | |
if (node.attrs.class) { | |
classlist = '.' + node.attrs.class.trim().replace(spaces, ' ').split(' ').join('.').trim(); | |
} | |
} | |
// if there is no attrs we need to include nodename | |
if ( (blank.test(id) && blank.test(classlist)) || (!id && !classlist) ) { | |
selector = name + nthType; | |
} | |
// if there is an id you we don't need anything else | |
else if (id) { | |
selector = id; | |
} | |
// if no id, take nodename and classlist | |
else if (!id && classlist) { | |
selector = name + classlist + nthType; | |
} | |
return selector; | |
}); | |
} | |
} | |
window.addEventListener('contextmenu', e => window.qs = new QuerySelector(e.target)); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment