mirror of
https://github.com/liriliri/eruda.git
synced 2026-04-01 10:18:35 +08:00
475 lines
12 KiB
JavaScript
475 lines
12 KiB
JavaScript
import Tool from '../DevTools/Tool.es6'
|
|
import CssStore from './CssStore.es6'
|
|
import stringify from '../lib/stringify.es6'
|
|
import Highlight from './Highlight.es6'
|
|
import Select from './Select.es6'
|
|
import util from '../lib/util'
|
|
|
|
export default class Elements extends Tool
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
util.evalCss(require('./Elements.scss'));
|
|
|
|
this.name = 'elements';
|
|
this._tpl = require('./Elements.hbs');
|
|
this._rmDefComputedStyle = true;
|
|
this._highlightElement = false;
|
|
this._selectElement = false;
|
|
}
|
|
init($el, parent)
|
|
{
|
|
super.init($el);
|
|
|
|
this._parent = parent;
|
|
|
|
$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._parent.$parent);
|
|
this._select = new Select();
|
|
this._bindEvent();
|
|
this._initCfg();
|
|
}
|
|
show()
|
|
{
|
|
super.show();
|
|
|
|
if (!this._curEl) this._setEl(this._htmlEl);
|
|
this._render();
|
|
}
|
|
set(e)
|
|
{
|
|
this._setEl(e);
|
|
this._render();
|
|
|
|
return this;
|
|
}
|
|
overrideEventTarget()
|
|
{
|
|
let winEventProto = getWinEventProto();
|
|
|
|
let origAddEvent = this._origAddEvent = winEventProto.addEventListener,
|
|
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()
|
|
{
|
|
let winEventProto = getWinEventProto();
|
|
|
|
if (this._origAddEvent) winEventProto.addEventListener = this._origAddEvent;
|
|
if (this._origRmEvent) winEventProto.removeEventListener = this._origRmEvent;
|
|
}
|
|
_back()
|
|
{
|
|
if (this._curEl === this._htmlEl) return;
|
|
|
|
var parentQueue = this._curParentQueue,
|
|
parent = parentQueue.shift();
|
|
|
|
while (!isElExist(parent)) parent = parentQueue.shift();
|
|
|
|
this.set(parent);
|
|
}
|
|
_bindEvent()
|
|
{
|
|
let self = this,
|
|
parent = this._parent,
|
|
select = this._select;
|
|
|
|
this._$el.on('click', '.eruda-child', function ()
|
|
{
|
|
var idx = util.$(this).data('idx'),
|
|
curEl = self._curEl,
|
|
el = curEl.childNodes[idx];
|
|
|
|
if (el && el.nodeType === 3)
|
|
{
|
|
let curTagName = curEl.tagName,
|
|
type;
|
|
|
|
switch (curTagName)
|
|
{
|
|
case 'SCRIPT': type = 'js'; break;
|
|
case 'STYLE': type = 'css'; break;
|
|
default: return;
|
|
}
|
|
|
|
let sources = parent.get('sources');
|
|
|
|
if (sources)
|
|
{
|
|
sources.set(type, el.nodeValue);
|
|
parent.showTool('sources');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
!isElExist(el) ? self._render() : self.set(el);
|
|
}).on('click', '.eruda-listener-content', function ()
|
|
{
|
|
let text = util.$(this).text(),
|
|
sources = parent.get('sources');
|
|
|
|
if (sources)
|
|
{
|
|
sources.set('js', text);
|
|
parent.showTool('sources');
|
|
}
|
|
}).on('click', '.eruda-breadcrumb', () =>
|
|
{
|
|
let data = this._elData || JSON.parse(stringify(this._curEl, {getterVal: true})),
|
|
sources = parent.get('sources');
|
|
|
|
this._elData = data;
|
|
|
|
if (sources)
|
|
{
|
|
sources.set('json', data);
|
|
parent.showTool('sources');
|
|
}
|
|
}).on('click', '.eruda-parent', function ()
|
|
{
|
|
let idx = util.$(this).data('idx'),
|
|
curEl = self._curEl,
|
|
el = curEl.parentNode;
|
|
|
|
while (idx-- && el.parentNode) el = el.parentNode;
|
|
|
|
!isElExist(el) ? self._render() : self.set(el);
|
|
}).on('click', '.toggle-all-computed-style', () => this._toggleAllComputedStyle());
|
|
|
|
let $bottomBar = this._$el.find('.eruda-bottom-bar');
|
|
|
|
$bottomBar.on('click', '.eruda-refresh', () => this._render())
|
|
.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();
|
|
}
|
|
_toggleHighlight()
|
|
{
|
|
if (this._selectElement) return;
|
|
|
|
this._$el.find('.eruda-highlight').toggleClass('eruda-active');
|
|
this._highlightElement = !this._highlightElement;
|
|
|
|
this._render();
|
|
}
|
|
_toggleSelect()
|
|
{
|
|
let 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._parent.hide();
|
|
} else
|
|
{
|
|
select.disable();
|
|
}
|
|
}
|
|
_setEl(el)
|
|
{
|
|
this._curEl = el;
|
|
this._elData = null;
|
|
this._curCssStore = new CssStore(el);
|
|
this._highlight.setEl(el);
|
|
this._rmDefComputedStyle = true;
|
|
|
|
let parentQueue = [];
|
|
|
|
let parent = el.parentNode;
|
|
while (parent)
|
|
{
|
|
parentQueue.push(parent);
|
|
parent = parent.parentNode;
|
|
}
|
|
this._curParentQueue = parentQueue;
|
|
}
|
|
_getData()
|
|
{
|
|
let ret = {};
|
|
|
|
let el = this._curEl,
|
|
cssStore = this._curCssStore;
|
|
|
|
let {className, id, attributes, tagName} = el;
|
|
|
|
ret.parents = getParents(el);
|
|
ret.children = formatChildNodes(el.childNodes);
|
|
ret.attributes = formatAttr(attributes);
|
|
ret.name = formatElName({tagName, id, className, attributes});
|
|
|
|
let events = el.erudaEvents;
|
|
if (events && util.keys(events).length !== 0) ret.listeners = events;
|
|
|
|
if (needNoStyle(tagName)) return ret;
|
|
|
|
let computedStyle = cssStore.getComputedStyle();
|
|
if (this._rmDefComputedStyle) computedStyle = rmDefComputedStyle(computedStyle);
|
|
processStyleRules(computedStyle);
|
|
ret.computedStyle = computedStyle;
|
|
|
|
let styles = cssStore.getMatchedCSSRules();
|
|
styles.unshift(getInlineStyle(el.style));
|
|
styles.forEach(style => processStyleRules(style.style));
|
|
ret.styles = styles;
|
|
|
|
return ret;
|
|
}
|
|
_render()
|
|
{
|
|
if (!isElExist(this._curEl)) return this._back();
|
|
|
|
this._highlight[this._highlightElement ? 'show' : 'hide']();
|
|
this._renderHtml(this._tpl(this._getData()));
|
|
}
|
|
_renderHtml(html)
|
|
{
|
|
if (html === this._lastHtml) return;
|
|
this._lastHtml = html;
|
|
this._$showArea.html(html);
|
|
}
|
|
_initCfg()
|
|
{
|
|
let cfg = this.config = util.createCfg('elements');
|
|
|
|
cfg.set(util.defaults(cfg.get(), {overrideEventTarget: true}));
|
|
|
|
if (cfg.get('overrideEventTarget')) this.overrideEventTarget();
|
|
|
|
cfg.on('change', (key, val) =>
|
|
{
|
|
switch (key)
|
|
{
|
|
case 'overrideEventTarget': return val ? this.overrideEventTarget(): this.restoreEventTarget();
|
|
}
|
|
});
|
|
|
|
let settings = this._parent.get('settings');
|
|
settings.text('Elements')
|
|
.switch(cfg, 'overrideEventTarget', 'Catch Event Listeners')
|
|
.separator();
|
|
}
|
|
}
|
|
|
|
function processStyleRules(style)
|
|
{
|
|
util.each(style, (val, key) => style[key] = processStyleRule(val));
|
|
}
|
|
|
|
let regColor = /rgba?\((.*?)\)/g,
|
|
regCssUrl = /url\("?(.*?)"?\)/g;
|
|
|
|
function processStyleRule(val)
|
|
{
|
|
return val.replace(regColor, '<span class="eruda-style-color" style="background-color: $&"></span>$&')
|
|
.replace(regCssUrl, (match, url) => `url("${wrapLink(url)}")`);
|
|
}
|
|
|
|
const isElExist = val => util.isEl(val) && val.parentNode;
|
|
|
|
function formatElName(data, {noAttr = false} = {})
|
|
{
|
|
let {id, className, attributes} = data;
|
|
|
|
let ret = `<span class="eruda-blue">${data.tagName.toLowerCase()}</span>`;
|
|
|
|
if (id !== '') ret += `#${id}`;
|
|
|
|
if (util.isStr(className))
|
|
{
|
|
util.each(className.split(/\s+/g), (val) =>
|
|
{
|
|
if (val.trim() === '') return;
|
|
ret += `.${val}`;
|
|
});
|
|
}
|
|
|
|
if (!noAttr)
|
|
{
|
|
util.each(attributes, (attr) =>
|
|
{
|
|
var name = attr.name;
|
|
if (name === 'id' || name === 'class' || name === 'style') return;
|
|
ret += ` ${name}="${attr.value}"`;
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
var formatAttr = attributes => util.map(attributes, attr =>
|
|
{
|
|
let {name, value} = attr;
|
|
value = util.escape(value);
|
|
|
|
let isLink = (name === 'src' || name === 'href') && !util.startWith(value, 'data');
|
|
if (isLink) value = wrapLink(value);
|
|
if (name === 'style') value = processStyleRule(value);
|
|
|
|
return {name, value};
|
|
});
|
|
|
|
function formatChildNodes(nodes)
|
|
{
|
|
let ret = [];
|
|
|
|
for (let i = 0, len = nodes.length; i < len; i++)
|
|
{
|
|
var child = nodes[i],
|
|
nodeType = child.nodeType;
|
|
|
|
if (nodeType === 3 || nodeType === 8)
|
|
{
|
|
var val = child.nodeValue.trim();
|
|
if (val !== '') ret.push({
|
|
text: val,
|
|
isCmt: nodeType === 8,
|
|
idx: i
|
|
});
|
|
continue;
|
|
}
|
|
|
|
var isSvg = !util.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)
|
|
{
|
|
let ret = [],
|
|
i = 0,
|
|
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)
|
|
{
|
|
let ret = {
|
|
selectorText: 'element.style',
|
|
style: {}
|
|
};
|
|
|
|
for (let i = 0, len = style.length; i < len; i++)
|
|
{
|
|
let s = style[i];
|
|
|
|
ret.style[s] = style[s];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
var defComputedStyle = require('./defComputedStyle.json');
|
|
|
|
function rmDefComputedStyle(computedStyle)
|
|
{
|
|
let ret = {};
|
|
|
|
util.each(computedStyle, (val, key) =>
|
|
{
|
|
if (val === defComputedStyle[key]) return;
|
|
|
|
ret[key] = val;
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
var NO_STYLE_TAG = ['script', 'style', 'meta', 'title', 'link', 'head'];
|
|
|
|
var needNoStyle = tagName => NO_STYLE_TAG.indexOf(tagName.toLowerCase()) > -1;
|
|
|
|
function addEvent(el, type, listener, useCapture = false)
|
|
{
|
|
if (!util.isFn(listener) || !util.isBool(useCapture)) return;
|
|
|
|
let 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 (!util.isFn(listener) || !util.isBool(useCapture)) return;
|
|
|
|
let events = el.erudaEvents;
|
|
|
|
if (!(events && events[type])) return;
|
|
|
|
let 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 (util.keys(events).length === 0) delete el.erudaEvents;
|
|
}
|
|
|
|
var getWinEventProto = () => util.safeGet(window, 'EventTarget.prototype') || window.Node.prototype;
|
|
|
|
var wrapLink = link => `<a href="${link}" target="_blank">${link}</a>`;
|