diff --git a/.gitignore b/.gitignore
index 5e5b952..fd2a642 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
batarang-release-*.zip
+*.build.js
diff --git a/README.md b/README.md
index ca16f11..194964e 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,12 @@ https://chrome.google.com/webstore/detail/ighdmehidhipcmcojjgiloacoafjmpfk
## Installing from Source
-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.
+1. Clone the repository: `git clone git://github.com/angular/angularjs-batarang`
+2. Install the bower dependencies: `bower install`
+3. Build the inject script: `node scripts/inline.js`
+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.
## Screencast
diff --git a/background.html b/background.html
index cdde193..87f891a 100644
--- a/background.html
+++ b/background.html
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/js/background.js b/background.js
similarity index 95%
rename from js/background.js
rename to background.js
index 9e2fffc..e57f6c8 100644
--- a/js/background.js
+++ b/background.js
@@ -36,6 +36,10 @@ chrome.extension.onConnect.addListener(function (port) {
}
}
+ if (!scopeCache[msg.appId]) {
+ scopeCache[msg.appId] = {};
+ }
+
// immediately populate the scopes tree from the cache
Object.keys(scopeCache[msg.appId]).forEach(function (scopeId) {
port.postMessage({ // [background] --> [devtools]
diff --git a/bower.json b/bower.json
index 0f9ea9a..0c3744d 100644
--- a/bower.json
+++ b/bower.json
@@ -9,4 +9,4 @@
"devDependencies": {
"angular-mocks": "~1.0.7"
}
-}
\ No newline at end of file
+}
diff --git a/content-scripts/README.md b/content-scripts/README.md
new file mode 100644
index 0000000..92723d0
--- /dev/null
+++ b/content-scripts/README.md
@@ -0,0 +1,20 @@
+# content-scripts/inject.js
+
+What does this do?
+
+This adds a script to the page that watches DOM mutation events until it sees that `window.angular` is available.
+Immediately after, before any applications have the opportunity to bootstrap, this decorates core Angular
+components to expose debugging information.
+
+## Building debug.js
+
+Why does this need a build step?
+
+Because this script does `fn.toString()` to construct the script tags, it's impossible to use any sort of
+code loading. The code needs to be inlined before being run.
+
+From the root of this repository, run:
+
+```shell
+node ./scripts/inline.js
+```
diff --git a/content-scripts/inject.js b/content-scripts/inject.js
new file mode 100644
index 0000000..df6eb47
--- /dev/null
+++ b/content-scripts/inject.js
@@ -0,0 +1,356 @@
+// content-scripts/inject.js
+// this file is run from the content script context (separate JS VM from the app, but same DOM)
+// but injects an 'instrumentation' script tag into the app context
+// confusing, right?
+
+var instument = function instument (window) {
+ // 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()) {
+ // 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);
+ instument(window);
+ }
+ if (oldOnload) {
+ oldOnload.apply(this, arguments);
+ }
+ };
+ }
+ };
+ document.addEventListener('DOMNodeInserted', areWeThereYet);
+ return;
+ }
+
+ // do not patch twice
+ if (window.__ngDebug) {
+ return;
+ }
+
+
+ // Helpers
+ // =======
+
+ var throttle = require('./lib/throttle.js');
+ var summarizeObject = require('./lib/summarizeObject.js');
+
+ // helper to extract dependencies from function arguments
+ // not all versions of AngularJS expose annotate
+ var annotate = angular.injector().annotate || require('./lib/annotate.js');
+
+ // polyfill for performance.now on older webkit
+ if (!performance.now) {
+ performance.now = performance.webkitNow;
+ }
+
+ // Send notifications from app context to devtools context
+ // in order to do this, we need to create a DOM element across which
+ // the app and content script contexts can communicate
+ var eventProxyElement = document.createElement('div');
+ eventProxyElement.id = '__ngDebugElement';
+ eventProxyElement.style.display = 'none';
+ document.body.appendChild(eventProxyElement);
+
+ var customEvent = document.createEvent('Event');
+ customEvent.initEvent('myCustomEvent', true, true);
+
+ var fireCustomEvent = function (data) {
+ data.appId = instrumentedAppId;
+ eventProxyElement.innerText = JSON.stringify(data);
+ eventProxyElement.dispatchEvent(customEvent);
+ };
+
+
+ // 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;
+ };
+
+
+
+ // 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: {},
+
+ // whether or not to emit profile data
+ profiling: false,
+
+ // map of $ids --> [] array of things being watched
+ modelWatchers: {},
+ // map of $id + watcher --> value
+ modelWatchersState: {},
+
+ // map of $ids --> refs to $rootScope objects
+ rootScopes: {},
+
+ deps: []
+ };
+
+ var popover = null;
+ var instrumentedAppId = window.location.host + '~' + Math.random();
+
+
+ // Utils
+ // =====
+
+ var getScopeTree = function (id) {
+
+ var names = api.niceNames();
+
+ var traverse = function (sc) {
+ var tree = {
+ id: sc.$id,
+ name: names[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;
+ };
+
+ var 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;
+ };
+
+ // Emit stuff
+ // ==========
+
+ var emit = {
+ modelChange: throttle(function (id, watchers) {
+ var scope = debug.scopes[id];
+ var changes = {};
+
+ watchers = watchers || debug.modelWatchers[id];
+
+ if (scope && debug.modelWatchers[id]) {
+
+ Object.keys(debug.modelWatchers[id]).
+ forEach(function (watcher) {
+ var newValue = api.getModel(id, watcher),
+ newString = JSON.stringify(newValue),
+ prop = id + '~' + watcher;
+
+ if (debug.modelWatchersState[prop] !== newString) {
+ changes[watcher] = newValue;
+ debug.modelWatchersState[prop] = newString;
+ }
+ });
+ }
+
+ if (Object.keys(changes).length > 0) {
+ fireCustomEvent({
+ action: 'modelChange',
+ id: id,
+ changes: changes
+ });
+ }
+ }, 50),
+
+ scopeChange: throttle(function (id) {
+ fireCustomEvent({
+ action: 'scopeChange',
+ id: id,
+ scope: getScopeTree(id)
+ });
+ }, 50),
+
+ scopeDeleted: function (id) {
+ fireCustomEvent({
+ action: 'scopeDeleted',
+ id: id
+ });
+ },
+
+ watcherChange: throttle(function (id) {
+ if (debug.modelWatchers[id]) {
+ fireCustomEvent({
+ action: 'watcherChange',
+ id: id,
+ watchers: debug.watchers[id]
+ });
+ }
+ }, 50),
+
+ watchPerfChange: throttle(function (str) {
+ if (debug.profiling) {
+ fireCustomEvent({
+ action: 'watchPerfChange',
+ watcher: str,
+ value: debug.watchPerf[str]
+ });
+ }
+ }, 50),
+
+ applyPerfChange: throttle(function (str) {
+ if (debug.profiling) {
+ fireCustomEvent({
+ action: 'applyPerfChange',
+ watcher: str,
+ value: debug.applyPerf[str]
+ });
+ }
+ }, 50),
+
+ // might be worth limiting
+ watchPerf: function () {
+ throw new Error('Implement me :c');
+ }
+ };
+
+
+ // Public API
+ // ==========
+
+ var api = window.__ngDebug = {
+
+ profiling: function (setting) {
+ debug.profiling = setting;
+ },
+
+ getDeps: function () {
+ return debug.deps;
+ },
+
+ getRootScopeIds: function () {
+ return Object.keys(debug.rootScopes);
+ },
+
+ getAppId: function () {
+ return instrumentedAppId;
+ },
+
+ fireCustomEvent: fireCustomEvent,
+
+ niceNames: require('./lib/niceNames.js'),
+ getModel: require('./lib/summarizeModel.js'),
+
+ setSomeModel: function (id, path, value) {
+ debug.scope[id].$apply(path + '=' + JSON.stringify(value));
+ },
+
+ watchModel: function (id, path) {
+ debug.modelWatchers[id] = debug.modelWatchers[id] || {};
+ debug.modelWatchers[id][path || ''] = true;
+ if (!path || path === '') {
+ debug.modelWatchersState = {};
+ }
+ emit.modelChange(id);
+ emit.watcherChange(id);
+ },
+
+ // unwatches all children of the given path
+ // Ex:
+ // if watching 'foo.bar.baz', 'foo.bar', and 'foo'
+ // unwatchModel('001', 'foo.bar')
+ // unwatches 'foo.bar.baz' and 'foo.bar'
+ unwatchModel: function (id, path) {
+ if (!debug.modelWatchers[id]) {
+ return;
+ }
+ if (path === undefined) {
+ path = '';
+ }
+ Object.keys(modelWatchers[id]).forEach(function (key) {
+ if (key.substr(0, path.length) === path) {
+ delete debug.modelWatchers[id][key];
+ }
+ });
+ },
+
+ enable: require('./lib/popover.js')
+ };
+
+ var recordDependencies = function (providerName, dependencies) {
+ debug.deps.push({
+ name: providerName,
+ imports: dependencies
+ });
+ };
+
+ require('./lib/decorate.js');
+
+};
+
+// inject into the application context from the content script context
+
+var inject = function () {
+ var script = window.document.createElement('script');
+ script.innerHTML = '(' + instument.toString() + '(window))';
+ document.head.appendChild(script);
+
+ // handle forwarding the events sent from the app context to the
+ // background page context
+ var eventProxyElement = document.getElementById('__ngDebugElement');
+
+ if (eventProxyElement) {
+ eventProxyElement.addEventListener('myCustomEvent', function () {
+ var eventData = JSON.parse(eventProxyElement.innerText);
+ chrome.extension.sendMessage(eventData);
+ });
+ document.removeEventListener('DOMContentLoaded', inject);
+ }
+};
+
+// only inject if cookie is set
+if (document.cookie.indexOf('__ngDebug=true') != -1) {
+ document.addEventListener('DOMContentLoaded', inject);
+}
diff --git a/content-scripts/lib/annotate.js b/content-scripts/lib/annotate.js
new file mode 100644
index 0000000..ef29864
--- /dev/null
+++ b/content-scripts/lib/annotate.js
@@ -0,0 +1,51 @@
+
+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;
+}
+
+module.exports = 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;
+};
diff --git a/content-scripts/lib/decorate.js b/content-scripts/lib/decorate.js
new file mode 100644
index 0000000..d434dbc
--- /dev/null
+++ b/content-scripts/lib/decorate.js
@@ -0,0 +1,251 @@
+
+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 () {
+ recordDependencies(name, annotate(tempGet));
+ return tempGet.apply(this, arguments);
+ };
+ });
+ } else if (definition instanceof Array) {
+ // it is a constructor with array syntax
+ var tempConstructor = definition[definition.length - 1];
+
+ definition[definition.length - 1] = function () {
+ recordDependencies(name, annotate(tempConstructor));
+ return tempConstructor.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 () {
+ recordDependencies(name, 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 () {
+ recordDependencies(name, annotate(tempGet));
+ return tempGet.apply(this, arguments);
+ });
+ } else {
+ recordDependencies(name, 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) {
+ var isArray = value instanceof Array;
+ var originalValue = isArray ? value[value.length - 1] : value;
+
+ var newValue = function () {
+ recordDependencies(key, annotate(originalValue));
+ return originalValue.apply(this, arguments);
+ };
+
+ if (isArray) {
+ value[value.length - 1] = newValue;
+ } else {
+ name[value] = newValue;
+ }
+ });
+ } else {
+ recordDependencies(name, annotate(definition));
+ }
+ return temp.apply(this, arguments);
+ };
+ });
+
+ $provide.decorator('$rootScope', function ($delegate) {
+
+ var watchFnToHumanReadableString = function (fn) {
+ if (fn.exp) {
+ return fn.exp.trim();
+ }
+ if (fn.name) {
+ return fn.name.trim();
+ }
+ 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);
+ emit.watcherChange(thatScope.$id);
+
+ // 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;
+ emit.watchPerfChange(watchStr);
+ 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;
+ emit.watchPerfChange(watchStr);
+ 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();
+
+ //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;
+ emit.applyPerfChange(applyStr);
+ return ret;
+ };
+ }
+
+ return _watch.apply(this, arguments);
+ };
+
+
+ // patch $destroy
+ // --------------
+ var _destroy = $delegate.__proto__.$destroy;
+ $delegate.__proto__.$destroy = function () {
+ [
+ 'watchers',
+ 'scopes'
+ ].forEach(function (prop) {
+ if (debug[prop][this.$id]) {
+ delete debug[prop][this.$id];
+ }
+ }, this);
+ emit.scopeDeleted(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;
+ emit.scopeChange(ret.$root.$id);
+ }
+
+ // 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;
+
+ return ret;
+ };
+
+
+ // patch $digest
+ // -------------
+ var _digest = $delegate.__proto__.$digest;
+ $delegate.__proto__.$digest = function (fn) {
+ var ret = _digest.apply(this, arguments);
+ emit.modelChange(this.$id);
+ 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();
+
+ // 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;
+ });
+});
diff --git a/content-scripts/lib/niceNames.js b/content-scripts/lib/niceNames.js
new file mode 100644
index 0000000..1e0c1da
--- /dev/null
+++ b/content-scripts/lib/niceNames.js
@@ -0,0 +1,58 @@
+
+module.exports = function niceNames () {
+ var ngScopeElts = document.getElementsByClassName('ng-scope');
+ ngScopeElts = Array.prototype.slice.call(ngScopeElts);
+ return ngScopeElts.
+ reduce(function (acc, elt) {
+ var $elt = angular.element(elt);
+ var scope = $elt.scope();
+
+ var name = {};
+
+ [
+ 'ng-app',
+ 'ng-controller',
+ 'ng-repeat'
+ ].
+ forEach(function (attr) {
+ var val = $elt.attr(attr),
+ className = $elt[0].className,
+ match,
+ lhs,
+ valueIdentifier,
+ keyIdentifier;
+
+ if (val) {
+ name[attr] = val;
+ if (attr === 'ng-repeat') {
+ match = /(.+) in/.exec(val);
+ lhs = match[1];
+
+ match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
+ valueIdentifier = match[3] || match[1];
+ keyIdentifier = match[2];
+
+ if (keyIdentifier) {
+ name.lhs = valueIdentifier + '["' + scope[keyIdentifier] + '"]' + summarizeObject(scope[valueIdentifier]);
+ } else {
+ name.lhs = valueIdentifier + summarizeObject(scope[valueIdentifier]);
+ }
+
+ }
+ } else if (className.indexOf(attr) !== -1) {
+ match = (new RegExp(attr + ': ([a-zA-Z0-9]+);')).exec(className);
+ name[attr] = match[1];
+ }
+ });
+
+ if (Object.keys(name).length === 0) {
+ name.tag = $elt[0].tagName.toLowerCase();
+ name.classes = $elt[0].className.
+ replace(/(\W*ng-scope\W*)/, ' ').
+ split(' ').
+ filter(function (i) { return i; });
+ }
+ acc[scope.$id] = name;
+ return acc;
+ }, {});
+};
diff --git a/content-scripts/lib/popover.js b/content-scripts/lib/popover.js
new file mode 100644
index 0000000..d041e0a
--- /dev/null
+++ b/content-scripts/lib/popover.js
@@ -0,0 +1,234 @@
+// TODO: this depends on global state and stuff
+
+module.exports = function () {
+ if (popover) {
+ return;
+ }
+ var angular = window.angular;
+ popover = angular.element(
+ '
' +
+ '
' +
+ '
{ Please select a scope }
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
');
+ 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('');
+ angular.forEach(data, function (val, key) {
+ var toAppend;
+ if (val === undefined) {
+ toAppend = 'undefined';
+ } else if (val === null) {
+ toAppend = 'null';
+ } else if (val instanceof Array) {
+ toAppend = '[ ... ]';
+ } else if (val instanceof Object) {
+ toAppend = '{ ... }';
+ } else {
+ toAppend = val.toString();
+ }
+ if (data instanceof Array) {
+ toAppend = '