Skip to content

Instantly share code, notes, and snippets.

@Pamblam
Created January 27, 2020 21:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pamblam/5aa97f26f84d62549b804d89050b58ef to your computer and use it in GitHub Desktop.
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.
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}'>&lt;</span><span style='color:${colors.tagname}'>`);
mode = modes.indexOf('reading_tagname');
}
break;
case "seeking_token":
if(char === '>'){
parsed.push(`<span style='color:${colors.neutral}'>&gt;</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}'>&gt;</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}'>&gt;</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}'>&gt;</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