refactor(*): basically change everything

🍕
test-unit-sauce
Brian Ford 10 years ago
parent 706a63cc51
commit b40377531e

@ -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;
}

@ -1,5 +1,5 @@
<html>
<body>
<script src="js/devtoolsBackground.js"></script>
<script src="devtoolsBackground.js"></script>
</body>
</html>
</html>

@ -3,6 +3,6 @@ var panels = chrome.devtools.panels;
var angularPanel = panels.create(
"AngularJS",
"img/angular.png",
"hint.html"
"panel/app.html"
);

@ -1,87 +0,0 @@
<!doctype html>
<html ng-csp ng-app="ngHintUI">
<head>
<link rel="stylesheet" href="bower_components/angular/angular-csp.css">
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="app.css">
</head>
<body>
<div ng-controller="HintController">
<div class="row" style="padding:10px">
<div class="col-md-12">
<ul class="nav nav-tabs">
<li ng-class="{active: module === modName}" ng-repeat="(modName, value) in messageData" ng-click="setModule(modName)">
<a data-toggle="tab">
{{modName}}
</a>
</li>
</ul>
<div class="row" style="padding: 5px 0px">
<div class="col-md-2">
<!--
<ul class="nav nav-pills nav-stacked">
<li ng-class="{active: type === label}" ng-repeat="label in labels" ng-click="setType(label)">
<a class="offsetTab">{{label}}</a>
</li>
</ul>
-->
<!--
<div ng-show="suppressedMessagesLength">
<hr>
<h5 class="offsetTab">Suppressed Errors:</h5>
<ul class="nav nav-pills nav-stacked">
<li ng-repeat="(key, message) in suppressedMessages">
<div class="suppressedMessage">
<div class='alert alert-warning condenseAlert'>
{{message}}
<button type="button" class="close" style="margin-top:-5px" ng-click="unsuppressMessage(key)">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
</div>
</div>
</li>
</ul>
</div>
-->
</div>
<div class="col-md-10">
<div class="input-group">
<div class="input-group-addon"><span class="glyphicon glyphicon-search"></span></div>
<input class="form-control" type="text" placeholder="Search Messages" ng-model="search">
</div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>No.</th>
<th>Module</th>
<th>Message</th>
<th>Severity</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="hint in hints">
<td>{{$index + 1}}</td>
<td>{{hint.module}}</td>
<td>{{hint.message}}</td>
<td>{{hint.severity}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- scripts -->
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="hintApp.js"></script>
</body>
</html>

@ -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);
}

@ -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);
});
}

@ -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);
});
});

@ -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);
});
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

@ -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);
});

@ -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'],
});
};
};

@ -21,7 +21,7 @@
]
},
"web_accessible_resources": [
"dist/hint.js"
"dist/hint.js"
],
"minimum_chrome_version": "21.0.1180.57"
}
}

@ -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": {

@ -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;
}

@ -0,0 +1,32 @@
<!doctype html>
<html ng-csp ng-app="batarang.app">
<head>
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="app.css">
<link rel="stylesheet" href="components/json-tree/json-tree.css">
<link rel="stylesheet" href="components/scope-tree/scope-tree.css">
<script src="../bower_components/angular/angular.js"></script>
<!-- components -->
<script src="components/inspected-app/inspected-app.js"></script>
<script src="components/code/code.js"></script>
<script src="components/json-tree/json-tree.js"></script>
<script src="components/scope-tree/scope-tree.js"></script>
<script src="components/tabs/tabs.js"></script>
<script src="components/vertical-split/vertical-split.js"></script>
<!-- panes -->
<script src="hints/hints.js"></script>
<script src="scopes/scopes.js"></script>
<script src="app.js"></script>
</head>
<body>
<bat-tabs>
<bat-pane title="Scopes" src="scopes/scopes.html"></bat-pane>
<bat-pane title="Hints" src="hints/hints.html"></bat-pane>
</bat-tabs>
</body>
</html>

@ -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]);

@ -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 ['<code>', contents, '</code>'].join('');
});
}

@ -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);
}
}
}

@ -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')
};
}

@ -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;
}

