Compare commits

..

No commits in common. 'master' and 'v0.4.2' have entirely different histories.

7
.gitignore vendored

@ -1,8 +1 @@
batarang-release-*.zip
*.build.js
hint.bundle.js
.idea
bower_components
node_modules
coverage/
package/

@ -1,21 +0,0 @@
language: node_js
node_js:
- "0.10"
env:
global:
- BROWSER_PROVIDER_READY_FILE=/tmp/sauce-connect-ready
- LOGS_DIR=/tmp/angular-hint-build/logs
- SAUCE_USERNAME=angular-ci
- SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
install:
- mkdir -p $LOGS_DIR
- ./scripts/sauce_connect_setup.sh
- npm install
- npm install -g gulp
- npm install -g karma-cli
- ./scripts/wait_for_browser_provider.sh
script:
- ./scripts/test_on_sauce.sh

@ -1,36 +0,0 @@
# Contributing
This document describes how to contribute to Batarang
## Installing from Source
1. Clone the repository: `git clone git://github.com/angular/angularjs-batarang`
2. Install the npm dependencies `npm install`
3. Build the inject script: `npm run build`
4. Navigate to `chrome://chrome/extensions/` and enable Developer Mode.
5. Choose "Load unpacked extension"
6. In the dialog, open the directory you just cloned.
## Running the tests
## Packaging a release
I (@btford) will do this periodically, but I'm adding these instructions here
for posterity.
1. Edit the version number in `manifest.json` and `package.json` with the new version.
2. Run `gulp zip`
3. `git add package.json manifest.json dist/`
4. `git commit -m "v1.2.3"`
5. `git tag v1.2.3`
6. `git push upstream master && git push upstream --tags`
7. Upload `batarang-v1.2.3.zip` to Web Store via the [Web Store Dashboard](https://chrome.google.com/webstore/developer/dashboard)
## Layout
The `panel` directory contains...
`panel/components` contains self-contained directives and services.
## Testing Batarang manually

@ -0,0 +1,15 @@
# Batarang FAQ
### How do I measure a directive's performance?
If your directive uses `$watch`, you should be able to see the watch expression wherever your directive is used.
### My $watch functions show up as just "function ()" in the performance tab
Use named functions for $watch:
```javascript
scope.$watch(function checkIfSomethingChanged() {
// ...
}, function whenThatChanges(newValue, oldValue) {
// ...
});
```

@ -0,0 +1,110 @@
var markdown = require('marked'),
semver = require('semver');
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('manifest.json'),
changelog: {
options: {
dest: 'CHANGELOG.md',
versionFile: 'manifest.json'
}
},
bump: {
options: {
file: 'manifest.json'
}
},
markdown: {
all: {
file: 'README.md',
dest: 'panes/help.html'
}
},
release: {
options: {
commitMessage: 'v<%= version %>',
tagName: 'v<%= version %>',
bump: false, // we have out own bump
npm: false,
file: 'manifest.json'
}
},
stage: {
files: ['CHANGELOG.md', 'pane/help.html']
},
zip: {
release: {
src: [
'css/*.css',
'img/**',
'js/**',
'panes/*.html',
'panel.html',
'LICENSE',
'manifest.json',
'background.html',
'devtoolsBackground.html'
],
dest: 'batarang-release-' + Date.now() + '.zip'
}
}
});
grunt.registerTask('bump', 'bump manifest version', function (type) {
var options = this.options({
file: grunt.config('pkgFile') || 'package.json'
});
function setup(file, type){
var pkg = grunt.file.readJSON(file);
var newVersion = pkg.version = semver.inc(pkg.version, type || 'patch');
return {file: file, pkg: pkg, newVersion: newVersion};
}
var config = setup(options.file, type);
grunt.file.write(config.file, JSON.stringify(config.pkg, null, ' ') + '\n');
grunt.log.ok('Version bumped to ' + config.newVersion);
});
grunt.registerMultiTask('markdown', 'compiles markdown README into html for the help pane', function() {
var md = grunt.file.read(this.data.file);
// pull out the install instructions, etc
var marker = '<!-- HELP TAB -->';
md = md.substr(md.indexOf(marker) + marker.length);
// fix image paths
md = md.replace(/https:\/\/github.com\/angular\/angularjs-batarang\/raw\/master\/img\//g, '/img/');
var html = markdown(md);
grunt.file.write(this.data.dest, html);
});
grunt.registerTask('stage', 'git add files before running the release task', function() {
grunt.util.spawn({
cmd: process.platform === 'win32' ?
'git.cmd' : 'git',
args: ['add'].append(this.data.files)
}, grunt.task.current.async());
});
grunt.registerTask('url', 'open the url for the chrome app dashboard', function() {
var url = 'https://chrome.google.com/webstore/developer/dashboard';
console.log('Publish to: ' + url);
grunt.util.spawn({
cmd: process.platform === 'win32' ?
'explorer' : 'open',
args: [ url ]
}, grunt.task.current.async());
});
grunt.loadNpmTasks('grunt-release');
grunt.loadNpmTasks('grunt-zip');
grunt.loadNpmTasks('grunt-conventional-changelog');
grunt.registerTask('default', ['bump', 'markdown', 'changelog', 'release', 'zip']);
};

