Created
January 27, 2020 21:04
-
-
Save Pamblam/5aa97f26f84d62549b804d89050b58ef to your computer and use it in GitHub Desktop.
When run in the console it creates a UI to help find selectors for any element on the page.
This file contains 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
var SelectorGenerator = (function(){ | |
var removed_elements = []; | |
var selectedElement = null; | |
var modalBg = null; | |
var outline = null; | |
var modal = null; | |
var running = false; | |
function highlightElement(){ | |
generateModalOverlay(); | |
generateSelectedOutline(); | |
} | |
function getSelecteddElementDimensions(){ | |
if(!selectedElement) return false; | |
var rect = selectedElement.getBoundingClientRect(); | |
var x = rect.left + window.scrollX; | |
var y = rect.top + window.scrollY; | |
var width = rect.width; | |
var height = rect.height; | |
return {x, y, width, height}; | |
} | |
function generateSelectedOutline(){ | |
if(!selectedElement || !modalBg) return false; | |
var {x, y, width, height} = getSelecteddElementDimensions(); | |
outline = document.createElement('div'); | |
outline.style.position = 'absolute'; | |
outline.style.top = y+"px"; | |
outline.style.left = x+"px"; | |
outline.style.height = height+"px"; | |
outline.style.width = width+"px"; | |
outline.style.margin = 0; | |
outline.style.padding = 0; | |
outline.style.background = 'rgba(252, 34, 34, 0.4)'; | |
outline.style.border = 'rgba(252, 34, 34, 0.89)'; | |
modalBg.appendChild(outline); | |
generateToolTip(x, y, width, height); | |
} | |
function isModalOrOutline(element){ | |
if(!element) return false; | |
if(element === document) return false; | |
if(element === modal) return true; | |
if(element === outline) return true; | |
return isModalOrOutline(element.parentElement) | |
} | |
function generateToolTip(x, y, width, height){ | |
if(!selectedElement || !modalBg) return false; | |
var htmlPreview = getOpeningTag(selectedElement); | |
modal = document.createElement('div'); | |
modal.style.border = '3px solid rgb(109, 128, 253)'; | |
modal.style.background = 'white'; | |
modal.style.position = 'absolute'; | |
modal.style.width = "100%"; | |
modal.style.maxWidth = "20em"; | |
modal.style.padding = ".25em"; | |
modal.innerHTML = `<div style='text-align: right;'> | |
<a href='#'>Remove from display</a> | |
</div> | |
<div> | |
<b>HTML Preview:</b> | |
<div style='font-family:monospace; background: #232327; padding:.25em; word-wrap: break-word;'> | |
${highlightHTMLOpeningTag(htmlPreview)} | |
</div> | |
</div> | |
<div> | |
<b>Long CSS Selector:</b> | |
<div style='font-family:monospace; background: #232327; padding:.25em; color: white; word-wrap: break-word;'> | |
${getLongSelector(selectedElement)} | |
</div> | |
</div> | |
<div> | |
<b>Short CSS Selector:</b> | |
<div style='font-family:monospace; background: #232327; padding:.25em; color: white; word-wrap: break-word;'> | |
${(new ShortSelector(selectedElement)).shortestSelector} | |
</div> | |
</div>`; | |
modalBg.appendChild(modal); | |
var rect = modal.getBoundingClientRect(); | |
var win = getDocumentDimensions(); | |
if(win.width-(x + width + rect.width) > 0){ | |
// right | |
modal.style.left = (x+width)+"px"; | |
}else if(x > rect.width){ | |
// left | |
modal.style.left = (x-rect.width)+"px"; | |
}else{ | |
// center | |
modal.style.left = ((x+(width/2))-(rect.width/2))+"px"; | |
} | |
if(y+rect.height <= win.height){ | |
// top | |
modal.style.top = y+"px"; | |
}else if(((y+height) - rect.height) >= 0){ | |
// bottom | |
modal.style.top = ((y+height) - rect.height)+"px"; | |
}else{ | |
// center | |
modal.style.top = ((y + (height/2)) - (rect.height/2))+"px"; | |
} | |
modal.querySelectorAll('a')[0].addEventListener('click', onRemoveLinkClick); | |
} | |
function onRemoveLinkClick(e){ | |
e.preventDefault(); | |
removed_elements.push(selectedElement); | |
modalBg.remove(); | |
modalBg = null; | |
outline = null; | |
modal = null; | |
} | |
function getDocumentDimensions(){ | |
var body = document.body, | |
html = document.documentElement; | |
var height = Math.max( | |
body.scrollHeight, | |
body.offsetHeight, | |
html.clientHeight, | |
html.scrollHeight, | |
html.offsetHeight | |
); | |
var width = Math.max( | |
body.scrollWidth, | |
body.offsetWidth, | |
html.clientWidth, | |
html.scrollWidth, | |
html.offsetWidth | |
); | |
return {width, height}; | |
} | |
function generateModalOverlay(){ | |
var {width, height} = getDocumentDimensions(); | |
modalBg = document.createElement('div'); | |
modalBg.style.position = 'absolute'; | |
modalBg.style.top = 0; | |
modalBg.style.left = 0; | |
modalBg.style.height = height+"px"; | |
modalBg.style.width = width+"px"; | |
modalBg.style.zIndex = getGreatestZIndex()+1; | |
modalBg.style.margin = 0; | |
modalBg.style.padding = 0; | |
modalBg.style.border = 0; | |
modalBg.style.background = 'rgba(0, 0, 0, 0.12)'; | |
document.body.appendChild(modalBg); | |
} | |
function getGreatestZIndex(){ | |
var largestZIndex = 0; | |
document.querySelectorAll("*").forEach(ele=>{ | |
var zIndex = getComputedStyle(ele).getPropertyValue('z-index'); | |
if(isNaN(zIndex)) return; | |
zIndex = +zIndex; | |
if(zIndex > largestZIndex) largestZIndex = zIndex | |
}); | |
return largestZIndex; | |
} | |
function onMouseOver(e){ | |
if(modalBg){ | |
if(!isModalOrOutline(e.target)){ | |
modalBg.remove(); | |
modalBg = null; | |
outline = null; | |
modal = null; | |
} | |
return; | |
} | |
if(~removed_elements.indexOf(e.target)) return; | |
selectedElement = e.target; | |
setTimeout(highlightElement, 5); | |
} | |
function getOpeningTag(ele){ | |
var markup = ele.outerHTML; | |
var preview = []; | |
var quoteType = null; | |
for(var i = 0; i < markup.length; i++) { | |
let char = markup.charAt(i); | |
if(!preview.length && char !== '<') continue; | |
if(!quoteType && (char === "'" || char === '"')) quoteType = char; | |
else if(quoteType === char) quoteType = null; | |
preview.push(char); | |
if(!quoteType && char === '>') break; | |
}; | |
return preview.join(''); | |
} | |
function onMouseOut(){ | |
if(modalBg) return; | |
ele = null; | |
} | |
function highlightHTMLOpeningTag(markup){ | |
markup = markup.replace(/\s+/g,' '); | |
markup = truncateLongAttributes(markup); | |
var colors = { | |
neutral: '#aeaeb0', | |
attr_name: '#ff81e8', | |
tagname: '#6ac1ff', | |
attr_value: '#bb92ff' | |
}; | |
var parsed = []; | |
var modes = [ | |
'seeking_start', | |
'seeking_token', | |
'reading_tagname', | |
'reading_attrname', | |
'seeking_equals', | |
'seeking_attrvalue', | |
'reading_attrvalue', | |
'done' | |
]; | |
var mode = '0'; | |
var quoteType = null; | |
var escaped = false; | |
for(var i = 0; i < markup.length; i++) { | |
let char = markup.charAt(i); | |
if(modes[mode] === 'done') break; | |
switch(modes[mode]){ | |
case "seeking_start": | |
if(char === '<'){ | |
parsed.push(`<span style='color:${colors.neutral}'><</span><span style='color:${colors.tagname}'>`); | |
mode = modes.indexOf('reading_tagname'); | |
} | |
break; | |
case "seeking_token": | |
if(char === '>'){ | |
parsed.push(`<span style='color:${colors.neutral}'>></span>`); | |
mode = modes.indexOf('done'); | |
}else if(/\s/.test(char)){ | |
parsed.push(` `); | |
}else{ | |
parsed.push(`<span style='color:${colors.attr_name}'>${char}`); | |
mode = modes.indexOf('reading_attrname'); | |
} | |
break; | |
case "reading_tagname": | |
if(/\s/.test(char)){ | |
parsed.push(`</span> `); | |
mode = modes.indexOf('seeking_token'); | |
}else{ | |
parsed.push(char); | |
} | |
break; | |
case "reading_attrname": | |
if('=' === char){ | |
parsed.push(`</span><span style='color:${colors.neutral}'>=</span>`); | |
mode = modes.indexOf('seeking_attrvalue'); | |
}else if(/\s/.test(char)){ | |
parsed.push(`</span> `); | |
mode = modes.indexOf('seeking_equals'); | |
}else{ | |
parsed.push(char); | |
} | |
break; | |
case "seeking_equals": | |
if(/\s/.test(char)){ | |
parsed.push(` `); | |
}else if('=' === char){ | |
parsed.push(`<span style='color:${colors.neutral}'>=</span>`); | |
mode = modes.indexOf('seeking_attrvalue'); | |
}else if('>' === char){ | |
parsed.push(`<span color='${colors.neutral}'>></span>`); | |
mode = modes.indexOf('done'); | |
}else{ | |
parsed.push(`<span style='color:${colors.attr_name};'>${char}`); | |
mode = modes.indexOf('reading_attrname'); | |
} | |
break; | |
case "seeking_attrvalue": | |
if('"' === char || "'" === char){ | |
quoteType = char; | |
parsed.push(`<span style='color:${colors.attr_value};'>${char}`); | |
mode = modes.indexOf('reading_attrvalue'); | |
}else if(/\s/.test(char)){ | |
parsed.push(` `); | |
}else if('=' === char){ | |
parsed.push(`</span><span style='color:${colors.neutral}'>=</span>`); | |
}else if('>' === char){ | |
parsed.push(`<span color='${colors.neutral}'>></span>`); | |
mode = modes.indexOf('done'); | |
}else if("\\" === char){ | |
escaped = true; | |
parsed.push(`<span style='color:${colors.attr_value};'>\\${char}`); | |
mode = modes.indexOf('reading_attrvalue'); | |
}else{ | |
parsed.push(`<span style='color:${colors.attr_value};'>${char}`); | |
mode = modes.indexOf('reading_attrvalue'); | |
} | |
break; | |
case "reading_attrvalue": | |
if(escaped){ | |
parsed.push(char); | |
escaped = false; | |
}else if("\\" === char){ | |
escaped = true; | |
parsed.push(`<span style='color:${colors.attr_value};'>\\`); | |
}else if(char === quoteType){ | |
quoteType = null; | |
parsed.push(`${char}</span>`); | |
mode = modes.indexOf('seeking_token'); | |
}else if(!quoteType && ">" === char){ | |
parsed.push(`</span><span style='color:${colors.neutral}'>></span>`); | |
mode = modes.indexOf('done'); | |
}else if(!quoteType && /\s/.test(char)){ | |
parsed.push(`</span> `); | |
mode = modes.indexOf('seeking_token'); | |
}else{ | |
parsed.push(char); | |
} | |
break; | |
} | |
}; | |
return parsed.join(''); | |
} | |
function truncateLongAttributes(markup){ | |
var regexes = [ | |
/[^\s]{50,}/g, | |
/"(?:[^"\\]|\\.)*"/g, | |
/'(?:[^'\\]|\\.)*'/g | |
]; | |
var match, replacments = []; | |
regexes.forEach(re=>{ | |
while((match = re.exec(markup)) !== null){ | |
match = match[0]; | |
if(match.length > 50){ | |
replace = match.substring(0, 24)+"..."+match.substring(match.length-22); | |
replacments.push({match, replace}); | |
} | |
} | |
replacments.forEach(r=>{ | |
markup = markup.replace(r.match, r.replace); | |
}); | |
}); | |
return markup; | |
} | |
function getLongSelector(el) { | |
var sel, parent, nthChild; | |
// Start with the tag name, lowe case for style | |
sel = el.tagName.toLowerCase(); | |
// If the element has an id, add it to the selector | |
if(el.id) sel += '#'+el.id; | |
// Add all of the class names to the selector | |
Array.from(el.classList).forEach(cls => sel += '.' + cls); | |
// The parent is either the parent element of the docuemtn itself | |
parent = el.parentElement || document; | |
// See if there are any sibling elements that have the same selector, | |
// If so, the only way to get a *unique* selector is to use nth-child | |
if(parent.querySelectorAll(sel).length > 1){ | |
// Loop over the sibling elements and figure out | |
// which one is our target element | |
Array.from(parent.children).forEach((child, idx)=>{ | |
if(child === el) nthChld = idx+1; | |
}); | |
sel = `:nth-child(${nthChld})`; | |
} | |
// If there is a parent element, recurse, else we're done | |
return el.parentElement ? getLongSelector(el.parentElement) + ">" + sel : sel; | |
} | |
class ShortSelector{ | |
constructor(el){ | |
this.el = el; | |
this.shortestSelector = null; | |
this.parent = parent = el.parentElement || document; | |
this.properties = []; | |
this.generateShortestSelector(); | |
} | |
generateShortestSelector(){ | |
this.generateProperties(); | |
this.calculateShortest(); | |
while(!this.isGloballyUnique(this.shortestSelector)){ | |
let parent = new ShortSelector(this.parent).shortestSelector; | |
this.shortestSelector = parent + ">" + this.shortestSelector; | |
} | |
} | |
calculateShortest(){ | |
var combos = this.combine(this.properties); | |
for(var i=combos.length; i--;){ | |
let sel = this.comboToStr(combos[i]); | |
if(this.isUniqueAmongSiblings(sel)){ | |
if(!this.shortestSelector){ | |
this.shortestSelector = sel; | |
}else if(sel.length < this.shortestSelector.length){ | |
this.shortestSelector = sel; | |
} | |
} | |
} | |
} | |
comboToStr(combo){ | |
return combo.map(p=>this.propToStr(p)).join(''); | |
} | |
propToStr(prop){ | |
switch(prop.type){ | |
case "tag": return prop.value; break; | |
case "id": return "#"+prop.value; break; | |
case "class": return "."+prop.value; break; | |
case "nthchild": return ":nth-child("+prop.value+")"; break; | |
} | |
} | |
generateProperties(){ | |
this.properties.push({type: 'tag', value: this.el.tagName.toLowerCase()}); | |
if(this.el.id) this.properties.push({type: 'id', value: this.el.id}); | |
[...this.el.classList].forEach(c=>this.properties.push({ | |
type: 'class', value: c | |
})); | |
var nthChld; | |
Array.from(this.parent.children).forEach((child, idx)=>{ | |
if(child === this.el) nthChld = idx+1; | |
}); | |
this.properties.push({type: 'nthchild', value: nthChld}); | |
} | |
isGloballyUnique(selector){ | |
if(!selector) return false; | |
return document.querySelectorAll(selector).length === 1; | |
} | |
isUniqueAmongSiblings(selector){ | |
if(!selector) return false; | |
return this.parent.querySelectorAll(selector).length === 1; | |
} | |
combine(a) { | |
var fn = function (n, src, got, all) { | |
if (n == 0) { | |
if (got.length > 0) { | |
all[all.length] = got; | |
} | |
return; | |
} | |
for (var j = 0; j < src.length; j++) { | |
fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all); | |
} | |
return; | |
} | |
var all = []; | |
for (var i = 0; i < a.length; i++) { | |
fn(i, a, [], all); | |
} | |
all.push(a); | |
return all; | |
} | |
} | |
function start(){ | |
if(!running){ | |
running = true; | |
document.addEventListener('mouseover', onMouseOver); | |
document.addEventListener('mouseout', onMouseOut); | |
} | |
return this; | |
} | |
function stop(){ | |
if(running){ | |
running = false; | |
document.removeEventListener('mouseover', onMouseOver); | |
document.removeEventListener('mouseout', onMouseOut); | |
if(modalBg){ | |
modalBg.remove(); | |
modalBg = null; | |
outline = null; | |
modal = null; | |
} | |
} | |
return this; | |
} | |
return {start, stop}; | |
})(); | |
SelectorGenerator.stop().start(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment