|
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 isEl from 'licia/isEl' |
|
import escape from 'licia/escape' |
|
import startWith from 'licia/startWith' |
|
import contain from 'licia/contain' |
|
import unique from 'licia/unique' |
|
import each from 'licia/each' |
|
import keys from 'licia/keys' |
|
import isNull from 'licia/isNull' |
|
import trim from 'licia/trim' |
|
import isFn from 'licia/isFn' |
|
import isBool from 'licia/isBool' |
|
import safeGet from 'licia/safeGet' |
|
import $ from 'licia/$' |
|
import h from 'licia/h' |
|
import extend from 'licia/extend' |
|
import MutationObserver from 'licia/MutationObserver' |
|
import CssStore from './CssStore' |
|
import Settings from '../Settings/Settings' |
|
import LunaModal from 'luna-modal' |
|
import LunaBoxModel from 'luna-box-model' |
|
import chobitsu from '../lib/chobitsu' |
|
import { formatNodeName } from './util' |
|
import { isErudaEl, classPrefix as c } from '../lib/util' |
|
|
|
export default class Detail { |
|
constructor($container, devtools) { |
|
this._$container = $container |
|
this._devtools = devtools |
|
this._curEl = document.documentElement |
|
this._initObserver() |
|
this._initCfg() |
|
this._initTpl() |
|
this._bindEvent() |
|
} |
|
show(el) { |
|
this._curEl = el |
|
this._rmDefComputedStyle = true |
|
this._computedStyleSearchKeyword = '' |
|
this._enableObserver() |
|
this._render() |
|
this._highlight() |
|
} |
|
hide = () => { |
|
this._$container.hide() |
|
this._disableObserver() |
|
chobitsu.domain('Overlay').hideHighlight() |
|
} |
|
destroy() { |
|
this._disableObserver() |
|
this.restoreEventTarget() |
|
this._rmCfg() |
|
} |
|
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) |
|
} |
|
} |
|
restoreEventTarget() { |
|
const winEventProto = getWinEventProto() |
|
|
|
if (this._origAddEvent) winEventProto.addEventListener = this._origAddEvent |
|
if (this._origRmEvent) winEventProto.removeEventListener = this._origRmEvent |
|
} |
|
_highlight = (type) => { |
|
const el = this._curEl |
|
|
|
const highlightConfig = { |
|
showInfo: false, |
|
} |
|
if (!type || type === 'all') { |
|
extend(highlightConfig, { |
|
showInfo: true, |
|
contentColor: 'rgba(111, 168, 220, .66)', |
|
paddingColor: 'rgba(147, 196, 125, .55)', |
|
borderColor: 'rgba(255, 229, 153, .66)', |
|
marginColor: 'rgba(246, 178, 107, .66)', |
|
}) |
|
} else if (type === 'margin') { |
|
highlightConfig.marginColor = 'rgba(246, 178, 107, .66)' |
|
} else if (type === 'border') { |
|
highlightConfig.borderColor = 'rgba(255, 229, 153, .66)' |
|
} else if (type === 'padding') { |
|
highlightConfig.paddingColor = 'rgba(147, 196, 125, .55)' |
|
} else if (type === 'content') { |
|
highlightConfig.contentColor = 'rgba(111, 168, 220, .66)' |
|
} |
|
|
|
const { nodeId } = chobitsu.domain('DOM').getNodeId({ node: el }) |
|
chobitsu.domain('Overlay').highlightNode({ |
|
nodeId, |
|
highlightConfig, |
|
}) |
|
} |
|
_initTpl() { |
|
const $container = this._$container |
|
|
|
const html = `<div class="${c('control')}"> |
|
<span class="${c('icon-arrow-left back')}"></span> |
|
<span class="${c('element-name')}"></span> |
|
<span class="${c('icon-refresh refresh')}"></span> |
|
</div> |
|
<div class="${c('element')}"> |
|
<div class="${c('attributes section')}"></div> |
|
<div class="${c('styles section')}"></div> |
|
<div class="${c('computed-style section')}"></div> |
|
<div class="${c('listeners section')}"></div> |
|
</div>` |
|
|
|
$container.html(html) |
|
|
|
this._$elementName = $container.find(c('.element-name')) |
|
this._$attributes = $container.find(c('.attributes')) |
|
this._$styles = $container.find(c('.styles')) |
|
this._$listeners = $container.find(c('.listeners')) |
|
this._$computedStyle = $container.find(c('.computed-style')) |
|
|
|
const boxModelContainer = h('div') |
|
this._$boxModel = $(boxModelContainer) |
|
this._boxModel = new LunaBoxModel(boxModelContainer) |
|
} |
|
_toggleAllComputedStyle() { |
|
this._rmDefComputedStyle = !this._rmDefComputedStyle |
|
|
|
this._render() |
|
} |
|
_render() { |
|
const data = this._getData(this._curEl) |
|
const $attributes = this._$attributes |
|
const $elementName = this._$elementName |
|
const $styles = this._$styles |
|
const $computedStyle = this._$computedStyle |
|
const $listeners = this._$listeners |
|
|
|
$elementName.html(data.name) |
|
|
|
let attributes = '<tr><td>Empty</td></tr>' |
|
if (!isEmpty(data.attributes)) { |
|
attributes = 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('') |
|
} |
|
attributes = `<h2>属性</h2> |
|
<div class="${c('table-wrapper')}"> |
|
<table> |
|
<tbody> |
|
${attributes} |
|
</tbody> |
|
</table> |
|
</div>` |
|
$attributes.html(attributes) |
|
|
|
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 = `<h2>スタイル</h2> |
|
<div class="${c('style-wrapper')}"> |
|
${style} |
|
</div>` |
|
$styles.html(styles).show() |
|
} else { |
|
$styles.hide() |
|
} |
|
|
|
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>`) |
|
} |
|
|
|
computedStyle = `<h2> |
|
計算済みのスタイル |
|
${toggleButton} |
|
<div class="${c('btn computed-style-search')}"> |
|
<span class="${c('icon-filter')}"></span> |
|
</div> |
|
${ |
|
data.computedStyleSearchKeyword |
|
? `<div class="${c('btn filter-text')}">${escape( |
|
data.computedStyleSearchKeyword |
|
)}</div>` |
|
: '' |
|
} |
|
</h2> |
|
<div class="${c('box-model')}"></div> |
|
<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>` |
|
|
|
$computedStyle.html(computedStyle).show() |
|
this._boxModel.setOption('element', this._curEl) |
|
$computedStyle.find(c('.box-model')).append(this._$boxModel.get(0)) |
|
} else { |
|
$computedStyle.text('').hide() |
|
} |
|
|
|
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 = `<h2>イベントリスナー</h2> |
|
<div class="${c('listener-wrapper')}"> |
|
${listeners} |
|
</div>` |
|
$listeners.html(listeners).show() |
|
} else { |
|
$listeners.hide() |
|
} |
|
|
|
this._$container.show() |
|
} |
|
_getData(el) { |
|
const ret = {} |
|
|
|
const cssStore = new CssStore(el) |
|
|
|
const { className, id, attributes, tagName } = el |
|
|
|
ret.computedStyleSearchKeyword = this._computedStyleSearchKeyword |
|
ret.attributes = formatAttr(attributes) |
|
ret.name = formatNodeName({ 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() |
|
|
|
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() { |
|
const devtools = this._devtools |
|
|
|
this._$container |
|
.on('click', c('.toggle-all-computed-style'), () => |
|
this._toggleAllComputedStyle() |
|
) |
|
.on('click', c('.computed-style-search'), () => { |
|
LunaModal.prompt('フィルター').then((filter) => { |
|
if (isNull(filter)) return |
|
filter = trim(filter) |
|
this._computedStyleSearchKeyword = filter |
|
this._render() |
|
}) |
|
}) |
|
.on('click', '.eruda-listener-content', function () { |
|
const text = $(this).text() |
|
const sources = devtools.get('sources') |
|
|
|
if (sources) { |
|
sources.set('js', text) |
|
devtools.showTool('sources') |
|
} |
|
}) |
|
.on('click', c('.element-name'), () => { |
|
const sources = devtools.get('sources') |
|
|
|
if (sources) { |
|
sources.set('object', this._curEl) |
|
devtools.showTool('sources') |
|
} |
|
}) |
|
.on('click', c('.back'), this.hide) |
|
.on('click', c('.refresh'), () => { |
|
this._render() |
|
devtools.notify('更新されました', { icon: 'success' }) |
|
}) |
|
|
|
this._boxModel.on('highlight', this._highlight) |
|
} |
|
_initObserver() { |
|
this._observer = new MutationObserver((mutations) => { |
|
each(mutations, (mutation) => this._handleMutation(mutation)) |
|
}) |
|
} |
|
_enableObserver() { |
|
this._observer.observe(document.documentElement, { |
|
attributes: true, |
|
childList: true, |
|
subtree: true, |
|
}) |
|
} |
|
_disableObserver() { |
|
this._observer.disconnect() |
|
} |
|
_handleMutation(mutation) { |
|
if (isErudaEl(mutation.target)) return |
|
|
|
if (mutation.type === 'attributes') { |
|
if (mutation.target !== this._curEl) return |
|
this._render() |
|
} |
|
} |
|
_rmCfg() { |
|
const cfg = this.config |
|
|
|
const settings = this._devtools.get('settings') |
|
|
|
if (!settings) return |
|
|
|
settings |
|
.remove(cfg, 'overrideEventTarget') |
|
.remove(cfg, 'observeElement') |
|
.remove('Elements') |
|
} |
|
_initCfg() { |
|
const cfg = (this.config = Settings.createCfg('elements', { |
|
overrideEventTarget: true, |
|
})) |
|
|
|
if (cfg.get('overrideEventTarget')) this.overrideEventTarget() |
|
|
|
cfg.on('change', (key, val) => { |
|
switch (key) { |
|
case 'overrideEventTarget': |
|
return val ? this.overrideEventTarget() : this.restoreEventTarget() |
|
} |
|
}) |
|
|
|
const settings = this._devtools.get('settings') |
|
if (!settings) return |
|
|
|
settings |
|
.text('要素') |
|
.switch(cfg, 'overrideEventTarget', 'イベントリスナーを捕捉') |
|
|
|
settings.separator() |
|
} |
|
} |
|
|
|
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 } |
|
}) |
|
|
|
const regColor = /rgba?\((.*?)\)/g |
|
const regCssUrl = /url\("?(.*?)"?\)/g |
|
|
|
function processStyleRule(val) { |
|
|
|
val = toStr(val) |
|
|
|
return val |
|
.replace( |
|
regColor, |
|
'<span class="eruda-style-color" style="background-color: $&"></span>$&' |
|
) |
|
.replace(regCssUrl, (match, url) => `url("${wrapLink(url)}")`) |
|
} |
|
|
|
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 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 |
|
} |
|
|