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 0000000..b1d6e68 Binary files /dev/null and b/img/angular.png differ diff --git a/img/statusbarButtonGlyphs.png b/img/statusbarButtonGlyphs.png new file mode 100644 index 0000000..5472776 Binary files /dev/null and b/img/statusbarButtonGlyphs.png differ diff --git a/img/statusbarButtonGlyphs_2x.png b/img/statusbarButtonGlyphs_2x.png new file mode 100644 index 0000000..ede1c30 Binary files /dev/null and b/img/statusbarButtonGlyphs_2x.png differ diff --git a/img/webstore-icon.png b/img/webstore-icon.png new file mode 100644 index 0000000..c9849e5 Binary files /dev/null and b/img/webstore-icon.png differ 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; + }; + +}