(function() { var logger = ST.logger.forClass('inspector/Inspect'), debug = ST.debug, instance; ST.inspector = {}; ST.inspector.Inspect = ST.define({ socketEventQueue: [], componentTree: null, componentTreeTimeStamp: null, statics: { getInstance: function () { if (!instance) { instance = new ST.inspector.Inspect(); instance.initInspect(); } return instance; } }, hasExt: function () { return !!(Ext && Ext.define); }, getClassName: function (node) { var className = ''; // let's make sure it's actually an element, and not some other fragment if (node && node.nodeType === 1) { // we'll use getAttribute() so that things like SVG elements won't bomb className = node.getAttribute('class'); } return className; }, initInspect: function() { /** * setup css styles for highlighter (from themerInspect.js) */ var styles = document.createElement('style'); styles.innerHTML = '.c-highlighter-hover { border: 1px dashed #FF8080; z-index: 99998; }' + '.c-highlighter-select { border: 1px solid #FF8080; z-index: 99999 !important; }' + '.c-highlighter-query { border: 1px solid #FF4040; z-index: 99999 !important; }' + '.c-highlighter-onematch { border: 2px solid #40FF40; z-index: 99999 !important; }' + '.c-highlighter-hover, .c-highlighter-query, .c-highlighter-select ' + '{ box-shadow: none !important; background-color: white; position: absolute !important;}'; document.head.insertBefore(styles, document.head.firstChild); this.Highlighter = function (type) { var cls = 'c-highlighter-' + (type || 'hover') + ' c-no-inspect'; var createFn = function (cfg) { var el = document.createElement('DIV') el.style.position = 'absolute'; el.style.top = 0; el.style.left = 0; el.setAttribute('class', cls); for (var key in cfg) { el.style[key] = cfg[key]; } return document.body.appendChild(el); }; this.target = undefined; this.topEl = createFn({height: '2px'}); this.bottomEl = createFn({height: '2px'}); this.leftEl = createFn({width: '2px'}); this.rightEl = createFn({width: '2px'}); } this.Highlighter.prototype = { toggle: function (direction) { var event = function (target) { target.style.display = direction === 'hide' ? 'none' : 'inline'; } event(this.topEl); event(this.bottomEl); event(this.leftEl); event(this.rightEl); }, hide: function() { this.toggle('hide'); }, show: function() { this.toggle('show'); }, /** * Highlights a specific element * @param {HTMLElement} [target] The element to highlight. If null the highlighter will be hidden. */ highlight: function(target) { var me = this; me.target = target; if (!target) { me.hide(); return; } me.show(); var target = ST.fly(target), rect = me.getRect(target.dom), xy = [rect.left, rect.top]; me.setXY(me.topEl, [xy[0] - 4, xy[1] - 4]); me.setWidth(me.topEl, rect.width + 8); me.setXY(me.bottomEl, [xy[0] - 4, xy[1] + rect.height + 2]); me.setWidth(me.bottomEl, rect.width + 8); me.setXY(me.leftEl, [xy[0] - 4, xy[1] - 4]); me.setHeight(me.leftEl, rect.height + 8); me.setXY(me.rightEl, [xy[0] + rect.width + 2, xy[1] - 4]); me.setHeight(me.rightEl, rect.height + 8); }, destroy: function () { var me = this; me.topEl.parentNode.removeChild(me.topEl); me.bottomEl.parentNode.removeChild(me.bottomEl); me.leftEl.parentNode.removeChild(me.leftEl); me.rightEl.parentNode.removeChild(me.rightEl); delete me.topEl; delete me.bottomEl; delete me.leftEl; delete me.rightEl; }, reHighlight: function() { this.highlight(this.target); }, getRect: function (target) { var rect = target.getBoundingClientRect(), opts = ST.apply({}, rect); opts.left = rect.left + window.pageXOffset; opts.top = rect.top + window.pageYOffset; return opts; }, getY: function (target) { return target.scrollTop; }, getX: function (target) { return target.scrollLeft; }, setXY: function (target, xy) { target.style.left = xy[0] + 'px'; target.style.top = xy[1] + 'px'; }, setWidth: function (target, width) { target.style.width = width + 'px'; }, setHeight: function (target, height) { target.style.height = height + 'px'; } }; this.watchComponentTree(); this.inspectEnabled = true; this.strategy = new ST.locator.Strategy(); this.initElementSelectors(); function addEvent(obj, evt, fn) { if (obj.addEventListener) { obj.addEventListener(evt, fn, false); } else if (obj.attachEvent) { obj.attachEvent("on" + evt, fn); } } // Toggle inspect on 'CmdOrCtrl+I' inside the app directly. addEvent(document, 'keydown', function(e) { if (((Ext.isMac && e.metaKey) || (!Ext.isMac && e.ctrlKey)) && e.key === 'i') { this.toggleInspectEnabled({value: !this.inspectEnabled}); e.preventDefault(); e.stopPropagation(); } }.bind(this)); addEvent(document, 'mouseout', function(e) { e = e || window.event; var from = e.relatedTarget || e.toElement; if (!from || from.nodeName === 'HTML') { this.hoverHighlighter.hide(); } }.bind(this)); addEvent(document, 'mouseover', function(e) { if (this.inspectEnabled) { this.hoverHighlighter.show(); } }.bind(this)); }, /** * Watches the component tree for changes */ watchComponentTree: function () { var me = this; try { new MutationObserver(function(mutations) { var changed = 0, m, mutation, i, node, className; for (m = 0; m < mutations.length; m++) { mutation = mutations[m]; if (mutation.addedNodes.length) { for (i=0; i<mutation.addedNodes.length; i++) { node = mutation.addedNodes[i]; className = me.getClassName(node); if (className && className.indexOf('c-no-inspect') != -1) { // do nothing } else { changed++; } } } if (mutation.removedNodes.length) { for (i=0; i<mutation.removedNodes.length; i++) { node = mutation.removedNodes[i]; className = me.getClassName(node); if (className && className.indexOf('c-no-inspect') != -1) { // do nothing } else { changed++; } } } } if (changed) { console.log('mutationobserver found a change, null out componentTree'); // TODO do the same for domTree this.componentTree = null; } }.bind(this)).observe(document.body, { childList: true, subtree: true }); } catch (e) { // expected for some older browsers, don't complain } }, /** * toggles highlight on a component in a viewer or app upon hovering a node in the tree * @param {Object} data - node data */ toggleHighlight : function (data) { var cmpId = data.cmpId; if (cmpId) { var cmp = Ext.get(data.cmpId); if (cmp) { data.highlight ? this.hoverHighlighter.highlight(cmp) : this.hoverHighlighter.hide(cmp); } } }, socketEmit: function(event, data) { var me = this; if(me.socket) { me.socket.emit(event, data); } else if (window._inspectCallback) { window._inspectCallback({event: event, data: data}); } else { me.socketEventQueue.push({event: event, data: data}); } }, privates: { /** * The currently selected element * @property {HTMLElement} */ selectedEl: null, /** * @property {Boolean} * True if the inspect toggle button is checked */ inspectEnabled: false }, // if target is a component then get it's element, otherwise just return the target getElement: function (target) { if (target.isElement) { return target; } if (this.hasExt()) { if (Ext.versions.touch) { return target.element || target; } else { return target.el || target; } } else { return target; } }, /** * Creates the element selectors for inspect mode */ initElementSelectors: function() { var me = this; me.selectHighlighter = new me.Highlighter('select'); me.hoverHighlighter = new me.Highlighter('hover'); document.body.addEventListener('mouseover', function(event) { if (!me.inspectEnabled) return; var target = event.target, fly = ST.fly(target), cmp = fly && fly.getComponent(); if (cmp) { target = me.getElement(cmp); } me.hoverEl = target; if (target && !ST.fly(target).hasCls('c-no-inspect')) { me.hoverHighlighter.highlight(target); } else { me.hoverHighlighter.hide(); target = null; } }); // prevent itemtap events in modern when inspect is enabled document.body.addEventListener('mouseup', function(event) { if (me.inspectEnabled) event.stopPropagation(); }, true); document.body.addEventListener('click', function(event) { var fly = ST.fly(me.hoverEl), target, xpath; if (fly) { target = fly.getComponent() || fly; } if (me.inspectEnabled && me.hoverEl && target && ((target.hasCls && !target.hasCls('c-no-inspect')) || !target.hasCls)) { xpath = target.isElement ? fly.getXPath() : null; var msg = { componentTree: JSON.stringify(me.getComponentTree()), domTree: JSON.stringify(me.getDomTree()), cmpId: target.id || xpath, xpath: xpath, url: window.location.toString() }; me.socketEmit('inspectEvent', msg); me.selectHighlighter.highlight(me.hoverEl); me.highlightQuery(); // remove existing query highlights event.stopPropagation(); // don't bubble event.preventDefault(); // don't do the default!!! } }, true); }, matchesDomSelector: function(el) { for (var selector in this.inspectData.domSelectors) { if (el.matches(selector)) { return true; } } }, /** * Turns inspection on or off * @param {Boolean} enabled * @param {Object} inspectData The contents of inspect.json */ toggleInspectEnabled: function(data) { var me = this; me.inspectEnabled = data.value; if (!me.inspectEnabled) { me.highlightQuery(); me.hoverHighlighter.hide(); } me.socketEmit('inspectEnabled',{ inspectEnabled: me.inspectEnabled }); }, batchQuery: function (locators) { var me = this, i, results, result, data; for (i=0; i<locators.length; i++) { data = locators[i]; // set defaults for xtype and futureType in case errors occur data.xtype = data.xtype || 'Unavailable'; data.futureType = data.futureType || data.type; try { results = ST.Locator.findAll(data.locator, true /* wrap */, null, null, true /* returnComponents */); if (results.length == 1) { result = results[0]; if (!result) { data.matches = 0; } else { data.matches = 1; if (result.isElement) { ST.apply(data, me.getFullNodeForElement(result.dom)); } else if (result.isComponent || result.isWidget) { ST.apply(data, me.getCompNodeForComp(result)); } else { data.error = 'result was not an element, component or widget'; } if (data.altLocator) { results = ST.Locator.findAll(data.altLocator, true, null, null, true); data.matches = results ? results.length : 0; data.error = results && (results.length > 1) && (results.length + ' matches'); } } } else { data.error = results.length + ' matches'; data.matches = results.length; } } catch (e) { data.error = e.toString(); } } return locators; }, /** * @param query - if null, remove previous highlights and don't highlight anything */ highlightQuery: function (query) { var me = this, lights = me.queryHighlights || [], highlightType = 'query', results, visibleCount = 0, hasExt = me.hasExt(); me.selectHighlighter.hide(); // less confusing if we remove the last clicked item for (var i in lights) { lights[i].hide(); lights[i].destroy(); } me.queryHighlights = lights = []; if (!query) { return { matches: 0, visibleCount: 0 }; // don't highlight anything } try { results = ST.Locator.findAll(query, true); } catch (e) { return { matches: 0, visibleCount: 0, error: e.toString() } } if (results.length === 1) { highlightType = 'onematch'; } if (results.length > 30) { return { matches: results.length, visibleCount: visibleCount, tooMany: true } } for (var i in results) { if (!results.hasOwnProperty(i)) continue var result = results[i], el = me.getElement(result), light = el && el.dom && new this.Highlighter(highlightType); if (!light) { continue } if (!el) { light.destroy(); continue } if (result.isVisible && result.isHidden) { visibleCount += Ext.toolkit === 'classic' ? result.isVisible() : !result.isHidden(true); } else if (el.isVisible && el.dom) { visibleCount += el.isVisible(); } lights.push(light); light.highlight(el.dom); } return { matches: results.length, xtype: results.length === 1 ? results[0].xtype : '', cmpId: results.length === 1 ? results[0].cmpId : '', visibleCount: visibleCount }; }, /** * Extracts data for the component tree */ getComponentTree: function() { var me = this; if (!me.componentTree) { if (!Ext || !Ext.ComponentManager) { return []; // no Ext so return empty set } if (Ext.ComponentManager.getAll) { me.componentTree = me.getComponentTreeNodes(Ext.ComponentManager.getAll()); } else { me.componentTree = me.getComponentTreeNodes(Ext.ComponentManager.all.getArray()); } me.componentTreeTimeStamp = new Date().getTime(); } return me.componentTree; }, getDomTree: function (node, parentxpath) { var me = this, nodes = [], children = [], node = node || document.body, blackList = ['style', 'script'], xpath = parentxpath || '//body', tagMap = {}, nodeData, child, childCls, noInspect, childFragment; nodeData = me.getNodeForElement(node); nodeData.xpath = parentxpath; nodeData.cmpId = parentxpath; if (node.hasChildNodes()) { for (var j = 0; j < node.childNodes.length; j++) { child = node.childNodes[j]; childCls = me.getClassName(child); noInspect = typeof childCls === 'string' && childCls.indexOf('c-no-inspect') !== -1; if (child.nodeType === 1 && blackList.indexOf(child.tagName.toLowerCase()) === -1 && !noInspect ) { tagMap[child.tagName] = tagMap[child.tagName] + 1 || 1; childFragment = child.tagName.toLowerCase() + '[' + tagMap[child.tagName] + ']'; children.push(me.getDomTree(child, xpath + '/' + childFragment)); } } if (children.length) { nodeData.children = children; } else { nodeData.leaf = true; } return nodeData; } else { nodeData.leaf = true; } return nodeData; // getNodeForElement }, /** * recursive function to loop through items / docked items and children. compiles master list of components. * @param {Array} comps - app / viewer components */ getComponentTreeNodes: function(comps) { var me = this, tree = [], inTree = {}; if (!comps) return; if (!Ext.isArray(comps)) { comps = [comps]; } var addCompToTree = function(comp, child) { var node = inTree[comp.id]; if (node) { if (child) { node.children.push(child); delete node.leaf; } return; } node = me.getCompNodeForComp(comp); inTree[comp.id] = node; if (child) { node.children.push(child); } else { node.leaf = true; } var parent = comp.up && comp.up(); if (parent) { addCompToTree(parent, node); } else { tree.push(node); } } Ext.each(comps, function(comp) { // don't show unused rows in modern buffered grids if (Ext.grid && Ext.grid.Row && comp instanceof Ext.grid.Row && comp.$hidden) return; // don't show the highlighters if (comp.hideFromComponentTree) return; addCompToTree(comp); }); return tree; }, getNodeForElement: function (el) { var tag = el.tagName && el.tagName.toLowerCase(), text = '<' + tag, className = this.getClassName(el); text += className ? ' class="' + className + '"' : ''; text += el.id ? ' id="' + el.id + '"' : ''; text += '></' + tag + '>'; return { text: text, xtype: tag, futureType: tag && tag === 'table' ? 'table' : 'element' }; }, getFullNodeForElement: function (el) { var me = this, data = me.getNodeForElement(el), targets = [], hasAttributes = el && el.hasAttributes ? el.hasAttributes() : false, fly = ST.fly(el), attributes, i, locator, xpath; data.xpath = fly.getXPath(); me.strategy.locate(el, targets, null /* event */, true /* noComponents */); data.locators = []; for (i in targets) { var item = {}; locator = targets[i]; fly = ST.fly(locator[0]); xpath = fly.getXPath(); locator = targets[i]; ST.apply(item, { name: 'locator', cmpId: xpath, xpath: xpath, value: locator[1], }); data.locators.push(item); } data.properties = []; attributes = hasAttributes ? el.attributes : []; for (var x = 0; x < attributes.length; x++) { node = attributes[x]; data.properties.push({ name: node.name, value: node.value }); } return data; }, /** * returns an active ui for a component * @param {Ext.Component} component - ext component */ getActiveUI : function (comp) { if (Ext.list && Ext.list.TreeItem && comp instanceof Ext.list.TreeItem) { return this.getActiveUI(comp.parent); // the UI is defined for the tree item, but configured on the treelist } return comp.getUi ? comp.getUi() : comp.getUI ? comp.getUI() : comp.activeUI; }, /** * retrieves best match for future api component type. * @param {Component} comp - component * @return {String} futureType */ getFutureAPIMatch: function (comp) { var mappings = [{ future: 'button', matchers: ['button'] }, { future: 'checkBox', matchers: ['checkboxfield'] }, { future: 'select', matchers: ['selectfield'] }, { future: 'comboBox', matchers: ['combobox'] }, { future: 'picker', matchers: ['pickerfield'] }, { future: 'textField', matchers: ['textfield'] }, { future: 'slider', matchers: ['sliderfield', 'multislider'] }, { future: 'field', matchers: ['field'] }, { future: 'grid', matchers: ['grid'] }, { future: 'dataView', matchers: ['dataview'] }, { future: 'panel', matchers: ['panel'] }, { future: 'component', matchers: ['component'] }]; if (!comp.getXTypes) { return 'component'; } var list = comp.getXTypes().split('/').reverse(), match, list, matchers, map, i, x; for (i=0; i<mappings.length; i++) { map = mappings[i]; matchers = map.matchers; for (x=0; x<matchers.length; x++) { if (Ext.Array.contains(list, matchers[x])) { match = map.future; break; } } if (match) { break; } } return match || 'component'; }, /** * retrieves component details. * @param {Component} comp - component */ getCompNodeForComp : function (comp) { var me = this, propMatrix = { 'component': ['itemId', 'userCls', 'ui'], 'field': ['label', 'value', 'name'], 'textfield': ['autoComplete', 'readOnly'], 'treelistitem': ['text'], 'button': ['text'] }, ignoreList = ['plugins', 'frameIdRegex', 'idCleanRegex', 'validIdRe'], xtype = comp.xtype, xtypes, properties, propNames = {}, // keep track of property names already handled locators = [], targets = [], el, config, data; // collect config properties if (ST && this.getElement(comp)) { el = this.getElement(comp); try { this.strategy.locate(el.dom, targets, null /* no event available */); } catch (e) { console.log(e); } for(var x in targets) { var target = targets[x]; locators.push({ name: 'locator', cmpId: target[0].id || '', value: target[1] }); } } if (comp.getXTypes) { xtypes = comp.getXTypes().split('/'); } else { xtypes = [xtype]; } propNames.id = true; properties = [{ name: 'id', value: comp.id }]; for (var i=0; i < xtypes.length; i++) { var xtype = xtypes[i], props = propMatrix[xtype]; if (props) { for (var j=0; j < props.length; j++) { var propName = props[j], value = comp[propName]; if (typeof value === 'undefined' && propName !== 'itemId') { var getter = 'get' + propName[0].toUpperCase() + (propName.length > 1 ? propName.slice(1) : ''); value = comp[getter] && comp[getter].apply(comp); } if (typeof value !== 'undefined' && value !== null) { // !== null??? propNames[propName] = true; // keep track so we don't repeat them below when parsing comp.config properties.push({ name: propName, value: value }) } } } } // stolen from Dan Gallo's Google Chrome extension config = comp.config || {}; data = {}; for (var key in config) { if (config.hasOwnProperty(key)) { var value = config[key]; // skip all tpl and xyzTpl if (/(^tpl)|(.*Tpl)$/.test(key)) { continue; } if (value != null && typeof value != 'undefined' && !Ext.Array.contains(ignoreList, key) && !Ext.isEmpty(value) && !Ext.isObject(value) && !Ext.isArray(value) && !Ext.isFunction(value) && !propNames[key]) { properties.push({ name: key, value: value.toString() }); // data[key] = value.toString(); } } } xtype = comp.$reactorComponentName || xtype; return { cmpId: comp.id || '', xtype: xtype, text: xtype, properties: properties, children: [], xtypes: xtypes, locators: locators, futureType: this.getFutureAPIMatch(comp) }; }, /** * Returns the name of the Ext JS class from which the component extends. * @param {Ext.Base} comp * @return {String} */ getClassForComp: function(comp) { var proto = comp.__proto__; while (proto && proto.$className.indexOf('Ext.') !== 0) { proto = proto.__proto__; } return proto; }, initPolyfills: function() { // Element.matches if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function(s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s), i = matches.length; while (--i >= 0 && matches.item(i) !== this) {} return i > -1; }; } } });}());