@ -1,23 +1,61 @@
# AngularJS Batarang
[![Build Status](https://travis-ci.org/angular/angularjs-batarang.svg)](https://travis-ci.org/angular/angularjs-batarang)
## Installing from the Web Store
https://chrome.google.com/webstore/detail/ighdmehidhipcmcojjgiloacoafjmpfk
## Installing Previous Versions
## Installing from Source
1. Download and extract one of the files from the [Batarang releases page on GitHub](https://github.com/angular/angularjs-batarang/releases)
1. Navigate to `chrome://chrome/extensions/` in Chrome
1. If you've installed Batarang from the web store, disable or remove that version
1. On the top right, check the checkbox for "Developer mode"
1. Click "Load unpacked extension..."
1. Select the directory where you extracted the extension
1. Close and re-open any inspected tabs
1. Clone the repository: `git clone git://github.com/angular/angularjs-batarang`
2. Navigate to `chrome://chrome/extensions/` and enable Developer Mode.
3. Choose "Load unpacked extension"
4. Open the directory you just cloned (should open with Chrome, otherwise try dragging/dropping the file into Chrome) and follow the prompts to install.
## Installing from Source
## Screencast
Check out [this screencast](http://www.youtube.com/embed/q-7mhcHXSfM) that walks you through the Batarang's features.
## Using the Batarang
First, navigate Chrome Canary to the AngularJS application that you want to debug. [Open the Developer Tools](https://developers.google.com/chrome-developer-tools/docs/overview#access). There should be an AngularJS icon. Click on it to open the AngularJS Batarang.
<!-- HELP TAB -->
In order to begin using the Batarang you need to click the "enable" checkbox. This will cause the application's tab to refresh, and the Batarang to begin collecting perfomance and debug information about the inspected app.
The Batarang has five tabs: Model, Performance, Dependencies, Options, and Help.
### Models
![Batarang screenshot](https://github.com/angular/angularjs-batarang/raw/master/img/models.png)
Starting at the top of this tab, there is the root selection. If the application has only one `ng-app` declaration (as most applications do) then you will not see the option to change roots.
Below that is a tree showing how scopes are nested, and which models are attached to them. Clicking on a scope name will take you to the Elements tab, and show you the DOM element associated with that scope. Models and methods attached to each scope are listed with bullet points on the tree. Just the name of methods attached to a scope are shown. Models with a simple value and complex objects are shown as JSON. You can edit either, and the changes will be reflected in the application being debugged.
### Performance
![Batarang performance tab screenshot](https://github.com/angular/angularjs-batarang/raw/master/img/perf.png)
The performance tab must be enabled separately because it causes code to be injected into AngularJS to track and report performance metrics. There is also an option to output performance metrics to the console.
Below that is a tree of watched expressions, showing which expressions are attached to which scopes. Much like the model tree, you can collapse sections by clicking on "toggle" and you can inspect the element that a scope is attached to by clicking on the scope name.
Underneath that is a graph showing the relative performance of all of the application's expressions. This graph will update as you interact with the application.
### Dependencies
![Batarang dependencies tab screenshot](https://github.com/angular/angularjs-batarang/raw/master/img/deps.png)
The dependencies tab shows a visualization of the application's dependencies. When you hover over a service name, services that depend on the hovered service turn green, and those the hovered service depend on turn red.
### Options
![Batarang options tab screenshot](https://github.com/angular/angularjs-batarang/raw/master/img/options.png)
Last, there is the options tab. The options tab has three checkboxes: one for "show applications," "show scopes," and "show bindings." Each of these options, when enabled, highlights the respective feature of the application being debugged; scopes will have a red outline, and bindings will have a blue outline, and applications a green outline.
### Elements
![Batarang console screenshot](https://github.com/angular/angularjs-batarang/raw/master/img/inspect.png)
The Batarang also hooks into some of the existing features of the Chrome developer tools. For AngularJS applications, there is now a properties pane on in the Elements tab. Much like the model tree in the AngularJS tab, you can use this to inspect the models attached to a given element's scope.
See the [instructions in the contributing guide](https://github.com/angular/angularjs-batarang/blob/master/CONTRIBUTING.md#installing-from-source)
### Console
![Batarang console screenshot](https://github.com/angular/angularjs-batarang/raw/master/img/console.png)
## License
MIT
The Batarang exposes some convenient features to the Chrome developer tools console. To access the scope of an element selected in the Elements tab of the developer tools, in console, you can type `$scope`. If you change value of some model on `$scope` and want to have this change reflected in the running application, you need to call `$scope.$apply()` after making the change.

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

@ -1,111 +0,0 @@
// tabId -> devtool port
var inspectedTabs = {};
// tabId -> buffered data
var data = {};
function bufferOrForward(message, sender) {
var tabId = sender.tab.id,
devToolsPort = inspectedTabs[tabId];
if (!data[tabId] || message === 'refresh') {
resetState(tabId);
// TODO: this is kind of a hack-y spot to put this
showPageAction(tabId);
}
// TODO: not sure how I feel about special-casing `refresh`
if (message !== 'refresh') {
message = JSON.parse(message);
}
bufferData(tabId, message);
if (devToolsPort) {
devToolsPort.postMessage(message);
}
}
function resetState(tabId) {
data[tabId] = {
hints: [],
scopes: {}
};
}
function bufferData(tabId, message) {
var tabData = data[tabId],
scope;
if (message.message) {
return tabData.hints.push(message);
}
if (message.event) {
if (message.event === 'scope:new') {
tabData.scopes[message.child] = {
parent: message.parent,
children: [],
models: {}
};
if (tabData.scopes[message.parent]) {
tabData.scopes[message.parent].children.push(message.child);
}
} else if (message.id && (scope = tabData.scopes[message.id])) {
if (message.event === 'scope:destroy') {
if (scope.parent) {
scope.parent.children.splice(scope.parent.children.indexOf(child), 1);
}
delete scopes[message.id];
} else if (message.event === 'model:change') {
scope.models[message.path] = (typeof message.value === 'undefined') ?
undefined : JSON.parse(message.value);
} else if (message.event === 'scope:link') {
scope.descriptor = message.descriptor;
}
}
}
}
// context script > background
chrome.runtime.onMessage.addListener(bufferOrForward);
chrome.runtime.onConnect.addListener(function(devToolsPort) {
devToolsPort.onMessage.addListener(registerInspectedTabId);
function registerInspectedTabId(inspectedTabId) {
inspectedTabs[inspectedTabId] = devToolsPort;
if (!data[inspectedTabId]) {
resetState(inspectedTabId);
}
devToolsPort.postMessage({
event: 'hydrate',
data: data[inspectedTabId]
});
devToolsPort.onDisconnect.addListener(function () {
delete inspectedTabs[inspectedTabId];
});
//devToolsPort.onMessage.removeListener(registerInspectedTabId);
}
});
chrome.tabs.onRemoved.addListener(function (tabId) {
if (data[tabId]) {
delete data[tabId];
}
});
function showPageAction(tabId) {
chrome.pageAction.show(tabId);
chrome.pageAction.setTitle({
tabId: tabId,
title: 'Batarang Active'
});
}

@ -1,21 +0,0 @@
/*
* karma.conf.js and karma.es5.conf.js optionally load this
*/
var CUSTOM_LAUNCHERS = {
'SL_Chrome': {
base: 'SauceLabs',
browserName: 'chrome',
version: '35'
}
};
module.exports = function(options) {
options.sauceLabs = {
testName: 'AngularJS Batarang Unit Tests',
startConnect: true
};
options.customLaunchers = CUSTOM_LAUNCHERS;
options.browsers = Object.keys(CUSTOM_LAUNCHERS);
options.reporters = ['dots', 'saucelabs'];
};

@ -1,24 +0,0 @@
/*
* karma.conf.js optionally loads this
*/
module.exports = function(options) {
if (!isTravis()) {
return;
} else if (!options.sauceLabs) {
throw new Error('This should be loaded after karma.sauce config');
}
options.sauceLabs.build = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')';
options.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER;
options.sauceLabs.startConnect = false;
// TODO(vojta): remove once SauceLabs supports websockets.
// This speeds up the capturing a bit, as browsers don't even try to use websocket.
options.transports = ['xhr-polling'];
options.singleRun = true;
};
function isTravis() {
return !!process.env.TRAVIS;
}

@ -0,0 +1,815 @@
/*!
* Bootstrap Responsive v2.0.4
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both;
}
.hide-text {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.input-block-level {
display: block;
width: 100%;
min-height: 28px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.hidden {
display: none;
visibility: hidden;
}
.visible-phone {
display: none !important;
}
.visible-tablet {
display: none !important;
}
.hidden-desktop {
display: none !important;
}
@media (max-width: 767px) {
.visible-phone {
display: inherit !important;
}
.hidden-phone {
display: none !important;
}
.hidden-desktop {
display: inherit !important;
}
.visible-desktop {
display: none !important;
}
}
@media (min-width: 768px) and (max-width: 979px) {
.visible-tablet {
display: inherit !important;
}
.hidden-tablet {
display: none !important;
}
.hidden-desktop {
display: inherit !important;
}
.visible-desktop {
display: none !important ;
}
}
@media (max-width: 480px) {
.nav-collapse {
-webkit-transform: translate3d(0, 0, 0);
}
.page-header h1 small {
display: block;
line-height: 18px;
}
input[type="checkbox"],
input[type="radio"] {
border: 1px solid #ccc;
}
.form-horizontal .control-group > label {
float: none;
width: auto;
padding-top: 0;
text-align: left;
}
.form-horizontal .controls {
margin-left: 0;
}
.form-horizontal .control-list {
padding-top: 0;
}
.form-horizontal .form-actions {
padding-right: 10px;
padding-left: 10px;
}
.modal {
position: absolute;
top: 10px;
right: 10px;
left: 10px;
width: auto;
margin: 0;
}
.modal.fade.in {
top: auto;
}
.modal-header .close {
padding: 10px;
margin: -10px;
}
.carousel-caption {
position: static;
}
}
@media (max-width: 767px) {
body {
padding-right: 20px;
padding-left: 20px;
}
.navbar-fixed-top,
.navbar-fixed-bottom {
margin-right: -20px;
margin-left: -20px;
}
.container-fluid {
padding: 0;
}
.dl-horizontal dt {
float: none;
width: auto;
clear: none;
text-align: left;
}
.dl-horizontal dd {
margin-left: 0;
}
.container {
width: auto;
}
.row-fluid {
width: 100%;
}
.row,
.thumbnails {
margin-left: 0;
}
[class*="span"],
.row-fluid [class*="span"] {
display: block;
float: none;
width: auto;
margin-left: 0;
}
.input-large,
.input-xlarge,
.input-xxlarge,
input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input {
display: block;
width: 100%;
min-height: 28px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.input-prepend input,
.input-append input,
.input-prepend input[class*="span"],
.input-append input[class*="span"] {
display: inline-block;
width: auto;
}
}
@media (min-width: 768px) and (max-width: 979px) {
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
margin-left: 20px;
}
.container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 724px;
}
.span12 {
width: 724px;
}
.span11 {
width: 662px;
}
.span10 {
width: 600px;
}
.span9 {
width: 538px;
}
.span8 {
width: 476px;
}
.span7 {
width: 414px;
}
.span6 {
width: 352px;
}
.span5 {
width: 290px;
}
.span4 {
width: 228px;
}
.span3 {
width: 166px;
}
.span2 {
width: 104px;
}
.span1 {
width: 42px;
}
.offset12 {
margin-left: 764px;
}
.offset11 {
margin-left: 702px;
}
.offset10 {
margin-left: 640px;
}
.offset9 {
margin-left: 578px;
}
.offset8 {
margin-left: 516px;
}
.offset7 {
margin-left: 454px;
}
.offset6 {
margin-left: 392px;
}
.offset5 {
margin-left: 330px;
}
.offset4 {
margin-left: 268px;
}
.offset3 {
margin-left: 206px;
}
.offset2 {
margin-left: 144px;
}
.offset1 {
margin-left: 82px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 28px;
margin-left: 2.762430939%;
*margin-left: 2.709239449638298%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .span12 {
width: 99.999999993%;
*width: 99.9468085036383%;
}
.row-fluid .span11 {
width: 91.436464082%;
*width: 91.38327259263829%;
}
.row-fluid .span10 {
width: 82.87292817100001%;
*width: 82.8197366816383%;
}
.row-fluid .span9 {
width: 74.30939226%;
*width: 74.25620077063829%;
}
.row-fluid .span8 {
width: 65.74585634900001%;
*width: 65.6926648596383%;
}
.row-fluid .span7 {
width: 57.182320438000005%;
*width: 57.129128948638304%;
}
.row-fluid .span6 {
width: 48.618784527%;
*width: 48.5655930376383%;
}
.row-fluid .span5 {
width: 40.055248616%;
*width: 40.0020571266383%;
}
.row-fluid .span4 {
width: 31.491712705%;
*width: 31.4385212156383%;
}
.row-fluid .span3 {
width: 22.928176794%;
*width: 22.874985304638297%;
}
.row-fluid .span2 {
width: 14.364640883%;
*width: 14.311449393638298%;
}
.row-fluid .span1 {
width: 5.801104972%;
*width: 5.747913482638298%;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 714px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 652px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 590px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 528px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 466px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 404px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 342px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 280px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 218px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 156px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 94px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 32px;
}
}
@media (min-width: 1200px) {
.row {
margin-left: -30px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
margin-left: 30px;
}
.container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 1170px;
}
.span12 {
width: 1170px;
}
.span11 {
width: 1070px;
}
.span10 {
width: 970px;
}
.span9 {
width: 870px;
}
.span8 {
width: 770px;
}
.span7 {
width: 670px;
}
.span6 {
width: 570px;
}
.span5 {
width: 470px;
}
.span4 {
width: 370px;
}
.span3 {
width: 270px;
}
.span2 {
width: 170px;
}
.span1 {
width: 70px;
}
.offset12 {
margin-left: 1230px;
}
.offset11 {
margin-left: 1130px;
}
.offset10 {
margin-left: 1030px;
}
.offset9 {
margin-left: 930px;
}
.offset8 {
margin-left: 830px;
}
.offset7 {
margin-left: 730px;
}
.offset6 {
margin-left: 630px;
}
.offset5 {
margin-left: 530px;
}
.offset4 {
margin-left: 430px;
}
.offset3 {
margin-left: 330px;
}
.offset2 {
margin-left: 230px;
}
.offset1 {
margin-left: 130px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 28px;
margin-left: 2.564102564%;
*margin-left: 2.510911074638298%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.45299145300001%;
*width: 91.3997999636383%;
}
.row-fluid .span10 {
width: 82.905982906%;
*width: 82.8527914166383%;
}
.row-fluid .span9 {
width: 74.358974359%;
*width: 74.30578286963829%;
}
.row-fluid .span8 {
width: 65.81196581200001%;
*width: 65.7587743226383%;
}
.row-fluid .span7 {
width: 57.264957265%;
*width: 57.2117657756383%;
}
.row-fluid .span6 {
width: 48.717948718%;
*width: 48.6647572286383%;
}
.row-fluid .span5 {
width: 40.170940171000005%;
*width: 40.117748681638304%;
}
.row-fluid .span4 {
width: 31.623931624%;
*width: 31.5707401346383%;
}
.row-fluid .span3 {
width: 23.076923077%;
*width: 23.0237315876383%;
}
.row-fluid .span2 {
width: 14.529914530000001%;
*width: 14.4767230406383%;
}
.row-fluid .span1 {
width: 5.982905983%;
*width: 5.929714493638298%;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 1160px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 1060px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 960px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 860px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 760px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 660px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 560px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 460px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 360px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 260px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 160px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 60px;
}
.thumbnails {
margin-left: -30px;
}
.thumbnails > li {
margin-left: 30px;
}
.row-fluid .thumbnails {
margin-left: 0;
}
}
@media (max-width: 979px) {
body {
padding-top: 0;
}
.navbar-fixed-top,
.navbar-fixed-bottom {
position: static;
}
.navbar-fixed-top {
margin-bottom: 18px;
}
.navbar-fixed-bottom {
margin-top: 18px;
}
.navbar-fixed-top .navbar-inner,
.navbar-fixed-bottom .navbar-inner {
padding: 5px;
}
.navbar .container {
width: auto;
padding: 0;
}
.navbar .brand {
padding-right: 10px;
padding-left: 10px;
margin: 0 0 0 -5px;
}
.nav-collapse {
clear: both;
}
.nav-collapse .nav {
float: none;
margin: 0 0 9px;
}
.nav-collapse .nav > li {
float: none;
}
.nav-collapse .nav > li > a {
margin-bottom: 2px;
}
.nav-collapse .nav > .divider-vertical {
display: none;
}
.nav-collapse .nav .nav-header {
color: #999999;
text-shadow: none;
}
.nav-collapse .nav > li > a,
.nav-collapse .dropdown-menu a {
padding: 6px 15px;
font-weight: bold;
color: #999999;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.nav-collapse .btn {
padding: 4px 10px 4px;
font-weight: normal;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.nav-collapse .dropdown-menu li + li a {
margin-bottom: 2px;
}
.nav-collapse .nav > li > a:hover,
.nav-collapse .dropdown-menu a:hover {
background-color: #222222;
}
.nav-collapse.in .btn-group {
padding: 0;
margin-top: 5px;
}
.nav-collapse .dropdown-menu {
position: static;
top: auto;
left: auto;
display: block;
float: none;
max-width: none;
padding: 0;
margin: 0 15px;
background-color: transparent;
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.nav-collapse .dropdown-menu:before,
.nav-collapse .dropdown-menu:after {
display: none;
}
.nav-collapse .dropdown-menu .divider {
display: none;
}
.nav-collapse .navbar-form,
.nav-collapse .navbar-search {
float: none;
padding: 9px 15px;
margin: 9px 0;
border-top: 1px solid #222222;
border-bottom: 1px solid #222222;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
}
.navbar .nav-collapse .nav.pull-right {
float: none;
margin-left: 0;
}
.nav-collapse,
.nav-collapse.collapse {
height: 0;
overflow: hidden;
}
.navbar .btn-navbar {
display: block;
}
.navbar-static .navbar-inner {
padding-right: 10px;
padding-left: 10px;
}
}
@media (min-width: 980px) {
.nav-collapse.collapse {
height: auto !important;
overflow: visible !important;
}
}

4978
css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

@ -0,0 +1,50 @@
path.arc {
fill: #fff;
}
.node {
font-size: 10px;
}
.node:hover {
fill: #1f77b4;
}
.link {
fill: none;
stroke: #1f77b4;
stroke-opacity: .4;
pointer-events: none;
}
.link.source, .link.target {
stroke-opacity: 1;
stroke-width: 2px;
}
.node.target {
fill: #d62728 !important;
}
.link.source {
stroke: #d62728;
}
.node.source {
fill: #2ca02c;
}
.link.target {
stroke: #2ca02c;
}
d3 {
display: block;
max-width: 600px;
height: 100%;
margin: auto;
}
svg text {
font-size: 1.4em;
}

@ -0,0 +1,106 @@
.col {
float: left;
width: 200px;
}
.col-2 {
float: left;
width: 400px;
}
.scope-branch {
margin-left: 30px;
background-color: rgba(0,0,0,0.06);
}
body {
margin: 10px;
}
.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;
}
bat-scope-tree .selected {
font-weight: bold;
text-decoration: underline;
color: #333;
}
/*
* Slider widget style based on jquery-ui-bootstrap
* http://addyosmani.github.com/jquery-ui-bootstrap
*/
.ui-slider {
position: relative;
text-align: left;
height: .8em;
border-radius: 4px;
border: 1px solid #aaaaaa;
background: #ffffff;
}
.ui-slider .ui-slider-handle {
position: absolute;
z-index: 2;
width: 1.2em;
height: 1.2em;
top: -.3em;
margin-left: -.6em;
cursor: default;
border-radius: 4px;
background-color: #e6e6e6;
background-repeat: no-repeat;
background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
/*background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);*/
font-size: 13px;
line-height: normal;
border: 1px solid #ccc;
border-bottom-color: #bbb;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-webkit-transition: 0.1s linear background-image;
transition: 0.1s linear background-image;
overflow: visible;
}
.ui-slider .ui-slider-range {
position: absolute;
top: 0;
height: 100%;
z-index: 1;
font-size: .7em;
display: block;
border: 0;
background-position: 0 0;
background-color: #0064cd;
background-repeat: repeat-x;
background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
/*background-image: linear-gradient(top, #049cdb, #0064cd);*/
}

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

2513
dist/hint.js vendored

File diff suppressed because it is too large Load Diff

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>yo</title>
</head>
<body ng-app="myApp">
<div ng-controller="myKontroller">
<h1>name: {{name}}</h1>
<input ng-model="name">
<h1>complex.a.b.c: {{complex.a.b.c}}</h1>
<input ng-model="complex.a.b.c">
<p>scope: {{$id}}</p>
<ul>
<li ng-repeat="num in items">{{num}}</li>
</ul>
</div>
<script src="../node_modules/angular/angular.js"></script>
<script>
angular.module('myApp', []).
controller('myKontroller', function ($scope) {
$scope.name = 'hey';
$scope.items = [1, 2, 3];
$scope.complex = { a: { b: { c: 'yo' }}};
var thing = 'document';
});
</script>
</body>
</html>

@ -1,42 +0,0 @@
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var browserify = require('browserify');
var zip = require('gulp-zip');
var main = require('./package.json').main;
// TODO: make sure manifest version === package.json version === bower.json version
var version = require('./manifest.json').version;
gulp.task('watch', function(){
gulp.watch(['hint.js', '!./dist/*.js'], ['browserify']);
});
gulp.task('browserify', function() {
var bundleStream = browserify('./' + main).bundle().pipe(source(main));
return bundleStream.pipe(gulp.dest('./dist'));
});
/*
* I use this to make a zip for the chrome store
*/
gulp.task('package', ['browserify'], function () {
return gulp.src([
'./dist/**',
'./img/**',
'./panel/**',
'background.js',
'devtoolsBackground.*',
'inject.js',
'manifest.json',
'./node_modules/angular/angular.js'
], {base: '.'})
.pipe(gulp.dest('./package'));
});
gulp.task('zip', ['package'], function () {
return gulp.src('package/**')
.pipe(zip('batarang-' + version + '.zip'))
.pipe(gulp.dest('.'));
});
gulp.task('default', ['browserify']);

@ -1,38 +0,0 @@
/*
* Batarang instrumentation
*
* This gets loaded into the context of the app you are inspecting
*/
require('./loader.js');
require('angular-hint');
angular.hint.onMessage = function (moduleName, message, messageType, category) {
if (!message) {
message = moduleName;
moduleName = 'Unknown'
}
if (typeof messageType === 'undefined') {
messageType = 1;
}
sendMessage({
module: moduleName,
message: message,
severity: messageType,
category: category
});
};
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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@ -1,28 +0,0 @@
if (document.cookie.indexOf('__ngDebug=true') != -1) {
bootstrapHint();
}
function bootstrapHint () {
chrome.extension.sendMessage('refresh');
var html = document.getElementsByTagName('html')[0];
var eventProxyElement = document.createElement('div');
eventProxyElement.id = '__ngBatarangElement';
eventProxyElement.style.display = 'none';
html.appendChild(eventProxyElement);
// inject into the application context from the content script context
var script = window.document.createElement('script');
script.src = chrome.extension.getURL('dist/hint.js');
eventProxyElement.addEventListener('batarangDataEvent', function () {
var eventData = eventProxyElement.innerText;
chrome.extension.sendMessage(eventData);
});
html.setAttribute('ng-hint', '');
html.appendChild(script);
}

@ -0,0 +1,19 @@
// notify of page refreshes
chrome.extension.onConnect.addListener(function(port) {
port.onMessage.addListener(function (msg) {
if (msg.action === 'register') {
var respond = function (tabId, changeInfo, tab) {
if (tabId !== msg.inspectedTabId) {
return;
}
port.postMessage('refresh');
};
chrome.tabs.onUpdated.addListener(respond);
port.onDisconnect.addListener(function () {
chrome.tabs.onUpdated.removeListener(respond);
});
}
});
});

@ -0,0 +1,9 @@
angular.module('panelApp').controller('DepsCtrl', function DepsCtrl($scope, appDeps) {
$scope.$on('poll', function () {
appDeps.get(function (deps) {
$scope.$apply(function () {
$scope.deps = deps;
});
});
});
});

@ -0,0 +1,68 @@
angular.module('panelApp').controller('ModelCtrl', function ModelCtrl($scope, appContext, appModel) {
$scope.inspect = function () {
appContext.inspect(this.val.id);
};
$scope.select = function () {
$scope.selectedScope = this.val.id;
};
// TODO: fix this
$scope.edit = function () {
appContext.executeOnScope(this.val.id, function (scope, elt, args) {
scope.$apply(function () {
scope[args.name] = args.value;
});
}, {
name: this.key,
value: JSON.parse(this.item)
});
};
$scope.roots = [];
$scope.model = null;
$scope.selectedRoot = null;
$scope.selectedScope = null;
$scope.enableInspector = appModel.enableInspector;
$scope.$on('poll', function () {
// get the list of root scopes
appModel.getRootScopes(function (rootScopes) {
$scope.$apply(function () {
$scope.roots = rootScopes;
if ($scope.roots.length === 0) {
$scope.selectedRoot = null;
} else if (!$scope.selectedRoot) {
$scope.selectedRoot = $scope.roots[0];
}
if ($scope.selectedRoot && !$scope.selectedScope) {
$scope.selectedScope = $scope.selectedRoot;
}
});
});
// get scope tree
if ($scope.selectedRoot) {
appModel.getScopeTree($scope.selectedRoot, function (tree) {
$scope.$apply(function () {
$scope.tree = tree;
});
});
}
// get models on the selected scope
if ($scope.selectedScope) {
appModel.getModel($scope.selectedScope, function (model) {
$scope.$apply(function () {
$scope.model = model;
});
});
}
});
});

@ -0,0 +1,40 @@
angular.module('panelApp').controller('OptionsCtrl', function OptionsCtrl($scope, appInfo, appHighlight) {
$scope.debugger = {
scopes: false,
bindings: false,
app: false
};
['scopes', 'bindings', 'app'].forEach(function (thing) {
$scope.$watch('debugger.' + thing, function (val) {
appHighlight[thing](val);
});
});
appInfo.getAngularVersion(function (version) {
$scope.$apply(function () {
$scope.version = version;
});
});
appInfo.getAngularSrc(function (status) {
$scope.$apply(function () {
switch(status) {
case 'good':
$scope.status = 'success';
$scope.explain = 'CDN detected';
break;
case 'bad':
$scope.status = 'important';
$scope.explain = 'You are using the old code.angularjs.org links, which are slow! You should switch to the new CDN link. See <a target="_blank" href="http://blog.angularjs.org/2012/07/angularjs-now-hosted-on-google-cdn.html">this post</a> for more info';
break;
case 'info':
$scope.status = 'info';
$scope.explain = 'You may want to use the CDN-hosted AngularJS files. See <a target="_blank" href="http://blog.angularjs.org/2012/07/angularjs-now-hosted-on-google-cdn.html">this post</a> for more info';
break;
}
});
});
});

@ -0,0 +1,47 @@
angular.module('panelApp').controller('PerfCtrl', function PerfCtrl($scope, appContext, appPerf, appModel, appWatch, filesystem) {
$scope.histogram = [];
$scope.roots = [];
$scope.min = 0;
$scope.max = 100;
$scope.clearHistogram = function () {
appPerf.clear();
};
$scope.exportData = function () {
filesystem.exportJSON('file.json', $scope.histogram);
};
$scope.$watch('log', function (newVal, oldVal) {
appContext.setLog(newVal);
});
$scope.inspect = function () {
appContext.inspect(this.val.id);
};
$scope.$on('poll', function () {
appPerf.get(function (histogram) {
$scope.$apply(function () {
$scope.histogram = histogram;
});
});
appModel.getRootScopes(function (rootScopes) {
$scope.$apply(function () {
$scope.roots = rootScopes;
if ($scope.roots.length === 0) {
$scope.selectedRoot = null;
} else if (!$scope.selectedRoot) {
$scope.selectedRoot = $scope.roots[0];
}
});
});
appWatch.getWatchTree($scope.selectedRoot, function (tree) {
$scope.tree = tree;
});
});
});

@ -6,11 +6,8 @@ var getPanelContents = function () {
if (window.angular && $0) {
//TODO: can we move this scope export into updateElementProperties
var scope = window.angular.element($0).scope();
var injector = window.angular.element($0).injector();
// Export $scope to the console
window.$scope = scope;
// Export $injector to the console
window.$injector = injector;
return (function (scope) {
var panelContents = {
__private__: {}
@ -33,15 +30,16 @@ var getPanelContents = function () {
};
panels.elements.createSidebarPane(
"$scope",
"AngularJS Properties",
function (sidebar) {
panels.elements.onSelectionChanged.addListener(function updateElementProperties() {
sidebar.setExpression("(" + getPanelContents.toString() + ")()");
});
});
// Angular panel
var angularPanel = panels.create(
"AngularJS",
"img/angular.png",
"panel/app.html"
"panel.html"
);

274
js/directives/d3.js vendored

@ -0,0 +1,274 @@
// D3 visualization
// TODO: D3 as a service
angular.module('panelApp').directive('batD3', function () {
return {
restrict: 'E',
terminal: true,
scope: {
val: '=val'
},
link: function (scope, element, attrs) {
// Based on code from: http://mbostock.github.com/d3/talk/20111116/bundle.html
// Initialize Element
// ------------------
var div = d3.select(element[0]);
// Constants
// ---------
var w = 600,
h = 600,
rx = w / 2,
ry = h / 2,
m0,
rotate = 0;
// Helpers
// -------
// generate element ids that do not have '$'
var sanitize = function (key) {
return key.replace('$', 'dollar')
}
// TODO: refactor the data transformation to make it faster
// For instance, build up the ideal structure in inject/degug.js
var packages = {
// Lazily construct the package hierarchy from class names.
root: function(classes) {
var map = {};
// add "classes" with no dependencies
var exist = {},
toAdd = [];
classes.forEach(function (cl) {
exist[cl.name] = true;
});
classes.forEach(function (cl) {
cl.imports.forEach(function (im) {
if (!exist[im]) {
toAdd.push(im);
exist[im] = true;
}
});
});
toAdd.forEach(function (a) {
classes.push({
name: a,
imports: []
});
});
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return map[""];
},
// Return a list of imports for the given array of nodes.
imports: function(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
};
// Instantiate and Style D3 Objects
// --------------------------------
var cluster = d3.layout.cluster().
size([360, ry - 120]).
sort(function(a, b) { return d3.ascending(a.key, b.key); });
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial().
interpolate("bundle").
tension(.85).
radius(function(d) { return d.y; }).
angle(function(d) { return d.x / 180 * Math.PI; });
var svg = div.append("svg:svg").
attr("preserveAspectRatio", "xMinYMin meet").
attr("viewBox", [0, 0, w, h].join(' ')).
attr("height", h).
append("svg:g").
attr("transform", "translate(" + rx + "," + ry + ")");
// Render the data whenever "val" changes
// --------------------------------------
scope.$watch('val', function (newVal, oldVal) {
var classes;
if (!newVal || newVal.length === 0) {
svg.selectAll('*').remove();
return;
}
if (oldVal && oldVal.length === newVal.length) {
var changed = false;
for (i = 0; i < oldVal.length; i++) {
if (oldVal[i].name !== newVal[i].name || newVal[i].imports.length !== oldVal[i].imports.length) {
changed = true;
break;
}
}
if (!changed) {
return;
}
}
classes = newVal.slice(0);
classes.sort(function (a, b) {
return .5 - (a.name < b.name);
});
svg.selectAll('*').remove();
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 120).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
.on("mousedown", mousedown);
var nodes = cluster.nodes(packages.root(classes)),
links = packages.imports(nodes),
splines = bundle(links);
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function(d) { return "link source-" + sanitize(d.source.key) + " target-" + sanitize(d.target.key); })
.attr("d", function(d, i) { return line(splines[i]); });
svg.selectAll("g.node")
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) { return "node-" + sanitize(d.key); })
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
.append("svg:text")
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
.text(function(d) { return d.key; })
.on("mouseover", mouseover)
.on("mouseout", mouseout);
});
/*
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
path.attr("d", function(d, i) { return line(splines[i]); });
});
*/
//TODO: decide where to attach these events
/*
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
*/
function mouse(e) {
return [e.pageX - rx, e.pageY - ry];
}
function mousedown() {
m0 = mouse(d3.event);
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
}
}
function mouseup() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
rotate += dm;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
m0 = null;
div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");
svg
.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) { return (d.x + rotate) % 360 < 180 ? 8 : -8; })
.attr("text-anchor", function(d) { return (d.x + rotate) % 360 < 180 ? "start" : "end"; })
.attr("transform", function(d) { return (d.x + rotate) % 360 < 180 ? null : "rotate(180)"; });
}
}
function mouseover(d) {
svg.selectAll("path.link.target-" + sanitize(d.key))
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + sanitize(d.key))
.classed("source", true)
.each(updateNodes("target", true));
}
function mouseout(d) {
svg.selectAll("path.link.source-" + sanitize(d.key))
.classed("source", false)
.each(updateNodes("target", false));
svg.selectAll("path.link.target-" + sanitize(d.key))
.classed("target", false)
.each(updateNodes("source", false));
}
function updateNodes(name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + sanitize(d[name].key)).classed(name, value);
};
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
}
};
});

@ -0,0 +1,65 @@
angular.module('panelApp').directive('batJsonTree', function($compile) {
return {
restrict: 'E',
terminal: true,
scope: {
val: '='
//edit: '=',
},
link: function (scope, element, attrs) {
// this is more complicated then it should be
// see: https://github.com/angular/angular.js/issues/898
var buildDom = function (object) {
var html = '';
var prop;
if (object === undefined) {
html += '<i>undefined</i>';
} else if (object === null) {
html += '<i>null</i>';
} else if (object instanceof Array) {
var i;
html += '<div class="scope-branch">[ ';
if (object.length > 0) {
html += buildDom(object[0]);
for (i = 1; i < object.length; i++) {
html += ', ' + buildDom(object[i]);
}
}
html += ' ]</div>';
} else if (object instanceof Object) {
html += ' { ';
for (prop in object) {
if (object.hasOwnProperty(prop)) {
html += '<div class="scope-branch">' + prop + ': ' + buildDom(object[prop]) + '</div>';
}
}
html += ' } ';
} else {
html += '<span>' + object.toString() + '</span>';
}
return html;
};
var isEmpty = function (object) {
var prop;
for (prop in object) {
if (object.hasOwnProperty(prop)) {
return false;
}
}
return true;
};
scope.$watch('val', function (newVal, oldVal) {
if (newVal === null) {
element.html('<div class="alert alert-info">Select a scope to view its models.</div>');
} else if (isEmpty(newVal)) {
element.html('<pre>{ This scope has no models }</pre>');
} else {
element.html('<pre>' + buildDom(newVal) + '</pre>');
}
});
}
};
});

@ -0,0 +1,46 @@
angular.module('panelApp').directive('batScopeTree', function ($compile) {
// make toggle settings persist across $compile
var modelState = {};
var scopeState = {};
var selected = null;
var template =
'<div class="scope-branch">' +
'<a href ng-click="inspect()">&lt;</a> ' +
'<a href ng-click="select()" ng-class="{selected: selectedScope == val.id}">Scope ({{val.id}})</a>' +
'<div ng-repeat="child in val.children">' +
'<bat-scope-tree ' +
'val="child" ' +
'inspect="inspect" ' +
'select="select" ' +
'selected-scope="selectedScope">' +
'</bat-scope-tree>' +
'</div>' +
'</div>';
return {
restrict: 'E',
terminal: true,
scope: {
val: '=',
select: '=',
selectedScope: '=',
inspect: '='
},
link: function (scope, element, attrs) {
// this is more complicated then it should be
// see: https://github.com/angular/angular.js/issues/898
element.append(template);
var childScope = scope.$new();
childScope.select = scope.select;
//childScope.selectedScope = scope.selectedScope;
childScope.inspect = scope.inspect;
$compile(element.contents())(childScope);
}
};
});

@ -0,0 +1,43 @@
// range slider
angular.module('panelApp').directive('batSlider', function($compile) {
return {
restrict: 'E',
terminal: true,
scope: {
minimum: '=minimum',
maximum: '=maximum'
},
link: function (scope, element, attrs) {
var dom = $('<div class="ui-slider">' +
'<a class="ui-slider-handle" href></a>' +
'<a class="ui-slider-handle" href></a>' +
'<div class="ui-slider-range"></div>' +
'</div>');
element.append(dom);
$compile(element.contents())(scope.$new());
dom.slider({
range: true,
values: [0, 100],
slide: function () {
var min = $(this).slider('values', 0);
var max = $(this).slider('values', 1);
scope.minimum = min;
scope.maximum = max;
scope.$apply();
},
stop: function () {
var min = $(this).slider('values', 0);
var max = $(this).slider('values', 1);
scope.minimum = min;
scope.maximum = max;
scope.$apply();
}
});
}
};
});

@ -1,23 +1,52 @@
angular.module('batarang.tabs', []).
directive('batTabs', function ($compile, $templateCache, $http, inspectedApp) {
angular.module('panelApp').directive('batTabs', function ($compile, $templateCache, $http) {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'components/tabs/tabs.html',
template:
'<div class="container-fluid">' +
'<div class="row-fluid">' +
'<ul class="nav nav-tabs span12">' +
'<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">'+
'<a href="" ng-click="select(pane)">{{pane.title}}</a>' +
'</li>' +
'<li>' +
'<div class="bat-nav-check">' +
'<input type="checkbox" ng-model="enable" id="enable-instrumentation"> ' +
'Enable' +
'</div>' +
'</li>' +
'</ul>' +
'</div>' +
'<div class="row-fluid bat-tabs-inside"></div>' +
'<div ng-transclude></div>' +
'</div>',
replace: true,
controller: function ($scope) {
controller: function ($scope, appContext) {
var panes = $scope.panes = [];
this.addPane = function(pane) {
panes.push(pane);
};
inspectedApp.getInstrumentationStatus().then(function (status) {
$scope.enabled = status;
$scope.$watch('enabled', inspectedApp.enableInstrumentation);
appContext.getDebug(function (result) {
$scope.enable = result;
$scope.$watch('enable', function (newVal, oldVal) {
appContext.setDebug(newVal);
if (!newVal) {
$scope.lastPane = $scope.currentPane;
$scope.select($scope.panes[$scope.panes.length - 1]);
} else {
$scope.select($scope.lastPane);
}
});
if (result) {
$scope.select($scope.panes[0]);
}
});
},
link: function (scope, element, attr) {
@ -33,6 +62,9 @@ directive('batTabs', function ($compile, $templateCache, $http, inspectedApp) {
}
scope.select = function (pane) {
if (!scope.enable && pane !== scope.panes[scope.panes.length - 1]) {
return;
}
$http.get(pane.src, { cache: $templateCache }).
then(function (response) {
var template = response.data;

@ -0,0 +1,43 @@
// watchers tree
angular.module('panelApp').directive('batWatcherTree', function($compile) {
// make toggle settings persist across $compile
var scopeState = {};
return {
restrict: 'E',
terminal: true,
scope: {
val: '=val',
inspect: '=inspect'
},
link: function (scope, element, attrs) {
// this is more complicated then it should be
// see: https://github.com/angular/angular.js/issues/898
element.append(
'<div class="scope-branch">' +
'<a href ng-click="inspect()">Scope ({{val.id}})</a> | ' +
'<a href ng-click="scopeState[val.id] = !scopeState[val.id]">toggle</a>' +
'<div ng-hide="scopeState[val.id]">' +
'<ul>' +
'<li ng-repeat="item in val.watchers">' +
'<a href ng-hide="item.split(\'\n\').length < 2" ng-click="showState = !showState">toggle</a> ' +
'<code ng-hide="showState && item.split(\'\n\').length > 1">{{item | first}}</code>' +
'<pre ng-hide="!showState || item.split(\'\n\').length < 2">' +
'{{item}}' +
'</pre>' +
'</li>' +
'</ul>' +
'<div ng-repeat="child in val.children">' +
'<bat-watcher-tree val="child" inspect="inspect"></bat-watcher-tree>' +
'</div>' +
'</div>' +
'</div>');
var childScope = scope.$new();
childScope.scopeState = scopeState;
$compile(element.contents())(childScope);
}
};
});

@ -0,0 +1,6 @@
// returns the first line of a multi-line string
angular.module('panelApp').filter('first', function () {
return function (input, output) {
return input.split("\n")[0];
};
});

@ -0,0 +1,6 @@
// returns the number's first 4 decimals
angular.module('panelApp').filter('precision', function () {
return function (input, output) {
return input.toPrecision(4);
};
});

@ -0,0 +1,20 @@
// Sort watchers by time
// Used by the performance tab
angular.module('panelApp').filter('sortByTime', function () {
return function (input, min, max) {
var copy = input.slice(0);
copy = copy.sort(function (a, b) {
return b.time - a.time;
});
if (typeof min !== 'number' || typeof max !== 'number') {
return copy;
}
var start = Math.floor(input.length * min/100);
var end = Math.ceil(input.length * max/100) - start;
return copy.splice(start, end);
};
});

@ -0,0 +1,877 @@
var inject = function () {
document.head.appendChild((function () {
var fn = function bootstrap (window) {
var angular = window.angular;
// Helper to determine if the root 'ng' module has been loaded
// window.angular may be available if the app is bootstrapped asynchronously, but 'ng' might
// finish loading later.
var ngLoaded = function () {
if (!window.angular) {
return false;
}
try {
window.angular.module('ng');
}
catch (e) {
return false;
}
return true;
};
if (!ngLoaded()) {
(function () {
// TODO: var name
var areWeThereYet = function (ev) {
if (ev.srcElement.tagName === 'SCRIPT') {
var oldOnload = ev.srcElement.onload;
ev.srcElement.onload = function () {
if (ngLoaded()) {
document.removeEventListener('DOMNodeInserted', areWeThereYet);
bootstrap(window);
}
if (oldOnload) {
oldOnload.apply(this, arguments);
}
};
}
};
document.addEventListener('DOMNodeInserted', areWeThereYet);
}());
return;
}
// do not patch twice
if (window.__ngDebug) {
return;
}
// Helpers
// =======
// polyfill for performance.now on older webkit
if (!performance.now) {
performance.now = performance.webkitNow;
}
// Based on cycle.js
// https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
// Make a deep copy of an object or array, assuring that there is at most
// one instance of each object or array in the resulting structure. The
// duplicate references (which might be forming cycles) are replaced with
// an object of the form
// {$ref: PATH}
// where the PATH is a JSONPath string that locates the first occurrence.
var decycle = function (object) {
var objects = [], // Keep a reference to each unique object or array
paths = []; // Keep the path to each unique object or array
return (function derez(value, path) {
var i, // The loop counter
name, // Property name
nu; // The new object or array
switch (typeof value) {
case 'object':
if (value instanceof HTMLElement) {
return value.innerHTML.toString().trim();
}
if (!value) {
return null;
}
for (i = 0; i < objects.length; i += 1) {
if (objects[i] === value) {
return {$ref: paths[i]};
}
}
objects.push(value);
paths.push(path);
if (value instanceof Array) {
nu = [];
for (i = 0; i < value.length; i += 1) {
nu[i] = derez(value[i], path + '[' + i + ']');
}
} else {
nu = {};
for (name in value) {
if (name[0] !== '$' && Object.prototype.hasOwnProperty.call(value, name)) {
nu[name] = derez(value[name],
path + '[' + JSON.stringify(name) + ']');
}
}
}
return nu;
case 'number':
case 'string':
case 'boolean':
return value;
}
}(object, '$'));
};
// End
// ===
// given a scope object, return an object with deep clones
// of the models exposed on that scope
var getScopeLocals = function (scope) {
var scopeLocals = {}, prop;
for (prop in scope) {
if (scope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') {
scopeLocals[prop] = decycle(scope[prop]);
}
}
return scopeLocals;
};
// helper to extract dependencies from function arguments
// not all versions of AngularJS expose annotate
var annotate = angular.injector().annotate;
if (!annotate) {
annotate = (function () {
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
// TODO: should I keep these assertions?
function assertArg(arg, name, reason) {
if (!arg) {
throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required"));
}
return arg;
}
function assertArgFn(arg, name, acceptArrayAnnotation) {
if (acceptArrayAnnotation && angular.isArray(arg)) {
arg = arg[arg.length - 1];
}
assertArg(angular.isFunction(arg), name, 'not a function, got ' +
(arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
return arg;
}
return function (fn) {
var $inject,
fnText,
argDecl,
last;
if (typeof fn == 'function') {
if (!($inject = fn.$inject)) {
$inject = [];
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
});
});
fn.$inject = $inject;
}
} else if (angular.isArray(fn)) {
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
};
}());
}
// throttle based on _.throttle from Lo-Dash
// https://github.com/bestiejs/lodash/blob/master/lodash.js#L4625
var throttle = function (func, wait) {
var args,
result,
thisArg,
timeoutId,
lastCalled = 0;
function trailingCall() {
lastCalled = new Date();
timeoutId = null;
result = func.apply(thisArg, args);
}
return function() {
var now = new Date(),
remaining = wait - (now - lastCalled);
args = arguments;
thisArg = this;
if (remaining <= 0) {
clearTimeout(timeoutId);
timeoutId = null;
lastCalled = now;
result = func.apply(thisArg, args);
}
else if (!timeoutId) {
timeoutId = setTimeout(trailingCall, remaining);
}
return result;
};
};
var debounce = function (func, wait, immediate) {
var args,
result,
thisArg,
timeoutId;
function delayed() {
timeoutId = null;
if (!immediate) {
result = func.apply(thisArg, args);
}
}
return function() {
var isImmediate = immediate && !timeoutId;
args = arguments;
thisArg = this;
clearTimeout(timeoutId);
timeoutId = setTimeout(delayed, wait);
if (isImmediate) {
result = func.apply(thisArg, args);
}
return result;
};
};
var updateScopeModelCache = function (scope) {
debug.models[scope.$id] = getScopeLocals(scope);
debug.scopeDirty[scope.$id] = false;
};
var popover = null;
// Public API
// ==========
var api = window.__ngDebug = {
getDeps: function () {
return debug.deps;
},
getRootScopeIds: function () {
var ids = [];
angular.forEach(debug.rootScopes, function (elt, id) {
ids.push(id);
});
return ids;
},
// returns null or cached scope
getModel: function (id) {
if (debug.scopeDirty[id]) {
updateScopeModelCache(debug.scopes[id]);
return debug.models[id];
}
},
getScopeTree: function (id) {
if (debug.scopeTreeDirty[id] === false) {
return;
}
var traverse = function (sc) {
var tree = {
id: sc.$id,
children: []
};
var child = sc.$$childHead;
if (child) {
do {
tree.children.push(traverse(child));
} while (child !== sc.$$childTail && (child = child.$$nextSibling));
}
return tree;
};
var root = debug.rootScopes[id];
var tree = traverse(root);
if (tree) {
debug.scopeTreeDirty[id] = false;
}
return tree;
},
getWatchPerf: function () {
var changes = [];
angular.forEach(debug.watchPerf, function (info, name) {
if (info.time > 0) {
changes.push({
name: name,
time: info.time
});
info.time = 0;
}
});
return changes;
},
getWatchTree: function (id) {
var traverse = function (sc) {
var tree = {
id: sc.$id,
watchers: debug.watchers[sc.$id],
children: []
};
var child = sc.$$childHead;
if (child) {
do {
tree.children.push(traverse(child));
} while (child !== sc.$$childTail && (child = child.$$nextSibling));
}
return tree;
};
var root = debug.rootScopes[id];
var tree = traverse(root);
return tree;
},
enable: function () {
if (popover) {
return;
}
var angular = window.angular;
popover = angular.element(
'<div style="position: fixed; left: 50px; top: 50px; z-index: 9999; background-color: #f1f1f1; box-shadow: 0 15px 10px -10px rgba(0, 0, 0, 0.5), 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;">' +
'<div style="position: relative" style="min-width: 300px; min-height: 100px;">' +
'<div style="min-width: 100px; min-height: 50px; padding: 5px;"><pre>{ Please select a scope }</pre></div>' +
'<button style="position: absolute; top: -15px; left: -15px; cursor: move;">⇱</button>' +
'<button style="position: absolute; top: -15px; left: 10px;">+</button>' +
'<button style="position: absolute; top: -15px; right: -15px;">x</button>' +
'<style>' +
'.ng-scope.bat-selected { border: 1px solid red; } ' +
'.bat-indent { margin-left: 20px; }' +
'</style>' +
'</div>' +
'</div>');
angular.element(window.document.body).append(popover);
var popoverContent = angular.element(angular.element(popover.children('div')[0]).children()[0]);
var dragElt = angular.element(angular.element(popover.children('div')[0]).children()[1]);
var selectElt = angular.element(angular.element(popover.children('div')[0]).children()[2]);
var closeElt = angular.element(angular.element(popover.children('div')[0]).children()[3]);
var currentScope = null,
currentElt = null;
function onMove (ev) {
var x = ev.clientX,
y = ev.clientY;
if (x > window.outerWidth - 100) {
x = window.outerWidth - 100;
} else if (x < 0) {
x = 0;
}
if (y > window.outerHeight - 100) {
y = window.outerHeight - 100;
} else if (y < 0) {
y = 0;
}
x += 5;
y += 5;
popover.css('left', x + 'px');
popover.css('top', y + 'px');
}
closeElt.bind('click', function () {
popover.remove();
popover = null;
});
selectElt.bind('click', bindSelectScope);
var selecting = false;
function bindSelectScope () {
if (selecting) {
return;
}
setTimeout(function () {
selecting = true;
selectElt.attr('disabled', true);
angular.element(document.body).css('cursor', 'crosshair');
angular.element(document.getElementsByClassName('ng-scope'))
.bind('click', onSelectScope)
.bind('mouseover', onHoverScope);
}, 30);
}
var hoverScopeElt = null;
function markHoverElt () {
if (hoverScopeElt) {
hoverScopeElt.addClass('bat-selected');
}
}
function unmarkHoverElt () {
if (hoverScopeElt) {
hoverScopeElt.removeClass('bat-selected');
}
}
function onSelectScope (ev) {
render(this);
angular.element(document.getElementsByClassName('ng-scope'))
.unbind('click', onSelectScope)
.unbind('mouseover', onHoverScope);
unmarkHoverElt();
selecting = false;
selectElt.attr('disabled', false);
angular.element(document.body).css('cursor', '');
hovering = false;
}
var hovering = false;
function onHoverScope (ev) {
if (hovering) {
return;
}
hovering = true;
var that = this;
setTimeout(function () {
unmarkHoverElt();
hoverScopeElt = angular.element(that);
markHoverElt();
hovering = false;
render(that);
}, 100);
}
function onUnhoverScope (ev) {
angular.element(this).css('border', '');
}
dragElt.bind('mousedown', function (ev) {
ev.preventDefault();
rendering = true;
angular.element(document).bind('mousemove', onMove);
});
angular.element(document).bind('mouseup', function () {
angular.element(document).unbind('mousemove', onMove);
setTimeout(function () {
rendering = false;
}, 120);
});
function renderTree (data) {
var tree = angular.element('<div class="bat-indent"></div>');
angular.forEach(data, function (val, key) {
var toAppend;
if (val === undefined) {
toAppend = '<i>undefined</i>';
} else if (val === null) {
toAppend = '<i>null</i>';
} else if (val instanceof Array) {
toAppend = '[ ... ]';
} else if (val instanceof Object) {
toAppend = '{ ... }';
} else {
toAppend = val.toString();
}
if (data instanceof Array) {
toAppend = '<div>' +
toAppend +
((key===data.length-1)?'':',') +
'</div>';
} else {
toAppend = '<div>' +
key +
': ' +
toAppend +
(key!==0?'':',') +
'</div>';
}
toAppend = angular.element(toAppend);
if (val instanceof Array || val instanceof Object) {
function recur () {
toAppend.unbind('click', recur);
toAppend.html('');
toAppend
.append(angular.element('<span>' +
key + ': ' +
((val instanceof Array)?'[':'{') +
'<span>').bind('click', collapse))
.append(renderTree(val))
.append('<span>' + ((val instanceof Array)?']':'}') + '<span>');
}
function collapse () {
toAppend.html('');
toAppend.append(angular.element('<div>' +
key +
': ' +
((val instanceof Array)?'[ ... ]':'{ ... }') +
'</div>').bind('click', recur));
}
toAppend.bind('click', recur);
}
tree.append(toAppend);
});
return tree;
}
var isEmpty = function (object) {
var prop;
for (prop in object) {
if (object.hasOwnProperty(prop)) {
return false;
}
}
return true;
};
var objLength = function (object) {
var prop, len = 0;
for (prop in object) {
if (object.hasOwnProperty(prop)) {
len += 1;
}
}
return len;
};
var rendering = false;
var render = function (elt) {
if (rendering) {
return;
}
rendering = true;
setTimeout(function () {
var scope = angular.element(elt).scope();
rendering = false;
if (scope === currentScope) {
return;
}
currentScope = scope;
currentElt = elt;
var models = getScopeLocals(scope);
popoverContent.children().remove();
if (isEmpty(models)) {
popoverContent.append(angular.element('<i>This scope has no models</i>'));
} else {
popoverContent.append(renderTree(models));
}
}, 100);
};
}
};
// Private state
// =============
//var bootstrap = window.angular.bootstrap;
var debug = {
// map of scopes --> watcher function name strings
watchers: {},
// maps of watch/apply exp/fns to perf data
watchPerf: {},
applyPerf: {},
// map of scope.$ids --> $scope objects
scopes: {},
// map of scope.$ids --> model objects
models: {},
// map of $ids --> bools
scopeDirty: {},
// map of $ids --> refs to $rootScope objects
rootScopes: {},
scopeTreeDirty: {},
deps: []
};
// Instrumentation
// ===============
var ng = angular.module('ng');
ng.config(function ($provide) {
// methods to patch
// $provide.provider
var temp = $provide.provider;
$provide.provider = function (name, definition) {
if (!definition) {
angular.forEach(name, function (definition, name) {
var tempGet = definition.$get;
definition.$get = function () {
debug.deps.push({
name: name,
imports: annotate(tempGet)
});
return tempGet.apply(this, arguments);
};
});
} else if (definition.$get instanceof Array) {
// it should have a $get
var tempGet = definition.$get[definition.$get.length - 1];
definition.$get[definition.$get.length - 1] = function () {
debug.deps.push({
name: name,
imports: annotate(tempGet)
});
return tempGet.apply(this, arguments);
};
} else if (typeof definition === 'object') {
// it should have a $get
var tempGet = definition.$get;
// preserve original annotations
definition.$get = annotate(definition.$get);
definition.$get.push(function () {
debug.deps.push({
name: name,
imports: annotate(tempGet)
});
return tempGet.apply(this, arguments);
});
} else {
debug.deps.push({
name: name,
imports: annotate(definition)
});
}
return temp.apply(this, arguments);
};
// $provide.(factory|service)
[
'factory',
'service'
].forEach(function (met) {
var temp = $provide[met];
$provide[met] = function (name, definition) {
if (typeof name === 'object') {
angular.forEach(name, function (value, key) {
name[key] = function () {
debug.deps.push({
name: key,
imports: annotate(value)
});
return value.apply(this, arguments);
};
});
} else {
debug.deps.push({
name: name,
imports: annotate(definition)
});
}
return temp.apply(this, arguments);
};
});
$provide.decorator('$rootScope', function ($delegate) {
var watchFnToHumanReadableString = function (fn) {
if (fn.exp) {
return fn.exp.trim();
} else if (fn.name) {
return fn.name.trim();
} else {
return fn.toString();
}
};
var applyFnToLogString = function (fn) {
var str;
if (fn) {
if (fn.name) {
str = fn.name;
} else if (fn.toString().split('\n').length > 1) {
str = 'fn () { ' + fn.toString().split('\n')[1].trim() + ' /* ... */ }';
} else {
str = fn.toString().trim().substr(0, 30) + '...';
}
} else {
str = '$apply';
}
return str;
};
// patch registering watchers
// ==========================
var _watch = $delegate.__proto__.$watch;
$delegate.__proto__.$watch = function (watchExpression, applyFunction) {
var thatScope = this;
var watchStr = watchFnToHumanReadableString(watchExpression);
if (!debug.watchPerf[watchStr]) {
debug.watchPerf[watchStr] = {
time: 0,
calls: 0
};
}
if (!debug.watchers[thatScope.$id]) {
debug.watchers[thatScope.$id] = [];
}
debug.watchers[thatScope.$id].push(watchStr);
// patch watchExpression
// ---------------------
var w = watchExpression;
if (typeof w === 'function') {
watchExpression = function () {
var start = performance.now();
var ret = w.apply(this, arguments);
var end = performance.now();
debug.watchPerf[watchStr].time += (end - start);
debug.watchPerf[watchStr].calls += 1;
return ret;
};
} else {
watchExpression = function () {
var start = performance.now();
var ret = thatScope.$eval(w);
var end = performance.now();
debug.watchPerf[watchStr].time += (end - start);
debug.watchPerf[watchStr].calls += 1;
return ret;
};
}
// patch applyFunction
// -------------------
if (typeof applyFunction === 'function') {
var applyStr = applyFunction.toString();
var unpatchedApplyFunction = applyFunction;
applyFunction = function () {
var start = performance.now();
var ret = unpatchedApplyFunction.apply(this, arguments);
var end = performance.now();
debug.scopeDirty[this.$id] = true;
//TODO: move these checks out of here and into registering the watcher
if (!debug.applyPerf[applyStr]) {
debug.applyPerf[applyStr] = {
time: 0,
calls: 0
};
}
debug.applyPerf[applyStr].time += (end - start);
debug.applyPerf[applyStr].calls += 1;
return ret;
};
}
return _watch.apply(this, arguments);
};
// patch $destroy
// --------------
var _destroy = $delegate.__proto__.$destroy;
$delegate.__proto__.$destroy = function () {
if (debug.watchers[this.$id]) {
delete debug.watchers[this.$id];
}
if (debug.models[this.$id]) {
delete debug.models[this.$id];
}
if (debug.scopes[this.$id]) {
delete debug.scopes[this.$id];
}
return _destroy.apply(this, arguments);
};
// patch $new
// ----------
var _new = $delegate.__proto__.$new;
$delegate.__proto__.$new = function () {
var ret = _new.apply(this, arguments);
if (ret.$root) {
debug.rootScopes[ret.$root.$id] = ret.$root;
debug.scopeTreeDirty[ret.$root.$id] = true;
}
// create empty watchers array for this scope
if (!debug.watchers[ret.$id]) {
debug.watchers[ret.$id] = [];
}
debug.scopes[ret.$id] = ret;
debug.scopes[this.$id] = this;
debug.scopeDirty[ret.$id] = true;
return ret;
};
// patch $digest
// -------------
var _digest = $delegate.__proto__.$digest;
$delegate.__proto__.$digest = function (fn) {
var ret = _digest.apply(this, arguments);
debug.scopeDirty[this.$id] = true;
return ret;
};
// patch $apply
// ------------
var _apply = $delegate.__proto__.$apply;
$delegate.__proto__.$apply = function (fn) {
var start = performance.now();
var ret = _apply.apply(this, arguments);
var end = performance.now();
debug.scopeDirty[this.$id] = true;
// If the debugging option is enabled, log to console
// --------------------------------------------------
if (debug.log) {
console.log(applyFnToLogString(fn) + '\t\t' + (end - start).toPrecision(4) + 'ms');
}
return ret;
};
return $delegate;
});
});
};
// Return a script element with the above code embedded in it
var script = window.document.createElement('script');
script.innerHTML = '(' + fn.toString() + '(window))';
return script;
}()));
};
// only inject if cookie is set
if (document.cookie.indexOf('__ngDebug=true') != -1) {
document.addEventListener('DOMContentLoaded', inject);
}

File diff suppressed because it is too large Load Diff

14760
js/lib/angular.js vendored

File diff suppressed because it is too large Load Diff

4645
js/lib/d3.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,6 @@
// Broadcast poll events
angular.module('panelApp', []).run(function ($rootScope) {
setInterval(function () {
$rootScope.$broadcast('poll');
}, 500);
});

@ -0,0 +1,101 @@
// Service for running code in the context of the application being debugged
angular.module('panelApp').factory('appContext', function (chromeExtension) {
// Public API
// ==========
return {
// TODO: Fix selection of scope
// https://github.com/angular/angularjs-batarang/issues/6
executeOnScope: function(scopeId, fn, args, cb) {
if (typeof args === 'function') {
cb = args;
args = {};
} else if (!args) {
args = {};
}
args.scopeId = scopeId;
args.fn = fn.toString();
chromeExtension.eval("function (window, args) {" +
"var elts = window.document.getElementsByClassName('ng-scope'), i;" +
"for (i = 0; i < elts.length; i++) {" +
"(function (elt) {" +
"var $scope = window.angular.element(elt).scope();" +
"if ($scope.$id === args.scopeId) {" +
"(" + args.fn + "($scope, elt, args));" +
"}" +
"}(elts[i]));" +
"}" +
"}", args, cb);
},
refresh: function (cb) {
chromeExtension.eval(function (window) {
window.document.location.reload();
}, cb);
},
inspect: function (scopeId) {
this.executeOnScope(scopeId, function (scope, elt) {
inspect(elt);
});
},
// Settings
// --------
// takes a bool
setDebug: function (setting) {
if (setting) {
chromeExtension.eval(function (window) {
window.document.cookie = '__ngDebug=true;';
window.document.location.reload();
});
} else {
chromeExtension.eval(function (window) {
window.document.cookie = '__ngDebug=false;';
window.document.location.reload();
});
}
},
getDebug: function (cb) {
chromeExtension.eval(function (window) {
return document.cookie.indexOf('__ngDebug=true') !== -1;
}, cb);
},
// takes a bool
setLog: function (setting) {
setting = !!setting;
chromeExtension.eval('function (window) {' +
'window.__ngDebug.log = ' + setting.toString() + ';' +
'}');
},
// Registering events
// ------------------
// TODO: depreciate this; only poll from now on?
// There are some cases where you need to gather data on a once-per-bootstrap basis, for
// instance getting the version of AngularJS
// TODO: move to chromeExtension?
watchRefresh: function (cb) {
var port = chrome.extension.connect();
port.postMessage({
action: 'register',
inspectedTabId: chrome.devtools.inspectedWindow.tabId
});
port.onMessage.addListener(function(msg) {
if (msg === 'refresh') {
cb();
}
});
port.onDisconnect.addListener(function (a) {
console.log(a);
});
}
};
});

@ -0,0 +1,23 @@
// Service for injecting CSS into the application
angular.module('panelApp').factory('appCss', function (chromeExtension) {
return {
addCssRule: function (args) {
chromeExtension.eval(function (window, args) {
var styleSheet = document.styleSheets[document.styleSheets.length - 1];
styleSheet.insertRule(args.selector + '{' + args.style + '}', styleSheet.cssRules.length);
}, args);
},
removeCssRule: function (args) {
chromeExtension.eval(function (window, args) {
var styleSheet = document.styleSheets[document.styleSheets.length - 1];
var i;
for (i = styleSheet.cssRules.length - 1; i >= 0; i -= 1) {
if (styleSheet.cssRules[i].cssText === args.selector + ' { ' + args.style + '; }') {
styleSheet.deleteRule(i);
}
}
}, args);
}
};
});

@ -0,0 +1,26 @@
// Service for retrieving and caching application dependencies
angular.module('panelApp').factory('appDeps', function (chromeExtension, appContext) {
var _depsCache = [];
// clear cache on page refresh
appContext.watchRefresh(function () {
_depsCache = [];
});
return {
get: function (callback) {
chromeExtension.eval(function (window) {
if (window.__ngDebug) {
return window.__ngDebug.getDeps();
}
},
function (data) {
if (data) {
_depsCache = data;
}
callback(_depsCache);
});
}
};
});

@ -0,0 +1,39 @@
// Service for highlighting parts of the application
angular.module('panelApp').factory('appHighlight', function (appCss) {
//TODO: improve look of highlighting; for instance, if an element is bound and a scope,
// you will only see the most recently applied outline
var styles = {
scopes: {
selector: '.ng-scope',
style: 'border: 1px solid red'
},
bindings: {
selector: '.ng-binding',
style: 'border: 1px solid blue'
},
app: {
selector: '[ng-app]',
style: 'border: 1px solid green'
}
};
var api = {};
for (i in styles) {
if (styles.hasOwnProperty(i)) {
// create closure around "styles"
(function () {
var style = styles[i];
api[i] = function (setting) {
if (setting) {
appCss.addCssRule(style);
} else {
appCss.removeCssRule(style);
}
}
}());
}
}
return api;
});

@ -0,0 +1,67 @@
// Service for running code in the context of the application being debugged
angular.module('panelApp').factory('appInfo', function (chromeExtension, appContext) {
var _versionCache = null,
_srcCache = null;
// clear cache on page refresh
appContext.watchRefresh(function () {
_versionCache = null;
_srcCache = null;
});
return {
getAngularVersion: function (callback) {
if (_versionCache) {
setTimeout(function () {
callback(_versionCache);
}, 0);
} else {
chromeExtension.eval(function () {
return window.angular.version.full +
' ' +
window.angular.version.codeName;
}, function (data) {
_versionCache = data;
callback(_versionCache);
});
}
},
getAngularSrc: function (callback) {
if (_srcCache) {
setTimeout(function () {
callback(_srcCache);
}, 0);
} else {
chromeExtension.eval(function (window, args) {
if (!window.angular) {
return 'info';
}
var elts = window.angular.element('script[src]');
var re = /\/angular(-\d+(\.(\d+))+(rc)?)?(\.min)?\.js$/;
var elt;
for (i = 0; i < elts.length; i++) {
elt = elts[i];
if (re.exec(elt.src)) {
if (elt.src.indexOf('code.angularjs.org') !== -1) {
return 'error';
} else if (elt.src.indexOf('ajax.googleapis.com') !== -1) {
return 'good';
} else {
return 'info';
}
}
}
return 'info';
}, function (src) {
if (src) {
_srcCache = src;
}
callback(_srcCache);
});
}
}
};
});

@ -0,0 +1,27 @@
// Service for highlighting parts of the application
angular.module('panelApp').factory('appInspect', function (chromeExtension) {
return {
enable: function () {
chromeExtension.eval(function (window) {
var angular = window.angular;
var popover = angular.element('<div style="position: fixed; left: 10px; top: 10px; z-index: 9999; background-color: white; padding: 10px;"></div>');
angular.element(window.document.body).append(popover);
angular.element('.ng-scope').
on('mouseover', function () {
var thisElt = this;
var thisScope = angular.element(this).scope();
var models = {};
for (prop in thisScope) {
if (thisScope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') {
models[prop] = thisScope[prop];
}
}
var str = JSON.stringify(models);
console.log(str);
//console.log(thisScope);
popover.html(str);
});
});
}
};
});

@ -0,0 +1,66 @@
// Service for running code in the context of the application being debugged
angular.module('panelApp').factory('appModel', function (chromeExtension, appContext) {
var _scopeTreeCache = {},
_scopeCache = {},
_rootScopeCache = [];
// clear cache on page refresh
appContext.watchRefresh(function () {
_scopeCache = {};
_rootScopeCache = [];
});
return {
getRootScopes: function (callback) {
chromeExtension.eval(function (window) {
if (!window.__ngDebug) {
return;
}
return window.__ngDebug.getRootScopeIds();
},
function (data) {
if (data) {
_rootScopeCache = data;
}
callback(_rootScopeCache);
});
},
// only runs callback if model has changed since last call
getModel: function (id, callback) {
if (!id) {
return;
}
chromeExtension.eval(function (window, args) {
return window.__ngDebug.getModel(args.id);
}, {id: id}, function (tree) {
if (tree) {
_scopeCache[id] = tree;
}
callback(_scopeCache[id]);
});
},
getScopeTree: function (id, callback) {
if (!id) {
return;
}
chromeExtension.eval(function (window, args) {
return window.__ngDebug.getScopeTree(args.id);
}, {id: id}, function (tree) {
if (tree) {
_scopeTreeCache[id] = tree;
}
callback(_scopeTreeCache[id]);
});
},
enableInspector: function (argument) {
chromeExtension.eval(function (window, args) {
return window.__ngDebug.enable();
});
}
};
});

@ -0,0 +1,62 @@
// Service for retrieving and caching performance data
angular.module('panelApp').factory('appPerf', function (chromeExtension, appContext) {
var _histogramCache = [],
_watchNameToPerf = {},
_totalCache = 0;
var clear = function () {
_histogramCache = [];
_watchNameToPerf = {};
_totalCache = 0;
};
// clear cache on page refresh
appContext.watchRefresh(function () {
clear();
});
var getHistogramData = function (callback) {
chromeExtension.eval(function (window) {
if (!window.__ngDebug) {
return {};
}
return window.__ngDebug.getWatchPerf();
},
function (data) {
if (data && data.length) {
updateHistogram(data);
}
callback();
});
};
var updateHistogram = function (data) {
data.forEach(function (info) {
_totalCache += info.time;
if (_watchNameToPerf[info.name]) {
_watchNameToPerf[info.name].time += info.time;
} else {
_watchNameToPerf[info.name] = info;
_histogramCache.push(info);
}
});
// recalculate all percentages
_histogramCache.forEach(function (item) {
item.percent = (100 * item.time / _totalCache).toPrecision(3);
});
};
// Public API
// ==========
return {
get: function (callback) {
getHistogramData(function () {
callback(_histogramCache);
});
},
clear: clear
};
});

@ -0,0 +1,22 @@
// Service for running code in the context of the application being debugged
angular.module('panelApp').factory('appWatch', function (chromeExtension) {
var _watchCache = {};
// Public API
// ==========
return {
getWatchTree: function (id, callback) {
chromeExtension.eval("function (window, args) {" +
"return window.__ngDebug.getWatchTree(args.id);" +
"}", {id: id}, function (tree) {
if (tree) {
_watchCache[id] = tree;
}
callback(_watchCache[id]);
});
}
};
});

@ -0,0 +1,27 @@
// abstraction layer for Chrome Extension APIs
angular.module('panelApp').value('chromeExtension', {
sendRequest: function (requestName, cb) {
chrome.extension.sendRequest({
script: requestName,
tab: chrome.devtools.inspectedWindow.tabId
}, cb || function () {});
},
// evaluates in the context of a window
//written because I don't like the API for chrome.devtools.inspectedWindow.eval;
// passing strings instead of functions are gross.
eval: function (fn, args, cb) {
// with two args
if (!cb && typeof args === 'function') {
cb = args;
args = {};
} else if (!args) {
args = {};
}
chrome.devtools.inspectedWindow.eval('(' +
fn.toString() +
'(window, ' +
JSON.stringify(args) +
'));', cb);
}
});

@ -0,0 +1,30 @@
// Service for exporting as JSON
angular.module('panelApp').factory('filesystem', function(chromeExtension) {
// taken from:
// http://html5-demos.appspot.com/static/html5storage/index.html#slide59
// TODO: error handlers?
return {
exportJSON: function (name, data) {
//TODO: file size/limits? 1024*1024
window.webkitRequestFileSystem(window.TEMPORARY, 1024*1024, function (fs) {
fs.root.getFile(name + '.json', {create: true}, function (fileEntry) {
fileEntry.createWriter(function(fileWriter) {
var blob = new Blob([ JSON.stringify(data) ], { type: 'text/plain' });
fileWriter.onwriteend = function () {
// navigate to file, will download
//location.href = fileEntry.toURL();
window.open(fileEntry.toURL());
};
fileWriter.write(blob);
}, function() {});
}, function() {});
}, function() {});
}
};
});

@ -0,0 +1,24 @@
files = [
JASMINE,
JASMINE_ADAPTER,
'js/lib/angular.js',
'js/lib/angular-mocks.js',
'js/panelApp.js',
'js/controllers/*.js',
'js/directives/*.js',
'js/filters/*.js',
'js/services/*.js',
'test/mock/*.js',
'test/*.js'
];
exclude = [];
autoWatch = true;
autoWatchInterval = 1;
logLevel = LOG_INFO;
logColors = true;

@ -1,31 +0,0 @@
/*
* This karma conf tests just the panel app
*/
var sauceConfig = require('./config/karma.sauce.conf');
var travisConfig = require('./config/karma.travis.conf');
module.exports = function(config) {
var options = {
frameworks: ['browserify', 'jasmine'],
files: [
'node_modules/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
'panel/app.js',
'panel/**/*.js',
'panel/**/*.spec.js'
],
exclude: [],
preprocessors: {
'hint.js': [ 'browserify' ]
},
browsers: ['Chrome'],
};
if (process.argv.indexOf('--sauce') > -1) {
sauceConfig(options);
travisConfig(options);
}
config.set(options);
};

@ -0,0 +1,24 @@
files = [
JASMINE,
JASMINE_ADAPTER,
'js/lib/angular.js',
'js/lib/angular-mocks.js',
'js/panelApp.js',
'js/controllers/*.js',
'js/directives/*.js',
'js/filters/*.js',
'js/services/*.js',
'test/mock/*.js',
'test/*.js'
];
exclude = [];
autoWatch = true;
autoWatchInterval = 1;
logLevel = LOG_INFO;
logColors = true;

@ -1,432 +0,0 @@
/**
* @license AngularJS v1.3.0-build.3042+sha.76e57a7
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function() {'use strict';
/**
* @description
*
* This object provides a utility for producing rich Error messages within
* Angular. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
*
* The above creates an instance of minErr in the example namespace. The
* resulting error will have a namespaced error code of example.one. The
* resulting error will replace {0} with the value of foo, and {1} with the
* value of bar. The object is not restricted in the number of arguments it can
* take.
*
* If fewer arguments are specified than necessary for interpolation, the extra
* interpolation markers will be preserved in the final string.
*
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
* using minErr('namespace') . Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
* @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance
*/
function minErr(module) {
return function () {
var code = arguments[0],
prefix = '[' + (module ? module + ':' : '') + code + '] ',
template = arguments[1],
templateArgs = arguments,
stringify = function (obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (typeof obj !== 'string') {
return JSON.stringify(obj);
}
return obj;
},
message, i;
message = prefix + template.replace(/\{\d+\}/g, function (match) {
var index = +match.slice(1, -1), arg;
if (index + 2 < templateArgs.length) {
arg = templateArgs[index + 2];
if (typeof arg === 'function') {
return arg.toString().replace(/ ?\{[\s\S]*$/, '');
} else if (typeof arg === 'undefined') {
return 'undefined';
} else if (typeof arg !== 'string') {
return toJson(arg);
}
return arg;
}
return match;
});
message = message + '\nhttp://errors.angularjs.org/1.3.0-build.3042+sha.76e57a7/' +
(module ? module + '/' : '') + code;
for (i = 2; i < arguments.length; i++) {
message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' +
encodeURIComponent(stringify(arguments[i]));
}
return new Error(message);
};
}
/**
* @ngdoc type
* @name angular.Module
* @module ng
* @description
*
* Interface for configuring angular {@link angular.module modules}.
*/
function setupModuleLoader(window) {
var $injectorMinErr = minErr('$injector');
var ngMinErr = minErr('ng');
function ensure(obj, name, factory) {
return obj[name] || (obj[name] = factory());
}
var angular = ensure(window, 'angular', Object);
// We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap
angular.$$minErr = angular.$$minErr || minErr;
return ensure(angular, 'module', function() {
/** @type {Object.<string, angular.Module>} */
var modules = {};
/**
* @ngdoc function
* @name angular.module
* @module ng
* @description
*
* The `angular.module` is a global place for creating, registering and retrieving Angular
* modules.
* All modules (angular core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
* When passed two or more arguments, a new module is created. If passed only one argument, an
* existing module (the name passed as the first argument to `module`) is retrieved.
*
*
* # Module
*
* A module is a collection of services, directives, controllers, filters, and configuration information.
* `angular.module` is used to configure the {@link auto.$injector $injector}.
*
* ```js
* // Create a new module
* var myModule = angular.module('myModule', []);
*
* // register a new service
* myModule.value('appName', 'MyCoolApp');
*
* // configure existing services inside initialization blocks.
* myModule.config(['$locationProvider', function($locationProvider) {
* // Configure existing providers
* $locationProvider.hashPrefix('!');
* }]);
* ```
*
* Then you can create an injector and load your modules like this:
*
* ```js
* var injector = angular.injector(['ng', 'myModule'])
* ```
*
* However it's more likely that you'll just use
* {@link ng.directive:ngApp ngApp} or
* {@link angular.bootstrap} to simplify this process for you.
*
* @param {!string} name The name of the module to create or retrieve.
* @param {!Array.<string>=} requires If specified then new module is being created. If
* unspecified then the module is being retrieved for further configuration.
* @param {Function=} configFn Optional configuration function for the module. Same as
* {@link angular.Module#config Module#config()}.
* @returns {module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
};
assertNotHasOwnProperty(name, 'module');
if (requires && modules.hasOwnProperty(name)) {
modules[name] = null;
}
return ensure(modules, name, function() {
if (!requires) {
throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
"the module name or forgot to load it. If registering a module ensure that you " +
"specify the dependencies as the second argument.", name);
}
/** @type {!Array.<Array.<*>>} */
var invokeQueue = [];
/** @type {!Array.<Function>} */
var configBlocks = [];
/** @type {!Array.<Function>} */
var runBlocks = [];
var config = invokeLater('$injector', 'invoke', 'push', configBlocks);
/** @type {angular.Module} */
var moduleInstance = {
// Private state
_configBlocks: [],
_runBlocks: runBlocks,
/**
* @ngdoc property
* @name angular.Module#requires
* @module ng
* @returns {Array.<string>} List of module names which must be loaded before this module.
* @description
* Holds the list of modules which the injector will load before the current module is
* loaded.
*/
requires: requires,
/**
* @ngdoc property
* @name angular.Module#name
* @module ng
* @returns {string} Name of the module.
* @description
*/
name: name,
/**
* @ngdoc method
* @name angular.Module#provider
* @module ng
* @param {string} name service name
* @param {Function} providerType Construction function for creating new instance of the
* service.
* @description
* See {@link auto.$provide#provider $provide.provider()}.
*/
provider: invokeLater('$provide', 'provider'),
/**
* @ngdoc method
* @name angular.Module#factory
* @module ng
* @param {string} name service name
* @param {Function} providerFunction Function for creating new instance of the service.
* @description
* See {@link auto.$provide#factory $provide.factory()}.
*/
factory: invokeLater('$provide', 'factory'),
/**
* @ngdoc method
* @name angular.Module#service
* @module ng
* @param {string} name service name
* @param {Function} constructor A constructor function that will be instantiated.
* @description
* See {@link auto.$provide#service $provide.service()}.
*/
service: invokeLater('$provide', 'service'),
/**
* @ngdoc method
* @name angular.Module#value
* @module ng
* @param {string} name service name
* @param {*} object Service instance object.
* @description
* See {@link auto.$provide#value $provide.value()}.
*/
value: invokeLater('$provide', 'value'),
/**
* @ngdoc method
* @name angular.Module#constant
* @module ng
* @param {string} name constant name
* @param {*} object Constant value.
* @description
* Because the constant are fixed, they get applied before other provide methods.
* See {@link auto.$provide#constant $provide.constant()}.
*/
constant: invokeLater('$provide', 'constant', 'unshift'),
/**
* @ngdoc method
* @name angular.Module#animation
* @module ng
* @param {string} name animation name
* @param {Function} animationFactory Factory function for creating new instance of an
* animation.
* @description
*
* **NOTE**: animations take effect only if the **ngAnimate** module is loaded.
*
*
* Defines an animation hook that can be later used with
* {@link ngAnimate.$animate $animate} service and directives that use this service.
*
* ```js
* module.animation('.animation-name', function($inject1, $inject2) {
* return {
* eventName : function(element, done) {
* //code to run the animation
* //once complete, then run done()
* return function cancellationFunction(element) {
* //code to cancel the animation
* }
* }
* }
* })
* ```
*
* See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and
* {@link ngAnimate ngAnimate module} for more information.
*/
animation: invokeLater('$animateProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#filter
* @module ng
* @param {string} name Filter name.
* @param {Function} filterFactory Factory function for creating new instance of filter.
* @description
* See {@link ng.$filterProvider#register $filterProvider.register()}.
*/
filter: invokeLater('$filterProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#controller
* @module ng
* @param {string|Object} name Controller name, or an object map of controllers where the
* keys are the names and the values are the constructors.
* @param {Function} constructor Controller constructor function.
* @description
* See {@link ng.$controllerProvider#register $controllerProvider.register()}.
*/
controller: invokeLater('$controllerProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#directive
* @module ng
* @param {string|Object} name Directive name, or an object map of directives where the
* keys are the names and the values are the factories.
* @param {Function} directiveFactory Factory function for creating new instance of
* directives.
* @description
* See {@link ng.$compileProvider#directive $compileProvider.directive()}.
*/
directive: invokeLater('$compileProvider', 'directive'),
/**
* @ngdoc method
* @name angular.Module#config
* @module ng
* @param {Function} configFn Execute this function on module load. Useful for service
* configuration.
* @description
* Use this method to register work which needs to be performed on module loading.
* For more about how to configure services, see
* {@link providers#providers_provider-recipe Provider Recipe}.
*/
config: config,
/**
* @ngdoc method
* @name angular.Module#run
* @module ng
* @param {Function} initializationFn Execute this function after injector creation.
* Useful for application initialization.
* @description
* Use this method to register work which should be performed when the injector is done
* loading all modules.
*/
run: function(block) {
runBlocks.push(block);
return this;
}
};
/**
* ANGULAR HINT ALTERATION
* To make this loader compatible with apps that are running
* both Angular 1.2 and 1.3, the loader must handle 1.3 applications
* that expect to initialize their config blocks after all providers
* are registered. Hence, the configBlocks are added to the end
* of the exisiting invokeQueue.
*/
Object.defineProperty(moduleInstance, '_invokeQueue', {
get: function() {
return invokeQueue.concat(configBlocks);
}
});
if (configFn) {
config(configFn);
}
return moduleInstance;
/**
* @param {string} provider
* @param {string} method
* @param {String=} insertMethod
* @returns {angular.Module}
*/
function invokeLater(provider, method, insertMethod, queue) {
if (!queue) queue = invokeQueue;
return function() {
queue[insertMethod || 'push']([provider, method, arguments]);
return moduleInstance;
};
}
});
};
});
}
setupModuleLoader(window);
})(window);
/**
* Closure compiler type information
*
* @typedef { {
* requires: !Array.<string>,
* invokeQueue: !Array.<Array.<*>>,
*
* service: function(string, Function):angular.Module,
* factory: function(string, Function):angular.Module,
* value: function(string, *):angular.Module,
*
* filter: function(string, Function):angular.Module,
*
* init: function(Function):angular.Module
* } }
*/
angular.Module;

@ -1,44 +1,22 @@
{
"name": "AngularJS Batarang",
"version": "0.7.4",
"version": "0.4.2",
"description": "Extends the Developer Tools, adding tools for debugging and profiling AngularJS applications.",
"background": {
"page": "background.html"
},
"devtools_page": "devtoolsBackground.html",
"manifest_version": 2,
"permissions": [
"tabs",
"<all_urls>"
],
"icons": {
"16": "img/webstore-icon.png",
"48": "img/webstore-icon.png",
"128": "img/webstore-icon.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["inject.js"],
"js": ["js/inject/debug.js"],
"run_at": "document_start"
}
],
"background": {
"scripts": [
"background.js"
]
},
"page_action": {
"default_icon": {
"19": "img/icon19.png",
"38": "img/icon38.png"
},
"default_title": "AngularJS Super-Powered"
},
"web_accessible_resources": [
"dist/hint.js"
],
"minimum_chrome_version": "21.0.1180.57"
}

@ -1,41 +1,13 @@
{
"name": "angularjs-batarang",
"version": "0.7.4",
"description": "chrome extension for inspecting angular apps",
"main": "hint.js",
"devDependencies": {
"angular-mocks": "^1.3.6",
"gulp-zip": "^2.0.2",
"karma-bro": "^0.6.0",
"karma-chrome-launcher": "^0.1.4",
"karma-sauce-launcher": "^0.2.9",
"karma-jasmine": "^0.1.5"
},
"dependencies": {
"angular": "^1.3.6",
"angular-hint": "~0.0.0",
"browserify": "^5.9.1",
"gulp": "^3.8.7",
"vinyl-source-stream": "^0.1.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "gulp"
},
"repository": {
"type": "git",
"url": "https://github.com/angular/angularjs-batarang.git"
},
"keywords": [
"angular",
"angularjs",
"chrome",
"extension"
],
"author": "Brian Ford <btford@umich.edu>",
"license": "MIT",
"bugs": {
"url": "https://github.com/angular/angularjs-batarang/issues"
},
"homepage": "https://github.com/angular/angularjs-batarang"
"grunt": "~0.4.1",
"grunt-contrib-jshint": "~0.1.1",
"grunt-conventional-changelog": "0.0.11",
"grunt-karma": "~0.4.3",
"grunt-release": "https://github.com/btford/grunt-release/archive/feat-tag-name.tar.gz",
"marked": "~0.2.8",
"grunt-zip": "~0.7.0",
"semver": "~1.1.4"
}
}

@ -0,0 +1,55 @@
<!doctype html>
<html ng-csp ng-app="panelApp">
<head>
<link rel="stylesheet" href="css/bootstrap.css">
<link rel="stylesheet" href="css/bootstrap-responsive.css">
<link rel="stylesheet" href="css/d3.css">
<link rel="stylesheet" href="css/panel.css">
<!-- libs -->
<script src="js/lib/angular.js"></script>
<script src="js/lib/jquery-1.7.2.min.js"></script>
<script src="js/lib/jquery-ui-1.8.21.custom.min.js"></script>
<script src="js/lib/d3.js"></script>
<script src="js/lib/d3.layout.js"></script>
<script src="js/panelApp.js"></script>
<script src="js/directives/d3.js"></script>
<script src="js/directives/jsonTree.js"></script>
<script src="js/directives/scopeTree.js"></script>
<script src="js/directives/slider.js"></script>
<script src="js/directives/tabs.js"></script>
<script src="js/directives/watcherTree.js"></script>
<script src="js/filters/first.js"></script>
<script src="js/filters/precision.js"></script>
<script src="js/filters/sortByTime.js"></script>
<script src="js/services/appContext.js"></script>
<script src="js/services/appCss.js"></script>
<script src="js/services/appDeps.js"></script>
<script src="js/services/appHighlight.js"></script>
<script src="js/services/appInfo.js"></script>
<script src="js/services/appModel.js"></script>
<script src="js/services/appPerf.js"></script>
<script src="js/services/appWatch.js"></script>
<script src="js/services/chromeExtension.js"></script>
<script src="js/services/filesystem.js"></script>
<script src="js/controllers/DepsCtrl.js"></script>
<script src="js/controllers/ModelCtrl.js"></script>
<script src="js/controllers/OptionsCtrl.js"></script>
<script src="js/controllers/PerfCtrl.js"></script>
</head>
<body>
<bat-tabs>
<bat-pane title="Models" src="panes/model.html"></bat-pane>
<bat-pane title="Performance" src="panes/perf.html"></bat-pane>
<bat-pane title="Dependencies" src="panes/deps.html"></bat-pane>
<bat-pane title="Options" src="panes/options.html"></bat-pane>
<bat-pane title="Help" src="panes/help.html"></bat-pane>
</bat-tabs>
</body>
</html>

@ -1,305 +0,0 @@
.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;
}

@ -1,32 +0,0 @@
<!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="../node_modules/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>

@ -1,16 +0,0 @@
'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]);

@ -1,28 +0,0 @@
'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('');
});
}

@ -1,131 +0,0 @@
'use strict';
angular.module('batarang.inspected-app', []).
service('inspectedApp', ['$rootScope', '$q', inspectedAppService]);
function inspectedAppService($rootScope, $q) {
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);
};
this.assign = function (scopeId, path, value) {
return invokeAngularHintMethod('assign', scopeId, path, value);
};
this.enableInstrumentation = function (setting) {
setting = !!setting;
chrome.devtools.inspectedWindow.eval(
"(function () {" +
"var prev = document.cookie.indexOf('__ngDebug=true') !== -1;" +
"if (prev !== " + setting + ") {" +
"window.document.cookie = '__ngDebug=" + setting + ";';" +
"window.document.location.reload();" +
"}" +
"}())"
);
};
this.getInstrumentationStatus = function () {
return $q(function(resolve, reject) {
chrome.devtools.inspectedWindow.eval(
"document.cookie.indexOf('__ngDebug=true') !== -1", resolve);
});
};
/*
* sets window.$scope to the scope of the given id
*/
this.inspectScope = function (scopeId) {
return invokeAngularHintMethod('inspectScope', scopeId);
};
function invokeAngularHintMethod(method, scopeId, path, value) {
var args = [parseInt(scopeId, 10), path || ''].
map(JSON.stringify).
concat(value ? [value] : []).
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();
$rootScope.$broadcast('refresh');
} else if (typeof msg === 'string') {
var hint = JSON.parse(msg);
onHintMessage(hint);
} else if (typeof msg === 'object') {
onHintMessage(msg);
}
});
});
port.onDisconnect.addListener(function (a) {
console.log(a);
});
function onHintMessage(hint) {
if (hint.message) {
hints.push(hint);
} else if (hint.event) {
if (hint.event === 'hydrate') {
Object.keys(hint.data.scopes).forEach(function (scopeId) {
scopes[scopeId] = hint.data.scopes[scopeId];
});
hint.data.hints.forEach(function (hint) {
hints.push(hint);
});
} else if (hint.event === 'scope:new') {
addNewScope(hint);
} else if (hint.id && scopes[hint.id]) {
var scope = scopes[hint.id];
if (hint.event === 'scope:destroy') {
if (scope.parent) {
scope.parent.children.splice(scope.parent.children.indexOf(child), 1);
}
delete scopes[hint.id];
} else if (hint.event === 'model:change') {
scope.models[hint.path] = (typeof hint.value === 'undefined') ?
undefined : JSON.parse(hint.value);
} else if (hint.event === 'scope:link') {
scope.descriptor = hint.descriptor;
}
}
$rootScope.$broadcast(hint.event, hint);
}
}
function onRefreshMessage() {
clear(scopes);
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);
}
}
function clear (obj) {
Object.keys(obj).forEach(function (key) {
delete obj[key];
});
}
}

@ -1,111 +0,0 @@
'use strict';
describe('inspectedApp', function() {
var inspectedApp, port;
beforeEach(function() {
module('batarang.inspected-app')
window.chrome = createMockChrome();
inject(function(_inspectedApp_) {
inspectedApp = _inspectedApp_;
});
});
describe('when instantiated', function () {
it('should post a message with the inspected tabId', function () {
expect(port.postMessage).
toHaveBeenCalledWith(window.chrome.devtools.inspectedWindow.tabId);
});
});
describe('messaging', function () {
it('should track hints', inject(function ($browser) {
port.onMessage.trigger(JSON.stringify({ message: 'hi' }));
$browser.defer.flush();
expect(inspectedApp.hints).toEqual([{message: 'hi'}]);
}));
it('should track new scopes', inject(function ($browser) {
port.onMessage.trigger(JSON.stringify({ event: 'scope:new', child: 1 }));
$browser.defer.flush();
expect(inspectedApp.scopes).toEqual({ 1: { parent: undefined, children: [], models: {} } });
}));
it('should track updates to scope descriptors', inject(function ($browser) {
port.onMessage.trigger(JSON.stringify({ event: 'scope:new', child: 1 }));
port.onMessage.trigger(JSON.stringify({ event: 'scope:link', id: 1, descriptor: 'pasta' }));
$browser.defer.flush();
expect(inspectedApp.scopes[1].descriptor).toBe('pasta');
}));
})
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,"")');
});
});
describe('inspectScope', function () {
it('should call chrome devtools APIs', function() {
inspectedApp.inspectScope(2);
expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalledWith('angular.hint.inspectScope(2,"")');
});
});
function createMockChrome() {
return {
extension: {
connect: function () {
return port = createMockSocket();
}
},
devtools: {
inspectedWindow: {
tabId: 1,
eval: jasmine.createSpy('inspectedWindowEval')
}
}
};
}
});
function createListenerSpy(name) {
var symbol = '_' + name;
var listener = {
addListener: function (fn) {
listener[symbol].push(fn);
},
removeListener: function (fn) {
listener[symbol].splice(fn, 1);
},
trigger: function () {
var args = arguments;
listener[symbol].forEach(function (fn) {
fn.apply(listener, args);
});
}
};
listener[symbol] = [];
return listener;
}
function createMockSocket() {
return {
onMessage: createListenerSpy('messageFunction'),
postMessage: jasmine.createSpy('postMessageFunction'),
onDisconnect: createListenerSpy('onDisconnect')
};
}

@ -1,76 +0,0 @@
/* 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,
.console-formatted-boolean {
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;
}

@ -1,188 +0,0 @@
angular.module('batarang.json-tree', []).
directive('batJsonTree', [batJsonTreeDirective]);
var BAT_JSON_TREE_TEMPLATE = '<div class="properties-tree"></div>';
var ENTER_KEY = 13,
EXIT_KEY = 27;
var BAT_JSON_TREE_UNEDITABLE = [
'$id',
// managed by ngRepeat
'$first',
'$last',
'$index',
'$even',
'$odd'
];
/*
* TODO: remove dependency on inspectedApp service
*/
function batJsonTreeDirective() {
return {
restrict: 'E',
terminal: true,
scope: {
batInspect: '&',
batAssign: '&',
batModel: '='
},
link: jsonTreeLinkFn
};
function jsonTreeLinkFn(scope, element, attrs) {
var root = angular.element(BAT_JSON_TREE_TEMPLATE);
element.append(root);
var branches = {
'': root
};
scope.$watch('batModel', function (val) {
if (!val) {
root = angular.element(BAT_JSON_TREE_TEMPLATE);
element.html('');
element.append(root);
branches = {
'': root
};
return;
}
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) {
if (Number.isNaN(parseInt(key, 10))) {
fullPath += '.' + key;
} else {
fullPath += '[' + key + ']';
}
} else {
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 () {
scope.batInspect({ path: 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 + '"';
}
// TODO: test this
// some properties (like $id) shouldn't be edited
if (BAT_JSON_TREE_UNEDITABLE.indexOf(fullPath) > -1) {
childElt = angular.element(
'<span class="console-formatted-' + (typeof val) + '">' +
val +
'</span>');
} else {
childElt = angular.element(
'<span contentEditable="true" class="console-formatted-' + (typeof val) + '">' +
val +
'</span>');
// TODO: test this
childElt.on('keydown', function (ev) {
if (ev.keyCode === ENTER_KEY || ev.keyCode === EXIT_KEY) {
ev.preventDefault();
childElt[0].blur();
doAssign();
}
});
// TODO: test this
childElt.on('blur', doAssign);
function doAssign() {
scope.batAssign({
path: fullPath,
value: childElt.text()
});
}
}
}
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;
}
}

@ -1,33 +0,0 @@
'use strict';
describe('batJsonTree', function () {
var $compile, $rootScope, element;
beforeEach(module('batarang.json-tree'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should not throw on an undefined model', function () {
expect(compileTree).not.toThrow();
});
it('should render a simple model', function () {
$rootScope.data = {
'': { '$id': 1 }
};
compileTree();
expect(element.text()).toBe('$id: 1');
});
function compileTree() {
element = compile('<bat-json-tree bat-model="data"></bat-json-tree>');
$rootScope.$apply();
}
function compile(template) {
return $compile(template)($rootScope);
}
});

@ -1,130 +0,0 @@
/* 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 {
white-space: nowrap;
}
/*
bat-scope-tree .selected {
font-weight: bold;
text-decoration: underline;
color: #333;
}
*/

@ -1,102 +0,0 @@
angular.module('batarang.scope-tree', []).
directive('batScopeTree', ['$compile', batScopeTreeDirective]);
function batScopeTreeDirective($compile) {
return {
restrict: 'E',
terminal: true,
scope: {
batModel: '='
},
link: batScopeTreeLink
};
function batScopeTreeLink(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);
});
// when a scope is linked, we can apply the descriptor info
scope.$on('scope:link', function (ev, data) {
renderScopeDescriptorElement(data.id, data.descriptor);
});
scope.$on('refresh', function () {
map = {};
element.html('');
});
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);
}
// TODO: also destroy elements corresponding to descendant scopes
scope.$on('scope:destroy', function (ev, data) {
var id = data.id;
var elt = map[id];
if (elt) {
elt.remove();
}
delete map[id];
});
}
}
// 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(''));
}

