From b40377531e385ece23280917a99bfc509bd00472 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Wed, 22 Oct 2014 07:43:29 -0700 Subject: [PATCH] refactor(*): basically change everything MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🍕 --- app.css | 17 - devtoolsBackground.html | 4 +- ...oolsBackground.js => devtoolsBackground.js | 2 +- hint.html | 87 ----- hint.js | 30 +- hintApp.js | 49 --- hintService_test.js | 49 --- hint_test.js | 220 ------------- img/angular.png | Bin 0 -> 7401 bytes img/statusbarButtonGlyphs.png | Bin 0 -> 6313 bytes img/statusbarButtonGlyphs_2x.png | Bin 0 -> 14957 bytes img/webstore-icon.png | Bin 0 -> 5435 bytes inject.js | 4 +- karma.conf.js | 14 +- manifest.json | 4 +- package.json | 2 + panel/app.css | 305 ++++++++++++++++++ panel/app.html | 32 ++ panel/app.js | 16 + panel/components/code/code.js | 28 ++ .../components/inspected-app/inspected-app.js | 75 +++++ .../inspected-app/inspected-app.spec.js | 54 ++++ panel/components/json-tree/json-tree.css | 75 +++++ panel/components/json-tree/json-tree.js | 129 ++++++++ panel/components/scope-tree/scope-tree.css | 127 ++++++++ panel/components/scope-tree/scope-tree.js | 102 ++++++ panel/components/tabs/tabs.html | 39 +++ panel/components/tabs/tabs.js | 70 ++++ .../vertical-split/vertical-split.js | 87 +++++ panel/hints/hints.html | 24 ++ panel/hints/hints.js | 24 ++ panel/reset.css | 26 ++ panel/scopes/scopes.html | 39 +++ panel/scopes/scopes.js | 22 ++ 34 files changed, 1313 insertions(+), 443 deletions(-) delete mode 100644 app.css rename js/devtoolsBackground.js => devtoolsBackground.js (85%) delete mode 100644 hint.html delete mode 100644 hintApp.js delete mode 100644 hintService_test.js delete mode 100644 hint_test.js create mode 100644 img/angular.png create mode 100644 img/statusbarButtonGlyphs.png create mode 100644 img/statusbarButtonGlyphs_2x.png create mode 100644 img/webstore-icon.png create mode 100644 panel/app.css create mode 100644 panel/app.html create mode 100644 panel/app.js create mode 100644 panel/components/code/code.js create mode 100644 panel/components/inspected-app/inspected-app.js create mode 100644 panel/components/inspected-app/inspected-app.spec.js create mode 100644 panel/components/json-tree/json-tree.css create mode 100644 panel/components/json-tree/json-tree.js create mode 100644 panel/components/scope-tree/scope-tree.css create mode 100644 panel/components/scope-tree/scope-tree.js create mode 100644 panel/components/tabs/tabs.html create mode 100644 panel/components/tabs/tabs.js create mode 100644 panel/components/vertical-split/vertical-split.js create mode 100644 panel/hints/hints.html create mode 100644 panel/hints/hints.js create mode 100644 panel/reset.css create mode 100644 panel/scopes/scopes.html create mode 100644 panel/scopes/scopes.js diff --git a/app.css b/app.css deleted file mode 100644 index f52ae07..0000000 --- a/app.css +++ /dev/null @@ -1,17 +0,0 @@ -.offsetTab { - margin-left: 15px; -} -.table-hover tr:hover td, -.table-hover tr:hover th { - background-color: #F3F3F3; -} - -.suppressedMessage { - margin-left: 15px; - font-size: 10px; -} - -.condenseAlert { - padding: 4px 8px; - margin: 4px 0px; -} diff --git a/devtoolsBackground.html b/devtoolsBackground.html index cfcae41..d0c006f 100644 --- a/devtoolsBackground.html +++ b/devtoolsBackground.html @@ -1,5 +1,5 @@ - + - \ No newline at end of file + diff --git a/js/devtoolsBackground.js b/devtoolsBackground.js similarity index 85% rename from js/devtoolsBackground.js rename to devtoolsBackground.js index e421816..05d49cf 100644 --- a/js/devtoolsBackground.js +++ b/devtoolsBackground.js @@ -3,6 +3,6 @@ var panels = chrome.devtools.panels; var angularPanel = panels.create( "AngularJS", "img/angular.png", - "hint.html" + "panel/app.html" ); diff --git a/hint.html b/hint.html deleted file mode 100644 index f3e5bc1..0000000 --- a/hint.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - -
-
-
- -
-
- - -
-
-
-
- -
- - - - - - - - - - - - - - - - - -
No.ModuleMessageSeverity
{{$index + 1}}{{hint.module}}{{hint.message}}{{hint.severity}}
-
-
-
-
-
- - - - - - - - - - diff --git a/hint.js b/hint.js index 86b34b7..ece3d1a 100644 --- a/hint.js +++ b/hint.js @@ -1,11 +1,10 @@ +/* + * This gets loaded into the context of the app you are inspecting + */ require('./loader.js'); require('angular-hint'); -var eventProxyElement = document.getElementById('__ngDebugElement'); -var customEvent = document.createEvent('Event'); -customEvent.initEvent('myCustomEvent', true, true); - -angular.hint.onMessage = function (moduleName, message, messageType) { +angular.hint.onMessage = function (moduleName, message, messageType, category) { if (!message) { message = moduleName; moduleName = 'Unknown' @@ -13,10 +12,25 @@ angular.hint.onMessage = function (moduleName, message, messageType) { if (typeof messageType === 'undefined') { messageType = 1; } - eventProxyElement.innerText = JSON.stringify({ + sendMessage({ module: moduleName, message: message, - severity: messageType + severity: messageType, + category: category }); - eventProxyElement.dispatchEvent(customEvent); }; + +angular.hint.emit = function (ev, data) { + data.event = ev; + sendMessage(data); +}; + +var eventProxyElement = document.getElementById('__ngBatarangElement'); + +var customEvent = document.createEvent('Event'); +customEvent.initEvent('batarangDataEvent', true, true); + +function sendMessage (obj) { + eventProxyElement.innerText = JSON.stringify(obj); + eventProxyElement.dispatchEvent(customEvent); +} diff --git a/hintApp.js b/hintApp.js deleted file mode 100644 index 23ea78b..0000000 --- a/hintApp.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -angular.module('ngHintUI', []). - controller('HintController', ['$scope', 'hintService', HintController]). - service('hintService', ['$rootScope', hintService]); - -function HintController($scope, hintService) { - resetMessageData(); - - hintService.onRefresh(resetMessageData); - - function resetMessageData() { - $scope.hints = []; - } - - hintService.onHint(function(hint) { - $scope.hints.push(hint); - }); -} - -function hintService($rootScope) { - var onHintCallback, - onRefreshCallback; - - this.onHint = function(cb) { - onHintCallback = cb; - }; - - this.onRefresh = function(cb) { - onRefreshCallback = cb; - }; - - var port = chrome.extension.connect(); - port.postMessage(chrome.devtools.inspectedWindow.tabId); - port.onMessage.addListener(function(msg) { - $rootScope.$apply(function () { - if (msg === 'refresh') { - onRefreshCallback(); - } else { - var hint = JSON.parse(msg); - onHintCallback(hint); - } - }); - }); - - port.onDisconnect.addListener(function (a) { - console.log(a); - }); -} diff --git a/hintService_test.js b/hintService_test.js deleted file mode 100644 index 0875fc9..0000000 --- a/hintService_test.js +++ /dev/null @@ -1,49 +0,0 @@ -describe('hintService', function() { - var hintService; - - beforeEach(module('ngHintUI')); - beforeEach(inject(function(_hintService_) { - hintService = _hintService_; - })); - - var messageFunction = { - addListener: jasmine.createSpy('messageFunction') - } - var postMessageFunction = jasmine.createSpy('postMessageFunction'); - var onDisconnectFunction = { - addListener: jasmine.createSpy('onDisconnect') - } - chrome = { - extension: { - connect: function() { - return { - onMessage: messageFunction, - postMessage: postMessageFunction, - onDisconnect: onDisconnectFunction - }; - } - }, - devtools: { - inspectedWindow: { - tabId: 1 - } - } - }; - - it('should set the function to be executed for each hint', function() { - var onHintFunction = function() { - console.log('Do this when passed a hint.'); - }; - hintService.setHintFunction(onHintFunction); - expect(hintService.getHintFunction()).toEqual(onHintFunction); - }); - - - it('should set the function to be executed on a refresh', function() { - var onRefreshFunction = function() { - console.log('Do this when the page is refreshed.'); - }; - hintService.setRefreshFunction(onRefreshFunction); - expect(hintService.getRefreshFunction()).toEqual(onRefreshFunction); - }); -}); diff --git a/hint_test.js b/hint_test.js deleted file mode 100644 index f8cf32b..0000000 --- a/hint_test.js +++ /dev/null @@ -1,220 +0,0 @@ -describe('angularHint', function() { - var $controller, $rootScope, hintService; - - beforeEach(module('ngHintUI')); - beforeEach(inject(function(_$controller_, _$rootScope_) { - $controller = _$controller_; - $rootScope = _$rootScope_; - })); - beforeEach(function() { - hintService = { - setHintFunction: function(funct) { - this.onHint = funct; - }, - setRefreshFunction: function(funct) { - this.onRefresh = funct; - } - } - }); - - describe('on receiving a hint', function() { - it('should give the hintService onHint a helpful function to format messages', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - expect(hintService.onHint.toString().indexOf("var result = msg.split('##')")).not.toEqual(-1); - }); - - - it('should create message data arrays for each module', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - var aFakeMessage = 'Directives##You spelled ng-repeat wrong!##Error Messages'; - var aFakeMessage2 = 'Modules##You did not load a module!##Error Messages'; - hintService.onHint(aFakeMessage); - hintService.onHint(aFakeMessage2); - var expectedMessageData = { - 'Error Messages': ['You spelled ng-repeat wrong!'], - 'Warning Messages': [], - 'Suggestion Messages': [], - 'All Messages': [{ - 'message': 'You spelled ng-repeat wrong!', - 'type': 'Error Messages' - }] - }; - var expectedMessageData2 = { - 'Error Messages': ['You did not load a module!'], - 'Warning Messages': [], - 'Suggestion Messages': [], - 'All Messages': [{ - 'message': 'You did not load a module!', - 'type': 'Error Messages' - }] - }; - expect(scope.messageData['Directives']).toEqual(expectedMessageData); - expect(scope.messageData['Modules']).toEqual(expectedMessageData2); - }); - - - it('should create message data arrays for each type of message', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - var aFakeErrorMessage = 'Modules##You did not load a module!##Error Messages'; - var aFakeWarningMessage = 'Modules##You used a bad module name!##Warning Messages'; - var aFakeSuggestionMessage = 'Modules##Maybe you should not make modules.##Suggestion Messages'; - hintService.onHint(aFakeErrorMessage); - hintService.onHint(aFakeWarningMessage); - hintService.onHint(aFakeSuggestionMessage); - - var expectedMessageData = { - 'Error Messages': ['You did not load a module!'], - 'Warning Messages': ['You used a bad module name!'], - 'Suggestion Messages': ['Maybe you should not make modules.'], - 'All Messages': [ - {'message': 'You did not load a module!', 'type': 'Error Messages'}, - {'message': 'You used a bad module name!', 'type': 'Warning Messages'}, - {'message': 'Maybe you should not make modules.', 'type': 'Suggestion Messages'} - ] - }; - expect(scope.messageData['Modules']).toEqual(expectedMessageData); - }); - - - it('should create an object to hold all recorded messages', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - var aFakeErrorMessage = 'Modules##You did not load a module!##Error Messages'; - var aFakeWarningMessage = 'Directives##Did you know you wrote ng-reepeet?##Warning Messages'; - var aFakeSuggestionMessage = 'Controllers##Maybe you should use a better name.##Suggestion Messages'; - hintService.onHint(aFakeErrorMessage); - hintService.onHint(aFakeWarningMessage); - hintService.onHint(aFakeSuggestionMessage); - - var expectedAllMessagesObject = { - 'Error Messages': ['You did not load a module!'], - 'Warning Messages': ['Did you know you wrote ng-reepeet?'], - 'Suggestion Messages': ['Maybe you should use a better name.'], - 'All Messages': [ - {'message': 'You did not load a module!', 'type': 'Error Messages', 'module': 'Modules'}, - {'message': 'Did you know you wrote ng-reepeet?', 'type': 'Warning Messages', 'module': 'Directives'}, - {'message': 'Maybe you should use a better name.', 'type': 'Suggestion Messages', 'module': 'Controllers'} - ] - }; - expect(scope.messageData['All']).toEqual(expectedAllMessagesObject); - }); - }); - - describe('onRefresh', function() { - it('should use the hintService to clear the old messages on refresh', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - var aFakeErrorMessage = 'Modules##You did not load a module!##Error Messages'; - var aFakeWarningMessage = 'Directives##Did you know you wrote ng-reepeet?##Warning Messages'; - var aFakeSuggestionMessage = 'Controllers##Maybe you should use a better name.##Suggestion Messages'; - hintService.onHint(aFakeErrorMessage); - hintService.onHint(aFakeWarningMessage); - hintService.onHint(aFakeSuggestionMessage); - - var expectedAllMessagesObject = { - 'Error Messages': ['You did not load a module!'], - 'Warning Messages': ['Did you know you wrote ng-reepeet?'], - 'Suggestion Messages': ['Maybe you should use a better name.'], - 'All Messages': [ - {'message': 'You did not load a module!', 'type': 'Error Messages', 'module': 'Modules'}, - {'message': 'Did you know you wrote ng-reepeet?', 'type': 'Warning Messages', 'module': 'Directives'}, - {'message': 'Maybe you should use a better name.', 'type': 'Suggestion Messages', 'module': 'Controllers'} - ] - }; - - expect(scope.messageData['All']).toEqual(expectedAllMessagesObject); - - hintService.onRefresh(); - var expectedRefreshData = { - 'Error Messages': [], - 'Warning Messages': [], - 'Suggestion Messages': [], - 'All Messages': [] - } - expect(scope.messageData['All']).toEqual(expectedRefreshData); - }); - }); - - describe('setModule', function() { - it('should to set the currently viewed module in the UI', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - scope.setModule('Directives'); - expect(scope.module).toEqual('Directives'); - }); - - - it('should be set to All by default', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - expect(scope.module).toEqual('All'); - }); - }); - - describe('setType', function() { - it('should set the type of message being viewed in the UI', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - scope.setType('Error Messages'); - expect(scope.type).toEqual('Error Messages'); - }); - - - it('should be set to All Messages by default', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope, hintService: hintService}); - expect(scope.type).toEqual('All Messages'); - }); - }); - - //TO DO CARLOS WHO WROTE THESE METHODS - describe('message suppression', function() { - describe('suppressMessage', function() { - it('should put a message into the list of suppressedMessages', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope}); - scope.suppressMessage('an error message that will be suppressed and hid from display and yay'); - expect(scope.suppressedMessages['suppressedandhid']).toEqual('...error message that will be suppressed and hid from display...'); - }); - it('should increment suppressedMessagesLength', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope}); - scope.suppressMessage('an error message that should increment the counter of messages and yay'); - expect(scope.suppressedMessagesLength).toBe(1); - }); - }); - - describe('unsuppressMessage', function() { - it('should remove a message into the list of suppressedMessages', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope}); - scope.suppressMessage('an error message that will be suppressed and hid from display and yay'); - scope.unsuppressMessage('suppressedandhid'); - expect(scope.suppressedMessages['suppressedandhid']).toBeUndefined(); - }); - it('should decrement suppressedMessagesLength', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope}); - scope.suppressMessage('an error message that should increment the counter of messages'); - scope.unsuppressMessage('suppressedandhid'); - expect(scope.suppressedMessagesLength).toBe(0); - }); - }); - - describe('isSuppressed', function() { - it('should detect if a message is currently suppressed', function() { - var scope = $rootScope.$new(); - var ctrl = $controller('HintController', {$scope: scope}); - scope.suppressMessage('an error message that will be suppressed and hid from display'); - var res = scope.isSuppressed('an error message that will be suppressed and hid from display'); - expect(res).toBe(true); - - res = scope.isSuppressed('this is an error message that has not ever been suppressed'); - expect(res).toBe(false); - }); - }); - }); -}); \ No newline at end of file diff --git a/img/angular.png b/img/angular.png new file mode 100644 index 0000000000000000000000000000000000000000..b1d6e687206f3bf5192054b65fafec7b88c836c3 GIT binary patch literal 7401 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 EKPJ`H+5i9m literal 0 HcmV?d00001 diff --git a/img/statusbarButtonGlyphs.png b/img/statusbarButtonGlyphs.png new file mode 100644 index 0000000000000000000000000000000000000000..5472776255d3289d478ff0880d28c11bc51eceba GIT binary patch literal 6313 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 I^-wYY0|cM-WdHyG literal 0 HcmV?d00001 diff --git a/img/statusbarButtonGlyphs_2x.png b/img/statusbarButtonGlyphs_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ede1c30bd005b8b7de3c640ab25f34eebfd6f8b5 GIT binary patch literal 14957 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 literal 0 HcmV?d00001 diff --git a/img/webstore-icon.png b/img/webstore-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c9849e55866ea6d3120e64039f0772b3d0eed201 GIT binary patch literal 5435 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 SA5)Tm0Ii4m$ZA!ai2no4RR8Dz literal 0 HcmV?d00001 diff --git a/inject.js b/inject.js index 6018517..6d03232 100644 --- a/inject.js +++ b/inject.js @@ -1,7 +1,7 @@ var html = document.getElementsByTagName('html')[0]; var eventProxyElement = document.createElement('div'); -eventProxyElement.id = '__ngDebugElement'; +eventProxyElement.id = '__ngBatarangElement'; eventProxyElement.style.display = 'none'; html.appendChild(eventProxyElement); @@ -10,7 +10,7 @@ html.appendChild(eventProxyElement); var script = window.document.createElement('script'); script.src = chrome.extension.getURL('dist/hint.js'); -eventProxyElement.addEventListener('myCustomEvent', function () { +eventProxyElement.addEventListener('batarangDataEvent', function () { var eventData = eventProxyElement.innerText; chrome.extension.sendMessage(eventData); }); diff --git a/karma.conf.js b/karma.conf.js index 1c93d0a..2fbbf68 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,14 +1,16 @@ +/* + * This karma conf tests just the panel app + */ + module.exports = function(config) { config.set({ frameworks: ['browserify', 'jasmine'], files: [ 'bower_components/angular/angular.js', 'bower_components/angular-mocks/angular-mocks.js', - 'hint.js', - 'hintApp.js', - 'hintCtrl.js', - 'hintService.js', - '*_test.js' + 'panel/app.js', + 'panel/**/*.js', + 'panel/**/*.spec.js' ], exclude: [], preprocessors: { @@ -16,4 +18,4 @@ module.exports = function(config) { }, browsers: ['Chrome'], }); -}; \ No newline at end of file +}; diff --git a/manifest.json b/manifest.json index 8d492d8..d0041ed 100644 --- a/manifest.json +++ b/manifest.json @@ -21,7 +21,7 @@ ] }, "web_accessible_resources": [ - "dist/hint.js" + "dist/hint.js" ], "minimum_chrome_version": "21.0.1180.57" -} \ No newline at end of file +} diff --git a/package.json b/package.json index f4063ef..91f461d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "dependencies": { "browserify": "^5.9.1", "gulp": "^3.8.7", + "karma-bro": "^0.6.0", + "karma-sauce-launcher": "^0.2.9", "vinyl-source-stream": "^0.1.1" }, "scripts": { diff --git a/panel/app.css b/panel/app.css new file mode 100644 index 0000000..13229c7 --- /dev/null +++ b/panel/app.css @@ -0,0 +1,305 @@ +.col { + float: left; + width: 200px; +} +.col-2 { + float: left; + width: 400px; +} +.scope-branch { + margin-left: 30px; + background-color: rgba(0,0,0,0.06); +} + + +.well-top { + border-radius: 4px 4px 0 0; + margin-bottom: 0; +} +.well-bottom { + border-radius: 0 0 4px 4px; + border-top: none; + background-color: #E0E0E0; +} + +.bat-nav-check { + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; + border-radius: 4px 4px 0 0; + + padding: 8px 12px 8px 12px; + margin-right: 2px; + line-height: 18px; +} +.bat-nav-check input[type="checkbox"] { + margin: 0; +} + +/* Mimic Chrome's Devtools */ +/* split */ + + +.split-view { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; +} + +.outline-disclosure, +.outline-disclosure ol { + list-style-type: none; + -webkit-padding-start: 12px; + margin: 0; +} + +.split-view-vertical > .split-view-contents-first { + left: 0; +} + +.split-view-vertical > .split-view-contents { + top: 0; + bottom: 0; +} + +.split-view-contents { + position: absolute; + overflow: auto; + cursor: default; +} + +.split-view-vertical > .split-view-sidebar.split-view-contents-second:not(.maximized) { + border-left: 1px solid rgb(64%, 64%, 64%); +} + +.sidebar-pane-stack > .sidebar-pane.visible:nth-last-of-type(1) { + border-bottom: 1px solid rgb(189, 189, 189); +} + +.split-view-vertical > .split-view-contents-second { + right: 0; +} + +.split-view-vertical > .split-view-resizer { + position: absolute; + top: 0; + bottom: 0; + width: 5px; + z-index: 1500; + cursor: ew-resize; +} + +.fill { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.sidebar-pane-title { + position: relative; + background: rgb(230, 230, 230); + height: 20px; + padding: 0 5px; + border-top: 1px solid rgb(189, 189, 189); + border-bottom: 1px solid rgb(189, 189, 189); + line-height: 18px; + background-origin: padding; + background-clip: padding; + margin-top: -1px; +} + +.sidebar-pane-title::before { + background-image: url(../img/statusbarButtonGlyphs.png); + background-size: 320px 144px; + background-position: -4px -96px; + opacity: 0.5; + float: left; + width: 11px; + height: 11px; + margin-right: 2px; + content: "a"; + color: transparent; + position: relative; + top: 3px; +} + +.sidebar-pane-title.expanded::before { + background-position: -20px -96px; +} + +.sidebar-pane-toolbar { + line-height: 18px; + left: 0; + right: 4px; + top: 0; + height: 20px; + position: absolute; + pointer-events: none; +} + +.sidebar-pane-toolbar > * { + pointer-events: auto; +} + +.sidebar-pane-subtitle { + position: absolute; + right: 0; +} + +.sidebar-pane-subtitle input, .section > .header input[type=checkbox] { + font-size: inherit; + height: 1em; + width: 1em; + margin-left: 0; + margin-top: 0; + margin-bottom: 0.25em; + vertical-align: bottom; +} + + +/* sidebar tree */ + +.sidebar-tree, +.sidebar-tree .children { + position: relative; + padding: 0; + margin: 0; + list-style: none; +} + +.sidebar-tree-item { + position: relative; + height: 36px; + padding: 0 5px 0 5px; + white-space: nowrap; + overflow-x: hidden; + overflow-y: hidden; + margin-top: 1px; + line-height: 34px; + border-top: 1px solid transparent; +} + +.sidebar-tree-item.selected { + color: white; + border-top: 1px solid rgb(151, 151, 151); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(180, 180, 180)), to(rgb(138, 138, 138))); + text-shadow: rgba(0, 0, 0, 0.33) 1px 1px 0; + background-origin: padding-box; + background-clip: padding-box; +} + +body:focus .sidebar-tree-item.selected { + border-top: 1px solid rgb(68, 128, 200); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(92, 147, 213)), to(rgb(21, 83, 170))); +} + +.sidebar-tree-item .icon { + float: left; + width: 32px; + height: 32px; + margin-top: 1px; + margin-right: 3px; +} + +.sidebar-tree-item .titles { + position: relative; + top: 5px; + line-height: 12px; + padding-bottom: 1px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.sidebar-tree-item .titles.no-subtitle { + top: 10px; +} + +.sidebar { + background-color: rgb(232, 232, 232); +} + +.split-view-vertical > .split-view-sidebar.split-view-contents-first:not(.maximized) { + border-right: 1px solid rgb(64%, 64%, 64%); +} + +.profile-launcher-view-tree-item > .icon { + padding: 15px; + background-image: url(../img/toolbarIcons.png); + background-position-x: -160px; +} + +li .status { + float: right; + height: 16px; + margin-top: 9px; + margin-left: 4px; + line-height: 1em; +} + +li .status:empty { + display: none; +} + +/* audit stuff */ + +.audit-result-view .severity-warning { + background-position: -246px -96px; +} + +/*@media (-webkit-min-device-pixel-ratio: 1.5) +.audit-result-view .severity-severe, +.audit-result-view .severity-warning, +.audit-result-view .severity-info { + background-image: url(../img/statusbarButtonGlyphs_2x.png); +}*/ + +.audit-result-tree, +.audit-result-tree ol { + list-style-type: none; + -webkit-padding-start: 12px; + margin: 0; +} +.audit-result-tree { + line-height: 16px; + -webkit-user-select: text; +} + +.audit-result-tree li.parent { + margin-left: -12px; +} + +.audit-result-tree li { + padding: 0 0 0 14px; + margin-top: 1px; + margin-bottom: 1px; + word-wrap: break-word; + margin-left: -2px; +} + +.audit-result { + font-weight: bold; +} + +.audit-result-tree li.parent::before { + background-position: -4px -96px; +} + +.audit-result-view .severity-severe, +.audit-result-view .severity-warning, +.audit-result-view .severity-info { + background-image: url(../img/statusbarButtonGlyphs.png); + background-size: 320px 144px; + display: inline-block; + width: 10px; + margin-right: -10px; + height: 10px; + position: relative; + left: -28px; + margin-top: 3px; +} diff --git a/panel/app.html b/panel/app.html new file mode 100644 index 0000000..69f9faf --- /dev/null +++ b/panel/app.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/panel/app.js b/panel/app.js new file mode 100644 index 0000000..0f39e16 --- /dev/null +++ b/panel/app.js @@ -0,0 +1,16 @@ +'use strict'; + +angular.module('batarang.app', [ + 'batarang.app.hint', + 'batarang.app.scopes', + + 'batarang.scope-tree', + 'batarang.code', + 'batarang.inspected-app', + 'batarang.json-tree', + 'batarang.scope-tree', + 'batarang.tabs', + 'batarang.vertical-split' +]). +// immediately instantiate this service +run(['inspectedApp', angular.noop]); diff --git a/panel/components/code/code.js b/panel/components/code/code.js new file mode 100644 index 0000000..dcc0512 --- /dev/null +++ b/panel/components/code/code.js @@ -0,0 +1,28 @@ +'use strict'; + +angular.module('batarang.code', []). + +directive('batCode', function() { + return { + restrict: 'A', + terminal: true, + scope: { + batCode: '=' + }, + link: function (scope, element, attrs) { + scope.$watch('batCode', function (newVal) { + if (newVal) { + element.html(replaceCodeInString(newVal)); + } + }); + } + }; +}); + +// super lite version of markdown +var CODE_RE = /\`(.+?)\`/g; +function replaceCodeInString(str) { + return str.replace(CODE_RE, function (match, contents) { + return ['', contents, ''].join(''); + }); +} diff --git a/panel/components/inspected-app/inspected-app.js b/panel/components/inspected-app/inspected-app.js new file mode 100644 index 0000000..3347da5 --- /dev/null +++ b/panel/components/inspected-app/inspected-app.js @@ -0,0 +1,75 @@ +'use strict'; + +angular.module('batarang.inspected-app', []). + service('inspectedApp', ['$rootScope', inspectedAppService]); + +function inspectedAppService($rootScope) { + + // TODO: maybe state should live elsewhere + var scopes = this.scopes = {}, + hints = this.hints = []; + + this.watch = function (scopeId, path) { + return invokeAngularHintMethod('watch', scopeId, path); + }; + + this.unwatch = function (scopeId, path) { + return invokeAngularHintMethod('unwatch', scopeId, path); + }; + + function invokeAngularHintMethod(method, scopeId, path) { + var args = [parseInt(scopeId, 10), path || ''].map(JSON.stringify).join(','); + chrome.devtools.inspectedWindow.eval('angular.hint.' + method + '(' + args + ')'); + } + + var port = chrome.extension.connect(); + port.postMessage(chrome.devtools.inspectedWindow.tabId); + port.onMessage.addListener(function(msg) { + $rootScope.$applyAsync(function () { + if (msg === 'refresh') { + onRefreshMessage(); + } else { + var hint = JSON.parse(msg); + onHintMessage(hint); + } + }); + }); + port.onDisconnect.addListener(function (a) { + console.log(a); + }); + + function onHintMessage(hint) { + if (hint.message) { + hints.push(hint); + } else if (hint.event === 'model:change') { + scopes[hint.id].models[hint.path] = (typeof hint.value === 'undefined') ? + undefined : JSON.parse(hint.value); + } else if (hint.event === 'scope:new') { + addNewScope(hint); + } else if (hint.event === 'scope:link') { + scopes[hint.id].descriptor = hint.descriptor; + } + + if (hint.event) { + $rootScope.$broadcast(hint.event, hint); + } + + console.log(hint); + } + + function onRefreshMessage() { + hints.length = 0; + } + + function addNewScope (hint) { + scopes[hint.child] = { + parent: hint.parent, + children: [], + models: {} + }; + if (scopes[hint.parent]) { + scopes[hint.parent].children.push(hint.child); + } + } + +} diff --git a/panel/components/inspected-app/inspected-app.spec.js b/panel/components/inspected-app/inspected-app.spec.js new file mode 100644 index 0000000..83db298 --- /dev/null +++ b/panel/components/inspected-app/inspected-app.spec.js @@ -0,0 +1,54 @@ +describe('inspectedApp', function() { + var inspectedApp; + + beforeEach(module('batarang.inspected-app')); + beforeEach(function() { + window.chrome = createMockChrome(); + }); + beforeEach(inject(function(_inspectedApp_) { + inspectedApp = _inspectedApp_; + })); + + describe('watch', function () { + it('should call chrome devtools APIs', function() { + inspectedApp.watch(1, ''); + expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalledWith('angular.hint.watch(1,"")'); + }); + }); + + describe('unwatch', function () { + it('should call chrome devtools APIs', function() { + inspectedApp.unwatch(1, ''); + expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalledWith('angular.hint.unwatch(1,"")'); + }); + }); + +}); + +function createMockChrome() { + return { + extension: { + connect: createMockSocket + }, + devtools: { + inspectedWindow: { + tabId: 1, + eval: jasmine.createSpy('inspectedWindowEval') + } + } + }; +} + +function createListenerSpy(name) { + return { + addListener: jasmine.createSpy(name) + }; +} + +function createMockSocket() { + return { + onMessage: createListenerSpy('messageFunction'), + postMessage: jasmine.createSpy('postMessageFunction'), + onDisconnect: createListenerSpy('onDisconnect') + }; +} \ No newline at end of file diff --git a/panel/components/json-tree/json-tree.css b/panel/components/json-tree/json-tree.css new file mode 100644 index 0000000..232dbab --- /dev/null +++ b/panel/components/json-tree/json-tree.css @@ -0,0 +1,75 @@ + + +/* bat-json-tree */ + +bat-json-tree { + font-size: 11px !important; + font-family: Menlo, monospace; + display: block; + margin-left: 5px; +} + +bat-json-tree .name { + color: rgb(136, 19, 145); +} + +bat-json-tree .console-formatted-string { + color: rgb(196, 26, 22); +} + +bat-json-tree + .console-formatted-null, + .console-formatted-undefined { + color: rgb(128, 128, 128); + } + +bat-json-tree .console-formatted-number { + color: rgb(28, 0, 207); +} + +bat-json-tree + .console-formatted-object, + .console-formatted-node, + .console-formatted-array { + color: #222; + } + +bat-json-tree .properties-tree li { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + -webkit-user-select: text; + cursor: default; + padding-top: 2px; + line-height: 12px; + list-style: none; +} + +bat-json-tree .properties-tree li.parent { + margin-left: -15px; +} +bat-json-tree .properties-tree li.parent li { + padding-left: 28px; +} + +bat-json-tree .properties-tree li.parent::before { + -webkit-user-select: none; + background-image: url(../../img/statusbarButtonGlyphs.png); + background-size: 320px 120px; + opacity: 0.5; + content: "a"; + width: 8px; + float: left; + margin-right: 2px; + color: transparent; + text-shadow: none; + margin-top: -2px; +} + +bat-json-tree .properties-tree li.parent::before { + background-position: -4px -96px; +} + +bat-json-tree .properties-tree li.parent.expanded::before { + background-position: -20px -96px; +} diff --git a/panel/components/json-tree/json-tree.js b/panel/components/json-tree/json-tree.js new file mode 100644 index 0000000..7fe0aa8 --- /dev/null +++ b/panel/components/json-tree/json-tree.js @@ -0,0 +1,129 @@ +angular.module('batarang.json-tree', []). + directive('batJsonTree', ['$compile', 'inspectedApp', batJsonTreeDirective]); + +var BAT_JSON_TREE_TEMPLATE = '
'; + +/* + * TODO: remove dependency on inspectedApp service + */ +function batJsonTreeDirective($compile, inspectedApp) { + return { + restrict: 'E', + terminal: true, + scope: { + scopeId: '=', + val: '=' + }, + link: jsonTreeLinkFn + }; + + function jsonTreeLinkFn(scope, element, attrs) { + var root = angular.element(BAT_JSON_TREE_TEMPLATE) + element.append(root); + + var branches = { + '': root + }; + + scope.$watch('val', function (val) { + Object. + keys(val). + filter(function (key) { + return key.substr(0, 2) !== '$$'; + }). + sort(byPathDepth). + forEach(function (key) { + buildDom(val[key], key); + }); + }, true); + + + function buildDom(object, depth) { + branches[depth].html(''); + + if (!typeof object === 'undefined') { + return; + } + + var buildBranch = function (key) { + var val = object[key]; + var fullPath = depth; + if (depth) { + fullPath += '.'; + } + fullPath += key; + + var parentElt = angular.element('
  • ' + + '' + key + '' + + ': ' + + '
  • '), + childElt; + + if (val === null) { + childElt = angular.element('null'); + } else if (val['~object'] || val['~array-length'] !== undefined) { + parentElt.addClass('parent'); + + // you can't expand an empty array + if (val['~array-length'] !== 0) { + parentElt.on('click', function () { + inspectedApp.watch(scope.scopeId, fullPath); + parentElt.addClass('expanded'); + }); + } + + if (val['~object']) { + childElt = angular.element('Object'); + } else { + childElt = angular.element( + 'Array[' + + val['~array-length'] + + ']'); + } + } else { + // TODO: what doe sregex look like? + if (typeof val === 'string') { + val = '"' + val + '"'; + } + childElt = angular.element( + '' + + val + + ''); + } + + parentElt.append(childElt); + branches[fullPath] = childElt; + + return parentElt; + }; + + var properties; + if (object instanceof Array) { + properties = object.map(function (item, i) { + return i; + }); + } else if (object != null) { + properties = Object.keys(object); + } else { + properties = []; + } + + properties. + map(buildBranch). + forEach(function (elt) { + branches[depth].append(elt); + }); + + }; + } +} + +function byPathDepth(a, b) { // sort '' first + if (a === '') { + return -1; + } else if (b === '') { + return 1; + } else { // sort by tree depth + return a.split('.').length - b.split('.').length; + } +} diff --git a/panel/components/scope-tree/scope-tree.css b/panel/components/scope-tree/scope-tree.css new file mode 100644 index 0000000..507a13e --- /dev/null +++ b/panel/components/scope-tree/scope-tree.css @@ -0,0 +1,127 @@ + +/* Stolen from WebKit Inspector CSS */ + +.source-code { + font-size: 11px !important; + font-family: Menlo, monospace; +} + +.source-code li { + display: list-item; + text-align: -webkit-match-parent; +} + +.outline-disclosure, +.outline-disclosure ol { + list-style-type: none; + -webkit-padding-start: 12px; + margin: 0; +} + +.outline-disclosure ol.children.expanded { + display: block; +} + +.outline-disclosure > ol { + position: relative; + padding: 2px 6px !important; + margin: 0; + cursor: default; + min-width: 100%; +} + +.outline-disclosure li { + padding: 0 0 0 14px; + margin-top: 1px; + margin-left: -2px; + word-wrap: break-word; +} + +.outline-disclosure li.selected .selection { + display: block; + background-color: rgb(212, 212, 212); +} + +.elements-tree-outline li.parent::before { + top: 0 !important; +} +.outline-disclosure li.parent::before { + -webkit-user-select: none; + background-image: url(../../img/statusbarButtonGlyphs.png); + background-size: 320px 144px; + opacity: 0.5; + float: left; + width: 8px; + height: 10px; + content: "a"; + color: transparent; + margin-left: 3px; + margin-right: 4px; + position: relative; + top: 2px; +} + +.outline-disclosure li.parent::before { + float: left; + width: 8px; + padding-right: 2px; +} + +.webkit-html-tag { + color: rgb(136, 18, 128); +} + +.webkit-html-attribute-name { + color: rgb(153, 69, 0); +} + +.webkit-html-attribute-value { + color: rgb(26, 26, 166); +} + +.webkit-html-doctype { + color: rgb(192, 192, 192); +} + +.webkit-html-comment { + color: rgb(35, 110, 37); +} + + +.outline-disclosure .selection { + display: none; + position: absolute; + left: 0; + right: 0; + height: 13px; + z-index: -1; +} + +.outline-disclosure .selection:not(.selected):hover { + display: block; + left: 3px; + right: 3px; + background-color: rgba(56, 121, 217, 0.1); + border-radius: 5px; +} + +.outline-disclosure .selection.selected { + display: block; + background-color: rgb(212, 212, 212); +} + + +:focus .sidebar-tree-item.selected { + border-top: 1px solid rgb(68, 128, 200); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(92, 147, 213)), to(rgb(21, 83, 170))); +} + +/* */ + +/* +bat-scope-tree .selected { + font-weight: bold; + text-decoration: underline; + color: #333; +} +*/ diff --git a/panel/components/scope-tree/scope-tree.js b/panel/components/scope-tree/scope-tree.js new file mode 100644 index 0000000..528a48e --- /dev/null +++ b/panel/components/scope-tree/scope-tree.js @@ -0,0 +1,102 @@ +angular.module('batarang.scope-tree', []). + +directive('batScopeTree', batScopeTreeDirective); + +// TODO: tabindex +function newBranchElement(descriptor) { + return angular.element([ + '
      ', + '
      ', + '', + '<', + 'Scope #', descriptor, '', + '>', + '', + '
    '].join('')); +} + +function batScopeTreeDirective($compile) { + return { + restrict: 'E', + terminal: true, + scope: { + batModel: '=' + }, + link: function (scope, element, attrs) { + + // scope.$id –> DOM node + var map = {}; + var selectedElt = angular.element(); + + // init + var scopes = scope.batModel; + if (scopes) { + Object.keys(scopes).forEach(function (scopeId) { + var parentId = scopes[scopeId].parent; + renderScopeElement(scopeId, parentId); + renderScopeDescriptorElement(scopeId, scopes[scopeId].descriptor); + }); + } + + scope.$on('scope:new', function (ev, data) { + renderScopeElement(data.child, data.parent); + }); + + scope.$on('scope:link', function (ev, data) { + renderScopeDescriptorElement(data.id, data.descriptor); + }); + + function renderScopeElement (id, parentId) { + if (map[id]) { + return; + } + var elt = map[id] = newBranchElement(id); + var parentElt = map[parentId] || element; + + elt.children().eq(1).on('click', function () { + scope.$apply(function () { + scope.$emit('inspected-scope:change', { + id: id + }); + selectedElt.children().eq(0).removeClass('selected'); + selectedElt.children().eq(1).removeClass('selected'); + + selectedElt = elt; + + selectedElt.children().eq(0).addClass('selected'); + selectedElt.children().eq(1).addClass('selected'); + }); + }) + + parentElt.append(elt); + } + + function renderScopeDescriptorElement (id, descriptor) { + var elt = map[id]; + if (!elt) { + return; + } + elt.children().eq(1).children().eq(1).html(descriptor); + } + + scope.$on('scope:destroy', function (ev, data) { + var id = data.id; + var elt = map[id]; + if (elt) { + elt.remove(); + } + delete map[id]; + }); + + } + }; +} + +function repeaterPredicate (child) { + return child.name && child.name['ng-repeat']; +} + +function notRepeatedPredicate (child) { + return !repeaterPredicate(child); +} + diff --git a/panel/components/tabs/tabs.html b/panel/components/tabs/tabs.html new file mode 100644 index 0000000..6913866 --- /dev/null +++ b/panel/components/tabs/tabs.html @@ -0,0 +1,39 @@ +
    + + + +
    +
    + +
    + +
    \ No newline at end of file diff --git a/panel/components/tabs/tabs.js b/panel/components/tabs/tabs.js new file mode 100644 index 0000000..326c5ed --- /dev/null +++ b/panel/components/tabs/tabs.js @@ -0,0 +1,70 @@ +angular.module('batarang.tabs', []). + +directive('batTabs', function ($compile, $templateCache, $http) { + return { + restrict: 'E', + transclude: true, + scope: {}, + templateUrl: 'components/tabs/tabs.html', + + replace: true, + controller: function ($scope) { + var panes = $scope.panes = []; + + this.addPane = function(pane) { + panes.push(pane); + }; + }, + link: function (scope, element, attr) { + + var lastScope; + var insideElt = angular.element(element[0].getElementsByClassName('bat-tabs-inside')[0]); + + function destroyLastScope() { + if (lastScope) { + lastScope.$destroy(); + lastScope = null; + } + } + + scope.select = function (pane) { + $http.get(pane.src, { cache: $templateCache }). + then(function (response) { + var template = response.data; + insideElt.html(template); + destroyLastScope(); + + var link = $compile(insideElt.contents()); + lastScope = scope.$new(); + link(lastScope); + }); + + angular.forEach(scope.panes, function(pane) { + pane.selected = false; + }); + pane.selected = true; + scope.currentPane = pane; + }; + + scope.lastPane = scope.panes[0]; + scope.select(scope.panes[scope.panes.length - 1]); + } + + }; +}). +directive('batPane', function() { + return { + require: '^batTabs', + restrict: 'E', + scope: { + title: '@', + src: '@' + }, + link: function (scope, element, attrs, tabsCtrl) { + tabsCtrl.addPane({ + title: attrs.title, + src: attrs.src + }); + } + }; +}); diff --git a/panel/components/vertical-split/vertical-split.js b/panel/components/vertical-split/vertical-split.js new file mode 100644 index 0000000..a1bdc9d --- /dev/null +++ b/panel/components/vertical-split/vertical-split.js @@ -0,0 +1,87 @@ +angular.module('batarang.vertical-split', []). +constant('defaultSplit', 360). +directive('batVerticalSplit', function ($document, defaultSplit) { + + var classes = [ + 'split-view', + 'split-view-vertical', + 'visible' + ]; + + var body = angular.element($document[0].body); + + return { + restrict: 'A', + compile: function (element) { + classes.forEach(element.addClass.bind(element)); + + var children = element.children(); + var left = angular.element(children[0]); + var right = angular.element(children[1]); + + + return function (scope, element, attr) { + var slider = angular.element('
    '); + + var drag = function (ev) { + var x = $document[0].body.clientWidth - ev.x; + left.css('right', x + 'px'); + right.css('width', x + 'px'); + slider.css('right', x + 'px'); + }; + + var oldCursor; + + slider.bind('mousedown', function (ev) { + drag(ev); + oldCursor = body.css('cursor'); + body.css('cursor', 'ew-resize'); + $document.bind('mousemove', drag); + $document.bind('mouseup', stopDrag); + }); + + var stopDrag = function () { + body.css('cursor', oldCursor); + $document.unbind('mousemove', drag); + $document.unbind('mouseup', stopDrag); + }; + + element.append(slider); + }; + } + }; +}). +directive('batVerticalLeft', function (defaultSplit) { + var classes = [ + 'split-view-contents', + 'scroll-target', + 'split-view-contents-first', + 'outline-disclosure' + ]; + + return { + require: '^batVerticalSplit', + restrict: 'A', + compile: function (element) { + classes.forEach(element.addClass.bind(element)); + element.css('right', defaultSplit + 'px'); + } + }; +}). +directive('batVerticalRight', function (defaultSplit) { + var classes = [ + 'split-view-contents', + 'scroll-target', + 'split-view-contents-second', + 'split-view-sidebar' + ]; + + return { + require: '^batVerticalSplit', + restrict: 'A', + compile: function (element) { + classes.forEach(element.addClass.bind(element)); + element.css('width', defaultSplit + 'px'); + } + }; +}); diff --git a/panel/hints/hints.html b/panel/hints/hints.html new file mode 100644 index 0000000..d4e202a --- /dev/null +++ b/panel/hints/hints.html @@ -0,0 +1,24 @@ + diff --git a/panel/hints/hints.js b/panel/hints/hints.js new file mode 100644 index 0000000..5fc6002 --- /dev/null +++ b/panel/hints/hints.js @@ -0,0 +1,24 @@ +'use strict'; + +angular.module('batarang.app.hint', []). + controller('HintController', ['$scope', 'inspectedApp', HintController]); + +function HintController($scope, inspectedApp) { + $scope.$watch(function () { + return inspectedApp.hints.length; + }, function () { + var newHints = inspectedApp.hints; + $scope.groupedHints = {}; + newHints.forEach(function (hint) { + var moduleName = hint.module || 'Hints'; + var category = hint.category || (moduleName + ' Stuff'); + if (!$scope.groupedHints[moduleName]) { + $scope.groupedHints[moduleName] = {}; + } + if (!$scope.groupedHints[moduleName][category]) { + $scope.groupedHints[moduleName][category] = []; + } + $scope.groupedHints[moduleName][category].push(hint); + }); + }); +} diff --git a/panel/reset.css b/panel/reset.css new file mode 100644 index 0000000..8847cfe --- /dev/null +++ b/panel/reset.css @@ -0,0 +1,26 @@ +/* reset */ + +* { + box-sizing: border-box; +} + +/* TODO: defaults for other platforms?? */ +body { + cursor: default; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; + font-size: 12px; + margin: 0; + tab-size: 4; + -webkit-user-select: none; + color: rgb(48, 57, 66); + font-family: 'Lucida Grande', sans-serif; +} + +img { + -webkit-user-drag: none; +} diff --git a/panel/scopes/scopes.html b/panel/scopes/scopes.html new file mode 100644 index 0000000..2680598 --- /dev/null +++ b/panel/scopes/scopes.html @@ -0,0 +1,39 @@ +
    + +
    + +
    + +
    + +
    + +
    diff --git a/panel/scopes/scopes.js b/panel/scopes/scopes.js new file mode 100644 index 0000000..2cd8b66 --- /dev/null +++ b/panel/scopes/scopes.js @@ -0,0 +1,22 @@ +'use strict'; + +angular.module('batarang.app.scopes', []). + controller('ScopesController', ['$scope', 'inspectedApp', ScopesController]); + +function ScopesController($scope, inspectedApp) { + $scope.scopes = inspectedApp.scopes; + + $scope.watch = inspectedApp.watch; + + $scope.inspectedScope = null; + + $scope.$on('inspected-scope:change', function (ev, data) { + inspectScope(data.id); + }); + + function inspectScope(scopeId) { + $scope.watch(scopeId); + $scope.inspectedScope = scopeId; + }; + +}