Files
eruda/src/Elements/Elements.js
2020-04-18 23:19:29 +08:00

653 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Tool from '../DevTools/Tool'
import CssStore from './CssStore'
import Highlight from './Highlight'
import Select from './Select'
import Settings from '../Settings/Settings'
import {
$,
keys,
MutationObserver,
each,
toStr,
isEl,
isStr,
map,
escape,
startWith,
isFn,
isBool,
safeGet,
pxToNum,
isNaN,
isNum,
nextTick,
Emitter,
contain,
unique,
isNull,
trim,
lowerCase,
pick
} from '../lib/util'
import { isErudaEl } from '../lib/extraUtil'
import evalCss from '../lib/evalCss'
export default class Elements extends Tool {
constructor() {
super()
this._style = evalCss(require('./Elements.scss'))
this.name = 'elements'
this._tpl = require('./Elements.hbs')
this._rmDefComputedStyle = true
this._highlightElement = false
this._selectElement = false
this._observeElement = true
this._computedStyleSearchKeyword = ''
this._history = []
Emitter.mixin(this)
}
init($el, container) {
super.init($el)
this._container = container
$el.html('<div class="eruda-show-area"></div>')
this._$showArea = $el.find('.eruda-show-area')
$el.append(require('./BottomBar.hbs')())
this._htmlEl = document.documentElement
this._highlight = new Highlight(this._container.$container)
this._select = new Select()
this._bindEvent()
this._initObserver()
this._initCfg()
nextTick(() => this._updateHistory())
}
show() {
super.show()
if (this._observeElement) this._enableObserver()
if (!this._curEl) this._setEl(this._htmlEl)
this._render()
}
hide() {
this._disableObserver()
return super.hide()
}
set(e) {
if (e === this._curEl) return
this._setEl(e)
this.scrollToTop()
this._render()
this._updateHistory()
this.emit('change', e)
return this
}
overrideEventTarget() {
const winEventProto = getWinEventProto()
const origAddEvent = (this._origAddEvent = winEventProto.addEventListener)
const origRmEvent = (this._origRmEvent = winEventProto.removeEventListener)
winEventProto.addEventListener = function(type, listener, useCapture) {
addEvent(this, type, listener, useCapture)
origAddEvent.apply(this, arguments)
}
winEventProto.removeEventListener = function(type, listener, useCapture) {
rmEvent(this, type, listener, useCapture)
origRmEvent.apply(this, arguments)
}
}
scrollToTop() {
const el = this._$showArea.get(0)
el.scrollTop = 0
}
restoreEventTarget() {
const winEventProto = getWinEventProto()
if (this._origAddEvent) winEventProto.addEventListener = this._origAddEvent
if (this._origRmEvent) winEventProto.removeEventListener = this._origRmEvent
}
destroy() {
super.destroy()
evalCss.remove(this._style)
this._select.disable()
this._highlight.destroy()
this._disableObserver()
this.restoreEventTarget()
this._rmCfg()
}
_back() {
if (this._curEl === this._htmlEl) return
const parentQueue = this._curParentQueue
let parent = parentQueue.shift()
while (!isElExist(parent)) parent = parentQueue.shift()
this.set(parent)
}
_bindEvent() {
const self = this
const container = this._container
const select = this._select
this._$el
.on('click', '.eruda-child', function() {
const idx = $(this).data('idx')
const curEl = self._curEl
const el = curEl.childNodes[idx]
if (el && el.nodeType === 3) {
const curTagName = curEl.tagName
let type
switch (curTagName) {
case 'SCRIPT':
type = 'js'
break
case 'STYLE':
type = 'css'
break
default:
return
}
const sources = container.get('sources')
if (sources) {
sources.set(type, el.nodeValue)
container.showTool('sources')
}
return
}
!isElExist(el) ? self._render() : self.set(el)
})
.on('click', '.eruda-listener-content', function() {
const text = $(this).text()
const sources = container.get('sources')
if (sources) {
sources.set('js', text)
container.showTool('sources')
}
})
.on('click', '.eruda-breadcrumb', () => {
const sources = container.get('sources')
if (sources) {
sources.set('object', this._curEl)
container.showTool('sources')
}
})
.on('click', '.eruda-parent', function() {
let idx = $(this).data('idx')
const curEl = self._curEl
let el = curEl.parentNode
while (idx-- && el.parentNode) el = el.parentNode
!isElExist(el) ? self._render() : self.set(el)
})
.on('click', '.eruda-toggle-all-computed-style', () =>
this._toggleAllComputedStyle()
)
.on('click', '.eruda-computed-style-search', () => {
let filter = prompt('Filter')
if (isNull(filter)) return
filter = trim(filter)
this._computedStyleSearchKeyword = filter
this._render()
})
const $bottomBar = this._$el.find('.eruda-bottom-bar')
$bottomBar
.on('click', '.eruda-refresh', () => {
this._render()
container.notify('Refreshed')
})
.on('click', '.eruda-highlight', () => this._toggleHighlight())
.on('click', '.eruda-select', () => this._toggleSelect())
.on('click', '.eruda-reset', () => this.set(this._htmlEl))
select.on('select', target => this.set(target))
}
_toggleAllComputedStyle() {
this._rmDefComputedStyle = !this._rmDefComputedStyle
this._render()
}
_enableObserver() {
this._observer.observe(this._htmlEl, {
attributes: true,
childList: true,
subtree: true
})
}
_disableObserver() {
this._observer.disconnect()
}
_toggleHighlight() {
if (this._selectElement) return
this._$el.find('.eruda-highlight').toggleClass('eruda-active')
this._highlightElement = !this._highlightElement
this._render()
}
_toggleSelect() {
const select = this._select
this._$el.find('.eruda-select').toggleClass('eruda-active')
if (!this._selectElement && !this._highlightElement) this._toggleHighlight()
this._selectElement = !this._selectElement
if (this._selectElement) {
select.enable()
this._container.hide()
} else {
select.disable()
}
}
_setEl(el) {
this._curEl = el
this._curCssStore = new CssStore(el)
this._highlight.setEl(el)
this._rmDefComputedStyle = true
const parentQueue = []
let parent = el.parentNode
while (parent) {
parentQueue.push(parent)
parent = parent.parentNode
}
this._curParentQueue = parentQueue
}
_getData() {
const ret = {}
const el = this._curEl
const cssStore = this._curCssStore
const { className, id, attributes, tagName } = el
ret.computedStyleSearchKeyword = this._computedStyleSearchKeyword
ret.parents = getParents(el)
ret.children = formatChildNodes(el.childNodes)
ret.attributes = formatAttr(attributes)
ret.name = formatElName({ tagName, id, className, attributes })
const events = el.erudaEvents
if (events && keys(events).length !== 0) ret.listeners = events
if (needNoStyle(tagName)) return ret
let computedStyle = cssStore.getComputedStyle()
function getBoxModelValue(type) {
let keys = ['top', 'left', 'right', 'bottom']
if (type !== 'position') keys = map(keys, key => `${type}-${key}`)
if (type === 'border') keys = map(keys, key => `${key}-width`)
return {
top: boxModelValue(computedStyle[keys[0]], type),
left: boxModelValue(computedStyle[keys[1]], type),
right: boxModelValue(computedStyle[keys[2]], type),
bottom: boxModelValue(computedStyle[keys[3]], type)
}
}
const boxModel = {
margin: getBoxModelValue('margin'),
border: getBoxModelValue('border'),
padding: getBoxModelValue('padding'),
content: {
width: boxModelValue(computedStyle['width']),
height: boxModelValue(computedStyle['height'])
}
}
if (computedStyle['position'] !== 'static') {
boxModel.position = getBoxModelValue('position')
}
ret.boxModel = boxModel
const styles = cssStore.getMatchedCSSRules()
styles.unshift(getInlineStyle(el.style))
styles.forEach(style => processStyleRules(style.style))
ret.styles = styles
if (this._rmDefComputedStyle) {
computedStyle = rmDefComputedStyle(computedStyle, styles)
}
ret.rmDefComputedStyle = this._rmDefComputedStyle
const computedStyleSearchKeyword = lowerCase(ret.computedStyleSearchKeyword)
if (computedStyleSearchKeyword) {
computedStyle = pick(computedStyle, (val, property) => {
return (
contain(property, computedStyleSearchKeyword) ||
contain(val, computedStyleSearchKeyword)
)
})
}
processStyleRules(computedStyle)
ret.computedStyle = computedStyle
return ret
}
_render() {
if (!isElExist(this._curEl)) return this._back()
this._highlight[this._highlightElement ? 'show' : 'hide']()
this._renderHtml(this._tpl(this._getData()))
}
_renderHtml(html) {
if (html === this._lastHtml) return
this._lastHtml = html
this._$showArea.html(html)
}
_updateHistory() {
const console = this._container.get('console')
if (!console) return
const history = this._history
history.unshift(this._curEl)
if (history.length > 5) history.pop()
for (let i = 0; i < 5; i++) {
console.setGlobal(`$${i}`, history[i])
}
}
_initObserver() {
this._observer = new MutationObserver(mutations => {
each(mutations, mutation => this._handleMutation(mutation))
})
}
_handleMutation(mutation) {
let i, len, node
if (isErudaEl(mutation.target)) return
if (mutation.type === 'attributes') {
if (mutation.target !== this._curEl) return
this._render()
} else if (mutation.type === 'childList') {
if (mutation.target === this._curEl) return this._render()
const addedNodes = mutation.addedNodes
for (i = 0, len = addedNodes.length; i < len; i++) {
node = addedNodes[i]
if (node.parentNode === this._curEl) return this._render()
}
const removedNodes = mutation.removedNodes
for (i = 0, len = removedNodes.length; i < len; i++) {
if (removedNodes[i] === this._curEl) return this.set(this._htmlEl)
}
}
}
_rmCfg() {
const cfg = this.config
const settings = this._container.get('settings')
if (!settings) return
settings
.remove(cfg, 'overrideEventTarget')
.remove(cfg, 'observeElement')
.remove('Elements')
}
_initCfg() {
const cfg = (this.config = Settings.createCfg('elements', {
overrideEventTarget: true,
observeElement: true
}))
if (cfg.get('overrideEventTarget')) this.overrideEventTarget()
if (cfg.get('observeElement')) this._observeElement = false
cfg.on('change', (key, val) => {
switch (key) {
case 'overrideEventTarget':
return val ? this.overrideEventTarget() : this.restoreEventTarget()
case 'observeElement':
this._observeElement = val
return val ? this._enableObserver() : this._disableObserver()
}
})
const settings = this._container.get('settings')
if (!settings) return
settings
.text('Elements')
.switch(cfg, 'overrideEventTarget', 'Catch Event Listeners')
if (this._observer) settings.switch(cfg, 'observeElement', 'Auto Refresh')
settings.separator()
}
}
function processStyleRules(style) {
each(style, (val, key) => (style[key] = processStyleRule(val)))
}
const regColor = /rgba?\((.*?)\)/g
const regCssUrl = /url\("?(.*?)"?\)/g
function processStyleRule(val) {
// For css custom properties, val is unable to retrieved.
val = toStr(val)
return val
.replace(
regColor,
'<span class="eruda-style-color" style="background-color: $&"></span>$&'
)
.replace(regCssUrl, (match, url) => `url("${wrapLink(url)}")`)
}
const isElExist = val => isEl(val) && val.parentNode
function formatElName(data, { noAttr = false } = {}) {
const { id, className, attributes } = data
let ret = `<span class="eruda-tag-name-color">${data.tagName.toLowerCase()}</span>`
if (id !== '') ret += `<span class="eruda-function-color">#${id}</span>`
if (isStr(className)) {
let classes = ''
each(className.split(/\s+/g), val => {
if (val.trim() === '') return
classes += `.${val}`
})
ret += `<span class="eruda-attribute-name-color">${classes}</span>`
}
if (!noAttr) {
each(attributes, attr => {
const name = attr.name
if (name === 'id' || name === 'class' || name === 'style') return
ret += ` <span class="eruda-attribute-name-color">${name}</span><span class="eruda-operator-color">="</span><span class="eruda-string-color">${attr.value}</span><span class="eruda-operator-color">"</span>`
})
}
return ret
}
const formatAttr = attributes =>
map(attributes, attr => {
let { value } = attr
const { name } = attr
value = escape(value)
const isLink =
(name === 'src' || name === 'href') && !startWith(value, 'data')
if (isLink) value = wrapLink(value)
if (name === 'style') value = processStyleRule(value)
return { name, value }
})
function formatChildNodes(nodes) {
const ret = []
for (let i = 0, len = nodes.length; i < len; i++) {
const child = nodes[i]
const nodeType = child.nodeType
if (nodeType === 3 || nodeType === 8) {
const val = child.nodeValue.trim()
if (val !== '')
ret.push({
text: val,
isCmt: nodeType === 8,
idx: i
})
continue
}
const isSvg = !isStr(child.className)
if (
nodeType === 1 &&
child.id !== 'eruda' &&
(isSvg || child.className.indexOf('eruda') < 0)
) {
ret.push({
text: formatElName(child),
isEl: true,
idx: i
})
}
}
return ret
}
function getParents(el) {
const ret = []
let i = 0
let parent = el.parentNode
while (parent && parent.nodeType === 1) {
ret.push({
text: formatElName(parent, { noAttr: true }),
idx: i++
})
parent = parent.parentNode
}
return ret.reverse()
}
function getInlineStyle(style) {
const ret = {
selectorText: 'element.style',
style: {}
}
for (let i = 0, len = style.length; i < len; i++) {
const s = style[i]
ret.style[s] = style[s]
}
return ret
}
function rmDefComputedStyle(computedStyle, styles) {
const ret = {}
let keepStyles = ['display', 'width', 'height']
each(styles, style => {
keepStyles = keepStyles.concat(keys(style.style))
})
keepStyles = unique(keepStyles)
each(computedStyle, (val, key) => {
if (!contain(keepStyles, key)) return
ret[key] = val
})
return ret
}
const NO_STYLE_TAG = ['script', 'style', 'meta', 'title', 'link', 'head']
const needNoStyle = tagName => NO_STYLE_TAG.indexOf(tagName.toLowerCase()) > -1
function addEvent(el, type, listener, useCapture = false) {
if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return
const events = (el.erudaEvents = el.erudaEvents || {})
events[type] = events[type] || []
events[type].push({
listener: listener,
listenerStr: listener.toString(),
useCapture: useCapture
})
}
function rmEvent(el, type, listener, useCapture = false) {
if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return
const events = el.erudaEvents
if (!(events && events[type])) return
const listeners = events[type]
for (let i = 0, len = listeners.length; i < len; i++) {
if (listeners[i].listener === listener) {
listeners.splice(i, 1)
break
}
}
if (listeners.length === 0) delete events[type]
if (keys(events).length === 0) delete el.erudaEvents
}
const getWinEventProto = () => {
return safeGet(window, 'EventTarget.prototype') || window.Node.prototype
}
const wrapLink = link => `<a href="${link}" target="_blank">${link}</a>`
function boxModelValue(val, type) {
if (isNum(val)) return val
if (!isStr(val)) return ''
const ret = pxToNum(val)
if (isNaN(ret)) return val
if (type === 'position') return ret
return ret === 0 ? '' : ret
}