@ -0,0 +1,129 @@
angular.module('batarang.json-tree', []).
directive('batJsonTree', ['$compile', 'inspectedApp', batJsonTreeDirective]);
var BAT_JSON_TREE_TEMPLATE = '<div class="properties-tree"></div>';
/*
* 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('<li title>' +
'<span class="name">' + key + '</span>' +
'<span class="separator">: </span>' +
'</li>'),
childElt;
if (val === null) {
childElt = angular.element('<span class="value console-formatted-null">null</span>');
} 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('<span class="console-formatted-object">Object</span>');
} else {
childElt = angular.element(
'<span class="console-formatted-object">Array[' +
val['~array-length'] +
']</span>');
}
} else {
// TODO: what doe sregex look like?
if (typeof val === 'string') {
val = '"' + val + '"';
}
childElt = angular.element(
'<span class="console-formatted-' + (typeof val) + '">' +
val +
'</span>');
}
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;
}
}

@ -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;
}
*/

@ -0,0 +1,102 @@
angular.module('batarang.scope-tree', []).
directive('batScopeTree', batScopeTreeDirective);
// TODO: tabindex
function newBranchElement(descriptor) {
return angular.element([
'<ol class="children expanded">',
'<div class="selection"></div>',
'<span>',
'<span class="webkit-html-tag">&lt;</span>',
'<span class="webkit-html-attribute">Scope #', descriptor, '</span>',
'<span class="webkit-html-tag">&gt;</span>',
'</span>',
'</ol>'].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);
}

@ -0,0 +1,39 @@
<div class="split-view split-view-vertical visible">
<div
class="split-view-contents
scroll-target
split-view-contents-first
split-view-sidebar
sidebar"
style="width: 200px;">
<ol class="sidebar-tree" tabindex="0">
<li ng-repeat="pane in panes"
ng-click="select(pane)"
class="sidebar-tree-item
profile-launcher-view-tree-item"
ng-class="{selected:pane.selected}">
<img class="icon">
<div class="status"></div>
<div class="titles no-subtitle">
<span class="title">{{pane.title}}</span>
<span class="subtitle"></span>
</div>
</li>
</ol>
</div>
<div
class="split-view-contents
scroll-target
split-view-contents-second
outline-disclosure
bat-tabs-inside"
style="left: 200px;">
</div>
<div ng-transclude></div>
</div>

@ -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
});
}
};
});

@ -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('<div class="split-view-resizer" style="right: ' + defaultSplit + 'px; margin-right: -2.5px;"></div>');
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');
}
};
});

@ -0,0 +1,24 @@
<div class="sidebar-pane-stack audit-result-view fill visible" ng-controller="HintController">
<div class="sidebar-pane-title expanded"
ng-repeat="(groupName, group) in groupedHints">{{groupName}}
<div class="sidebar-pane-toolbar"></div>
<div class="sidebar-pane visible" ng-repeat="(summary, hints) in group">
<div class="body audit-result-tree">
<ol>
<li class="parent audit-result">
<div class="severity-warning"></div>{{summary}} ({{hints.length}})
</li>
<li class="parent-expanded expanded selected">
Controller names should start with an uppercase character and end with the suffix <code>Controller</code>. For example: <code>UserController</code>.
</li>
<ol class="children expanded">
<li class="selected" ng-repeat="hint in hints" bat-code="hint.message"></li>
</ol>
</ol>
</div>
</div>
</div>
</div>

@ -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);
});
});
}

@ -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;
}

@ -0,0 +1,39 @@
<div bat-vertical-split ng-controller="ScopesController">
<div bat-vertical-left class="source-code">
<bat-scope-tree bat-model="scopes"></bat-scope-tree>
</div>
<div bat-vertical-right>
<div class="sidebar-pane-stack fill visible">
<div class="sidebar-pane-title"
ng-class="{expanded: modelsExpanded}"
ng-click="modelsExpanded = !modelsExpanded">Models</div>
<div class="sidebar-pane visible" ng-if="modelsExpanded">
<div class="body">
<div class="section expanded">
<bat-json-tree val="scopes[inspectedScope].models" scope-id="inspectedScope"></bat-json-tree>
</div>
</div>
</div>
<div class="sidebar-pane-title"
ng-class="{expanded: watchExpanded}"
ng-click="watchExpanded = !watchExpanded">Watchers</div>
<div class="sidebar-pane visible" ng-if="watchExpanded">
<div class="body">
<div class="section expanded">
<div class="watcher-list">
<li ng-repeat="watcher in watchers">{{watcher}}</li>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

@ -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;
};
}