Last active
December 5, 2018 20:58
-
-
Save AsaAyers/d50940bea80c6e279171a622a27d682f to your computer and use it in GitHub Desktop.
js console tools
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
/* eslint-disable func-names, no-plusplus, babel/semi, arrow-parens, no-ternary, no-undefined */ | |
/* eslint semi: ["error", "never"] */ | |
/* global inlineTools:true, copy */ | |
inlineTools = (function () { | |
const replacer = (c) => `-${c.toLowerCase()}` | |
const toCSSProperty = (property) => property.replace(/[A-Z]/g, replacer) | |
.replace(/^webkit/, '-webkit') | |
const jsonClone = (obj) => JSON.parse(JSON.stringify(obj)) | |
const getStyle = (element, pseudoElt = null) => { | |
// When element is in an iframe, it uses that iframe's window to gather the | |
// styles | |
const doc = element.ownerDocument | |
const win = doc.defaultView | |
const styles = jsonClone( | |
win.getComputedStyle(element, pseudoElt), | |
) | |
const styleMap = typeof element.computedStyleMap === 'function' | |
? element.computedStyleMap() | |
: undefined | |
Object.keys(styles).forEach(key => { | |
// I don't know why, but sometimes getComputedStyle returns a bunch of | |
// numbered values | |
if (!Number.isNaN(Number(key))) { | |
delete styles[key] | |
return | |
} | |
if (styleMap) { | |
const result = styleMap.get(toCSSProperty(key)) | |
if (result == null || result.value == null) { | |
return | |
} | |
switch (result.unit) { | |
case 'percent': | |
styles[key] = `${result.value}%` | |
break | |
case 's': | |
case 'px': | |
styles[key] = `${result.value}${result.unit}` | |
break | |
case 'number': | |
case undefined: | |
styles[key] = result.value | |
break | |
default: | |
console.warn('Unknown unit:', key, result) | |
} | |
} | |
}) | |
return styles | |
} | |
function cloneIntoDoc(doc, element, pseudoElt = null) { | |
const clone = doc.createElement(element.tagName) | |
doc.body.append(clone) | |
const style = getStyle(clone, pseudoElt) | |
doc.body.removeChild(clone) | |
return style | |
} | |
const iframe = document.createElement('iframe') | |
const elementCache = {} | |
const getStyleWithIframe = (element, pseudoElt = null) => { | |
const key = `${element.tagName}${pseudoElt || ''}` | |
if (elementCache[key] == null) { | |
document.body.appendChild(iframe) | |
elementCache[key] = cloneIntoDoc(iframe.contentDocument, element, pseudoElt) | |
document.body.removeChild(iframe) | |
} | |
return elementCache[key] | |
} | |
const pseudoSelectors = [ | |
'before', | |
'after', | |
] | |
function compareAppliedStyles(styled, base) { | |
const blockKeys = [ | |
// Skipping font will let me gather its pieces individually | |
'font', | |
] | |
// If the last key we processed was `background`, then don't process `backgroundColor` | |
let last = null | |
// By only iterating on `styled`, this can only remove styles | |
const styles = Object.keys(styled).reduce((styles, key) => { | |
// If we just collected `border`, don't collect things like `borderLeft`. | |
// But also check for a capital letter so that colleciting `d` doesn't | |
// block collecting of `display`. | |
if (key.match(new RegExp(`^${last}[A-Z]`))) { | |
return styles | |
} | |
if (blockKeys.indexOf(key) >= 0) { | |
return styles | |
} | |
last = key | |
if (String(base[key]) !== String(styled[key])) { | |
styles[key] = styled[key] | |
} | |
return styles | |
}, {}) | |
copy(styles) | |
return styles | |
} | |
function getAppliedStyles(el, pseudoElt = null) { | |
const base = getStyleWithIframe(el, pseudoElt) | |
const style = getStyle(el, pseudoElt) | |
const result = compareAppliedStyles(style, base) | |
copy(result) | |
return result | |
} | |
function toCSS(obj, selector = '.dummy') { | |
const rules = Object.keys(obj) | |
.sort() | |
.map((key) => { | |
const cssKey = toCSSProperty(key) | |
return ` ${cssKey}: ${obj[key]};` | |
}) | |
if (rules.length === 0) return '' | |
const css = [`${selector} {`] | |
.concat(rules) | |
.concat(['}']) | |
const result = css.join('\n') | |
copy(result) | |
return result | |
} | |
function addStyleString(str, doc = document) { | |
const node = doc.createElement('style') | |
node.innerHTML = `\n${str}\n` | |
doc.body.appendChild(node) | |
return node | |
} | |
function deepClone(rootElement, baseClass) { | |
let styles = {} | |
const dataKey = `cn-${Math.floor(Math.random() * 100)}` | |
function sortTagsFirst(a, b) { | |
// Wild cards should come first | |
if (a.indexOf('*') >= 0) return -1 | |
if (b.indexOf('*') >= 0) return 1 | |
// tag selectors like `div {` have all been scoped to this component with | |
// `.baseClass div{` | |
const aTag = a.indexOf(`.${baseClass}__`) === -1 | |
const bTag = b.indexOf(`.${baseClass}__`) === -1 | |
if (aTag && bTag) { | |
return b.length - a.length | |
} | |
if (aTag && !bTag) { | |
return -1 | |
} | |
if (!aTag && bTag) { | |
return 1 | |
} | |
// When encountering 2 classes, just keep them in the same order | |
return 0 | |
} | |
const originalStyles = {} | |
function styleHelper(el, selector) { | |
const elStyles = {} | |
// compare to this element in a style-free iframe | |
elStyles[selector] = getAppliedStyles(el, null) | |
originalStyles[selector] = jsonClone(elStyles[selector]) | |
pseudoSelectors.forEach(p => { | |
// Compare the pseudoElement to one in a style-less iframe | |
elStyles[`${selector}:${p}`] = getAppliedStyles(el, `::${p}`) | |
originalStyles[`${selector}:${p}`] = jsonClone(elStyles[`${selector}:${p}`]) | |
// Remove styles that were just inherited from the element | |
elStyles[`${selector}:${p}`] = compareAppliedStyles(elStyles[`${selector}:${p}`], elStyles[selector]) | |
}) | |
return elStyles | |
} | |
function gatherStyles(el) { | |
let counter = 1 | |
let className | |
let selector | |
const tagName = el.tagName.toLowerCase() | |
do { | |
className = `${baseClass}__${tagName}${counter++}` | |
selector = `.${className}` | |
} while (styles[selector] != null) | |
styles = { | |
...styles, | |
...styleHelper(el, selector), | |
} | |
for (let i = 0; i < el.children.length; i++) { | |
const child = el.children[i] | |
gatherStyles(child) | |
} | |
el.dataset[dataKey] = className | |
return className | |
} | |
gatherStyles(rootElement) | |
// const originalStyles = JSON.parse(JSON.stringify(styles)) | |
function validateSelector(el, selector, p) { | |
if (!originalStyles[selector]) { | |
console.warn(`Missing original style for: ${selector}`) | |
} | |
const style = getStyle(el, p) | |
const changed = compareAppliedStyles( | |
style, | |
{ ...style, ...originalStyles[selector] } | |
) | |
if (Object.keys(changed).length === 0) { | |
console.info('Validated', selector) | |
} else { | |
const k = Object.keys(changed).shift() | |
console.warn('changed', selector, k, style[k], originalStyles[selector][k], originalStyles[selector]) | |
} | |
} | |
function validateStyle(el) { | |
if (el.className.length > 0) { | |
const selector = `.${el.className}` | |
validateSelector(el, selector, null) | |
pseudoSelectors.forEach(p => { | |
validateSelector(el, `${selector}:${p}`, p) | |
}) | |
} | |
for (let i = 0; i < el.children.length; i++) { | |
const child = el.children[i] | |
validateStyle(child) | |
} | |
} | |
let css = Object.keys(styles) | |
.map((selector) => toCSS(styles[selector], selector)) | |
.join('\n\n') | |
const iframe = document.createElement('iframe') | |
document.body.appendChild(iframe) | |
const root = iframe.contentDocument.createElement('div') | |
root.className = baseClass | |
root.innerHTML = rootElement.outerHTML | |
function applyClassNames(el) { | |
const className = el.dataset[dataKey] | |
if (className) { | |
delete el.dataset[dataKey] | |
el.className = className | |
} | |
for (let i = 0; i < el.children.length; i++) { | |
const child = el.children[i] | |
applyClassNames(child) | |
} | |
} | |
// const root = iframe.contentDocument.body.children[0] | |
applyClassNames(root) | |
iframe.contentDocument.body.appendChild(root) | |
let styleNode = addStyleString(css, iframe.contentDocument) | |
iframe.width = window.outerWidth | |
iframe.height = iframe.contentDocument.body.scrollHeight | |
function compressTags() { | |
const selectors = Object.keys(styles).sort() | |
const classes = selectors.filter(sel => sel.indexOf(':') === -1) | |
const invertedTree = {} | |
selectors.forEach(selector => { | |
Object.keys(styles[selector]).forEach(property => { | |
const value = styles[selector][property] | |
invertedTree[property] = invertedTree[property] || {} | |
invertedTree[property][value] = invertedTree[property][value] || [] | |
invertedTree[property][value].push(selector) | |
}) | |
delete styles[selector] | |
}) | |
Object.keys(invertedTree).forEach(property => { | |
Object.keys(invertedTree[property]).forEach(value => { | |
let selector = invertedTree[property][value].join(',\n') | |
if (invertedTree[property][value].length === classes.length) { | |
selector = `.${baseClass} *` | |
} | |
styles[selector] = styles[selector] || {} | |
styles[selector][property] = value | |
}) | |
}) | |
} | |
compressTags() | |
css = Object.keys(styles) | |
.sort(sortTagsFirst) | |
.map((selector) => toCSS(styles[selector], selector)) | |
.filter(css => css.length > 0) | |
.join('\n\n') | |
const iBody = iframe.contentDocument.body | |
iBody.removeChild(styleNode) | |
styleNode = addStyleString(css, iframe.contentDocument) | |
iBody.insertBefore(styleNode, root) | |
validateStyle(root.children[0]) | |
// I'm not sure why, but I couldn't get copy to work here. I also couldn't | |
// seem to copy(inlineTools.deepClone(...)) | |
return iBody.innerHTML.replace(/\s*class=""/g, '') | |
} | |
function gatherClassNames(el) { | |
let classNames = [] | |
for (let i = 0; i < el.children.length; i++) { | |
const child = el.children[i] | |
classNames = classNames.concat( | |
gatherClassNames(child), | |
) | |
} | |
return classNames.concat([...el.classList]) | |
} | |
return { | |
gatherClassNames: (el) => [...new Set(gatherClassNames(el))], | |
deepClone, | |
getStyle: (el) => { | |
const styles = getStyle(el) | |
copy(styles) | |
return styles | |
}, | |
getAppliedStyles, | |
toCSS, | |
compareAppliedStyles: (el, base) => compareAppliedStyles(getStyle(el), base), | |
} | |
}()) | |
tmp = document.getElementsByClassName('container')[0] | |
inlineTools.deepClone(tmp, 'error-boundary') | |
// You need to run copy manually after deepClone finishes | |
// copy($_) | |
// inlineTools.gatherClassNames($0) | |
// after copying this into the console, select an element and run | |
// inlineTools.getAppliedStyles($0, '::before') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment