module.exports = function (eruda) {
// eruda の util を使う(必要ならここで require にフォールバックできます)
const { evalCss } = eruda.util
const each = eruda.util.each
const $ = eruda.util.$
const toArr = eruda.util.toArr
class Dom extends eruda.Tool {
constructor() {
super()
this.name = 'dom'
this.displayName = 'ドム'
this._style = evalCss(require('./style.scss'))
this._isInit = false
this._htmlTagTpl = require('./htmlTag.hbs')
this._textNodeTpl = require('./textNode.hbs')
this._selectedEl = document.documentElement
this._htmlCommentTpl = require('./htmlComment.hbs')
this._elementChangeHandler = (el) => {
if (this._selectedEl === el) return
this.select(el)
}
}
init($el, container) {
super.init($el)
this._container = container
this._eruda = eruda // 明示的に eruda を保持
$el.html(require('./template.hbs')())
this._$domTree = $el.find('.eruda-dom-tree')
this._bindEvent()
}
show() {
super.show()
if (!this._isInit) this._initTree()
}
hide() {
super.hide()
}
select(el) {
const els = []
els.push(el)
while (el.parentElement) {
els.unshift(el.parentElement)
el = el.parentElement
}
while (els.length > 0) {
el = els.shift()
const erudaDom = el.erudaDom
if (erudaDom) {
if (erudaDom.close && erudaDom.open) {
erudaDom.close()
erudaDom.open()
}
} else {
break
}
if (els.length === 0 && el.erudaDom) {
el.erudaDom.select()
}
}
}
destroy() {
super.destroy()
evalCss.remove(this._style)
const elements = this._eruda.get('elements')
if (elements && elements.off) {
elements.off('change', this._elementChangeHandler)
}
}
_bindEvent() {
const elements = this._eruda.get('elements')
if (elements && elements.on) {
elements.on('change', this._elementChangeHandler)
}
this._$el.on('click', '.eruda-inspect', () => {
this._setElement(this._selectedEl)
// container.showTool -> eruda.showTool に変更(確実に eruda のメソッドを使う)
if (elements) this._eruda.showTool('elements')
})
}
_setElement(el) {
const elements = this._eruda.get('elements')
if (!elements || typeof elements.set !== 'function') return
elements.set(el)
}
_initTree() {
this._isInit = true
this._renderChildren(null, this._$domTree)
this.select(document.body)
}
_renderChildren(node, $container) {
let children
if (!node) {
children = [document.documentElement]
} else {
children = toArr(node.childNodes)
}
const container = $container.get(0)
if (node) {
children.push({
nodeType: 'END_TAG',
node
})
}
each(children, (child) => this._renderChild(child, container))
}
_renderChild(child, container) {
const $tag = createEl('li')
let isEndTag = false
$tag.addClass('eruda-tree-item')
// ノードタイプ判定はグローバル Node 定数を使う
if (child.nodeType === Node.ELEMENT_NODE) {
const childCount = child.childNodes.length
const expandable = childCount > 0
const data = {
...getHtmlTagData(child),
hasTail: expandable
}
const hasOneTextNode =
childCount === 1 && child.childNodes[0].nodeType === Node.TEXT_NODE
if (hasOneTextNode) {
data.text = child.childNodes[0].nodeValue
}
$tag.html(this._htmlTagTpl(data))
if (expandable && !hasOneTextNode) {
$tag.addClass('eruda-expandable')
}
} else if (child.nodeType === Node.TEXT_NODE) {
const value = child.nodeValue
if (value.trim() === '') return
$tag.html(
this._textNodeTpl({
value
})
)
} else if (child.nodeType === Node.COMMENT_NODE) {
const value = child.nodeValue
if (value.trim() === '') return
$tag.html(
this._htmlCommentTpl({
value
})
)
} else if (child.nodeType === 'END_TAG') {
isEndTag = true
child = child.node
$tag.html(
`</${child.tagName.toLowerCase()}>`
)
} else {
return
}
const $children = createEl('ul')
$children.addClass('eruda-children')
container.appendChild($tag.get(0))
container.appendChild($children.get(0))
if (child.nodeType !== Node.ELEMENT_NODE) return
let erudaDom = {}
if ($tag.hasClass('eruda-expandable')) {
const open = () => {
$tag.html(
this._htmlTagTpl({
...getHtmlTagData(child),
hasTail: false
})
)
$tag.addClass('eruda-expanded')
this._renderChildren(child, $children)
}
const close = () => {
$children.html('')
$tag.html(
this._htmlTagTpl({
...getHtmlTagData(child),
hasTail: true
})
)
$tag.rmClass && $tag.rmClass('eruda-expanded')
// rmClass が無ければ代替で removeClass を試す
if ($tag.removeClass && !$tag.rmClass) $tag.removeClass('eruda-expanded')
}
const toggle = () => {
if ($tag.hasClass('eruda-expanded')) {
close()
} else {
open()
}
}
$tag.on('click', '.eruda-toggle-btn', (e) => {
e.stopPropagation()
toggle()
})
erudaDom = {
open,
close
}
}
const select = () => {
// rmClass が無ければ代替実装
const $sel = this._$el.find('.eruda-selected')
if ($sel && $sel.rmClass) $sel.rmClass('eruda-selected')
else if ($sel && $sel.removeClass) $sel.removeClass('eruda-selected')
$tag.addClass('eruda-selected')
this._selectedEl = child
this._setElement(child)
}
$tag.on('click', select)
erudaDom.select = select
if (!isEndTag) child.erudaDom = erudaDom
}
}
function getHtmlTagData(el) {
const ret = {}
ret.tagName = el.tagName.toLowerCase()
const attributes = []
each(el.attributes, (attribute) => {
const { name, value } = attribute
attributes.push({
name,
value,
underline: isUrlAttribute(el, name)
})
})
ret.attributes = attributes
return ret
}
function isUrlAttribute(el, name) {
const tagName = el.tagName
// 正しいタグ名 (例: IMG) をチェック
if (
tagName === 'SCRIPT' ||
tagName === 'IMG' ||
tagName === 'VIDEO' ||
tagName === 'AUDIO'
) {
if (name === 'src') return true
}
if (tagName === 'LINK') {
if (name === 'href') return true
}
return false
}
function createEl(name) {
// eruda.util.$ が使える想定
return $(document.createElement(name))
}
return new Dom()
}