@ -1,49 +0,0 @@
<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 class="sidebar-tree-item
profile-launcher-view-tree-item">
<img class="icon">
<div class="status"></div>
<div class="titles no-subtitle">
<span class="title ng-binding">Enable <input type="checkbox" ng-model="enabled"></span>
<span class="subtitle"></span>
</div>
</li>
<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>

@ -1,87 +0,0 @@
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');
}
};
});

@ -1,28 +0,0 @@
<div class="sidebar-pane-stack audit-result-view fill visible" ng-controller="HintController">
<div></div>
<div class="sidebar-pane-title expanded"
ng-repeat-start="(groupName, group) in groupedHints">{{groupName}}
<div class="sidebar-pane-toolbar"></div>
</div>
<div class="sidebar-pane visible"
ng-repeat-end
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>

@ -1,24 +0,0 @@
'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;
if (!$scope.groupedHints[moduleName]) {
$scope.groupedHints[moduleName] = {};
}
if (!$scope.groupedHints[moduleName][category]) {
$scope.groupedHints[moduleName][category] = [];
}
$scope.groupedHints[moduleName][category].push(hint);
});
});
}

@ -1,26 +0,0 @@
/* 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;
}

@ -1,43 +0,0 @@
<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
bat-model="scopes[inspectedScope].models"
bat-inspect="inspect(path)"
bat-assign="assign(path, value)"></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>

@ -1,42 +0,0 @@
'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.inspect = function (path) {
inspectedApp.watch($scope.inspectedScope, path);
};
$scope.assign = function (path, value) {
inspectedApp.assign($scope.inspectedScope, path, value);
};
$scope.inspectedScope = null;
$scope.$on('refresh', function () {
$scope.inspectedScope = null;
});
// expand models the fist time we inspect a scope
var cancelWatch = $scope.$watch('inspectedScope', function (newScope) {
if (newScope) {
$scope.modelsExpanded = true;
cancelWatch();
}
});
$scope.$on('inspected-scope:change', function (ev, data) {
inspectScope(data.id);
});
function inspectScope(scopeId) {
$scope.watch(scopeId);
$scope.inspectedScope = scopeId;
inspectedApp.inspectScope(scopeId);
};
}

@ -0,0 +1,8 @@
<div ng-controller="DepsCtrl">
<div class="span12">
<h2>Service Dependencies</h2>
<div class="well">
<bat-d3 val="deps"></bat-d3>
</div>
</div>
</div>

@ -0,0 +1,23 @@
<p>In order to begin using the Batarang you need to click the &quot;enable&quot; checkbox. This will cause the application&#39;s tab to refresh, and the Batarang to begin collecting perfomance and debug information about the inspected app.</p>
<p>The Batarang has five tabs: Model, Performance, Dependencies, Options, and Help.</p>
<h3>Models</h3>
<p><img src="/img/models.png" alt="Batarang screenshot"></p>
<p>Starting at the top of this tab, there is the root selection. If the application has only one <code>ng-app</code> declaration (as most applications do) then you will not see the option to change roots.</p>
<p>Below that is a tree showing how scopes are nested, and which models are attached to them. Clicking on a scope name will take you to the Elements tab, and show you the DOM element associated with that scope. Models and methods attached to each scope are listed with bullet points on the tree. Just the name of methods attached to a scope are shown. Models with a simple value and complex objects are shown as JSON. You can edit either, and the changes will be reflected in the application being debugged.</p>
<h3>Performance</h3>
<p><img src="/img/perf.png" alt="Batarang performance tab screenshot"></p>
<p>The performance tab must be enabled separately because it causes code to be injected into AngularJS to track and report performance metrics. There is also an option to output performance metrics to the console.</p>
<p>Below that is a tree of watched expressions, showing which expressions are attached to which scopes. Much like the model tree, you can collapse sections by clicking on &quot;toggle&quot; and you can inspect the element that a scope is attached to by clicking on the scope name.</p>
<p>Underneath that is a graph showing the relative performance of all of the application&#39;s expressions. This graph will update as you interact with the application.</p>
<h3>Dependencies</h3>
<p><img src="/img/deps.png" alt="Batarang dependencies tab screenshot"></p>
<p>The dependencies tab shows a visualization of the application&#39;s dependencies. When you hover over a service name, services that depend on the hovered service turn green, and those the hovered service depend on turn red.</p>
<h3>Options</h3>
<p><img src="/img/options.png" alt="Batarang options tab screenshot"></p>
<p>Last, there is the options tab. The options tab has three checkboxes: one for &quot;show applications,&quot; &quot;show scopes,&quot; and &quot;show bindings.&quot; Each of these options, when enabled, highlights the respective feature of the application being debugged; scopes will have a red outline, and bindings will have a blue outline, and applications a green outline.</p>
<h3>Elements</h3>
<p><img src="/img/inspect.png" alt="Batarang console screenshot"></p>
<p>The Batarang also hooks into some of the existing features of the Chrome developer tools. For AngularJS applications, there is now a properties pane on in the Elements tab. Much like the model tree in the AngularJS tab, you can use this to inspect the models attached to a given element&#39;s scope.</p>
<h3>Console</h3>
<p><img src="/img/console.png" alt="Batarang console screenshot"></p>
<p>The Batarang exposes some convenient features to the Chrome developer tools console. To access the scope of an element selected in the Elements tab of the developer tools, in console, you can type <code>$scope</code>. If you change value of some model on <code>$scope</code> and want to have this change reflected in the running application, you need to call <code>$scope.$apply()</code> after making the change.</p>

@ -0,0 +1,29 @@
<div ng-controller="ModelCtrl">
<div class="span6">
<h2>Scopes</h2>
<div ng-show="roots.length > 1">
<label for="select-root">Root
<select
ng-options="root.toString() for root in roots"
ng-model="selectedRoot"></select>
</label>
</div>
<pre>
<bat-scope-tree
val="tree"
inspect="inspect"
select="select"
selected-scope="selectedScope"
edit="edit"></bat-scope-tree>
</pre>
</div>
<div class="span6">
<h2>Models<span ng-show="selectedScope"> for ({{selectedScope}})</span></h2>
<bat-json-tree val="model"></bat-json-tree>
</div>
<div class="span6">
<button class="btn" ng-click="enableInspector()">Enable Inspector</button>
</div>
</div>

@ -0,0 +1,30 @@
<div ng-controller="OptionsCtrl">
<div class="span6">
<h2>Options</h2>
<form class="well">
<label class="checkbox" for="app">
<input type="checkbox" ng-model="debugger.app" id="app"> Show applications
</label>
<label class="checkbox" for="bindings">
<input type="checkbox" ng-model="debugger.bindings" id="bindings"> Show bindings
</label>
<label class="checkbox" for="scopes">
<input type="checkbox" ng-model="debugger.scopes" id="scopes"> Show scopes
</label>
</form>
</div>
<div class="span6">
<h2>Info</h2>
<div class="well">
<p>Angular version: {{version}}</p>
<p>Angular CDN status: <span class="label" ng-class="'label-' + status" ng-bind-html-unsafe="explain"></span></p>
</div>
</div>
</div>

@ -0,0 +1,48 @@
<div ng-controller="PerfCtrl">
<h2>Performance</h2>
<form class="well form-inline" class="row-fluid">
<label class="checkbox span4" for="log">
<input type="checkbox" ng-model="log" id="log"> Log to console
</label>
</form>
<div class="row-fluid">
<div class="span6">
<h3>Watch Tree</h3>
<div class="well well-top" style="height: 400px; overflow-y: auto;">
<bat-watcher-tree val="tree" inspect="inspect"></bat-watcher-tree>
</div>
<div class="well well-bottom">
<label for="select-root" ng-hide="roots.length <= 1">Root
<select id="select-root" ng-options="p for p in roots" ng-model="selectedRoot"></select>
</label>
</div>
</div>
<div class="span6">
<h3>Watch Expressions</h3>
<div class="well well-top" style="height: 400px; overflow-y: auto;">
<div ng-repeat="watch in histogram|sortByTime:min:max">
<span style="font-family: monospace;">{{watch.name | first}} </span>
<span> | {{watch.percent}}% | {{watch.time | precision}}ms</span>
<div class="progress">
<div ng-style="{width: (watch.percent) + '%'}" class= "bar">
</div>
</div>
</div>
</div>
<div class="well well-bottom">
<form class="form-inline">
<label>Filter expressions</label>
<bat-slider minimum="min" maximum="max"></bat-slider>
</form>
<button class="btn btn-success" ng-click="exportData()"><i class="icon-download-alt icon-white"></i> Save Data as JSON</button>
<button class="btn btn-danger" ng-click="clearHistogram()">Clear Data</button>
</div>
</div>
</div>
</div>

@ -1,29 +0,0 @@
#!/bin/bash
#
# Switch a dependency to git repo.
# Remove the NPM package and link it to a repo in parent directory.
DEP_NAME=$1
SCRIPT_DIR=$(dirname $0)
cd $SCRIPT_DIR/..
if [ -L ./node_modules/$DEP_NAME ]; then
echo "$DEP_NAME is already a symlink"
else
PKG_INFO=($($SCRIPT_DIR/read-pkg-url.js ./node_modules/$DEP_NAME/package.json))
URL=${PKG_INFO[0]}
DIR_NAME=${PKG_INFO[1]}
echo "Switching $DEP_NAME"
rm -rf ./node_modules/$DEP_NAME
if [ -d ../$DIR_NAME ]; then
echo "Repo already cloned in ../$DIR_NAME"
else
cd ..
git clone $URL $DIR_NAME
cd -
fi
echo "Link ./node_modules/$DEP_NAME -> ../$DIR_NAME"
ln -s ../../$DIR_NAME ./node_modules/$DEP_NAME
fi

@ -1,15 +0,0 @@
#!/bin/bash
#
# Switch a dependency to NPM.
# Remove the symlink and install from NPM.
DEP_NAME=$1
SCRIPT_DIR=$(dirname $0)
cd $SCRIPT_DIR/..
if [ ! -L ./node_modules/$DEP_NAME ]; then
echo "$DEP_NAME is not a symlink"
else
rm ./node_modules/$DEP_NAME
npm install $DEP_NAME
fi

@ -1,11 +0,0 @@
#!/bin/bash
LOG_FILES=$LOGS_DIR/*
for FILE in $LOG_FILES; do
echo -e "\n\n\n"
echo "=================================================================="
echo " $FILE"
echo "=================================================================="
cat $FILE
done

Some files were not shown because too many files have changed in this diff Show More