UNPKG

electron-devtools-vendor

Version:

<div align="center"> <h2>electron-devtools-vendor</h2> <img alt="MIT" src="https://img.shields.io/github/license/BlackHole1/electron-devtools-vendor?color=9cf&style=flat-square"> <img alt="GitHub repo size" src="https://img.shields.io/github/r

1,765 lines (1,561 loc) 272 kB
(function(adapter, env) { let define = window.define, requireModule = window.requireModule; if (typeof define !== 'function' || typeof requireModule !== 'function') { (function() { let registry = {}, seen = {}; define = function(name, deps, callback) { if (arguments.length < 3) { callback = deps; deps = []; } registry[name] = { deps, callback }; }; requireModule = function(name) { if (seen[name]) { return seen[name]; } seen[name] = {}; let mod = registry[name]; if (!mod) { throw new Error(`Module: '${name}' not found.`); } let deps = mod.deps; let callback = mod.callback; let reified = []; let exports; for (let i = 0, l = deps.length; i < l; i++) { if (deps[i] === 'exports') { reified.push(exports = {}); } else { reified.push(requireModule(deps[i])); } } let value = callback.apply(this, reified); seen[name] = exports || value; return seen[name]; }; define.registry = registry; define.seen = seen; })(); } define('ember-debug/adapters/basic', ['exports', 'ember-debug/utils/on-ready'], function (exports, _onReady) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); /* globals requireModule */ /* eslint no-console: 0 */ const Ember = window.Ember; const { A, computed, RSVP, Object: EmberObject } = Ember; const { Promise, resolve } = RSVP; exports.default = EmberObject.extend({ init() { resolve(this.connect(), 'ember-inspector').then(() => { this.onConnectionReady(); }, null, 'ember-inspector'); }, /** * Uses the current build's config module to determine * the environment. * * @property environment * @type {String} */ environment: computed(function () { return requireModule('ember-debug/config')['default'].environment; }), debug() { return console.debug(...arguments); }, log() { return console.log(...arguments); }, /** * A wrapper for `console.warn`. * * @method warn */ warn() { return console.warn(...arguments); }, /** Used to send messages to EmberExtension @param {Object} type the message to the send */ sendMessage() /* options */{}, /** Register functions to be called when a message from EmberExtension is received @param {Function} callback */ onMessageReceived(callback) { this.get('_messageCallbacks').pushObject(callback); }, /** Inspect a specific element. This usually means using the current environment's tools to inspect the element in the DOM. For example, in chrome, `inspect(elem)` will open the Elements tab in dev tools and highlight the element. @param {DOM Element} elem */ inspectElement() /* elem */{}, _messageCallbacks: computed(function () { return A(); }), _messageReceived(message) { this.get('_messageCallbacks').forEach(callback => { callback(message); }); }, /** * Handle an error caused by EmberDebug. * * This function rethrows in development and test envs, * but warns instead in production. * * The idea is to control errors triggered by the inspector * and make sure that users don't get mislead by inspector-caused * bugs. * * @method handleError * @param {Error} error */ handleError(error) { if (this.get('environment') === 'production') { if (error && error instanceof Error) { error = `Error message: ${error.message}\nStack trace: ${error.stack}`; } this.warn(`Ember Inspector has errored.\n` + `This is likely a bug in the inspector itself.\n` + `You can report bugs at https://github.com/emberjs/ember-inspector.\n${error}`); } else { this.warn('EmberDebug has errored:'); throw error; } }, /** A promise that resolves when the connection with the inspector is set up and ready. @return {Promise} */ connect() { return new Promise((resolve, reject) => { (0, _onReady.onReady)(() => { if (this.isDestroyed) { reject(); } this.interval = setInterval(() => { if (document.documentElement.dataset.emberExtension) { clearInterval(this.interval); resolve(); } }, 10); }); }, 'ember-inspector'); }, willDestroy() { this._super(); clearInterval(this.interval); }, _isReady: false, _pendingMessages: computed(function () { return A(); }), send(options) { if (this._isReady) { this.sendMessage(...arguments); } else { this.get('_pendingMessages').push(options); } }, /** Called when the connection is set up. Flushes the pending messages. */ onConnectionReady() { // Flush pending messages const messages = this.get('_pendingMessages'); messages.forEach(options => this.sendMessage(options)); messages.clear(); this._isReady = true; } }); }); define('ember-debug/adapters/bookmarklet', ['exports', 'ember-debug/adapters/basic'], function (exports, _basic) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = _basic.default.extend({ init() { this._super(); this._listen(); }, sendMessage(options) { options = options || {}; window.emberInspector.w.postMessage(options, window.emberInspector.url); }, _listen() { window.addEventListener('message', e => { if (e.origin !== window.emberInspector.url) { return; } const message = e.data; if (message.from === 'devtools') { this._messageReceived(message); } }); window.onunload = () => { this.sendMessage({ unloading: true }); }; } }); }); define("ember-debug/adapters/chrome", ["exports", "ember-debug/adapters/web-extension"], function (exports, _webExtension) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = _webExtension.default.extend(); }); define("ember-debug/adapters/firefox", ["exports", "ember-debug/adapters/web-extension"], function (exports, _webExtension) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = _webExtension.default.extend({ debug() { // WORKAROUND: temporarily workaround issues with firebug console object: // - https://github.com/tildeio/ember-extension/issues/94 // - https://github.com/firebug/firebug/pull/109 // - https://code.google.com/p/fbug/issues/detail?id=7045 try { this._super(...arguments); } catch (e) {} }, log() { // WORKAROUND: temporarily workaround issues with firebug console object: // - https://github.com/tildeio/ember-extension/issues/94 // - https://github.com/firebug/firebug/pull/109 // - https://code.google.com/p/fbug/issues/detail?id=7045 try { this._super(...arguments); } catch (e) {} } }); }); define('ember-debug/adapters/web-extension', ['exports', 'ember-debug/adapters/basic'], function (exports, _basic) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const Ember = window.Ember; const { run, typeOf } = Ember; const { isArray } = Array; const { keys } = Object; exports.default = _basic.default.extend({ init() { this.set('_channel', new MessageChannel()); this.set('_chromePort', this.get('_channel.port1')); this._super(...arguments); }, connect() { const channel = this.get('_channel'); return this._super(...arguments).then(() => { window.postMessage('debugger-client', '*', [channel.port2]); this._listen(); }, null, 'ember-inspector'); }, sendMessage(options = {}) { // If prototype extensions are disabled, `Ember.A()` arrays // would not be considered native arrays, so it's not possible to // "clone" them through postMessage unless they are converted to a // native array. options = deepClone(options); this.get('_chromePort').postMessage(options); }, /** * Open the devtools "Elements" and select an element. * * NOTE: * This method was supposed to call `inspect` which is a Chrome specific function * that can either be called from the console or from code evaled using `inspectedWindow.eval` * (which is how this code is executed). See https://developer.chrome.com/extensions/devtools#evaluating-js. * However for some reason Chrome 52+ has started throwing an Error that `inspect` * is not a function when called from this code. The current workaround is to * message the Ember Ibspector asking it to execute `inspected.Window.eval('inspect(element)')` * for us. * * @param {HTMLElement} elem The element to select */ inspectElement(elem) { /* inspect(elem); */ this.get('namespace.port').send('view:inspectDOMElement', { elementSelector: `#${elem.getAttribute('id')}` }); }, _listen() { let chromePort = this.get('_chromePort'); chromePort.addEventListener('message', event => { const message = event.data; run(() => { this._messageReceived(message); }); }); chromePort.start(); } }); /** * Recursively clones all arrays. Needed because Chrome * refuses to clone Ember Arrays when extend prototypes is disabled. * * If the item passed is an array, a clone of the array is returned. * If the item is an object or an array, or array properties/items are cloned. * * @param {Mixed} item The item to clone * @return {Mixed} */ function deepClone(item) { let clone = item; if (isArray(item)) { clone = new Array(item.length); item.forEach((child, key) => { clone[key] = deepClone(child); }); } else if (item && typeOf(item) === 'object') { clone = {}; keys(item).forEach(key => { clone[key] = deepClone(item[key]); }); } return clone; } }); define('ember-debug/adapters/websocket', ['exports', 'ember-debug/adapters/basic', 'ember-debug/utils/on-ready'], function (exports, _basic, _onReady) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const Ember = window.Ember; const { computed, run, RSVP: { Promise } } = Ember; exports.default = _basic.default.extend({ sendMessage(options = {}) { this.get('socket').emit('emberInspectorMessage', options); }, socket: computed(function () { return window.EMBER_INSPECTOR_CONFIG.remoteDebugSocket; }), _listen() { this.get('socket').on('emberInspectorMessage', message => { run(() => { this._messageReceived(message); }); }); }, _disconnect() { this.get('socket').removeAllListeners("emberInspectorMessage"); }, connect() { return new Promise((resolve, reject) => { (0, _onReady.onReady)(() => { if (this.isDestroyed) { reject(); } const EMBER_INSPECTOR_CONFIG = window.EMBER_INSPECTOR_CONFIG; if (typeof EMBER_INSPECTOR_CONFIG === 'object' && EMBER_INSPECTOR_CONFIG.remoteDebugSocket) { resolve(); } }); }).then(() => { this._listen(); }); }, willDestroy() { this._disconnect(); } }); }); define('ember-debug/container-debug', ['exports', 'ember-debug/mixins/port-mixin'], function (exports, _portMixin) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const Ember = window.Ember; const { Object: EmberObject, computed } = Ember; const { readOnly } = computed; exports.default = EmberObject.extend(_portMixin.default, { namespace: null, objectInspector: readOnly('namespace.objectInspector'), container: computed('namespace.owner', function () { // should update this to use real owner API return this.get('namespace.owner.__container__'); }), portNamespace: 'container', TYPES_TO_SKIP: computed(function () { return ['component-lookup', 'container-debug-adapter', 'resolver-for-debugging', 'event_dispatcher']; }), typeFromKey(key) { return key.split(':').shift(); }, nameFromKey(key) { return key.split(':').pop(); }, shouldHide(type) { return type[0] === '-' || this.get('TYPES_TO_SKIP').indexOf(type) !== -1; }, instancesByType() { let key; let instancesByType = {}; let cache = this.get('container').cache; // Detect if InheritingDict (from Ember < 1.8) if (typeof cache.dict !== 'undefined' && typeof cache.eachLocal !== 'undefined') { cache = cache.dict; } for (key in cache) { const type = this.typeFromKey(key); if (this.shouldHide(type)) { continue; } if (instancesByType[type] === undefined) { instancesByType[type] = []; } instancesByType[type].push({ fullName: key, instance: cache[key] }); } return instancesByType; }, getTypes() { let key; let types = []; const instancesByType = this.instancesByType(); for (key in instancesByType) { types.push({ name: key, count: instancesByType[key].length }); } return types; }, getInstances(type) { const instances = this.instancesByType()[type]; if (!instances) { return null; } return instances.map(item => ({ name: this.nameFromKey(item.fullName), fullName: item.fullName, inspectable: this.get('objectInspector').canSend(item.instance) })); }, messages: { getTypes() { this.sendMessage('types', { types: this.getTypes() }); }, getInstances(message) { let instances = this.getInstances(message.containerType); if (instances) { this.sendMessage('instances', { instances, status: 200 }); } else { this.sendMessage('instances', { status: 404 }); } }, sendInstanceToConsole(message) { const instance = this.get('container').lookup(message.name); this.get('objectToConsole').sendValueToConsole(instance); } } }); }); define('ember-debug/data-debug', ['exports', 'ember-debug/mixins/port-mixin'], function (exports, _portMixin) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const Ember = window.Ember; const { Object: EmberObject, computed, guidFor, A, set } = Ember; const { alias } = computed; exports.default = EmberObject.extend(_portMixin.default, { init() { this._super(); this.sentTypes = {}; this.sentRecords = {}; }, releaseTypesMethod: null, releaseRecordsMethod: null, /* eslint-disable ember/no-side-effects */ adapter: computed('namespace.owner', function () { const owner = this.get('namespace.owner'); // dataAdapter:main is deprecated let adapter = this._resolve('data-adapter:main') && owner.lookup('data-adapter:main'); // column limit is now supported at the inspector level if (adapter) { set(adapter, 'attributeLimit', 100); return adapter; } }), /* eslint-enable ember/no-side-effects */ _resolve(name) { const owner = this.get('namespace.owner'); return owner.resolveRegistration(name); }, namespace: null, port: alias('namespace.port'), objectInspector: alias('namespace.objectInspector'), portNamespace: 'data', modelTypesAdded(types) { let typesToSend; typesToSend = types.map(type => this.wrapType(type)); this.sendMessage('modelTypesAdded', { modelTypes: typesToSend }); }, modelTypesUpdated(types) { let typesToSend = types.map(type => this.wrapType(type)); this.sendMessage('modelTypesUpdated', { modelTypes: typesToSend }); }, wrapType(type) { const objectId = guidFor(type.object); this.sentTypes[objectId] = type; return { columns: type.columns, count: type.count, name: type.name, objectId }; }, recordsAdded(recordsReceived) { let records = recordsReceived.map(record => this.wrapRecord(record)); this.sendMessage('recordsAdded', { records }); }, recordsUpdated(recordsReceived) { let records = recordsReceived.map(record => this.wrapRecord(record)); this.sendMessage('recordsUpdated', { records }); }, recordsRemoved(index, count) { this.sendMessage('recordsRemoved', { index, count }); }, wrapRecord(record) { const objectId = guidFor(record.object); let columnValues = {}; let searchKeywords = []; this.sentRecords[objectId] = record; // make objects clonable for (let i in record.columnValues) { columnValues[i] = this.get('objectInspector').inspect(record.columnValues[i]); } // make sure keywords can be searched and clonable searchKeywords = A(record.searchKeywords).filter(keyword => typeof keyword === 'string' || typeof keyword === 'number'); return { columnValues, searchKeywords, filterValues: record.filterValues, color: record.color, objectId }; }, releaseTypes() { if (this.releaseTypesMethod) { this.releaseTypesMethod(); this.releaseTypesMethod = null; this.sentTypes = {}; } }, releaseRecords() { if (this.releaseRecordsMethod) { this.releaseRecordsMethod(); this.releaseRecordsMethod = null; this.sentRecords = {}; } }, willDestroy() { this._super(); this.releaseRecords(); this.releaseTypes(); }, messages: { checkAdapter() { this.sendMessage('hasAdapter', { hasAdapter: !!this.get('adapter') }); }, getModelTypes() { this.releaseTypes(); this.releaseTypesMethod = this.get('adapter').watchModelTypes(types => { this.modelTypesAdded(types); }, types => { this.modelTypesUpdated(types); }); }, releaseModelTypes() { this.releaseTypes(); }, getRecords(message) { const type = this.sentTypes[message.objectId]; this.releaseRecords(); let typeOrName; if (this.get('adapter.acceptsModelName')) { // Ember >= 1.3 typeOrName = type.name; } let releaseMethod = this.get('adapter').watchRecords(typeOrName, recordsReceived => { this.recordsAdded(recordsReceived); }, recordsUpdated => { this.recordsUpdated(recordsUpdated); }, (...args) => { this.recordsRemoved(...args); }); this.releaseRecordsMethod = releaseMethod; }, releaseRecords() { this.releaseRecords(); }, inspectModel(message) { this.get('objectInspector').sendObject(this.sentRecords[message.objectId].object); }, getFilters() { this.sendMessage('filters', { filters: this.get('adapter').getFilters() }); } } }); }); define('ember-debug/deprecation-debug', ['exports', 'ember-debug/mixins/port-mixin', 'ember-debug/libs/source-map'], function (exports, _portMixin, _sourceMap) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const Ember = window.Ember; const { Debug, Object: EmberObject, computed, guidFor, run, RSVP, A } = Ember; const { resolve, all } = RSVP; const { readOnly } = computed; const { registerDeprecationHandler } = Debug; exports.default = EmberObject.extend(_portMixin.default, { portNamespace: 'deprecation', adapter: readOnly('port.adapter'), sourceMap: computed(function () { return _sourceMap.default.create(); }), emberCliConfig: readOnly('namespace.generalDebug.emberCliConfig'), init() { this._super(); this.deprecations = A(); this.deprecationsToSend = A(); this.groupedDeprecations = {}; this.options = { toggleDeprecationWorkflow: false }; this.handleDeprecations(); }, /** * Checks if ember-cli and looks for source maps. */ fetchSourceMap(stackStr) { if (this.get('emberCliConfig') && this.get('emberCliConfig.environment') === 'development') { return this.get('sourceMap').map(stackStr).then(mapped => { if (mapped && mapped.length > 0) { let source = mapped.find(item => item.source && !!item.source.match(new RegExp(this.get('emberCliConfig.modulePrefix')))); if (source) { source.found = true; } else { source = mapped.get('firstObject'); source.found = false; } return source; } }, null, 'ember-inspector'); } else { return resolve(null, 'ember-inspector'); } }, sendPending() { if (this.isDestroyed) { return; } let deprecations = A(); let promises = all(this.get('deprecationsToSend').map(deprecation => { let obj; let promise = resolve(undefined, 'ember-inspector'); let grouped = this.get('groupedDeprecations'); this.get('deprecations').pushObject(deprecation); const id = guidFor(deprecation.message); obj = grouped[id]; if (obj) { obj.count++; obj.url = obj.url || deprecation.url; } else { obj = deprecation; obj.count = 1; obj.id = id; obj.sources = A(); grouped[id] = obj; } let found = obj.sources.findBy('stackStr', deprecation.stackStr); if (!found) { let stackStr = deprecation.stackStr; promise = this.fetchSourceMap(stackStr).then(map => { obj.sources.pushObject({ map, stackStr }); if (map) { obj.hasSourceMap = true; } }, null, 'ember-inspector'); } return promise.then(() => { delete obj.stackStr; deprecations.addObject(obj); }, null, 'ember-inspector'); })); promises.then(() => { this.sendMessage('deprecationsAdded', { deprecations }); this.get('deprecationsToSend').clear(); this.sendCount(); }, null, 'ember-inspector'); }, sendCount() { if (this.isDestroyed) { return; } this.sendMessage('count', { count: this.get('deprecations.length') + this.get('deprecationsToSend.length') }); }, messages: { watch() { this._watching = true; let grouped = this.get('groupedDeprecations'); let deprecations = []; for (let i in grouped) { if (!grouped.hasOwnProperty(i)) { continue; } deprecations.push(grouped[i]); } this.sendMessage('deprecationsAdded', { deprecations }); this.sendPending(); }, sendStackTraces(message) { let deprecation = message.deprecation; deprecation.sources.forEach(source => { let stack = source.stackStr; stack = stack.split('\n'); stack.unshift(`Ember Inspector (Deprecation Trace): ${deprecation.message || ''}`); this.get('adapter').log(stack.join('\n')); }); }, getCount() { this.sendCount(); }, clear() { run.cancel(this.debounce); this.get('deprecations').clear(); this.set('groupedDeprecations', {}); this.sendCount(); }, release() { this._watching = false; }, setOptions({ options }) { this.options.toggleDeprecationWorkflow = options.toggleDeprecationWorkflow; } }, willDestroy() { run.cancel(this.debounce); return this._super(...arguments); }, handleDeprecations() { registerDeprecationHandler((message, options, next) => { /* global __fail__*/ let error; // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome try { __fail__.fail(); } catch (e) { error = e; } let stack; let stackStr = ''; if (error.stack) { // var stack; if (error['arguments']) { // Chrome stack = error.stack.replace(/^\s+at\s+/gm, '').replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2').replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); stack.shift(); } else { // Firefox stack = error.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n'); } stackStr = `\n ${stack.slice(2).join('\n ')}`; } let url; if (options && typeof options === 'object') { url = options.url; } const deprecation = { message, stackStr, url }; // For ember-debug testing we usually don't want // to catch deprecations if (!this.get('namespace').IGNORE_DEPRECATIONS) { this.get('deprecationsToSend').pushObject(deprecation); run.cancel(this.debounce); if (this._watching) { this.debounce = run.debounce(this, 'sendPending', 100); } else { this.debounce = run.debounce(this, 'sendCount', 100); } if (!this._warned) { this.get('adapter').warn('Deprecations were detected, see the Ember Inspector deprecations tab for more details.'); this._warned = true; } } if (this.options.toggleDeprecationWorkflow) { next(message, options); } }); } }); }); define('ember-debug/general-debug', ['exports', 'ember-debug/mixins/port-mixin'], function (exports, _portMixin) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const Ember = window.Ember; /* eslint no-empty:0 */ const { Object: EmberObject } = Ember; let { libraries } = Ember; /** * Class that handles gathering general information of the inspected app. * ex: * - Determines if the app was booted * - Gathers the libraries. Found in the info tab of the inspector. * - Gathers ember-cli configuration information from the meta tags. * * @module ember-debug/general-debug */ exports.default = EmberObject.extend(_portMixin.default, { /** * Fetches the ember-cli configuration info and sets them on * the `emberCliConfig` property. */ init() { this._super(...arguments); let found = findMetaTag('name', /environment$/); if (found) { try { let config = JSON.parse(unescape(found.getAttribute('content'))); this.set('emberCliConfig', config); } catch (e) {} } }, /** * Passed on creation. * * @type {EmberDebug} */ namespace: null, /** * Used by the PortMixin * * @type {String} */ portNamespace: 'general', /** * Set on creation. * Contains ember-cli configuration info. * * Info used to determine the file paths of an ember-cli app. * * @return {Object} * {String} environment ex: 'development' * {String} modulePrefix ex: 'my-app' * {String} podModulePrefix ex: 'my-app/pods' * {Boolean} usePodsByDefault */ emberCliConfig: null, /** * Sends a reply back indicating if the app has been booted. * * `__inspector__booted` is a property set on the application instance * when the ember-debug is inserted into the target app. * see: startup-wrapper. */ sendBooted() { this.sendMessage('applicationBooted', { booted: this.get('namespace.owner.__inspector__booted') }); }, /** * Sends a reply back indicating that ember-debug has been reset. * We need to reset ember-debug to remove state between tests. */ sendReset() { this.sendMessage('reset'); }, messages: { /** * Called from the inspector to check if the inspected app has been booted. */ applicationBooted() { this.sendBooted(); }, /** * Called from the inspector to fetch the libraries that are displayed in * the info tab. */ getLibraries() { this.sendMessage('libraries', { libraries: libraries._registry }); }, /** * Called from the inspector to refresh the inspected app. * Used in case the inspector was opened late and therefore missed capturing * all info. */ refresh() { window.location.reload(); } } }); /** * Finds a meta tag by searching through a certain meta attribute. * * @param {String} attribute * @param {RegExp} regExp * @return {Element} */ function findMetaTag(attribute, regExp = /.*/) { let metas = document.querySelectorAll(`meta[${attribute}]`); for (let i = 0; i < metas.length; i++) { let match = metas[i].getAttribute(attribute).match(regExp); if (match) { return metas[i]; } } return null; } }); define('ember-debug/libs/glimmer-tree', ['exports', 'ember-debug/utils/name-functions'], function (exports, _nameFunctions) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); /** * This class contains functionality related to for Ember versions * using Glimmer 2 (Ember >= 2.9): * * It has the following main responsibilities: * * - Building the view tree. * - Highlighting components/outlets when the view tree is hovered. * - Highlighting components/outlets when the views themselves are hovered. * - Finding the model of a specific outlet/component. * * The view tree is a hierarchy of nodes (optionally) containing the following info: * - name * - template * - id * - view class * - duration * - tag name * - model * - controller * - type * * Once the view tree is generated it can be sent to the Ember Inspector to be displayed. * * @class GlimmerTree */ const Ember = window.Ember; const { Object: EmberObject, typeOf, isNone, Controller, ViewUtils, get, A } = Ember; const { getRootViews, getChildViews, getViewBoundingClientRect } = ViewUtils; exports.default = class { /** * Sets up the initial options. * * @method constructor * @param {Object} options * - {owner} owner The Ember app's owner. * - {Function} retainObject Called to retain an object for future inspection. * - {Object} options Options whether to show components or not. * - {Object} durations Hash containing time to render per view id. * - {Function} highlightRange Called to highlight a range of elements. * - {Object} ObjectInspector Used to inspect models. * - {Object} viewRegistry Hash containing all currently rendered components by id. */ constructor({ owner, retainObject, options, durations, highlightRange, objectInspector, viewRegistry }) { this.owner = owner; this.retainObject = retainObject; this.options = options; this.durations = durations; this.highlightRange = highlightRange; this.objectInspector = objectInspector; this.viewRegistry = viewRegistry; } /** * @method updateOptions * @param {Object} options */ updateOptions(options) { this.options = options; } /** * @method updateDurations * @param {Object} durations */ updateDurations(durations) { this.duations = durations; } /** * Builds the view tree. The view tree may or may not contain * components depending on the current options. * * The view tree has the top level outlet as the root of the tree. * The format is: * { * value: |hash of properties|, * children: [ * { * value: |hash of properties|, * children: [] * }, * { * value: |hash of properties|, * children: [...] * }] * } * * We are building the tree is by doing the following steps: * - Build the outlet tree by walking the outlet state. * - Build several component trees, each tree belonging to one controller. * - Assign each controller-specific component tree as a child of the outlet corresponding * to that specific controller. * * @method build * @return {Object} The view tree */ build() { if (this.getRoot()) { let outletTree = this.buildOutletTree(); let componentTrees = this.options.components ? this.buildComponentTrees(outletTree) : []; return this.addComponentsToOutlets(outletTree, componentTrees); } } /** * Starts with the root and walks the tree till * the leaf outlets. The format is: * { * value: |inspected outlet|, * children: * [ * { * value: |inspected outlet|, * children: [...] * } * ] * } * * @method buildOutletTree * @return {Object} Tree of inspected outlets */ buildOutletTree() { let outletTree = this.makeOutletTree(this.getApplicationOutlet()); // set root element's id let rootElement = this.elementForRoot(); if (rootElement instanceof HTMLElement) { outletTree.value.elementId = rootElement.getAttribute('id'); } outletTree.value.tagName = 'div'; return outletTree; } /** * The recursive part of building the outlet tree. * * Return format: * { * value: |inspected outlet| * controller: |controller instance| * children: [...] * } * * @method makeOutletTree * @param {Object} outletState * @return {Object} The inspected outlet tree */ makeOutletTree(outletState) { let { render: { controller }, outlets } = outletState; let node = { value: this.inspectOutlet(outletState), controller, children: [] }; for (let key in outlets) { // disconnectOutlet() resets the controller value as undefined (https://github.com/emberjs/ember.js/blob/v2.6.2/packages/ember-routing/lib/system/route.js#L2048). // So skip building the tree, if the outletState doesn't have a controller. if (this.controllerForOutlet(outlets[key])) { node.children.push(this.makeOutletTree(outlets[key])); } } return node; } /** * Builds the component trees. Each tree corresponds to one controller. * A component's controller is determined by its target (or ancestor's target). * * Has the following format: * { * controller: |The controller instance|, * components: [|component tree|] * } * * @method buildComponentTrees * @param {Object} outletTree * @return {Array} The component tree */ buildComponentTrees(outletTree) { let controllers = this.controllersFromOutletTree(outletTree); return controllers.map(controller => { let components = this.componentsForController(this.topComponents(), controller); return { controller, components }; }); } /** * Builds a tree of components that have a specific controller * as their target. If a component does not match the given * controller, we ignore it and move on to its children. * * Format: * [ * { * value: |inspected component|, * children: [...] * }, * { * value: |inspected component| * children: [{ * value: |inspected component| * children: [...] * }] * } * ] * * @method componentsForController * @param {Array} components Subtree of components * @param {Controller} controller * @return {Array} Array of inspected components */ componentsForController(components, controller) { let arr = []; components.forEach(component => { let currentController = this.controllerForComponent(component); if (!currentController) { return; } let children = this.componentsForController(this.childComponents(component), controller); if (currentController === controller) { arr.push({ value: this.inspectComponent(component), children }); } else { arr = arr.concat(children); } }); return arr; } /** * Given a component, return its children. * * @method childComponents * @param {Component} component The parent component * @return {Array} Array of components (children) */ childComponents(component) { return getChildViews(component); } /** * Get the top level components. * * @method topComponents * @return {Array} Array of components */ topComponents() { return getRootViews(this.owner); } /** * Assign each component tree to it matching outlet * by comparing controllers. * * Return format: * { * value: |inspected root outlet| * children: [ * { * value: |inspected outlet or component| * chidren: [...] * }, * { * value: |inspected outlet or component| * chidren: [...] * } * ] * } * * @method addComponentsToOutlets * @param {Object} outletTree * @param {Object} componentTrees */ addComponentsToOutlets(outletTree, componentTrees) { let { value, controller, children } = outletTree; children = children.map(child => this.addComponentsToOutlets(child, componentTrees)); let { components } = A(componentTrees).findBy('controller', controller) || { components: [] }; return { value, children: children.concat(components) }; } /** * @method controllersFromOutletTree * * @param {Controller} inspectedOutlet * @return {Array} List of controllers */ controllersFromOutletTree({ controller, children }) { return [controller].concat(...children.map(this.controllersFromOutletTree.bind(this))); } /** * @method getRouter * @return {Router} */ getRouter() { return this.owner.lookup('router:main'); } /** * Returns the current top level view. * * @method getRoot * @return {OutletView} */ getRoot() { return this.getRouter().get('_toplevelView'); } /** * Returns the application (top) outlet. * * @return {Object} The application outlet state */ getApplicationOutlet() { // Support multiple paths to outletState for various Ember versions const outletState = this.getRoot().outletState || this.getRoot().state.ref.outletState; return outletState.outlets.main; } /** * The root's DOM element. The root is the only outlet view * with a DOM element. * * @method elementForRoot * @return {Element} */ elementForRoot() { let renderer = this.owner.lookup('renderer:-dom'); return renderer._roots && renderer._roots[0] && renderer._roots[0].result && renderer._roots[0].result.firstNode(); } /** * Returns a component's template name. * * @method templateForComponent * @param {Component} component * @return {String} The template name */ templateForComponent(component) { let template = component.get('layoutName'); if (!template) { let layout = component.get('layout'); if (!layout) { let componentName = component.get('_debugContainerKey'); if (componentName) { let layoutName = componentName.replace(/component:/, 'template:components/'); layout = this.owner.lookup(layoutName); } } template = this.nameFromLayout(layout); } return template; } /** * Inspects and outlet state. Extracts the name, controller, template, * and model. * * @method inspectOutlet * @param {Object} outlet The outlet state * @return {Object} The inspected outlet */ inspectOutlet(outlet) { let name = this.nameForOutlet(outlet); let template = this.templateForOutlet(outlet); let controller = this.controllerForOutlet(outlet); let value = { controller: this.inspectController(controller), template, name, isComponent: false, // Outlets (except root) don't have elements tagName: '' }; let model = controller.get('model'); if (model) { value.model = this.inspectModel(model); } return value; } /** * Represents the controller as a short and long name + guid. * * @method inspectController * @param {Controller} controller * @return {Object} The inspected controller. */ inspectController(controller) { return { name: (0, _nameFunctions.shortControllerName)(controller), completeName: (0, _nameFunctions.shortControllerName)(controller), objectId: this.retainObject(controller) }; } /** * Represent a component as a hash containing a template, * name, objectId, class, render duration, tag, model. * * @method inspectComponent * @param {Component} component * @return {Object} The inspected component */ inspectComponent(component) { let viewClass = (0, _nameFunctions.shortViewName)(component); let completeViewClass = viewClass; let tagName = component.get('tagName'); let objectId = this.retainObject(component); let duration = this.durations[objectId]; let name = (0, _nameFunctions.shortViewName)(component); let template = this.templateForComponent(component); let value = { template, name, objectId, viewClass, duration, completeViewClass, isComponent: true, tagName: isNone(tagName) ? 'div' : tagName }; let model = this.modelForComponent(component); if (model) { value.model = this.inspectModel(model); } return value; } /** * Simply returns the component's model if it * has one. * * @method modelForComponent * @param {Component} component * @return {Any} The model property */ modelForComponent(component) { return component.get('model'); } /** * Represent a model as a short name, long name, * guid, and type. * * @method inspectModel * @param {Any} model * @return {Object} The inspected model. */ inspectModel(model) { if (EmberObject.detectInstance(model) || typeOf(model) === 'array') { return { name: (0, _nameFunctions.shortModelName)(model), completeName: (0, _nameFunctions.modelName)(model), objectId: this.retainObject(model), type: 'type-ember-object' }; } return { name: this.objectInspector.inspect(model), type: `type-${typeOf(model)}` }; } /** * Uses the module name that was set during compilation. * * @method nameFromLayout * @param {Layout} layout * @return {String} The layout's name */ nameFromLayout(layout) { let moduleName = layout && get(layout, 'meta.moduleName'); if (moduleName) { return moduleName.replace(/\.hbs$/, ''); } } /** * Taekes an outlet state and extracts the controller from it. * * @method controllerForOutlet * @param {Controller} outletState * @return {Controller} */ controllerForOutlet(outletState) { return outletState.render.controller; } /** * The outlet's name. * * @method nameForOutlet * @param {Object} outletState * @return {String} */ nameForOutlet(outletState) { return outletState.render.name; } /** * The outlet's template name. Uses the module name attached during compilation. * * @method templateForOutlet * @param {Object} outletState * @return {String} The template name */ templateForOutlet(outletState) { let template = outletState.render.template; return this.nameFromLayout(template); } /** * Returns a component's controller. The controller is either the component's * target object, or the target object of one of its ancestors. That is why * the method is recursive. * * @method controllerForComponent * @param {Component} component * @return {Controller} The target controller. */ controllerForComponent(component) { let controller = component.get('_target') || component.get('_targetObject'); if (!controller) { return null; } if (controller instanceof Controller) { return controller; } else { return this.controllerForComponent(controller); } } /** * Renders a rectangle around a component's element. This happens * when the user either hovers over the view tree components * or clicks on the "inspect" magnifying glass and starts * hovering over the components themselves. * * Pass `isPreview` if you want the highlight to be hidden * when the mouse leaves the component. Set `isPreview` to false * to render a [permanent] rectangle until the (x) button is clicked. * * * @method highlightComponent * @param {Element} element The element to highlight * @param {Boolean} isPreview Whether it's a preview or not */ highlightComponent(component, isPreview = false) { let rect = getViewBoundingClientRect(component); let options = { isPreview, view: { name: (0, _nameFunctions.shortViewName)(component), object: component } }; let templateName = this.templateForComponent(component); if (templateName) { options.template = { name: templateName }; } this.highlightRange(rect, options); } /** * Renders a rectangle around the top level outlet's element. This happens * when the user either hovers over the view tree root outlets * or clicks on the "inspect" magnifying glass and starts * hovering over the application template. * * Pass `isPreview` if you want the highlight to be hidden * when the mouse leaves the root. Set `isPreview` to false * to render a [permanent] rectangle until the (x) button is clicked. * * @method highlightRoot * @param {Boolean} isPreview */ highlightRoot(isPreview = false) { let applicationOutlet = this.getApplicationOutlet(); let element = this.elementForRoot(); if (!element) { return; } let options = { isPreview, element, template: { name: this.templateForOutlet(applicationOutlet) } }; let controller = this.controllerForOutlet(applicationOutlet); if (controller) { options.controller = { name: (0, _nameFunctions.shortControllerName)(controller), object: controller }; let model = controller.get('model'); if (model) { let modelName = this.objectInspector.inspect(model); options.model = { name: modelName, object: model }; } } let rect = this.getBoundingClientRect(element); this.highlightRange(rect, options); } /** * Same as `ViewUtils.getBoundingClientRect` except this applies to * HTML elements instead of components. * * @method getBoundingClientRect * @param {Element} element * @return {DOMRect */ getBoundingClientRect(element) { let range = document.createRange(); range.setStartBefore(element); range.setEndAfter(element); return range.getBoundingClientRect(); } /** * Highlight an element only if it is a root. * * @method highlightIfRoot * @param {String} elementId * @param isPreview */ highlightIfRoot(elementId, isPreview = false) { let element = document.getElementById(elementId); if (this.isRootElement(element)) { this.highlightRoot(isPreview); } } /** * Call this method when you have the id of an element you want * to highlight but are unsure if that element represents a component