UNPKG

eruda2

Version:

Console for Mobile Browsers

737 lines (605 loc) 19.5 kB
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.config) 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 } eventsCss = [] _render() { this.eventsCss.forEach(item => $(item.el).off(item.fn)) this.eventsCss = [] if (!isElExist(this._curEl)) return this._back() this._highlight[this._highlightElement ? 'show' : 'hide']() const data = this._getData() this._renderHtml(this._tpl(data), data) } _renderHtml(html, data) { if (html === this._lastHtml) return this._lastHtml = html this._$showArea.html(html) /*eslint no-unused-vars: 'error'*/ const fn = () => { const input = window.prompt('New attrbite: ') if (!input ? false : true) { const [key, val] = input.split(':') this._curEl.setAttribute(key, (val || '').replace(/^ /, '')) this._render() } } const el = this._$showArea.find('[data-attrs] [data-add]')[0] this.eventsCss.push({ el, fn }) $(el).on('click', fn) /*eslint no-unused-vars: 'error'*/ this._$showArea.find('[data-attrs] [data-edit]').each((index, item) => { const key = item.getAttribute('data-edit') const fn = () => { const newVal = window.prompt(`New value for "${key}:`, this._curEl.getAttribute(key)) /*eslint no-extra-boolean-cast: 'error'*/ if (!newVal ? false : true) { this._curEl.setAttribute(key, newVal) this._render() } } $(item).on('click', fn) this.eventsCss.push({ el: item, fn }) }) /*eslint no-unused-vars: 'error'*/ this._$showArea.find('[data-styles] [data-add]').each((index, item) => { const path = item.getAttribute('data-add').split('_') let style = data.styles[path[0]] if (path.length == 2) { style = style.blocks[path[1]] } const fn = () => { const input = window.prompt('New Css:') /*eslint no-extra-boolean-cast: "error"*/ if (!input ? false : true) { const [key, val] = input.split(':') style.styleRoot[key] = val.replace(/^ /, '') this._render() /*$(item).parent().append(` <div class="rule" data-editor="${path.join('_')}_${$(item).parent()[0].children.length}"> <span>${key}</span>: <span data-value>${val};</span> </div> `)*/ } } $(item).on('click', fn) this.eventsCss.push({ el: item, fn }) }) /*eslint no-unused-vars: 'error'*/ this._$showArea.find('[data-styles] [data-editor]').each((index, item) => { const path = item.getAttribute('data-editor').split('_') let style = data.styles[path[0]] if (path.length == 3) { style = style.blocks[path[1]] } const name = path[path.length - 1] item.removeAttribute('data-editor') const fn = () => { const oldVal = style.styleRoot[name] const newVal = window.prompt(`Set CSS ${name}`, oldVal) if (!!newVal && oldVal != newVal) { style.styleRoot[name] = newVal $(item).find('[data-value]').text(newVal) } } $(item).on('click', fn) this.eventsCss.push({ el: item, fn }) }) } _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, showKeyframes: 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') .switch(cfg, 'showKeyframes', 'Show @keyframes') 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: {}, styleRoot: 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 }