@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
1,256 lines (1,064 loc) • 37.8 kB
JavaScript
import AbstractControl from './abstract-control.js';
export class FxDomInspector extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.delegatedEvents = [];
}
connectedCallback() {
this.render();
}
render(){
const style = `
:host {
display:block;
}
`;
const html = `
<slot></slot>
`;
this.shadowRoot.innerHTML = `
<style>
${style}
</style>
${html}
`;
}
// Node types shim -- creates Node type constants if necessary
nodeTypesShim() {
if (!window.Node) {
return {
ELEMENT_NODE : 1,
ATTRIBUTE_NODE : 2,
TEXT_NODE : 3,
CDATA_SECTION_NODE : 4,
ENTITY_REFERENCE_NODE : 5,
ENTITY_NODE : 6,
PROCESSING_INSTRUCTION_NODE : 7,
COMMENT_NODE : 8,
DOCUMENT_NODE : 9,
DOCUMENT_TYPE_NODE : 10,
DOCUMENT_FRAGMENT_NODE : 11,
NOTATION_NODE : 12
};
}
}
addEvent(elem, evt, fn, capture) {
if (typeof elem !== 'object') {
throw "addEvent: Expected argument elem of type object, " + typeof elem + " given.";
}
if (window.addEventListener) {
if (!capture) {
capture = false;
}
elem.addEventListener(evt, fn, capture);
} else {
elem.attachEvent('on' + evt, fn);
}
}
// Simple cross-browser event handler that enables simple event delegation.
// Note that the selector must be a string and no nesting is supported.
// Selector is expected to be in one of formats listed below and works for all children
// in the particular element.
// Store parameter enables storing the reference to custom event handler.
// Exclude parameter will exclude the particular element and all of its children, this works
// only for id selectors.
// Selector formats: tag name ("div"), class name (".my-class"), id ("#my-id") and any ("*").
addEventDelegate(elem, evt, fn, capture, selector, store, exclude) {
// custom event handler is registered
var handler = function(e) {
// check if target corresponds to the selector
var target = e ? e.target : window.event.srcElement,
sel = selector.substr(1),
delegate = false;
if (exclude) {
var node = target;
while (node !== document) {
if (node.id === exclude) {
return;
}
node = node.parentNode;
}
}
// should the event be delegated?
if (selector.indexOf('#') === 0) { // ID
delegate = target.id === sel;
} else if (selector.indexOf('.') === 0) { // class
delegate = target.className.indexOf(sel) !== -1;
} else if (selector === '*') { // any
delegate = true;
} else { // tag name
delegate = target.nodeName.toLowerCase() === selector;
}
// delegate the event handling
if (delegate) {
fn.call(this, e);
}
};
// save the reference
if (store) {
delegatedEvents.push({
'handle' : handler,
'elem' : elem,
'fn' : fn,
'evt': evt
});
}
// add custom event
addEvent(elem, evt, handler, capture);
}
// Simple cross-browser event removing
removeEvent(elem, evt, fn, wasDelegated) {
if (typeof elem !== 'object') {
throw "addEvent: Expected argument elem of type object, " + typeof elem + " given.";
}
// try to find stored delegated event
var stored = null;
if (wasDelegated) {
for (var i = 0, len = delegatedEvents.length; i < len; ++i) {
stored = delegatedEvents[i];
if (stored.elem === elem && stored.evt === evt && stored.fn === fn) {
fn = stored.handle;
delegatedEvents.splice(i, 1);
break;
}
}
}
if (window.addEventListener) {
elem.removeEventListener(evt, fn, false);
} else {
elem.dettachEvent('on' + evt, fn);
}
}
// Stops event propagation and also prevents the default behavior.
pauseEvent(e){
if(e.stopPropagation) {
e.stopPropagation();
}
if(e.preventDefault) {
e.preventDefault();
}
e.cancelBubble = true;
e.returnValue = false;
return false;
}
// Create element wrapper -- allows to set attributes using the config object.
newElement(elem, attrs) {
var el = document.createElement(elem);
attrs = attrs || {};
for (var attr in attrs) {
// work only with direct (non-inherited) properties
if (attrs.hasOwnProperty(attr)) {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
}
// Function adds a class to the element, only if the class does not already exist.
// Cls parameter may be either a string or an array listing multiple classes.
// Implementation uses modern element.classList API if available, dummy shim provided for older
// browsers.
addClass(elem, cls) {
if (typeof elem !== 'object') {
throw "addClass: Expected argument elem of type object, " + typeof elem + " given.";
}
// normalize to array
if (typeof cls === 'string') {
cls = [cls];
}
// iterate over classes and add new if necessary
for (var i = 0, len = cls.length; i < len; ++i) {
if (supported('classList')) {
elem.classList.add(cls[i]);
} else {
// prevents the match when new class is only a substring of another class name
if (!new RegExp('(?:^|\\s)' + cls[i] + '(?:\\s|$)').test(elem.className)) {
elem.className += ' ' + cls[i];
}
}
}
}
// Function removes a class from the element, only if the class exists.
// Cls parameter may be either a string or an array listing multiple classes.
// Implementation uses modern element.classList API if available, dummy shim provided for older
// browsers.
removeClass(elem, cls) {
if (typeof elem !== 'object') {
throw "removeClass: Expected argument elem of type object, " + typeof elem + " given.";
}
// normalize to array
if (typeof cls === 'string') {
cls = [cls];
}
// iterate over classes and remove if necessary
for (var i = 0, len = cls.length; i < len; ++i) {
if (supported('classList')) {
elem.classList.remove(cls[i]);
} else {
// removes the class if it exists
var newClassName = elem.className.replace(new RegExp('(?:^|\\s)' + cls[i] + '(?:\\s|$)', 'g'), ' ');
elem.className = newClassName.replace(/^\s+|\s+$/g, '');
}
}
}
// Functions checks whether the feature is supported.
supported(key) {
switch (key) {
case 'localStorage':
try {
return 'localStorage' in window && !!window.localStorage;
} catch (e) {
return false;
}
break;
case 'classList':
return 'classList' in document.createElement('a');
default:
throw "supported: Unknown or unsupported key.";
}
}
// AcID DOM Inspector definition (using module pattern).
var ADI = (function() {
// private methods and variables
var Node = window.Node || nodeTypesShim(),
uiView = null,
menuView = null,
domView = null,
attrView = null,
pathView = null,
optsView = null,
activeElement = null,
vertResizing = false,
horizResizing = false,
pathScrolling = null,
elemLookup = false,
styleBackup = '',
xPos = 0,
options = {
align: 'right', // NOTE: left is not supported in this version
width: 500,
minWidth: 260,
split: 50,
minSplit: 30,
visible: true,
saving: false,
transparent: true,
omitEmptyText: true,
makeVisible: true,
foldText: true,
nodeTypes: [1, 3, 8, 9]
};
// Returns selected element or null
function getSelected() {
if (!activeElement) {
return null;
}
var elem = document,
path = JSON.parse(activeElement.getAttribute('data-js-path'));
if (path[0] !== "") {
for (var i = 0, len = path.length; i < len; ++i) {
elem = elem.childNodes[path[i]];
}
}
return elem;
}
// Loads user defined options stored in HTML5 storage (if available)
function loadOptions() {
var userOptions = {};
if (supported('localStorage')) {
userOptions = JSON.parse(localStorage.getItem('ADI.options')) || {};
}
// merge with defaults
for (var opt in userOptions) {
options[opt] = userOptions[opt];
}
}
// Saves user defined options into the HTML5 storage (if available)
function saveOptions() {
if (supported('localStorage') && options.saving) {
localStorage.setItem('ADI.options', JSON.stringify(options));
}
}
// Resets user defined options and removes them from the HTML5 storage
function resetOptions() {
if (supported('localStorage')) {
localStorage.removeItem('ADI.options');
}
}
// Returns CSS and JS paths to the element
// Result is an object with two variables (cssPath, jsPath) where cssPath is a string
// which holds the css path starting from the HTML element, and jsPath is an array which
// contains indexes for childNodes arrays (starting at document object).
//
// Inspired by the selector function from Rochester Oliveira's jQuery plugin
// http://rockingcode.com/tutorial/element-dom-tree-jquery-plugin-firebug-like-functionality/
function getElemPaths(elem) {
if (typeof elem !== 'object') {
throw "getElemPaths: Expected argument elem of type object, " + typeof elem + " given.";
}
var css = "",
js = "",
parent = "",
i, len;
while (elem !== document) {
parent = elem.parentNode;
// javascript selector
for (i = 0, len = parent.childNodes.length; i < len; ++i) {
if (parent.childNodes[i] === elem) {
js = i + "," + js;
break;
}
}
// CSS selector
var cssTmp = elem.nodeName;
if (elem.id) {
cssTmp += '#' + elem.id;
}
if (elem.className) {
// use classList if available
var classList = elem.classList || elem.className.split(' ');
for (i = 0, len = classList.length; i < len; ++i) {
cssTmp += '.' + classList[i];
}
}
css = cssTmp + ' ' + css;
elem = elem.parentNode;
}
js = js.slice(0, -1).split(',');
return {
cssPath: css.toLowerCase(),
jsPath: js
};
}
// Checks if a node has some child nodes and if at least on of them is of a supported type
function hasRequiredNodes(node) {
if (typeof node !== 'object') {
throw "hasRequiredNodes: Expected argument node of type object, " + typeof node + " given.";
}
if (node.hasChildNodes()) {
for (var i = 0, len = node.childNodes.length; i < len; i++) {
if (options.nodeTypes.indexOf(node.childNodes[i].nodeType) !== -1) {
return true;
}
}
}
return false;
}
// Checks whether the text node is not empty or contains only the EOL
function isEmptyTextNode(node) {
if (typeof node !== 'object') {
throw "isEmptyTextNode: Expected argument node of type object, " + typeof node + " given.";
}
return (/^\s*$/).test(node.textContent);
}
// Checks whether the node or its children contains only text information
function containsOnlyText(node, checkChildren) {
if (typeof node !== 'object') {
throw "containsOnlyText: Expected argument node of type object, " + typeof node + " given.";
}
checkChildren = checkChildren || false;
var result = false,
nodeTmp = null;
// does the node contain only text nodes?
if (checkChildren) {
for (var i = 0, len = node.childNodes.length; i < len; ++i) {
nodeTmp = node.childNodes[i];
result = nodeTmp.nodeType === Node.TEXT_NODE
|| nodeTmp.nodeType === Node.COMMENT_NODE
|| nodeTmp.nodeType === Node.CDATA_SECTION_NODE;
if (!result) {
break;
}
}
} else {
// check the node type if it doesn't have any children
result = node.nodeType === Node.TEXT_NODE
|| node.nodeType === Node.COMMENT_NODE
|| node.nodeType === Node.CDATA_SECTION_NODE;
}
return result;
}
// Creates a starting markup for a new DOM tree view node
function newTreeNode(node) {
if (typeof node !== 'object') {
throw "newTreeNode: Expected argument node of type object, " + typeof node + " given.";
}
var withChildren = hasRequiredNodes(node),
omit = false,
elem = newElement('li', {
class: (withChildren ? 'adi-node' : '')
});
// do not show ADI DOM nodes in the DOM view
if (node === uiView) {
return null;
}
// generate UI for elements with children
if (withChildren) {
elem.appendChild(newElement('span', { class: 'adi-trigger' }));
}
// we can omit empty text nodes if allowed in options
if (options.omitEmptyText && node.nodeType === Node.TEXT_NODE) {
omit = isEmptyTextNode(node);
}
if (!omit) {
var path = getElemPaths(node),
tagStart = newElement('span', {
'data-css-path' : path.cssPath,
'data-js-path' : JSON.stringify(path.jsPath)
}),
tagEnd = null;
if (containsOnlyText(node)) {
if (node.nodeType === Node.COMMENT_NODE) {
addClass(tagStart, 'adi-comment-node');
if (typeof tagStart.innerText === 'string') {
tagStart.innerText = '<!-- ' + node.textContent + ' -->';
} else {
tagStart.textContent = '<!-- ' + node.textContent + ' -->';
}
} else {
addClass(tagStart, 'adi-text-node');
tagStart.textContent = node.textContent;
}
} else {
addClass(tagStart, 'adi-normal-node');
if (node.nodeType !== Node.DOCUMENT_NODE) {
// tagStart.textContent = '<' + node.nodeName.toLowerCase() + '>';
if(node.nodeName === 'FX-BIND'){
tagStart.textContent = `<${node.nodeName.toLowerCase()} ref="${node.getAttribute('ref')}">`;
}else if(node.nodeName === 'FX-INSTANCE'){
tagStart.textContent = `<${node.nodeName.toLowerCase()} id="${node.id}">`;
}else if(node.nodeName === 'FX-CONTROL'){
tagStart.textContent = `<${node.nodeName.toLowerCase()} ref="${node.getAttribute('ref')}">`;
}else if(node.nodeName === 'FX-SEND'){
tagStart.textContent = `<${node.nodeName.toLowerCase()} submission="${node.getAttribute('submission')}">`;
}else{
tagStart.textContent = `<${node.nodeName.toLowerCase()}>`;
}
if (withChildren) {
tagEnd = newElement('span');
addClass(tagEnd, 'adi-end-node');
tagEnd.textContent = '</' + node.nodeName.toLowerCase() + '>';
}
} else {
tagStart.textContent = node.nodeName.toLowerCase();
}
}
elem.appendChild(tagStart);
/*
const icon = document.createElement('span');
icon.textContent = 'x';
icon.classList.add(('icon'))
const icon2 = document.createElement('span');
icon2.textContent = '<-';
icon2.classList.add(('icon'))
elem.appendChild(icon);
elem.appendChild(icon2);
*/
if(node.nodeName.startsWith('FX-')){
tagStart.classList.add('fore-node');
}
if (tagEnd) {
elem.appendChild(tagEnd);
if(node.nodeName.startsWith('FX-')){
tagEnd.classList.add('fore-node');
}
}
return elem;
} else {
return null;
}
}
// Renders the DOM Tree view
function drawDOM(root, elem, isRoot) {
if (typeof root !== 'object') {
throw "drawDOM: Expected argument root of type object, " + typeof root + " given.";
}
var newNode = null,
isOpen = true;
if (isRoot && options.nodeTypes.indexOf(root.nodeType) !== -1) {
elem.innerHTML = '';
newNode = newTreeNode(root);
if (hasRequiredNodes(root)) {
newNode.appendChild(newElement('ul', { 'data-open' : true }));
addClass(newNode.querySelector('.adi-trigger'), 'opened');
}
elem.appendChild(newNode);
elem = elem.querySelector('ul');
}
// recursive DOM traversal
for (var i = 0, len = root.childNodes.length; i < len; ++i) {
var node = root.childNodes[i],
withChildren = hasRequiredNodes(node);
if (options.nodeTypes.indexOf(node.nodeType) !== -1) {
newNode = newTreeNode(node);
if (newNode) {
if (withChildren) {
if (options.foldText) {
isOpen = containsOnlyText(node, true) ? false : true;
} else {
isOpen = true;
}
if (node.nodeType === Node.DOCUMENT_NODE) {
newNode.appendChild(newElement('ul', { 'data-open' : isOpen }));
} else {
newNode.insertBefore(newElement('ul', { 'data-open' : isOpen }), newNode.lastChild);
}
addClass(newNode.querySelector('.adi-trigger'), isOpen ? 'opened' : 'closed');
}
elem.appendChild(newNode);
if (withChildren) {
drawDOM(node, newNode.querySelector('ul'), false);
}
}
}
}
}
// Show/hide the options view
function toggleOptions() {
if (optsView.className.indexOf('adi-hidden') !== -1) {
removeClass(optsView, 'adi-hidden');
} else {
addClass(optsView, 'adi-hidden');
pathView.textContent = '';
attrView.querySelector('.adi-content').innerHTML = '';
refreshUI();
drawDOM(document, domView.querySelector('.adi-tree-view'), true);
if (options.saving) {
saveOptions();
} else {
resetOptions();
}
}
}
// Helper function for options view
function drawOptionRow(optionCode, optionText) {
var row = newElement('span', { class: 'adi-opt' });
row.innerHTML = '<label><input type="checkbox" data-opt="' + optionCode + '">' + optionText + '</label>';
return row;
}
// Renders the options panel
function drawOptions() {
var ui = newElement('div', { id: 'adi-opts-view', class: 'adi-hidden' }),
head1 = newElement('span', { class: 'adi-opt-heading' }),
head2 = newElement('span', { class: 'adi-opt-heading' }),
close = newElement('span', { class: 'adi-opt-close' });
head1.textContent = 'General options';
head2.textContent = 'Observed nodes';
ui.appendChild(head1);
ui.appendChild(drawOptionRow('saving', 'Enable saving of settings'));
ui.appendChild(drawOptionRow('makeVisible', 'Scroll to the active element in DOM View'));
ui.appendChild(drawOptionRow('omitEmptyText', 'Hide empty text nodes'));
ui.appendChild(drawOptionRow('foldText', 'Fold the text nodes'));
ui.appendChild(drawOptionRow('transparent', 'Enable transparent background'));
ui.appendChild(head2);
ui.appendChild(drawOptionRow('nodeTypes-3', 'Text node'));
ui.appendChild(drawOptionRow('nodeTypes-8', 'Comment node'));
// ui.appendChild(drawOptionRow('nodeTypes-1', 'Element node'));
// ui.appendChild(drawOptionRow('nodeTypes-9', 'Document node'));
ui.appendChild(close);
return ui;
}
// Renders the UI
function drawUI() {
var wrapper = newElement('div', {
id: 'adi-wrapper',
class: options.transparent ? 'transparent' : ''
}),
domViewWrap = newElement('div', { id: 'adi-dom-view' }),
domViewContent = newElement('div', { class: 'adi-content' }),
attrViewWrap = newElement('div', { id: 'adi-attr-view' }),
attrViewContent = newElement('div', { class: 'adi-content' }),
horizSplit = newElement('div', { id: 'adi-horiz-split' }),
vertSplit = newElement('div', { id: 'adi-vert-split' }),
domTree = newElement('ul', { class: 'adi-tree-view' }),
domPathWrap = newElement('div', { class: 'adi-path-wrap' }),
domPath = newElement('div', { class: 'adi-path' }),
domPathScrollLeft = newElement('span', { class: 'adi-path-left' }),
domPathScrollRight = newElement('span', { class: 'adi-path-right' }),
naviWrap = newElement('div', { id: 'adi-panel' }),
naviButtons = newElement('div', { class: 'adi-menu-wrap' }),
naviConfig = newElement('a', { class: 'adi-menu-config', title: 'Settings' }),
naviLookup = newElement('a', { class: 'adi-menu-lookup', title: 'Lookup tool' }),
optionsView = drawOptions();
// put UI together
domViewContent.appendChild(domTree);
domViewWrap.appendChild(domViewContent);
attrViewWrap.appendChild(attrViewContent);
domPathWrap.appendChild(domPath);
domPathWrap.appendChild(domPathScrollLeft);
domPathWrap.appendChild(domPathScrollRight);
naviButtons.appendChild(naviLookup);
naviButtons.appendChild(naviConfig);
naviWrap.appendChild(domPathWrap);
naviWrap.appendChild(naviButtons);
wrapper.appendChild(naviWrap);
wrapper.appendChild(optionsView);
wrapper.appendChild(domViewWrap);
wrapper.appendChild(horizSplit);
wrapper.appendChild(attrViewWrap);
// wrapper.appendChild(naviWrap);
wrapper.appendChild(vertSplit);
// cache UI object and append to the DOM
// #### here it attaches to the body
document.getElementsByTagName('body')[0].appendChild(wrapper);
// document.querySelector('#inspector').appendChild(wrapper);
uiView = wrapper;
menuView = naviWrap;
domView = uiView.querySelector('#adi-dom-view');
attrView = uiView.querySelector('#adi-attr-view');
const adiPanel = attrView.querySelector('.adi-content');
adiPanel.setAttribute('id','detailsView');
pathView = domPath;
optsView = optionsView;
refreshUI(true);
}
// Refreshes the global UI
function refreshUI(refreshOpts) {
if (uiView === null) {
return false;
}
// load options if requested (e.g. before the first UI refresh)
if (refreshOpts) {
loadOptions();
}
// Options view refresh
if (refreshOpts) {
optsView.querySelector('[data-opt="transparent"]').checked = options.transparent;
optsView.querySelector('[data-opt="saving"]').checked = options.saving;
optsView.querySelector('[data-opt="omitEmptyText"]').checked = options.omitEmptyText;
optsView.querySelector('[data-opt="makeVisible"]').checked = options.makeVisible;
optsView.querySelector('[data-opt="foldText"]').checked = options.foldText;
optsView.querySelector('[data-opt="nodeTypes-3"]').checked = options.nodeTypes.indexOf(3) !== -1;
optsView.querySelector('[data-opt="nodeTypes-8"]').checked = options.nodeTypes.indexOf(8) !== -1;
// optsView.querySelector('[data-opt="nodeTypes-1"]').checked = options.nodeTypes.indexOf(1) !== -1;
// optsView.querySelector('[data-opt="nodeTypes-9"]').checked = options.nodeTypes.indexOf(9) !== -1;
}
// UI appearance refresh
uiView.className = options.transparent ? 'transparent' : '';
uiView.style.display = options.visible ? 'block' : 'none';
uiView.style.width = options.width + 'px';
menuView.style.width = options.width + 'px';
domView.style.height = options.split + '%';
attrView.style.height = (100 - options.split) + '%';
domView.querySelector('.adi-content').style.height = domView.clientHeight + 'px';
attrView.querySelector('.adi-content').style.height = (attrView.clientHeight - menuView.clientHeight) + 'px';
addClass(uiView, options.align);
}
// UI visibility toggle handler
function toggleVisibilityUI() {
if (uiView === null) {
return false;
}
uiView.style.display = options.visible ? 'none' : 'block';
options.visible = !options.visible;
saveOptions();
}
// Helper function for attributes view
function drawAttrRow(attrName, attrValue) {
var row = newElement('span', { class: 'adi-attr' });
switch (attrName.toLowerCase()) {
case 'defaultaction':
row.innerHTML = '<label>' + attrName + ': <select data-attr="' + attrName + '" value="' + attrValue + '"><option>perform</option><option>cancel</option></label>';
break;
case 'delay':
row.innerHTML = '<label>' + attrName + ': <input type="number" data-attr="' + attrName + '" value="' + attrValue + '"></label>';
break;
default:
row.innerHTML = '<label>' + attrName + ': <input type="text" data-attr="' + attrName + '" value="' + attrValue + '"></label>';
}
return row;
}
// Renders the attribute view
function drawAttrs(elem) {
//todo: hook element-def.json in here
if(elem.nodeName.startsWith('FX-')){
console.log('got a fore element')
}
if (typeof elem !== 'object') {
throw "drawAttrs: Expected argument elem of type object, " + typeof elem + " given.";
}
var content = attrView.querySelector('.adi-content'),
attrsMain = {
'id': '',
'class': '',
'style': ''
},
attrsOther = {},
keys = [],
attr, i, len;
// prepare attributes
content.innerHTML = '';
for (i = 0, len = elem.attributes.length; i < len; ++i) {
attr = elem.attributes[i];
switch (attr.nodeName.toLowerCase()) {
case 'id':
attrsMain['id'] = attr.nodeValue;
break;
case 'class':
attrsMain['class'] = attr.nodeValue;
break;
case 'style':
attrsMain['style'] = styleBackup;
break;
default:
attrsOther[attr.nodeName.toLowerCase()] = attr.nodeValue;
}
}
if(elem.nodeName.startsWith('FX-')){
switch (elem.nodeName) {
case 'FX-BIND':
attrsOther['calculate'] = attrsOther['calculate'] || '';
attrsOther['constraint'] = attrsOther['constraint'] || '';
attrsOther['readonly'] = attrsOther['readonly'] || '';
attrsOther['relevant'] = attrsOther['relevant'] || '';
attrsOther['required'] = attrsOther['required'] || '';
break;
case 'FX-MESSAGE':
case 'FX-SEND':
case 'FX-ACTION':
attrsOther['defaultaction'] = attrsOther['defaultaction'] || '';
attrsOther['delay'] = attrsOther['delay'] || '';
attrsOther['event'] = attrsOther['event'] || '';
attrsOther['handler'] = attrsOther['handler'] || '';
attrsOther['if'] = attrsOther['if'] || '';
attrsOther['phase'] = attrsOther['phase'] || '';
attrsOther['propagate'] = attrsOther['propagate'] || '';
attrsOther['target'] = attrsOther['target'] || '';
attrsOther['while'] = attrsOther['while'] || '';
break;
default:
attrsOther[attr.nodeName.toLowerCase()] = attr.nodeValue;
}
}
// sort attributes
for (var key in attrsOther) {
keys.push(key);
}
keys.sort();
// render the content
content.appendChild(drawAttrRow('id', attrsMain['id']));
content.appendChild(drawAttrRow('class', attrsMain['class']));
content.appendChild(drawAttrRow('style', attrsMain['style']));
content.appendChild(newElement('hr'));
for (i = 0, len = keys.length; i < len; ++i) {
content.appendChild(drawAttrRow(keys[i], attrsOther[keys[i]]));
}
}
// Handles attribute changes
function changeAttribute(e) {
var target = e ? e.target : window.event.srcElement,
attr = target.getAttribute('data-attr'),
val = target.value,
elem = getSelected();
// remove attribute if the new value is empty
if (val === '') {
elem.removeAttribute(attr);
} else {
elem.setAttribute(attr, val);
}
}
// Handles option changes
function changeOption(e) {
var target = e ? e.target : window.event.srcElement,
data = target.getAttribute('data-opt'),
val = target.checked;
if (data.indexOf('nodeTypes') !== -1) {
var type = parseInt(data.match(/\d+/)[0]);
if (val) {
options.nodeTypes.push(type);
} else {
options.nodeTypes.splice(options.nodeTypes.indexOf(type), 1);
}
} else {
options[data] = val;
}
}
// Key events processing
function processKey(e) {
e = e || window.event;
var code = e.keyCode || e.which;
switch (code) {
case 272: // ctrl + alt + d
toggleVisibilityUI();
break;
}
}
// Vertical splitter resize handler
function verticalResize(e) {
if (!vertResizing) {
return;
}
e = e || window.event;
document.documentElement.style.cursor = 'e-resize';
var nWidth = options.width + xPos - e.clientX;
if (nWidth >= options.minWidth) {
options.width = nWidth;
xPos = e.clientX;
refreshUI();
saveOptions();
}
checkPathOverflow();
}
// Horizontal splitter resize handler
function horizontalResize(e) {
if (!horizResizing) {
return;
}
e = e || window.event;
document.documentElement.style.cursor = 'n-resize';
var nSplit = Math.floor(e.clientY / uiView.clientHeight * 100);
if (nSplit >= options.minSplit && nSplit <= 100 - options.minSplit) {
options.split = nSplit;
refreshUI();
saveOptions();
}
}
// Dom view folding handler
function handleFolding(e) {
var target = e ? e.target : window.event.srcElement,
ul = target.parentNode.querySelector('ul');
if (ul.getAttribute('data-open') === "true") {
removeClass(target, 'opened');
addClass(target, 'closed');
ul.setAttribute('data-open', "false");
} else {
removeClass(target, 'closed');
addClass(target, 'opened');
ul.setAttribute('data-open', "true");
}
}
// Handles active element selection
function handleActive(e) {
var target = e ? e.target : window.event.srcElement,
active = domView.querySelector('.adi-active-node');
if (active) {
removeClass(active, 'adi-active-node');
}
// clicked on normal-node or end-node?
if (target.className === 'adi-end-node') {
target = target.parentNode.querySelector('.adi-normal-node');
}
activeElement = target;
addClass(target, 'adi-active-node');
pathView.textContent = target.getAttribute('data-css-path');
this.dispatchEvent(
new CustomEvent('handle-active', {
composed: true,
bubbles: true,
detail: { active: activeElement, selected: getSelected()},
}),
);
// make it visible (scroll)
if (options.makeVisible) {
var wrap = domView.querySelector('.adi-content');
if (target.offsetTop >= wrap.clientHeight || target.offsetTop <= wrap.scrollTop) {
wrap.scrollTop = target.offsetTop - (Math.floor(wrap.clientHeight / 2));
}
}
checkPathOverflow();
drawAttrs(getSelected());
const fore = document.createElement('fx-fore');
fore.setAttribute('src','inspector/inspector.html');
// const detailsView = document.getElementById('detailsView');
// detailsView.appendChild(fore);
}
// Checks if pathView is overflowing or not
function checkPathOverflow() {
if (pathView.scrollWidth > pathView.clientWidth) {
addClass(pathView.parentNode, 'adi-overflowing');
} else {
removeClass(pathView.parentNode, 'adi-overflowing');
}
}
// Handles scroll behavior for overflowing pathView
function scrollPathView(e) {
var target = e ? e.target : window.event.srcElement,
maxScroll = pathView.scrollWidth - pathView.clientWidth,
scroll = pathView.scrollLeft,
change = 5;
if (target.className === "adi-path-right") {
pathView.scrollLeft = (scroll <= maxScroll - change) ? scroll + change : maxScroll;
} else {
pathView.scrollLeft = (scroll - change >= 0) ? scroll - change : 0;
}
if (!pathScrolling) {
pathScrolling = setInterval(scrollPathView, 20, e);
}
}
// Highlights an element on page
function highlightElement(e) {
var target = e ? e.target : window.event.srcElement,
node = document,
path;
if (target.className === 'adi-end-node') {
target = target.parentNode.querySelector('.adi-normal-node');
}
path = JSON.parse(target.getAttribute('data-js-path'));
// find the element
for (var i = 0, len = path.length; i < len; ++i) {
node = node.childNodes[path[i]];
}
if (node) {
if (e.type === 'mouseover') {
styleBackup = node.getAttribute('style') || '';
node.setAttribute('style', 'outline: 1px dashed red; ' + styleBackup);
} else {
if (styleBackup === '') {
node.removeAttribute('style');
} else {
node.setAttribute('style', styleBackup);
}
}
}
}
// Handles element lookup on page
function handleLookup(e) {
var target = e ? e.target : window.event.srcElement;
if (target.className.indexOf('adi-menu-lookup') !== -1) {
// enable/disable interactive lookup
if (elemLookup) {
removeClass(target, 'adi-active');
elemLookup = false;
removeEvent(document.body, 'mouseover', handleLookup, true);
removeEvent(document.body, 'mouseout', handleLookup, true);
removeEvent(document.body, 'click', handleLookup, true);
} else {
addClass(target, 'adi-active');
elemLookup = true;
addEventDelegate(document.body, 'mouseover', handleLookup, false, '*', true, 'adi-wrapper');
addEventDelegate(document.body, 'mouseout', handleLookup, false, '*', true, 'adi-wrapper');
addEventDelegate(document.body, 'click', handleLookup, false, '*', true, 'adi-wrapper');
}
} else {
// handle lookup events
if (e.type === 'mouseover') {
styleBackup = target.getAttribute('style') || '';
target.setAttribute('style', 'outline: 1px dashed red; ' + styleBackup);
} else if (e.type === 'mouseout') {
target.setAttribute('style', styleBackup);
} else {
elemLookup = false;
removeClass(menuView.querySelector('.adi-menu-lookup'), 'adi-active');
target.setAttribute('style', styleBackup);
removeEvent(document.body, 'mouseover', handleLookup, true);
removeEvent(document.body, 'mouseout', handleLookup, true);
removeEvent(document.body, 'click', handleLookup, true);
pauseEvent(e);
// find corresponding node in the DOM view
var path = getElemPaths(target),
active = domView.querySelector('[data-js-path=\'' + JSON.stringify(path.jsPath) + '\']');
// activate it
if (active) {
active.click();
}
// open the whole path in DOM view
var node = active.parentNode,
tmp;
if (node.querySelector('ul')) {
node.querySelector('ul').setAttribute('data-open', 'true');
}
while(node !== domView.querySelector('.adi-content')) {
if (node.className.indexOf('adi-node') !== -1) {
tmp = node.querySelector('.adi-trigger');
removeClass(tmp, 'closed');
addClass(tmp, 'opened');
node = node.parentNode; // ul node
node.setAttribute('data-open', 'true');
}
node = node.parentNode;
}
// make it visible (scroll)
if (options.makeVisible) {
var wrap = domView.querySelector('.adi-content');
if (active.offsetTop >= wrap.clientHeight || active.offsetTop <= wrap.scrollTop) {
wrap.scrollTop = active.offsetTop - (Math.floor(wrap.clientHeight / 2));
}
}
}
}
}
// Event registration
function registerEvents() {
var vertSplit = document.getElementById('adi-vert-split'),
horizSplit = document.getElementById('adi-horiz-split');
// events for splitters
addEvent(vertSplit, 'mousedown', function(e) {
e = e || window.event;
pauseEvent(e);
vertResizing = true;
xPos = e.clientX;
}, false);
addEvent(horizSplit, 'mousedown', function(e) {
e = e || window.event;
pauseEvent(e);
horizResizing = true;
}, false);
addEvent(document, 'mouseup', function() {
document.documentElement.style.cursor = 'default';
vertResizing = false;
horizResizing = false;
}, false);
addEvent(document, 'mousemove', verticalResize, false);
addEvent(document, 'mousemove', horizontalResize, false);
// window resize
addEvent(window, 'resize', refreshUI, false);
// keypress events
addEvent(document, 'keypress', processKey, false);
// dom tree view folding
addEventDelegate(domView, 'click', handleFolding, false, '.adi-trigger');
// active element
addEventDelegate(domView, 'click', handleActive, false, '.adi-normal-node');
addEventDelegate(domView, 'click', handleActive, false, '.adi-end-node');
// path view scrolling
addEventDelegate(pathView.parentNode, 'mousedown', scrollPathView, false, '.adi-path-left');
addEventDelegate(pathView.parentNode, 'mousedown', scrollPathView, false, '.adi-path-right');
addEventDelegate(pathView.parentNode, 'mouseup', function() {
clearInterval(pathScrolling);
pathScrolling = false;
}, false, '.adi-path-left');
addEventDelegate(pathView.parentNode, 'mouseup', function() {
clearInterval(pathScrolling);
pathScrolling = false;
}, false, '.adi-path-right');
// matching tag highlighting
addEventDelegate(domView, 'mouseover', function(e) {
var target = e ? e.target : window.event.srcElement;
addClass(target.parentNode.querySelector('.adi-normal-node'), 'hover');
}, false, '.adi-end-node');
addEventDelegate(domView, 'mouseout', function(e) {
var target = e ? e.target : window.event.srcElement;
removeClass(target.parentNode.querySelector('.adi-normal-node'), 'hover');
}, false, '.adi-end-node');
// page element highlighting
addEventDelegate(domView, 'mouseover', highlightElement, false, '.adi-end-node');
addEventDelegate(domView, 'mouseover', highlightElement, false, '.adi-normal-node');
addEventDelegate(domView, 'mouseout', highlightElement, false, '.adi-end-node');
addEventDelegate(domView, 'mouseout', highlightElement, false, '.adi-normal-node');
// element lookup
addEvent(menuView.querySelector('.adi-menu-lookup'), 'click', handleLookup, false);
// options events
addEventDelegate(optsView, 'change', changeOption, false, 'input');
addEventDelegate(optsView, 'click', toggleOptions, false, '.adi-opt-close');
addEvent(menuView.querySelector('.adi-menu-config'), 'click', toggleOptions, false);
// attributes events
addEventDelegate(attrView, 'change', changeAttribute, false, 'input');
}
drawUI();
registerEvents();
drawDOM(document, domView.querySelector('.adi-tree-view'), true);
return {
// TODO: public methods and variables (this will be visible to the global scope)
getSelectedElement: getSelected,
toggle: toggleVisibilityUI
};
})();
appInit() {
// make public API visible to the global scope
window.ADI = ADI;
}
// Launch the app when the DOM is ready and all assets are loaded
addEvent(window, 'load', appInit, false);
}
if (!customElements.get('fx-dom-inspector')) {
customElements.define('fx-dom-inspector', FxDomInspector);
}