Skip to content

Instantly share code, notes, and snippets.

@AsaAyers
Last active December 5, 2018 20:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AsaAyers/d50940bea80c6e279171a622a27d682f to your computer and use it in GitHub Desktop.
Save AsaAyers/d50940bea80c6e279171a622a27d682f to your computer and use it in GitHub Desktop.
js console tools
/* 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