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() }