Skip to content

Commit

Permalink
Context menu item for inspecting components (emberjs#843)
Browse files Browse the repository at this point in the history
* Context menu item for "Inspect Ember Component"

* Try to set context menu when inspector loads

Relying on the `emberVersion` message as the trigger point for setting
the context menu isn't always reliable.

* Scroll component tree to selected item

When an item is inspected via the context menu we try to put it into
view in the component tree.  We make an educated guess as to how far
down we should scroll based on the item's index and a magic height
number

* Add specs for inspecting item via context menu
  • Loading branch information
Bestra authored and cyril-sf committed Mar 30, 2022
1 parent 46d8d72 commit fe59fc0
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 18 deletions.
38 changes: 38 additions & 0 deletions app/controllers/component-tree.js
Expand Up @@ -13,6 +13,10 @@ import {
isEmpty
} from '@ember/utils';

import {
schedule
} from '@ember/runloop';

import ComponentViewItem from 'ember-inspector/models/component-view-item';

/**
Expand Down Expand Up @@ -71,12 +75,19 @@ const flattenSearchTree = (

export default Controller.extend({
application: controller(),
queryParams: ['pinnedObjectId'],

/**
* The entry in the component tree corresponding to the pinnedObjectId
* will be selected
*/
pinnedObjectId: null,
inspectingViews: false,
components: true,
options: {
components: true,
},
viewTreeLoaded: false,

/**
* Bound to the search field to filter the component list.
Expand Down Expand Up @@ -138,13 +149,36 @@ export default Controller.extend({
this.set('expandedStateCache', {});
},

/**
* Expands the component tree so that entry for the given view will
* be shown. Recursively expands the entry's parents up to the root.
* @param {*} objectId The id of the ember view to show
*/
expandToNode(objectId) {
let node = this.get('filteredArray').find(item => item.get('id') === objectId);
if (node) {
node.expandParents();
}
},

/**
* This method is basically a trick to get the `{{vertical-collection}}` in the vicinity
* of the item that's been selected. We can't directly scroll to the element but we
* can guess at how far down the list the item is. Then we can manually set the scrollTop
* of the virtual scroll.
*/
scrollTreeToItem(objectId) {
let selectedItemIndex = this.get('displayedList').findIndex(item => item.view.objectId === objectId);

if (selectedItemIndex) {
const averageItemHeight = 22;
schedule('afterRender', () => {
document.querySelector('.js-component-tree').scrollTop = averageItemHeight * selectedItemIndex;
});
}
},


actions: {
previewLayer({
view: {
Expand Down Expand Up @@ -199,12 +233,16 @@ export default Controller.extend({
if (objectId) {
this.set('pinnedObjectId', objectId);
this.expandToNode(objectId);
this.scrollTreeToItem(objectId);
this.get('port').send('objectInspector:inspectById', {
objectId,
});
}
},

/**
* Scrolls the main page to put the selected element into view
*/
scrollToElement(elementId) {
this.get('port').send('view:scrollToElement', {
elementId
Expand Down
10 changes: 10 additions & 0 deletions app/routes/application.js
Expand Up @@ -18,6 +18,7 @@ export default Route.extend({
port.on('objectInspector:updateErrors', this, this.updateErrors);
port.on('objectInspector:droppedObject', this, this.droppedObject);
port.on('deprecation:count', this, this.setDeprecationCount);
port.on('view:inspectComponent', this, this.inspectComponent);
port.send('deprecation:getCount');
},

Expand All @@ -28,6 +29,15 @@ export default Route.extend({
port.off('objectInspector:updateErrors', this, this.updateErrors);
port.off('objectInspector:droppedObject', this, this.droppedObject);
port.off('deprecation:count', this, this.setDeprecationCount);
port.off('view:inspectComponent', this, this.inspectComponent);
},

inspectComponent({ viewId }) {
this.transitionTo('component-tree', {
queryParams: {
pinnedObjectId: viewId
}
});
},

updateObject(options) {
Expand Down
33 changes: 29 additions & 4 deletions app/routes/component-tree.js
@@ -1,13 +1,20 @@
import TabRoute from "ember-inspector/routes/tab";

export default TabRoute.extend({
queryParams: {
pinnedObjectId: {
replace: true
}
},

setupController() {
this._super(...arguments);
this.get('port').on('view:viewTree', this, this.setViewTree);
this.get('port').on('view:stopInspecting', this, this.stopInspecting);
this.get('port').on('view:startInspecting', this, this.startInspecting);
this.get('port').on('view:inspectDOMElement', this, this.inspectDOMElement);
this.get('port').on('view:inspectComponent', this, this.inspectComponent);

this.set('controller.viewTreeLoaded', false);
this.get('port').send('view:setOptions', { options: this.get('controller.options') });
this.get('port').send('view:getTree');
},
Expand All @@ -17,12 +24,17 @@ export default TabRoute.extend({
this.get('port').off('view:stopInspecting', this, this.stopInspecting);
this.get('port').off('view:startInspecting', this, this.startInspecting);
this.get('port').off('view:inspectDOMElement', this, this.inspectDOMElement);
this.get('port').off('view:inspectComponent', this, this.inspectComponent);

},

setViewTree(options) {
this.set('controller.viewTree', options.tree);
this.set('controller.viewTreeLoaded', true);

// If we're waiting for view tree to inspect a component
const componentToInspect = this.get('controller.pinnedObjectId');
if (componentToInspect) {
this.inspectComponent(componentToInspect);
}
},

startInspecting() {
Expand All @@ -33,11 +45,24 @@ export default TabRoute.extend({
this.set('controller.inspectingViews', false);
},

inspectComponent({ viewId }) {
inspectComponent(viewId) {
if (!this.get('controller.viewTreeLoaded')) {
return;
}

this.get('controller').send('inspect', viewId);
},

inspectDOMElement({ elementSelector }) {
this.get('port.adapter').inspectDOMElement(elementSelector);
},

actions: {
queryParamsDidChange(params) {
const { pinnedObjectId } = params;
if (pinnedObjectId) {
this.inspectComponent(pinnedObjectId);
}
}
}
});
2 changes: 1 addition & 1 deletion app/templates/component-tree.hbs
@@ -1,4 +1,4 @@
<div class="list__content" style="height: 100%;">
<div class="list__content js-component-tree" style="height: 100%;">
{{#vertical-collection displayedList estimateHeight=20 as |item i|}}
<div onmouseenter={{action "previewLayer" item}}
onmouseleave={{action "hidePreview"}}>
Expand Down
3 changes: 3 additions & 0 deletions ember_debug/main.js
Expand Up @@ -66,6 +66,9 @@ const EmberDebug = EmberObject.extend({
this.reset();

this.get('adapter').debug('Ember Inspector Active');
this.get('adapter').sendMessage({
type: 'inspectorLoaded'
});
},

destroyContainer() {
Expand Down
4 changes: 3 additions & 1 deletion ember_debug/object-inspector.js
Expand Up @@ -172,7 +172,9 @@ export default EmberObject.extend(PortMixin, {
},
inspectById(message) {
const obj = this.sentObjects[message.objectId];
this.sendObject(obj);
if (obj) {
this.sendObject(obj);
}
},
inspectByContainerLookup(message) {
const container = this.get('namespace.owner');
Expand Down
24 changes: 24 additions & 0 deletions ember_debug/view-debug.js
Expand Up @@ -111,6 +111,9 @@ export default EmberObject.extend(PortMixin, {
if (model) {
this.get('objectInspector').sendValueToConsole(model);
}
},
contextMenu() {
this.inspectComponentForNode(this.lastClickedElement);
}
},

Expand All @@ -131,6 +134,14 @@ export default EmberObject.extend(PortMixin, {
previewDiv.setAttribute('data-label', 'preview-div');
document.body.appendChild(previewDiv);

// Store last clicked element for context menu
this.lastClickedHandler = (event) => {
if (event.button === 2) {
this.lastClickedElement = event.target;
}
};
window.addEventListener('mousedown', this.lastClickedHandler);

this.resizeHandler = () => {
if (this.glimmerTree) {
this.hideLayer();
Expand All @@ -155,6 +166,18 @@ export default EmberObject.extend(PortMixin, {
}
},

inspectComponentForNode(domNode) {
let viewElem = this.findNearestView(domNode);
if (!viewElem) {
this.get('adapter').log('No Ember component found.');
return;
}

this.sendMessage('inspectComponent', {
viewId: viewElem.id
});
},

updateDurations(durations) {
for (let guid in durations) {
if (!durations.hasOwnProperty(guid)) {
Expand Down Expand Up @@ -187,6 +210,7 @@ export default EmberObject.extend(PortMixin, {
willDestroy() {
this._super();
window.removeEventListener('resize', this.resizeHandler);
window.removeEventListener('mousedown', this.lastClickedHandler);
document.body.removeChild(layerDiv);
document.body.removeChild(previewDiv);
this.get('_lastNodes').clear();
Expand Down
65 changes: 54 additions & 11 deletions skeletons/web-extension/background-script.js
Expand Up @@ -16,6 +16,8 @@
"use strict";

var activeTabs = {},
activeTabId,
contextMenuAdded = false,
emberInspectorChromePorts = {};

/**
Expand All @@ -33,7 +35,7 @@
* Creates the title for the pageAction for the current ClientApp
* @param {Number} tabId - the current tab
*/
function setActionTitle(tabId){
function setActionTitle(tabId) {
chrome.pageAction.setTitle({
tabId: tabId,
title: generateVersionsTooltip(activeTabs[tabId])
Expand All @@ -46,7 +48,7 @@
* is updated to display the ClientApp's information in the tooltip.
* @param {Number} tabId - the current tab
*/
function updateTabAction(tabId){
function updateTabAction(tabId) {
chrome.storage.sync.get("options", function(data) {
if (!data.options || !data.options.showTomster) { return; }
chrome.pageAction.show(tabId);
Expand All @@ -59,11 +61,44 @@
* Typically used to clearout the icon after reload.
* @param {Number} tabId - the current tab
*/
function hideAction(tabId){
delete activeTabs[tabId];
function hideAction(tabId) {
if (!activeTabs[tabId]) {
return;
}

chrome.pageAction.hide(tabId);
}

/**
* Update the tab's contextMenu: https://developer.chrome.com/extensions/contextMenus
* Add a menu item called "Inspect Ember Component" that shows info
* about the component in the inspector.
* @param {Boolean} force don't use the activeTabs array to check for an existing context menu
*/
function updateContextMenu(force) {
// Only add context menu item when an Ember app has been detected
var isEmberApp = !!activeTabs[activeTabId] || force;
if (!isEmberApp && contextMenuAdded) {
chrome.contextMenus.remove('inspect-ember-component');
contextMenuAdded = false;
}

if (isEmberApp && !contextMenuAdded) {
chrome.contextMenus.create({
id: 'inspect-ember-component',
title: 'Inspect Ember Component',
contexts: ['all'],
onclick: function() {
chrome.tabs.sendMessage(activeTabId, {
from: 'devtools',
type: 'view:contextMenu'
});
}
});
contextMenuAdded = true;
}
}

/**
* Listen for a connection request from the EmberInspector.
* When the EmberInspector connects to the extension a messageListener
Expand Down Expand Up @@ -113,29 +148,37 @@
} else if (request && request.type === 'emberVersion') {
// set the version info and update title
activeTabs[sender.tab.id] = request.versions;

updateTabAction(sender.tab.id);
updateContextMenu();
} else if (request && request.type === 'resetEmberIcon') {
// hide the Tomster icon
hideAction(sender.tab.id);
} else if (request && request.type === 'inspectorLoaded') {
updateContextMenu(true);
} else {
// forward the message to EmberInspector
var emberInspectorChromePort = emberInspectorChromePorts[sender.tab.id];
if (emberInspectorChromePort) { emberInspectorChromePort.postMessage(request); }
}
});



/**
* Event listener for when the tab is updated, usually reloaded.
* Check to see if a ClientApp exists for this tab, and reset the icon
* to show the latest data.
* @param {Number} tabId - the current tab
* Keep track of which browser tab is active and update the context menu.
*/
chrome.tabs.onUpdated.addListener(function(tabId) {
chrome.tabs.onActivated.addListener(({ tabId }) => {
activeTabId = tabId;
if (activeTabs[tabId]) {
updateTabAction(tabId);
}
updateContextMenu();
});

/**
* Only keep track of active tabs
*/
chrome.tabs.onRemoved.addListener(({ tabId }) => {
delete activeTabs[tabId];
});

}());
3 changes: 2 additions & 1 deletion skeletons/web-extension/manifest.json
Expand Up @@ -13,7 +13,8 @@

"permissions": [
"<all_urls>",
"storage"
"storage",
"contextMenus"
],

"content_security_policy": "script-src 'self'; object-src 'self'",
Expand Down

0 comments on commit fe59fc0

Please sign in to comment.