mirror of
https://github.com/liriliri/eruda.git
synced 2026-05-20 08:47:20 +08:00
feat(elements): integrate dom viewer
This commit is contained in:
@@ -65,6 +65,7 @@ module.exports = {
|
||||
path.resolve(__dirname, '../node_modules/luna-tab'),
|
||||
path.resolve(__dirname, '../node_modules/luna-data-grid'),
|
||||
path.resolve(__dirname, '../node_modules/luna-object-viewer'),
|
||||
path.resolve(__dirname, '../node_modules/luna-dom-viewer'),
|
||||
],
|
||||
use: [
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"licia": "^1.37.0",
|
||||
"luna-console": "^1.2.0",
|
||||
"luna-data-grid": "^0.3.1",
|
||||
"luna-dom-viewer": "^1.0.2",
|
||||
"luna-dom-viewer": "^1.1.1",
|
||||
"luna-modal": "^1.0.0",
|
||||
"luna-notification": "^0.1.4",
|
||||
"luna-object-viewer": "^0.2.4",
|
||||
|
||||
430
src/Elements/Detail.js
Normal file
430
src/Elements/Detail.js
Normal file
@@ -0,0 +1,430 @@
|
||||
import isEmpty from 'licia/isEmpty'
|
||||
import lowerCase from 'licia/lowerCase'
|
||||
import pick from 'licia/pick'
|
||||
import toStr from 'licia/toStr'
|
||||
import map from 'licia/map'
|
||||
import escape from 'licia/escape'
|
||||
import startWith from 'licia/startWith'
|
||||
import contain from 'licia/contain'
|
||||
import unique from 'licia/unique'
|
||||
import isStr from 'licia/isStr'
|
||||
import isNaN from 'licia/isNaN'
|
||||
import isNum from 'licia/isNum'
|
||||
import each from 'licia/each'
|
||||
import keys from 'licia/keys'
|
||||
import CssStore from './CssStore'
|
||||
import { pxToNum, classPrefix as c } from '../lib/util'
|
||||
|
||||
export default class Detail {
|
||||
constructor($container) {
|
||||
this._$container = $container
|
||||
this._bindEvent()
|
||||
}
|
||||
show(el) {
|
||||
const data = this._getData(el)
|
||||
|
||||
let parents = ''
|
||||
if (!isEmpty(data.parents)) {
|
||||
parents = `<ul class="${c('parents')}">
|
||||
${map(data.parents, ({ text, idx }) => {
|
||||
return `<li>
|
||||
<div class="${c('parent')}" data-idx="${idx}">${text}</div>
|
||||
<span class="${c('icon-arrow-right')}"></span>
|
||||
</li>`
|
||||
}).join('')}
|
||||
</ul>`
|
||||
}
|
||||
|
||||
let children = ''
|
||||
if (data.children) {
|
||||
children = `<ul class="${c('children')}">
|
||||
${map(data.children, ({ isCmt, idx, isEl, text }) => {
|
||||
return `<li class="${c(
|
||||
`child ${isCmt ? 'green' : ''} ${isEl ? 'active-effect' : ''}`
|
||||
)}" data-idx="${idx}">${text}</li>`
|
||||
}).join('')}
|
||||
</ul>`
|
||||
}
|
||||
|
||||
let attribute = '<tr><td>Empty</td></tr>'
|
||||
if (!isEmpty(data.attributes)) {
|
||||
attribute = map(data.attributes, ({ name, value }) => {
|
||||
return `<tr>
|
||||
<td class="${c('attribute-name-color')}">${escape(name)}</td>
|
||||
<td class="${c('string-color')}">${value}</td>
|
||||
</tr>`
|
||||
}).join('')
|
||||
}
|
||||
attribute = `<div class="${c('attributes section')}">
|
||||
<h2>Attributes</h2>
|
||||
<div class="${c('table-wrapper')}">
|
||||
<table>
|
||||
<tbody>
|
||||
${attribute}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
let styles = ''
|
||||
if (!isEmpty(data.styles)) {
|
||||
const style = map(data.styles, ({ selectorText, style }) => {
|
||||
style = map(style, (val, key) => {
|
||||
return `<div class="${c('rule')}"><span>${escape(
|
||||
key
|
||||
)}</span>: ${val};</div>`
|
||||
}).join('')
|
||||
return `<div class="${c('style-rules')}">
|
||||
<div>${escape(selectorText)} {</div>
|
||||
${style}
|
||||
<div>}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
styles = `<div class="${c('styles section')}">
|
||||
<h2>Styles</h2>
|
||||
<div class="${c('style-wrapper')}">
|
||||
${style}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
let computedStyle = ''
|
||||
if (data.computedStyle) {
|
||||
let toggleButton = c(`<div class="btn toggle-all-computed-style">
|
||||
<span class="icon-expand"></span>
|
||||
</div>`)
|
||||
if (data.rmDefComputedStyle) {
|
||||
toggleButton = c(`<div class="btn toggle-all-computed-style">
|
||||
<span class="icon-compress"></span>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
const boxModel = data.boxModel
|
||||
// prettier-ignore
|
||||
const boxModelHtml = [`<div class="${c('box-model')}">`,
|
||||
boxModel.position ? `<div class="${c('position')}">` : '',
|
||||
boxModel.position ? `<div class="${c('label')}">position</div><div class="${c('top')}">${boxModel.position.top}</div><br><div class="${c('left')}">${boxModel.position.left}</div>` : '',
|
||||
`<div class="${c('margin')}">`,
|
||||
`<div class="${c('label')}">margin</div><div class="${c('top')}">${boxModel.margin.top}</div><br><div class="${c('left')}">${boxModel.margin.left}</div>`,
|
||||
`<div class="${c('border')}">`,
|
||||
`<div class="${c('label')}">border</div><div class="${c('top')}">${boxModel.border.top}</div><br><div class="${c('left')}">${boxModel.border.left}</div>`,
|
||||
`<div class="${c('padding')}">`,
|
||||
`<div class="${c('label')}">padding</div><div class="${c('top')}">${boxModel.padding.top}</div><br><div class="${c('left')}">${boxModel.padding.left}</div>`,
|
||||
`<div class="${c('content')}">`,
|
||||
`<span>${boxModel.content.width}</span> × <span>${boxModel.content.height}</span>`,
|
||||
'</div>',
|
||||
`<div class="${c('right')}">${boxModel.padding.right}</div><br><div class="${c('bottom')}">${boxModel.padding.bottom}</div>`,
|
||||
'</div>',
|
||||
`<div class="${c('right')}">${boxModel.border.right}</div><br><div class="${c('bottom')}">${boxModel.border.bottom}</div>`,
|
||||
'</div>',
|
||||
`<div class="${c('right')}">${boxModel.margin.right}</div><br><div class="${c('bottom')}">${boxModel.margin.bottom}</div>`,
|
||||
'</div>',
|
||||
boxModel.position ? `<div class="${c('right')}">${boxModel.position.right}</div><br><div class="${c('bottom')}">${boxModel.position.bottom}</div>` : '',
|
||||
boxModel.position ? '</div>' : '',
|
||||
'</div>'].join('')
|
||||
|
||||
computedStyle = `<div class="${c('computed-style section')}">
|
||||
<h2>
|
||||
Computed Style
|
||||
${toggleButton}
|
||||
<div class="${c('btn computed-style-search')}">
|
||||
<span class="${c('icon-filter')}"></span>
|
||||
</div>
|
||||
${
|
||||
data.computedStyleSearchKeyword
|
||||
? `<div class="${c('btn search-keyword')}">${escape(
|
||||
data.computedStyleSearchKeyword
|
||||
)}</div>`
|
||||
: ''
|
||||
}
|
||||
</h2>
|
||||
${boxModelHtml}
|
||||
<div class="${c('table-wrapper')}">
|
||||
<table>
|
||||
<tbody>
|
||||
${map(data.computedStyle, (val, key) => {
|
||||
return `<tr>
|
||||
<td class="${c('key')}">${escape(key)}</td>
|
||||
<td>${val}</td>
|
||||
</tr>`
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
let listeners = ''
|
||||
if (data.listeners) {
|
||||
listeners = map(data.listeners, (listeners, key) => {
|
||||
listeners = map(listeners, ({ useCapture, listenerStr }) => {
|
||||
return `<li ${useCapture ? `class="${c('capture')}"` : ''}>${escape(
|
||||
listenerStr
|
||||
)}</li>`
|
||||
}).join('')
|
||||
return `<div class="${c('listener')}">
|
||||
<div class="${c('listener-type')}">${escape(key)}</div>
|
||||
<ul class="${c('listener-content')}">
|
||||
${listeners}
|
||||
</ul>
|
||||
</div>`
|
||||
}).join('')
|
||||
listeners = `<div class="${c('listeners section')}">
|
||||
<h2>Event Listeners</h2>
|
||||
<div class="${c('listener-wrapper')}">
|
||||
${listeners}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
const html = `${parents}
|
||||
<div class="${c('breadcrumb')}">
|
||||
${data.name}
|
||||
</div>
|
||||
${children}
|
||||
${attribute}
|
||||
${styles}
|
||||
${computedStyle}
|
||||
${listeners}`
|
||||
|
||||
this._$container.html(html).show()
|
||||
}
|
||||
_getData(el) {
|
||||
const ret = {}
|
||||
|
||||
const cssStore = new CssStore(el)
|
||||
|
||||
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
|
||||
}
|
||||
_bindEvent() {}
|
||||
}
|
||||
|
||||
function processStyleRules(style) {
|
||||
each(style, (val, key) => (style[key] = processStyleRule(val)))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)}")`)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,35 +1,24 @@
|
||||
import Tool from '../DevTools/Tool'
|
||||
import CssStore from './CssStore'
|
||||
import Highlight from './Highlight'
|
||||
import Select from './Select'
|
||||
import Settings from '../Settings/Settings'
|
||||
import $ from 'licia/$'
|
||||
import isEmpty from 'licia/isEmpty'
|
||||
import keys from 'licia/keys'
|
||||
import MutationObserver from 'licia/MutationObserver'
|
||||
import each from 'licia/each'
|
||||
import toStr from 'licia/toStr'
|
||||
import isEl from 'licia/isEl'
|
||||
import isStr from 'licia/isStr'
|
||||
import map from 'licia/map'
|
||||
import escape from 'licia/escape'
|
||||
import startWith from 'licia/startWith'
|
||||
import isFn from 'licia/isFn'
|
||||
import isBool from 'licia/isBool'
|
||||
import safeGet from 'licia/safeGet'
|
||||
import isNaN from 'licia/isNaN'
|
||||
import isNum from 'licia/isNum'
|
||||
import nextTick from 'licia/nextTick'
|
||||
import Emitter from 'licia/Emitter'
|
||||
import contain from 'licia/contain'
|
||||
import unique from 'licia/unique'
|
||||
import isNull from 'licia/isNull'
|
||||
import trim from 'licia/trim'
|
||||
import lowerCase from 'licia/lowerCase'
|
||||
import pick from 'licia/pick'
|
||||
import LunaModal from 'luna-modal'
|
||||
import { pxToNum, isErudaEl, classPrefix as c } from '../lib/util'
|
||||
import LunaDomViewer from 'luna-dom-viewer'
|
||||
import { isErudaEl, classPrefix as c } from '../lib/util'
|
||||
import evalCss from '../lib/evalCss'
|
||||
import Detail from './Detail'
|
||||
|
||||
export default class Elements extends Tool {
|
||||
constructor() {
|
||||
@@ -52,26 +41,14 @@ export default class Elements extends Tool {
|
||||
|
||||
this._container = container
|
||||
|
||||
$el.html('<div class="eruda-show-area"></div>')
|
||||
this._$showArea = $el.find('.eruda-show-area')
|
||||
$el.append(
|
||||
c(`<div class="bottom-bar">
|
||||
<div class="btn select">
|
||||
<span class="icon icon-select"></span>
|
||||
</div>
|
||||
<div class="btn refresh">
|
||||
<span class="icon icon-refresh"></span>
|
||||
</div>
|
||||
<div class="btn highlight">
|
||||
<span class="icon icon-eye"></span>
|
||||
</div>
|
||||
<div class="btn reset">
|
||||
<span class="icon icon-reset"></span>
|
||||
</div>
|
||||
</div>`)
|
||||
)
|
||||
|
||||
this._initTpl()
|
||||
this._htmlEl = document.documentElement
|
||||
this._detail = new Detail(this._$detail)
|
||||
this._domViewer = new LunaDomViewer(this._$domViewer.get(0), {
|
||||
node: this._htmlEl,
|
||||
ignore: (node) => isErudaEl(node),
|
||||
})
|
||||
this._domViewer.expand()
|
||||
this._highlight = new Highlight(this._container.$container)
|
||||
this._select = new Select()
|
||||
this._bindEvent()
|
||||
@@ -96,7 +73,6 @@ export default class Elements extends Tool {
|
||||
if (e === this._curEl) return
|
||||
|
||||
this._setEl(e)
|
||||
this.scrollToTop()
|
||||
this._render()
|
||||
this._updateHistory()
|
||||
|
||||
@@ -120,11 +96,6 @@ export default class Elements extends Tool {
|
||||
origRmEvent.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
scrollToTop() {
|
||||
const el = this._$showArea.get(0)
|
||||
|
||||
el.scrollTop = 0
|
||||
}
|
||||
restoreEventTarget() {
|
||||
const winEventProto = getWinEventProto()
|
||||
|
||||
@@ -141,6 +112,24 @@ export default class Elements extends Tool {
|
||||
this.restoreEventTarget()
|
||||
this._rmCfg()
|
||||
}
|
||||
_initTpl() {
|
||||
const $el = this._$el
|
||||
|
||||
$el.html(
|
||||
c(`<div class="control">
|
||||
<span class="icon icon-select select"></span>
|
||||
<span class="icon icon-eye show"></span>
|
||||
</div>
|
||||
<div class="dom-viewer-container">
|
||||
<div class="dom-viewer"></div>
|
||||
</div>
|
||||
<div class="detail"></div>`)
|
||||
)
|
||||
|
||||
this._$detail = $el.find(c('.detail'))
|
||||
this._$domViewer = $el.find(c('.dom-viewer'))
|
||||
this._$control = $el.find(c('.control'))
|
||||
}
|
||||
_back() {
|
||||
if (this._curEl === this._htmlEl) return
|
||||
|
||||
@@ -227,16 +216,9 @@ export default class Elements extends Tool {
|
||||
})
|
||||
})
|
||||
|
||||
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))
|
||||
this._$control
|
||||
.on('click', c('.select'), () => this._toggleSelect())
|
||||
.on('click', c('.show'), () => this._detail.show(this._curEl))
|
||||
|
||||
select.on('select', (target) => this.set(target))
|
||||
}
|
||||
@@ -266,20 +248,21 @@ export default class Elements extends Tool {
|
||||
_toggleSelect() {
|
||||
const select = this._select
|
||||
|
||||
this._$el.find('.eruda-select').toggleClass('eruda-active')
|
||||
if (!this._selectElement && !this._highlightElement) this._toggleHighlight()
|
||||
this._$el.find(c('.select')).toggleClass(c('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._domViewer.select(el)
|
||||
this._highlight.setEl(el)
|
||||
this._rmDefComputedStyle = true
|
||||
|
||||
@@ -292,250 +275,10 @@ export default class Elements extends Tool {
|
||||
}
|
||||
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']()
|
||||
|
||||
const data = this._getData()
|
||||
|
||||
let parents = ''
|
||||
if (!isEmpty(data.parents)) {
|
||||
parents = `<ul class="${c('parents')}">
|
||||
${map(data.parents, ({ text, idx }) => {
|
||||
return `<li>
|
||||
<div class="${c('parent')}" data-idx="${idx}">${text}</div>
|
||||
<span class="${c('icon-arrow-right')}"></span>
|
||||
</li>`
|
||||
}).join('')}
|
||||
</ul>`
|
||||
}
|
||||
|
||||
let children = ''
|
||||
if (data.children) {
|
||||
children = `<ul class="${c('children')}">
|
||||
${map(data.children, ({ isCmt, idx, isEl, text }) => {
|
||||
return `<li class="${c(
|
||||
`child ${isCmt ? 'green' : ''} ${isEl ? 'active-effect' : ''}`
|
||||
)}" data-idx="${idx}">${text}</li>`
|
||||
}).join('')}
|
||||
</ul>`
|
||||
}
|
||||
|
||||
let attribute = '<tr><td>Empty</td></tr>'
|
||||
if (!isEmpty(data.attributes)) {
|
||||
attribute = map(data.attributes, ({ name, value }) => {
|
||||
return `<tr>
|
||||
<td class="${c('attribute-name-color')}">${escape(name)}</td>
|
||||
<td class="${c('string-color')}">${value}</td>
|
||||
</tr>`
|
||||
}).join('')
|
||||
}
|
||||
attribute = `<div class="${c('attributes section')}">
|
||||
<h2>Attributes</h2>
|
||||
<div class="${c('table-wrapper')}">
|
||||
<table>
|
||||
<tbody>
|
||||
${attribute}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
let styles = ''
|
||||
if (!isEmpty(data.styles)) {
|
||||
const style = map(data.styles, ({ selectorText, style }) => {
|
||||
style = map(style, (val, key) => {
|
||||
return `<div class="${c('rule')}"><span>${escape(
|
||||
key
|
||||
)}</span>: ${val};</div>`
|
||||
}).join('')
|
||||
return `<div class="${c('style-rules')}">
|
||||
<div>${escape(selectorText)} {</div>
|
||||
${style}
|
||||
<div>}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
styles = `<div class="${c('styles section')}">
|
||||
<h2>Styles</h2>
|
||||
<div class="${c('style-wrapper')}">
|
||||
${style}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
let computedStyle = ''
|
||||
if (data.computedStyle) {
|
||||
let toggleButton = c(`<div class="btn toggle-all-computed-style">
|
||||
<span class="icon-expand"></span>
|
||||
</div>`)
|
||||
if (data.rmDefComputedStyle) {
|
||||
toggleButton = c(`<div class="btn toggle-all-computed-style">
|
||||
<span class="icon-compress"></span>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
const boxModel = data.boxModel
|
||||
// prettier-ignore
|
||||
const boxModelHtml = [`<div class="${c('box-model')}">`,
|
||||
boxModel.position ? `<div class="${c('position')}">` : '',
|
||||
boxModel.position ? `<div class="${c('label')}">position</div><div class="${c('top')}">${boxModel.position.top}</div><br><div class="${c('left')}">${boxModel.position.left}</div>` : '',
|
||||
`<div class="${c('margin')}">`,
|
||||
`<div class="${c('label')}">margin</div><div class="${c('top')}">${boxModel.margin.top}</div><br><div class="${c('left')}">${boxModel.margin.left}</div>`,
|
||||
`<div class="${c('border')}">`,
|
||||
`<div class="${c('label')}">border</div><div class="${c('top')}">${boxModel.border.top}</div><br><div class="${c('left')}">${boxModel.border.left}</div>`,
|
||||
`<div class="${c('padding')}">`,
|
||||
`<div class="${c('label')}">padding</div><div class="${c('top')}">${boxModel.padding.top}</div><br><div class="${c('left')}">${boxModel.padding.left}</div>`,
|
||||
`<div class="${c('content')}">`,
|
||||
`<span>${boxModel.content.width}</span> × <span>${boxModel.content.height}</span>`,
|
||||
'</div>',
|
||||
`<div class="${c('right')}">${boxModel.padding.right}</div><br><div class="${c('bottom')}">${boxModel.padding.bottom}</div>`,
|
||||
'</div>',
|
||||
`<div class="${c('right')}">${boxModel.border.right}</div><br><div class="${c('bottom')}">${boxModel.border.bottom}</div>`,
|
||||
'</div>',
|
||||
`<div class="${c('right')}">${boxModel.margin.right}</div><br><div class="${c('bottom')}">${boxModel.margin.bottom}</div>`,
|
||||
'</div>',
|
||||
boxModel.position ? `<div class="${c('right')}">${boxModel.position.right}</div><br><div class="${c('bottom')}">${boxModel.position.bottom}</div>` : '',
|
||||
boxModel.position ? '</div>' : '',
|
||||
'</div>'].join('')
|
||||
|
||||
computedStyle = `<div class="${c('computed-style section')}">
|
||||
<h2>
|
||||
Computed Style
|
||||
${toggleButton}
|
||||
<div class="${c('btn computed-style-search')}">
|
||||
<span class="${c('icon-filter')}"></span>
|
||||
</div>
|
||||
${
|
||||
data.computedStyleSearchKeyword
|
||||
? `<div class="${c('btn search-keyword')}">${escape(
|
||||
data.computedStyleSearchKeyword
|
||||
)}</div>`
|
||||
: ''
|
||||
}
|
||||
</h2>
|
||||
${boxModelHtml}
|
||||
<div class="${c('table-wrapper')}">
|
||||
<table>
|
||||
<tbody>
|
||||
${map(data.computedStyle, (val, key) => {
|
||||
return `<tr>
|
||||
<td class="${c('key')}">${escape(key)}</td>
|
||||
<td>${val}</td>
|
||||
</tr>`
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
let listeners = ''
|
||||
if (data.listeners) {
|
||||
listeners = map(data.listeners, (listeners, key) => {
|
||||
listeners = map(listeners, ({ useCapture, listenerStr }) => {
|
||||
return `<li ${useCapture ? `class="${c('capture')}"` : ''}>${escape(
|
||||
listenerStr
|
||||
)}</li>`
|
||||
}).join('')
|
||||
return `<div class="${c('listener')}">
|
||||
<div class="${c('listener-type')}">${escape(key)}</div>
|
||||
<ul class="${c('listener-content')}">
|
||||
${listeners}
|
||||
</ul>
|
||||
</div>`
|
||||
}).join('')
|
||||
listeners = `<div class="${c('listeners section')}">
|
||||
<h2>Event Listeners</h2>
|
||||
<div class="${c('listener-wrapper')}">
|
||||
${listeners}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
const html = `${parents}
|
||||
<div class="${c('breadcrumb')}">
|
||||
${data.name}
|
||||
</div>
|
||||
${children}
|
||||
${attribute}
|
||||
${styles}
|
||||
${computedStyle}
|
||||
${listeners}`
|
||||
|
||||
this._renderHtml(html)
|
||||
}
|
||||
_renderHtml(html) {
|
||||
if (html === this._lastHtml) return
|
||||
@@ -628,159 +371,6 @@ export default class Elements extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -818,17 +408,4 @@ 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
|
||||
}
|
||||
const isElExist = (val) => isEl(val) && val.parentNode
|
||||
|
||||
@@ -2,11 +2,26 @@
|
||||
@import '../style/mixin';
|
||||
|
||||
#elements {
|
||||
padding-bottom: 40px;
|
||||
padding-top: 40px;
|
||||
font-size: 14px;
|
||||
.show-area {
|
||||
.control {
|
||||
@include control();
|
||||
padding: 10px 0;
|
||||
.icon-eye {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
.dom-viewer-container {
|
||||
@include overflow-auto(y);
|
||||
height: 100%;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.detail {
|
||||
@include absolute();
|
||||
@include overflow-auto(y);
|
||||
z-index: 10;
|
||||
display: none;
|
||||
background: var(--background);
|
||||
}
|
||||
.parents {
|
||||
@include overflow-auto(x);
|
||||
@@ -250,31 +265,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.bottom-bar {
|
||||
height: 40px;
|
||||
background: var(--darker-background);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
font-size: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
width: 25%;
|
||||
display: inline-block;
|
||||
transition: background $anim-duration, color $anim-duration;
|
||||
&:active {
|
||||
background: var(--highlight);
|
||||
color: var(--select-foreground);
|
||||
}
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,11 +227,9 @@ export default class Network extends Tool {
|
||||
_initTpl() {
|
||||
const $el = this._$el
|
||||
$el.html(
|
||||
c(`<div class="title">
|
||||
Request
|
||||
<div class="btn clear-request">
|
||||
<span class="icon-clear"></span>
|
||||
</div>
|
||||
c(`<div class="control">
|
||||
<span class="title">Request</span>
|
||||
<span class="icon-clear clear-request"></span>
|
||||
</div>
|
||||
<div class="requests"></div>
|
||||
<div class="detail"></div>`)
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
|
||||
#network {
|
||||
padding-top: 40px;
|
||||
.title {
|
||||
@include absolute(100%, 40px);
|
||||
@include right-btn();
|
||||
background: var(--darker-background);
|
||||
padding: $padding;
|
||||
color: var(--primary);
|
||||
height: 40px;
|
||||
.control {
|
||||
@include control();
|
||||
padding: 10px;
|
||||
border-bottom: none;
|
||||
.title {
|
||||
font-size: $font-size;
|
||||
}
|
||||
.icon-clear {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
.request-error {
|
||||
color: var(--console-error-foreground);
|
||||
@@ -32,8 +35,8 @@
|
||||
padding-top: 40px;
|
||||
background: var(--background);
|
||||
.control {
|
||||
@include control();
|
||||
padding: 10px 35px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
.url {
|
||||
font-size: $font-size-s;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -208,8 +208,8 @@ export function isErudaEl(el) {
|
||||
if (!parentNode) return false
|
||||
|
||||
while (parentNode) {
|
||||
if (parentNode.id === 'eruda') return true
|
||||
parentNode = parentNode.parentNode
|
||||
if (parentNode && parentNode.id === 'eruda') return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
transition: color $anim-duration;
|
||||
&:active {
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user