├── app
├── d3.html
├── scripts
│ ├── models
│ │ ├── Node.spec.js
│ │ ├── Module.spec.js
│ │ ├── models.module.js
│ │ ├── App.js
│ │ ├── Component.js
│ │ ├── Module.js
│ │ ├── ModuleStats.js
│ │ └── Node.js
│ ├── controls
│ │ ├── controls.module.js
│ │ └── dgSearchNode.js
│ ├── infoPanel
│ │ ├── infoPanel.module.js
│ │ ├── InfoPanelCtrl.js
│ │ ├── OptionsCtrl.js
│ │ ├── dgInfoPanelList.html
│ │ ├── dgInfoPanelList.js
│ │ └── infoPanel.html
│ ├── about
│ │ ├── AboutCtrl.js
│ │ └── about.html
│ ├── app.module.js
│ ├── triggerComponents
│ │ ├── TriggerComponentsCtrl.js
│ │ └── triggerComponents.html
│ ├── util
│ │ ├── util.spec.js
│ │ └── util.js
│ ├── analytics.js
│ ├── inject
│ │ ├── inject.spec.js
│ │ └── inject.js
│ ├── core
│ │ ├── Const.js
│ │ ├── chromeExtension.js
│ │ ├── Graph.js
│ │ ├── inspectedApp.spec.js
│ │ ├── nodeFactory.js
│ │ ├── appContext.js
│ │ ├── inspectedApp.js
│ │ ├── dev.js
│ │ ├── currentView.js
│ │ └── storage.js
│ ├── main
│ │ ├── main.html
│ │ └── MainCtrl.js
│ ├── app
│ │ └── AppCtrl.js
│ ├── tour
│ │ └── tour.js
│ └── graph
│ │ └── dgGraph.js
├── background.html
├── img
│ ├── angular.png
│ ├── square-500.png
│ └── webstore-icon.png
├── devtoolsBackground.html
├── styles
│ ├── _components.scss
│ ├── _about.scss
│ ├── _colors.scss
│ ├── _graph.scss
│ ├── app.scss
│ ├── _infoPanel.scss
│ ├── _controls.scss
│ └── lib
│ │ └── shepherd
│ │ ├── shepherd-theme-arrows.css
│ │ ├── shepherd-theme-arrows-plain-buttons.css
│ │ ├── shepherd-theme-dark.css
│ │ ├── shepherd-theme-square.css
│ │ ├── shepherd-theme-default.css
│ │ └── shepherd-theme-square-dark.css
├── background.js
├── devtoolsBackground.js
├── index.html
└── vendor
│ ├── shepherd.min.js
│ └── tether.min.js
├── .gitignore
├── gulp
├── release-tasks.js
├── changelog.js
├── styles.js
├── inject.js
├── e2e-tests.js
├── server.js
├── unit-tests.js
├── versioning.js
└── proxy.js
├── zip_chrome_extension.sh
├── test
└── mocks
│ ├── simpleData.js
│ └── chromeMock.js
├── CHANGELOG.md
├── manifest.json
├── karma.conf.js
├── LICENSE
├── .jshintrc
├── README.md
├── package.json
└── gulpfile.js
/app/d3.html:
--------------------------------------------------------------------------------
1 | d3.html
--------------------------------------------------------------------------------
/app/scripts/models/Node.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | xdescribe('Node', function() {
4 |
5 | });
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 | tasks.todo
4 | app.css
5 |
6 | chrome_extension.zip
7 |
--------------------------------------------------------------------------------
/app/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/img/angular.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filso/ng-dependency-graph/HEAD/app/img/angular.png
--------------------------------------------------------------------------------
/app/scripts/models/Module.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | xdescribe('Module', function() {
4 |
5 | });
--------------------------------------------------------------------------------
/app/img/square-500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filso/ng-dependency-graph/HEAD/app/img/square-500.png
--------------------------------------------------------------------------------
/app/img/webstore-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filso/ng-dependency-graph/HEAD/app/img/webstore-icon.png
--------------------------------------------------------------------------------
/app/scripts/controls/controls.module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph.controls', []);
--------------------------------------------------------------------------------
/app/scripts/models/models.module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph.models', []);
4 |
--------------------------------------------------------------------------------
/app/devtoolsBackground.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/scripts/infoPanel/infoPanel.module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph.infoPanel', []);
4 |
--------------------------------------------------------------------------------
/app/scripts/about/AboutCtrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .controller('AboutCtrl', function() {
5 |
6 |
7 | });
8 |
--------------------------------------------------------------------------------
/app/scripts/infoPanel/InfoPanelCtrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .controller('InfoPanelCtrl', function() {
5 | });
6 |
--------------------------------------------------------------------------------
/app/scripts/infoPanel/OptionsCtrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .controller('OptionsCtrl', function($scope, currentView) {
5 | });
--------------------------------------------------------------------------------
/gulp/release-tasks.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | // pass along gulp reference to have tasks imported
5 | require('gulp-release-tasks')(gulp);
6 |
--------------------------------------------------------------------------------
/zip_chrome_extension.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rm chrome_extension.zip
3 | mv node_modules/ /tmp/dep_node_modules
4 | zip -r chrome_extension.zip *
5 | mv /tmp/dep_node_modules node_modules
6 |
--------------------------------------------------------------------------------
/app/scripts/app.module.js:
--------------------------------------------------------------------------------
1 | angular.module('ngDependencyGraph', ['ngDependencyGraph.infoPanel'])
2 | .run(function($rootScope, dev, currentView) {
3 | dev.exposeGlobalObject();
4 | $rootScope.currentView = currentView;
5 | });
--------------------------------------------------------------------------------
/app/scripts/models/App.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('App', function() {
5 |
6 | function App(name, deps) {
7 | this.name = name;
8 | this.deps = deps;
9 | }
10 |
11 | return App;
12 |
13 | });
14 |
--------------------------------------------------------------------------------
/app/scripts/triggerComponents/TriggerComponentsCtrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .controller('TriggerComponentsCtrl', function($scope, currentView, Const) {
5 | $scope.change = function() {
6 | currentView.scope = Const.Scope.COMPONENTS;
7 | };
8 | });
9 |
--------------------------------------------------------------------------------
/app/scripts/models/Component.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('Component', function(Node) {
5 |
6 | function Component(_data) {
7 | this.isModule = false;
8 | Node.apply(this, arguments);
9 | }
10 |
11 | Component.prototype = Object.create(Node.prototype);
12 |
13 | return Component;
14 | });
--------------------------------------------------------------------------------
/app/scripts/models/Module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('Module', function(Node) {
5 |
6 | function Module(_data) {
7 | Node.apply(this, arguments);
8 | this.isModule = true;
9 | }
10 |
11 | Module.prototype = Object.create(Node.prototype);
12 |
13 | return Module;
14 |
15 | });
16 |
--------------------------------------------------------------------------------
/app/scripts/infoPanel/dgInfoPanelList.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ ::title }} ({{ list.length }}):
3 |
7 |
8 |
--------------------------------------------------------------------------------
/gulp/changelog.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var conventionalChangelog = require('conventional-changelog');
5 |
6 | var fs = require('fs');
7 |
8 | module.exports = function(options) {
9 |
10 | gulp.task('changelog', function() {
11 |
12 | conventionalChangelog({
13 | preset: 'angular'
14 | }).pipe(fs.createWriteStream('CHANGELOG.md'));
15 |
16 | });
17 |
18 | };
19 |
--------------------------------------------------------------------------------
/app/scripts/models/ModuleStats.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('ModuleStats', function() {
5 |
6 | function ModuleStats(module) {
7 | this.module = module;
8 | }
9 |
10 | _.assign(ModuleStats.prototype, {
11 |
12 | mostUsed: function() {
13 | var nodes = this.module.nodes;
14 | }
15 |
16 | });
17 |
18 | return ModuleStats;
19 |
20 | });
21 |
--------------------------------------------------------------------------------
/app/styles/_components.scss:
--------------------------------------------------------------------------------
1 | $color-module: $red;
2 | $color-service: lighten($blue, 15%);
3 | $color-controller: lighten($olive, 15%);
4 | $color-directive: $orange;
5 | $color-directive-controller: $yellow;
6 | $color-filter: $teal;
7 | $color-value: $gray;
8 |
9 | $components: module, service, controller, directive, filter, value;
10 | $components-colors: $color-module, $color-service,
11 | $color-controller, $color-directive, $color-filter, $color-value;
12 |
--------------------------------------------------------------------------------
/app/scripts/util/util.spec.js:
--------------------------------------------------------------------------------
1 | // TODO: fix these tests
2 | xdescribe('util', function() {
3 |
4 | var util;
5 |
6 | beforeEach(module('ngDependencyGraph'));
7 |
8 | beforeEach(function(_util_) {
9 | util = _util_;
10 | });
11 |
12 | it('extractMasks()', function() {
13 | var str = util.extractMasks('ble , ba , bom');
14 | console.log(str);
15 | });
16 |
17 | it('wildcardToRegexp', function() {
18 | var str = util.wildcardToRegexp('*ng-temp*');
19 |
20 | });
21 |
22 | });
--------------------------------------------------------------------------------
/test/mocks/simpleData.js:
--------------------------------------------------------------------------------
1 | var rawMockData = rawMockData || {};
2 |
3 | rawMockData.simple = {
4 | angularVersion: 'bleble',
5 | apps: ['app'],
6 | modules: [{
7 | name: 'app',
8 | deps: ['mod1', 'mod2'],
9 | components: [{
10 | name: 'comp1',
11 | deps: ['dep1', 'dep2'],
12 | type: 'controller'
13 | }, {
14 | name: 'dir1',
15 | deps: ['dep3', 'dep2'],
16 | type: 'directive'
17 | },
18 | ]
19 | }
20 | ]
21 | };
22 |
--------------------------------------------------------------------------------
/app/scripts/util/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('util', function() {
5 |
6 | var util = {
7 | extractMasks: function(str) {
8 | return str.split(/[,;]/g).map(function(s) {
9 | return util.wildcardToRegexp(s.trim());
10 | });
11 | },
12 | wildcardToRegexp: function(str) {
13 | var newStr = str.replace(/[*]/g, '.*');
14 | return new RegExp('^' + newStr + '$');
15 | }
16 | };
17 |
18 | return util;
19 |
20 | });
--------------------------------------------------------------------------------
/gulp/styles.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var browserSync = require('browser-sync');
5 | var sass = require('gulp-sass');
6 |
7 | module.exports = function(options) {
8 |
9 |
10 | gulp.task('sass', function() {
11 | return gulp.src('./app/styles/app.scss')
12 | .pipe(sass({
13 | outputStyle: "compressed",
14 | includePaths: ["./app"]
15 | }))
16 | .on('error', options.errorHandler('Sass'))
17 | .pipe(gulp.dest('./app/styles'))
18 | .pipe(browserSync.reload({ stream: true }));
19 | });
20 |
21 | };
22 |
--------------------------------------------------------------------------------
/app/scripts/analytics.js:
--------------------------------------------------------------------------------
1 | // Standard Google Universal Analytics code
2 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
3 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
4 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
5 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); // Note: https protocol here
6 |
7 | ga('create', 'UA-65840547-1', 'auto');
8 | ga('set', 'checkProtocolTask', function(){});
9 | ga('require', 'displayfeatures');
10 | ga('send', 'pageview', '/index.html');
--------------------------------------------------------------------------------
/app/scripts/inject/inject.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('inject', function() {
4 |
5 | describe('is initialised correctly', function() {
6 |
7 | it('with angular.bootstrap', function() {
8 | // console.log(inject);
9 | injectCode();
10 | });
11 |
12 | it('with angular.bootstrap', function() {
13 | // console.log(inject);
14 | injectCode();
15 | });
16 |
17 | xit('when angular.bootstrap when more then 1 app is present', function() {
18 | // it should just ignore subsequent angular.bootstrap calls
19 | });
20 |
21 | });
22 |
23 | });
--------------------------------------------------------------------------------
/app/scripts/infoPanel/dgInfoPanelList.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .directive('dgInfoPanelList', function($rootScope, $parse, currentView) {
5 |
6 |
7 | return {
8 | restrict: 'A',
9 | templateUrl: 'scripts/infoPanel/dgInfoPanelList.html',
10 | replace: true,
11 | scope: true,
12 | link: function(scope, elm, attrs) {
13 | scope.$watch(attrs.dgInfoPanelList, function(newList) {
14 | scope.list = _.sortBy(newList, 'name');
15 | });
16 |
17 | scope.title = attrs.title;
18 | }
19 | };
20 |
21 |
22 | });
23 |
--------------------------------------------------------------------------------
/app/background.js:
--------------------------------------------------------------------------------
1 | // notify of page refreshes
2 | chrome.extension.onConnect.addListener(function (port) {
3 | port.onMessage.addListener(function (msg) {
4 | if (msg.action === "register") {
5 | var respond = function (tabId, changeInfo, tab) {
6 | if (tabId !== msg.inspectedTabId) {
7 | return;
8 | }
9 | port.postMessage({ action: "refresh", changeInfo: changeInfo });
10 | };
11 |
12 | chrome.tabs.onUpdated.addListener(respond);
13 | port.onDisconnect.addListener(function () {
14 | chrome.tabs.onUpdated.removeListener(respond);
15 | });
16 | }
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## 0.2.4 (2015-07-27)
3 |
4 |
5 | ### Bug Fixes
6 |
7 | * ***:** fix "X" button on dialogue ([418130b](https://github.com/filso/ng-dependency-graph/commit/418130b))
8 | * ***:** handle annotated reps properly ([13331dc](https://github.com/filso/ng-dependency-graph/commit/13331dc))
9 |
10 | ### Features
11 |
12 | * ***:** change default ignore list ([9260fe3](https://github.com/filso/ng-dependency-graph/commit/9260fe3))
13 | * ***:** get meta data on demand ([b6bd4b8](https://github.com/filso/ng-dependency-graph/commit/b6bd4b8))
14 | * ***:** polling for inspected app ([d228676](https://github.com/filso/ng-dependency-graph/commit/d228676))
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/gulp/inject.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 |
5 | var $ = require('gulp-load-plugins')();
6 |
7 |
8 |
9 | module.exports = function(options) {
10 |
11 | var appStream = gulp.src(options.paths.appScripts);
12 |
13 | gulp.task('inject', [], function () {
14 | var injectScripts = appStream
15 | .pipe($.angularFilesort()).on('error', options.errorHandler('AngularFilesort'));
16 |
17 | var injectOptions = {
18 | ignorePath: ['.'],
19 | addRootSlash: false,
20 | relative: true
21 | };
22 |
23 | return gulp.src('./app/index.html')
24 | .pipe($.inject(injectScripts, injectOptions))
25 | .pipe(gulp.dest('./app/'));
26 |
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/app/scripts/models/Node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('Node', function() {
5 |
6 | function Node(_data) {
7 | this._id = _.uniqueId();
8 | this.name = _data.name;
9 | this._data = _data;
10 | this.type = _data.type;
11 | this.deps = [];
12 | this.provides = [];
13 | }
14 |
15 | _.assign(Node.prototype, {
16 | linkDep: function(node) {
17 | this.deps.push(node);
18 | },
19 | linkProvides: function(node) {
20 | this.provides.push(node);
21 | },
22 | resetLinks: function() {
23 | this.deps = [];
24 | this.provides = [];
25 | }
26 | });
27 |
28 |
29 | return Node;
30 | });
31 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AngularJS Graph",
3 | "version": "0.2.10",
4 | "description": "AngularJS dependency graph.",
5 | "background": {
6 | "page": "app/background.html"
7 | },
8 | "devtools_page": "app/devtoolsBackground.html",
9 | "options_page": "app/index.html",
10 | "content_security_policy": "script-src 'self' https://www.google-analytics.com; object-src 'self'",
11 | "manifest_version": 2,
12 | "icons": {
13 | "500": "app/img/square-500.png"
14 | },
15 | "permissions": ["storage", ""],
16 | "content_scripts": [
17 | {
18 | "matches": [""],
19 | "js": ["app/scripts/inject/inject.js"],
20 | "run_at": "document_start"
21 | }
22 | ],
23 | "minimum_chrome_version": "22"
24 | }
25 |
--------------------------------------------------------------------------------
/app/scripts/triggerComponents/triggerComponents.html:
--------------------------------------------------------------------------------
1 |
4 |
7 |
10 |
13 |
--------------------------------------------------------------------------------
/app/scripts/core/Const.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .constant('Const', {
5 | // loader.js required, first introduced in 1.0
6 | AngularVersionRequired: '1.0.0',
7 |
8 | INJECTED_POLL_INTERVAL: 500,
9 |
10 | COOKIE_NAME: '__ngDependencyGraph',
11 | TOUR_KEY: 'tour_done',
12 |
13 | Events: {
14 | UPDATE_GRAPH: 'updateGraph',
15 | CHOOSE_NODE: 'chooseNode',
16 | INIT_MAIN: 'initMain'
17 | },
18 |
19 | ComponentType: {
20 | CONTROLLER: 'controller',
21 | DIRECTIVE: 'directive',
22 | SERVICE: 'service',
23 | FILTER: 'filter'
24 | },
25 |
26 | Scope: {
27 | COMPONENTS: 'components',
28 | MODULES: 'modules'
29 | },
30 |
31 | View: {
32 | HOVER_TRANSITION_TIME: '500'
33 | },
34 |
35 | FilterModules: {
36 | DEFAULT_FILTER: '',
37 | DEFAULT_IGNORE: 'ngLocale, ui.*, *.html',
38 | DELIMITER: ','
39 | }
40 |
41 | });
--------------------------------------------------------------------------------
/app/styles/_about.scss:
--------------------------------------------------------------------------------
1 | .about {
2 | margin: 10px;
3 | color: #ddd;
4 | display: flex;
5 |
6 | font-size: 14px;
7 |
8 | img {
9 | width: 128px;
10 | height: 128px;
11 | }
12 | h3 {
13 | color: white;
14 | margin: 0;
15 | }
16 |
17 | & > div > div {
18 | margin-top: 25px;
19 | }
20 |
21 | a.open-graph {
22 | cursor: pointer;
23 | display: inline-block;
24 | padding: 5px;
25 | margin: 8px 0px;
26 | border-radius: 5px;
27 | background: linear-gradient(to right, $angular-light-red, $angular-dark-red);
28 |
29 | border: 2px solid $angular-gray;
30 |
31 | &.current {
32 | padding: 10px;
33 | font-size: 1.2em;
34 | }
35 |
36 | }
37 |
38 | a {
39 | color: white;
40 | }
41 |
42 | input[type="text"] {
43 | margin: 10px;
44 | }
45 |
46 | .cancel {
47 | vertical-align: middle;
48 | cursor: pointer;
49 | text-decoration: underline;
50 | font-size: 1.2em;
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/app/scripts/core/chromeExtension.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // abstraction layer for Chrome Extension APIs
4 | angular.module('ngDependencyGraph').value('chromeExtension', {
5 | sendRequest: function(requestName, cb) {
6 | chrome.extension.sendRequest({
7 | script: requestName,
8 | tab: chrome.devtools.inspectedWindow.tabId
9 | }, cb || function() {});
10 | },
11 |
12 | isExtensionContext: function() {
13 | return window.chrome !== undefined && window.chrome.extension !== undefined;
14 | },
15 |
16 | /**
17 | * @btford:
18 | * written because I don't like the API for chrome.devtools.inspectedWindow.eval;
19 | * passing strings instead of functions are gross.
20 | */
21 | eval: function(fn, args, cb) {
22 | // with two args
23 | if (!cb && typeof args === 'function') {
24 | cb = args;
25 | args = {};
26 | } else if (!args) {
27 | args = {};
28 | }
29 | chrome.devtools.inspectedWindow.eval('(' +
30 | fn.toString() +
31 | '(window, ' +
32 | JSON.stringify(args) +
33 | '));', cb);
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/gulp/e2e-tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 |
5 | var $ = require('gulp-load-plugins')();
6 |
7 | var browserSync = require('browser-sync');
8 |
9 | module.exports = function(options) {
10 | // Downloads the selenium webdriver
11 | gulp.task('webdriver-update', $.protractor.webdriver_update);
12 |
13 | gulp.task('webdriver-standalone', $.protractor.webdriver_standalone);
14 |
15 | function runProtractor(done) {
16 |
17 | gulp.src(options.e2e + '/**/*.js')
18 | .pipe($.protractor.protractor({
19 | configFile: 'protractor.conf.js'
20 | }))
21 | .on('error', function(err) {
22 | // Make sure failed tests cause gulp to exit non-zero
23 | throw err;
24 | })
25 | .on('end', function() {
26 | // Close browser sync server
27 | browserSync.exit();
28 | done();
29 | });
30 | }
31 |
32 | gulp.task('protractor', ['protractor:src']);
33 | gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor);
34 | gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor);
35 | };
36 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(config) {
4 |
5 | var configuration = {
6 | autoWatch: false,
7 |
8 | frameworks: ['jasmine'],
9 |
10 | ngHtml2JsPreprocessor: {
11 | stripPrefix: 'src/',
12 | moduleName: 'gulpAngular'
13 | },
14 |
15 | browsers: ['PhantomJS'],
16 |
17 | plugins: [
18 | 'karma-phantomjs-launcher',
19 | 'karma-jasmine',
20 | 'karma-ng-html2js-preprocessor'
21 | ],
22 |
23 | preprocessors: {
24 | 'src/**/*.html': ['ng-html2js']
25 | }
26 | };
27 |
28 | // This block is needed to execute Chrome on Travis
29 | // If you ever plan to use Chrome and Travis, you can keep it
30 | // If not, you can safely remove it
31 | // https://github.com/karma-runner/karma/issues/1144#issuecomment-53633076
32 | if (configuration.browsers[0] === 'Chrome' && process.env.TRAVIS) {
33 | configuration.customLaunchers = {
34 | 'chrome-travis-ci': {
35 | base: 'Chrome',
36 | flags: ['--no-sandbox']
37 | }
38 | };
39 | configuration.browsers = ['chrome-travis-ci'];
40 | }
41 |
42 | config.set(configuration);
43 | };
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2010-2012 Google, Inc. http://angularjs.org
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/app/styles/_colors.scss:
--------------------------------------------------------------------------------
1 | // http://clrs.cc/
2 | //
3 | // COLOR VARIABLES
4 | //
5 | // - Cool
6 | // - Warm
7 | // - Gray Scale
8 | //
9 |
10 | // Cool
11 |
12 | $aqua: #7FDBFF;
13 | $blue: #0074D9;
14 | $navy: #001F3F;
15 | $teal: #39CCCC;
16 | $green: #2ECC40;
17 | $olive: #3D9970;
18 | $lime: #01FF70;
19 |
20 | // Warm
21 |
22 | $yellow: #FFDC00;
23 | $orange: #FF851B;
24 | $red: #FF4136;
25 | $fuchsia: #F012BE;
26 | $purple: #B10DC9;
27 | $maroon: #85144B;
28 |
29 | // Gray Scale
30 |
31 | $white: #fff;
32 | $silver: #ddd;
33 | $gray: #aaa;
34 | $black: #111;
35 |
36 |
37 | $angular-gray: #B6B6B6;
38 | $angular-light-red: #DD1B16;
39 | $angular-dark-red: #A6120D;
40 |
41 | $color-module: $red;
42 |
43 | $color-service: lighten($blue, 15%);
44 | $color-controller: lighten($olive, 15%);
45 | $color-directive: $orange;
46 | $color-directive-controller: $yellow;
47 | $color-filter: desaturate($yellow, 10%);
48 | $color-value: lighten($purple, 20%);
49 |
50 | $components: module, service, controller, directive, filter, value;
51 | $components-colors: $color-module, $color-service,
52 | $color-controller, $color-directive, $color-filter, $color-value;
53 |
54 | $color-graph-bg: #333;
55 |
56 |
--------------------------------------------------------------------------------
/app/scripts/core/Graph.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('Graph', function(nodeFactory) {
5 |
6 | function Graph(nodes, links, scope) {
7 | this.scope = scope;
8 | this.origNodes = this.nodes = nodes;
9 | this.origLinks = this.links = links;
10 | }
11 |
12 | Graph.createFromRawNodes = function(rawNodes, scope, oldGraph) {
13 |
14 | var obj = nodeFactory.createNodes(rawNodes, oldGraph);
15 | var nodes = obj.nodes;
16 | var links = obj.links;
17 |
18 | return new Graph(nodes, links, scope);
19 | };
20 |
21 | Graph.prototype.filterNodes = function(fn) {
22 | var nodes = this.nodes = _.filter(this.nodes, fn);
23 | this.links = _.filter(this.links, function(l) {
24 | return nodes.indexOf(l.target) !== -1 && nodes.indexOf(l.source) !== -1;
25 | });
26 | };
27 |
28 | Graph.prototype.resetFilter = function() {
29 | this.nodes = this.origNodes;
30 | this.links = this.origLinks;
31 | };
32 |
33 | Graph.prototype.filterByName = function(name) {
34 | var nameLow = name.toLowerCase();
35 | this.filterNodes(function(node) {
36 | return node.name.toLowerCase().indexOf(nameLow) !== -1;
37 | });
38 |
39 | };
40 |
41 | return Graph;
42 |
43 | });
44 |
--------------------------------------------------------------------------------
/gulp/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var browserSync = require('browser-sync');
5 | var browserSyncSpa = require('browser-sync-spa');
6 |
7 | var middleware = require('./proxy');
8 |
9 | module.exports = function(options) {
10 |
11 | function browserSyncInit(baseDir, browser) {
12 | browser = browser === undefined ? 'default' : browser;
13 |
14 | var routes = null;
15 |
16 | var server = {
17 | baseDir: baseDir,
18 | routes: routes
19 | };
20 |
21 | if(middleware.length > 0) {
22 | server.middleware = middleware;
23 | }
24 |
25 | browserSync.instance = browserSync.init({
26 | startPath: '/',
27 | server: server,
28 | browser: browser
29 | });
30 | }
31 |
32 | browserSync.use(browserSyncSpa({
33 | selector: '[ng-app]'// Only needed for angular apps
34 | }));
35 |
36 | gulp.task('serve', ['watch'], function () {
37 | browserSyncInit('./app');
38 | });
39 |
40 | gulp.task('serve:dist', ['build'], function () {
41 | browserSyncInit(options.dist);
42 | });
43 |
44 | gulp.task('serve:e2e', ['inject'], function () {
45 | browserSyncInit([options.tmp + '/serve', options.src], []);
46 | });
47 |
48 | gulp.task('serve:e2e-dist', ['build'], function () {
49 | browserSyncInit(options.dist, []);
50 | });
51 | };
52 |
--------------------------------------------------------------------------------
/app/scripts/core/inspectedApp.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('inspectedApp', function() {
4 |
5 | var data = {
6 | apps: {}
7 | };
8 | var returnedData;
9 |
10 | var $timeout;
11 | var inspectedApp;
12 | var chromeExtension = {
13 | isExtensionContext: function() {
14 | return true;
15 | },
16 | eval: function(injectedFn, appNames, callback) {
17 | callback(returnedData);
18 | }
19 | };
20 |
21 | beforeEach(module('ngDependencyGraph'));
22 |
23 | beforeEach(module(function($provide) {
24 | $provide.value('chromeExtension', chromeExtension);
25 | }));
26 |
27 | beforeEach(inject(function(_inspectedApp_, _$timeout_) {
28 | inspectedApp = _inspectedApp_;
29 | $timeout = _$timeout_;
30 | }));
31 |
32 | describe('.loadInspectedAppData()', function() {
33 |
34 | it('polls for injected data', function() {
35 | var promise = inspectedApp.loadInspectedAppData();
36 |
37 | expect(promise.$$state.status).toEqual(0);
38 | $timeout.flush(2000);
39 | expect(promise.$$state.status).toEqual(0);
40 | // after 2 seconds load app
41 | returnedData = data;
42 | $timeout.flush(3000);
43 |
44 | expect(promise.$$state.status).toEqual(1);
45 | expect(promise.$$state.value).toBe(data);
46 |
47 | });
48 |
49 | });
50 |
51 |
52 | });
53 |
--------------------------------------------------------------------------------
/app/scripts/main/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
Modules
10 |
11 |
Components
12 |
13 |
14 |
Tutorial
15 |
Disable graph
16 |
17 |
18 |
Mouse wheel - zoom
19 |
Mouse drag - move
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/mocks/chromeMock.js:
--------------------------------------------------------------------------------
1 | function createChromeExtensionMock() {
2 |
3 | var extend = function(obj, source) {
4 | for (var prop in source) {
5 | obj[prop] = source[prop];
6 | }
7 | return obj;
8 | };
9 |
10 | // TODO: rename the "jQuery" stuff
11 | var jQueryResult = [];
12 |
13 | var defaultMock = {
14 | document: {
15 | getElementsByClassName: function (arg) {
16 | if (arg === 'ng-scope') {
17 | return jQueryResult;
18 | }
19 | throw new Error('unknown selector');
20 | }
21 | }
22 | };
23 |
24 | var windowMock = defaultMock;
25 |
26 | return {
27 | eval: function (fn, args, cb) {
28 | if (!cb && typeof args === 'function') {
29 | cb = args;
30 | args = {};
31 | } else if (!args) {
32 | args = {};
33 | }
34 | var res = fn(windowMock, args);
35 | if (typeof cb === 'function') {
36 | cb(res);
37 | }
38 | },
39 | __registerWindow: function (win) {
40 | windowMock = extend(windowMock, win);
41 | },
42 | __registerQueryResult: function (res) {
43 | jQueryResult = res;
44 |
45 | jQueryResult.each = function (fn) {
46 | var i;
47 | for (i = 0; i < this.length; i++) {
48 | fn(i, this[i]);
49 | }
50 | };
51 | },
52 | sendRequest: jasmine.createSpy('sendRequest')
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": "nofunc",
12 | "sub": true,
13 | "boss": true,
14 | "eqnull": true,
15 | "newcap": false,
16 | "noarg": true,
17 | "quotmark": "both",
18 | "regexp": true,
19 | "undef": true,
20 | "unused": "vars",
21 | "strict": false,
22 | "trailing": true,
23 | "smarttabs": true,
24 | "white": false,
25 |
26 | "globals": {
27 | "define": true,
28 | "require": true,
29 | "window" : true,
30 | "browser": true,
31 | "jquery": true,
32 | "console": true,
33 |
34 | "jasmine": true,
35 | "runs": true,
36 | "describe": true,
37 | "it": true,
38 | "ddescribe": true,
39 | "iit": true,
40 | "xdescribe": true,
41 | "xit": true,
42 | "expect": true,
43 | "beforeEach": true,
44 | "afterEach": true,
45 | "spyOn": true,
46 | "element": true,
47 | "input": true,
48 | "pause": true,
49 | "sleep": true,
50 | "waitsFor": true,
51 |
52 | "inject": true,
53 | "protractor": true,
54 | "by": true,
55 |
56 | "$": true,
57 | "_": true,
58 | "angular": true,
59 | "d3": true,
60 | "chrome": true,
61 | "Shepherd": true,
62 | "angular": true,
63 |
64 | "ga": false
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## AngularJS dependency graph
2 |
3 | [](https://gitter.im/filso/ng-dependency-graph?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 |
5 | AngularJS dependency graph browser.
6 | Implemented as a [Chrome extension](https://chrome.google.com/webstore/detail/angularjs-dependency-grap/gghbihjmlhobaiedlbhcaellinkmlogj). Once you install the extension, you can access the graph in Chrome inspector panel.
7 |
8 | http://angularjs-graph.org
9 |
10 | ## Installing from Chrome web store
11 | https://chrome.google.com/webstore/detail/angularjs-dependency-grap/gghbihjmlhobaiedlbhcaellinkmlogj
12 |
13 | ### Installing from Source - development version
14 |
15 | 1. Clone the repository: `git clone git://github.com/filso/ng-dependency-graph`
16 | 2. Navigate to `chrome://chrome/extensions/` and enable Developer Mode.
17 | 3. Choose "Load unpacked extension"
18 | 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.
19 |
20 | ### Features
21 | - components and modules view
22 | - update graph on reload
23 | - ignore and filter modules
24 | - sticky nodes
25 | - zooming and panning
26 | - filtering by component type
27 | - works for apps loaded asynchronously (`angular.bootstrap`)
28 |
29 | ### Other
30 | This app uses semantic versioning: http://semver.org/
31 |
--------------------------------------------------------------------------------
/app/devtoolsBackground.js:
--------------------------------------------------------------------------------
1 | var panels = chrome.devtools.panels;
2 |
3 | // The function below is executed in the context of the inspected page.
4 |
5 | var getPanelContents = function() {
6 | if (window.angular && $0) {
7 | //TODO: can we move this scope export into updateElementProperties
8 | var scope = window.angular.element($0).scope();
9 | // Export $scope to the console
10 | window.$scope = scope;
11 | return (function(scope) {
12 | var panelContents = {
13 | __private__: {}
14 | };
15 |
16 | for (var prop in scope) {
17 | if (scope.hasOwnProperty(prop)) {
18 | if (prop.substr(0, 2) === '$$') {
19 | panelContents.__private__[prop] = scope[prop];
20 | } else {
21 | panelContents[prop] = scope[prop];
22 | }
23 | }
24 | }
25 | // TODO: try running $apply on change in injected window
26 | // console.log('ok change', panelContents);
27 | // Object.observe(scope, function(changes) {
28 | // console.log('changes', changes);
29 | // });
30 | return panelContents;
31 | }(scope));
32 | } else {
33 | return {};
34 | }
35 | };
36 |
37 | panels.elements.createSidebarPane(
38 | 'AngularJS Scope',
39 | function(sidebar) {
40 | panels.elements.onSelectionChanged.addListener(function updateElementProperties() {
41 | sidebar.setExpression('(' + getPanelContents.toString() + ')()');
42 | });
43 | });
44 |
45 | panels.create(
46 | 'AngularJS Graph',
47 | 'app/img/angular.png',
48 | 'app/index.html'
49 | );
50 |
--------------------------------------------------------------------------------
/app/styles/_graph.scss:
--------------------------------------------------------------------------------
1 | .link {
2 | stroke: #ccc;
3 | stroke-width: 1.5px;
4 | }
5 |
6 | svg > g {
7 | transition: all 0.3s;
8 | }
9 |
10 | .node {
11 |
12 | /**
13 | * Node colors
14 | */
15 | @each $type in $components {
16 | $i: index($components, $type);
17 | &.#{$type} {
18 | & > circle {
19 | fill: nth($components-colors, $i);
20 | }
21 | &.fixed > circle {
22 | fill: lighten(nth($components-colors, $i), 20%);
23 | }
24 | }
25 | }
26 |
27 | /**
28 | * Circle in node
29 | */
30 | circle {
31 | stroke: $white;
32 | stroke-width: 1px;
33 | r: 8;
34 | transition: all 0.3s;
35 | }
36 |
37 | &.fixed circle {
38 | r: 11;
39 | // stroke: #ccc;
40 | }
41 |
42 | &:hover circle {
43 | r: 10;
44 | }
45 |
46 | &.selected circle {
47 | r: 12;
48 | stroke-width: 2px;
49 | stroke: $white;
50 | }
51 |
52 | &.selected.module circle {
53 | r: 12;
54 | stroke-width: 2px;
55 | fill: lighten($color-module, 20%);
56 | }
57 |
58 | }
59 |
60 |
61 | text {
62 | cursor: pointer;
63 | font: 12px sans-serif;
64 | pointer-events: none;
65 |
66 | paint-order: stroke;
67 | stroke: #000000;
68 | stroke-width: 4px;
69 | stroke-linecap: butt;
70 | stroke-linejoin: miter;
71 | font-weight: 800;
72 |
73 | }
74 |
75 |
76 | .filters__component-type {
77 | label {
78 | user-select: none;
79 | }
80 | }
--------------------------------------------------------------------------------
/gulp/unit-tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 |
5 | var karma = require('karma');
6 | var concat = require('concat-stream');
7 | var _ = require('lodash');
8 |
9 | module.exports = function(options) {
10 | function listFiles(callback) {
11 |
12 | var specFiles = [
13 | 'app/**/*.spec.js',
14 | 'app/**/*.mock.js'
15 | ];
16 |
17 | var htmlFiles = [
18 | 'app/**/*.html'
19 | ];
20 |
21 | // TODO: move vendor files to gulpfile.js
22 | var srcFiles = [
23 | 'app/vendor/jquery-2.1.1.min.js',
24 | 'app/vendor/angular.js',
25 | 'app/vendor/angular-animate.js',
26 | 'app/vendor/angular-mocks.js',
27 | 'app/vendor/d3.min.js',
28 | 'app/vendor/lodash.js',
29 |
30 | 'app/scripts/app.module.js',
31 | 'app/scripts/**/*.js',
32 | ].concat(specFiles.map(function(file) {
33 | return '!' + file;
34 | }));
35 |
36 |
37 | gulp.src(srcFiles)
38 | .pipe(concat(function(files) {
39 | callback(_.map(files, 'path')
40 | .concat(htmlFiles)
41 | .concat(specFiles));
42 | }));
43 | }
44 |
45 | function runTests(singleRun, done) {
46 | listFiles(function(files) {
47 | karma.server.start({
48 | configFile: __dirname + '/../karma.conf.js',
49 | files: files,
50 | singleRun: singleRun,
51 | autoWatch: !singleRun
52 | }, done);
53 | });
54 | }
55 |
56 | gulp.task('test', ['scripts'], function(done) {
57 | runTests(true, done);
58 | });
59 | gulp.task('test:auto', ['watch'], function(done) {
60 | runTests(false, done);
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-dependency-graph",
3 | "version": "0.2.10",
4 | "author": {
5 | "name": "Filip Sobczak",
6 | "email": "filsob@gmail.com",
7 | "url": "http://filso.dev/"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/filso/ng-dependency-graph.git"
12 | },
13 | "devDependencies": {
14 | "browser-sync": "^2.7.6",
15 | "browser-sync-spa": "^1.0.2",
16 | "chalk": "^1.0.0",
17 | "concat-stream": "^1.4.10",
18 | "conventional-changelog": "0.4.3",
19 | "gulp": "~3.9.0",
20 | "gulp-angular-filesort": "^1.1.1",
21 | "gulp-bump": "~0.3.1",
22 | "gulp-cached": "~1.1.0",
23 | "gulp-clean": "~0.3.1",
24 | "gulp-concat": "~2.6.0",
25 | "gulp-connect": "~2.2.0",
26 | "gulp-exec": "~2.1.1",
27 | "gulp-inject": "^2.2.0",
28 | "gulp-jshint": "~1.11.2",
29 | "gulp-livereload": "~3.8.0",
30 | "gulp-load-plugins": "^1.0.0-rc.1",
31 | "gulp-plumber": "~1.0.1",
32 | "gulp-release-tasks": "filso/gulp-release-tasks",
33 | "gulp-sass": "~2.0.4",
34 | "gulp-uglify": "~1.4.1",
35 | "gulp-util": "~3.0.1",
36 | "gutil": "^1.6.4",
37 | "http-proxy": "^1.11.1",
38 | "jasmine": "^2.3.1",
39 | "jasmine-core": "^2.3.4",
40 | "jshint-stylish": "~2.0.1",
41 | "karma": "^0.13.9",
42 | "karma-chrome-launcher": "^0.2.0",
43 | "karma-jasmine": "^0.3.5",
44 | "karma-ng-html2js-preprocessor": "^0.1.2",
45 | "karma-phantomjs-launcher": "^0.2.0",
46 | "lodash": "~4.7.0",
47 | "lodash-node": "~3.10.1",
48 | "phantomjs": "^1.9.17",
49 | "plumber": "^0.4.8",
50 | "run-sequence": "~1.1.2",
51 | "yargs": "~3.25.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/scripts/app/AppCtrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // TODO(filip): refactor this mess ;)
4 | angular.module('ngDependencyGraph')
5 | .controller('AppCtrl', function($rootScope, $scope, inspectedApp, Const, storage, appContext, currentView) {
6 | var _this = this;
7 |
8 | var templates = {
9 | ABOUT: 'scripts/about/about.html',
10 | MAIN: 'scripts/main/main.html'
11 | };
12 |
13 | _this.loadSampleApp = function() {
14 | inspectedApp.loadSampleData();
15 | _this.appTemplate = templates.MAIN;
16 | };
17 |
18 | _this.insertCookieAndRefresh = function(appName) {
19 | appContext.setCookie(appName);
20 | };
21 |
22 | _this.inspectedApp = inspectedApp;
23 |
24 | function init() {
25 | inspectedApp.waitingForAppData = false;
26 |
27 | appContext.getCookie(function(appName) {
28 | if (appName !== null && appName !== 'true') {
29 | // App enabled for this page.
30 | _this.appName = appName;
31 | inspectedApp.loadInspectedAppData([appName]).then(function() {
32 | if (_this.appTemplate !== templates.MAIN) {
33 | _this.appTemplate = templates.MAIN;
34 | } else {
35 | $scope.$broadcast(Const.Events.INIT_MAIN);
36 | }
37 | });
38 | } else {
39 | // Cookie not set yet, so check if Angular is present.
40 | inspectedApp.getAppsInfo().then(function(data) {
41 | _this.appsInfo = data;
42 | _this.appTemplate = templates.ABOUT;
43 | });
44 | }
45 | });
46 | }
47 |
48 | if (chrome.extension) {
49 | appContext.watchRefresh(init);
50 | init();
51 | } else {
52 | // just load sample app, not in a tab, development / test
53 | _this.loadSampleApp();
54 | $scope.$broadcast(Const.Events.INIT_MAIN);
55 | }
56 |
57 | });
58 |
--------------------------------------------------------------------------------
/app/scripts/controls/dgSearchNode.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .directive('dgSearchNode', function(currentView, Const, $rootScope) {
5 |
6 | return {
7 | scope: true,
8 | link: function(scope, elm) {
9 |
10 | var allNodes;
11 | var inputElm = $('input', elm);
12 |
13 | function updateNodes() {
14 | allNodes = currentView.modulesGraph.nodes.concat(currentView.componentsGraph.nodes);
15 | }
16 |
17 | function findMatches(q, cb) {
18 | var substrRegex = new RegExp(q, 'i');
19 | var arr = _.filter(allNodes, function(node) {
20 | return substrRegex.test(node.name);
21 | });
22 | cb(arr);
23 | }
24 |
25 | function suggestionTemplateFn(node) {
26 | return '' + node.name + '' + node.type + '
';
27 | }
28 |
29 | function clearInput() {
30 | inputElm.typeahead('val', '');
31 | }
32 |
33 | scope.$on(Const.Events.UPDATE_GRAPH, function() {
34 | updateNodes();
35 | });
36 |
37 | updateNodes();
38 |
39 |
40 | inputElm.typeahead({
41 | hint: true,
42 | highlight: true,
43 | minLength: 1
44 | },
45 | {
46 | display: 'name',
47 | source: findMatches,
48 | templates: {
49 | suggestion: suggestionTemplateFn
50 | }
51 | });
52 |
53 | inputElm.bind('typeahead:select', function(ev, node) {
54 | $rootScope.$apply(function() {
55 | currentView.chooseNode(node, true);
56 | });
57 | clearInput();
58 | });
59 |
60 | inputElm.bind('focus', function() {
61 | clearInput();
62 | });
63 |
64 |
65 | }
66 | };
67 |
68 |
69 | });
70 |
--------------------------------------------------------------------------------
/app/styles/app.scss:
--------------------------------------------------------------------------------
1 | $info-panel-width: 300px;
2 |
3 | @import "colors";
4 | @import "graph";
5 | @import "controls";
6 | @import "infoPanel";
7 | @import "about";
8 |
9 |
10 | @mixin font-family-mix {
11 | font-family: 'Lucida Sans Unicode', 'Lucida Grande', sans-serif ;
12 | // font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
13 | }
14 |
15 | html {
16 | @include font-family-mix;
17 | margin: 0;
18 | font-size: 14px;
19 | background: $color-graph-bg;;
20 | overflow-x: hidden;
21 | }
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | html, body, .app, .about, .main {
28 | height: 100%;
29 | }
30 |
31 | $dg-graph-width: calc(100% - #{$info-panel-width} - 30px);
32 |
33 | .dg-graph {
34 | margin-right: $info-panel-width;
35 | position: relative;
36 | height: 100%;
37 |
38 | box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.55);
39 |
40 | background: $color-graph-bg;
41 | color: white;
42 |
43 | svg {
44 | width: 100%;
45 | height: 100%;
46 | cursor: move;
47 |
48 | circle {
49 | z-index: -3;
50 | }
51 |
52 | text {
53 | z-index: 0;
54 | fill: white;
55 | @include font-family-mix;
56 | }
57 |
58 | line {
59 | z-index: 3;
60 | fill: white;
61 | }
62 | }
63 |
64 | .node {
65 | cursor: pointer;
66 | }
67 |
68 | .legend {
69 | position: absolute;
70 | bottom: 0px;
71 | left: 0px;
72 | }
73 |
74 | .reset-tour {
75 | cursor: pointer;
76 | text-decoration: underline;
77 | color: white;
78 | position: absolute;
79 | display: inline-block;
80 | bottom: 10px;
81 | right: 10px;
82 | }
83 |
84 |
85 | .disable-graph {
86 | cursor: pointer;
87 | text-decoration: underline;
88 | color: white;
89 | position: absolute;
90 | display: inline-block;
91 | font-size: 0.8em;
92 | top: 5px;
93 | right: 10px;
94 | }
95 |
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/app/scripts/core/nodeFactory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('nodeFactory', function(Component, Module) {
5 |
6 | /**
7 | * Create, optionally reusing nodes from oldGraph
8 | * Algorithm to update nodes and links is the same:
9 | * - check if oldGraph contains node / link
10 | * - if it does, reuse, otherwise create new one
11 | * - drop otherwise
12 | * D3 identifies nodes / links by `_id` field;
13 | * TODO(filip): make sure that memory for old, unused nodes + links doesn't leak
14 | */
15 | function createNodes(rawNodes, oldGraph) {
16 | var nodes = [];
17 | var links = [];
18 |
19 | _.each(rawNodes, function(rawNode) {
20 | var node;
21 | if (oldGraph) {
22 | node = _.find(oldGraph.nodes, {name: rawNode.name});
23 | }
24 |
25 | if (node === undefined) {
26 | if (rawNode.type === 'module') {
27 | node = new Module(rawNode);
28 | } else {
29 | node = new Component(rawNode);
30 | }
31 | }
32 | nodes.push(node);
33 | });
34 |
35 | // TODO(filip): Not reusing links at this time... Do I need to do that? D3 force layout doesn't seem to care
36 | _.each(nodes, function(node) {
37 | node.resetLinks();
38 | });
39 |
40 | _.each(nodes, function(node1) {
41 |
42 | var node1Deps = _.filter(nodes, function(item) {
43 | return _.contains(node1._data.deps, item._data.name);
44 | });
45 |
46 | _.each(node1Deps, function(node2) {
47 | node1.linkDep(node2);
48 | node2.linkProvides(node1);
49 | links.push({target: node1, source: node2, _id: _.uniqueId()});
50 | });
51 |
52 | });
53 |
54 | return {
55 | nodes: nodes,
56 | links: links
57 | };
58 | }
59 |
60 | return {
61 | createNodes: createNodes
62 | };
63 | });
64 |
--------------------------------------------------------------------------------
/app/scripts/about/about.html:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
AngularJS Dependency Graph browser
5 |
6 |
Loading, please wait...
7 |
8 |
9 |
10 | No AngularJS app detected. Please open in a tab with AngularJS app.
11 |
Open sample app
12 |
13 |
14 |
25 |
26 |
27 |
28 | Waiting for app '{{ app.appName }}' to load...
Cancel
29 |
30 |
31 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/gulp/versioning.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var conventionalChangelog = require('conventional-changelog');
3 | var runSequence = require('run-sequence');
4 | var bump = require('gulp-bump');
5 | var gulpExec = require('gulp-exec');
6 | var fs = require('fs');
7 |
8 | /**
9 | * Version release code
10 | */
11 | module.exports = function(options) {
12 |
13 | gulp.task('release', function(callback) {
14 | runSequence('bump-minor', 'release-tasks', callback);
15 | });
16 |
17 | gulp.task('patch', function(callback) {
18 | runSequence('bump-patch', 'release-tasks', callback);
19 | });
20 |
21 | gulp.task('bump-minor', function() {
22 | gulp.src('./package.json')
23 | .pipe(bump({
24 | type: 'minor'
25 | }))
26 | .pipe(gulp.dest('./'));
27 | });
28 |
29 | gulp.task('bump-patch', function() {
30 | gulp.src('./package.json')
31 | .pipe(bump({
32 | type: 'patch'
33 | }))
34 | .pipe(gulp.dest('./'));
35 | });
36 |
37 | // https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#heading=h.we1fxubeuwef
38 | gulp.task('release-tasks', function() {
39 | var jsonFile = require('./package.json'),
40 | commitMsg = "chore(release): " + jsonFile.version;
41 |
42 | function changeParsed(err, log) {
43 | if (err) {
44 | return done(err);
45 | }
46 | fs.writeFile('CHANGELOG.md', log);
47 | }
48 | var repository, version;
49 | repository = jsonFile.repository, version = jsonFile.version;
50 | conventionalChangelog({
51 | // repository: repository.url,
52 | version: version
53 | }, changeParsed);
54 |
55 | return gulp.src(['package.json', 'CHANGELOG.md'])
56 | .pipe(gulp.dest('.'))
57 | .pipe(gulpExec('git add -A')) // so the following git commands only execute once
58 | .pipe(gulpExec("git commit -m '" + commitMsg + "'"))
59 | .pipe(gulpExec("git tag -a " + jsonFile.version + " -m '" + commitMsg + "'"));
60 | });
61 |
62 | };
63 |
--------------------------------------------------------------------------------
/gulp/proxy.js:
--------------------------------------------------------------------------------
1 | /*jshint unused:false */
2 |
3 | /***************
4 |
5 | This file allow to configure a proxy system plugged into BrowserSync
6 | in order to redirect backend requests while still serving and watching
7 | files from the web project
8 |
9 | IMPORTANT: The proxy is disabled by default.
10 |
11 | If you want to enable it, watch at the configuration options and finally
12 | change the `module.exports` at the end of the file
13 |
14 | ***************/
15 |
16 | 'use strict';
17 |
18 | var httpProxy = require('http-proxy');
19 | var chalk = require('chalk');
20 |
21 | /*
22 | * Location of your backend server
23 | */
24 | var proxyTarget = 'http://server/context/';
25 |
26 | var proxy = httpProxy.createProxyServer({
27 | target: proxyTarget
28 | });
29 |
30 | proxy.on('error', function(error, req, res) {
31 | res.writeHead(500, {
32 | 'Content-Type': 'text/plain'
33 | });
34 |
35 | console.error(chalk.red('[Proxy]'), error);
36 | });
37 |
38 | /*
39 | * The proxy middleware is an Express middleware added to BrowserSync to
40 | * handle backend request and proxy them to your backend.
41 | */
42 | function proxyMiddleware(req, res, next) {
43 | /*
44 | * This test is the switch of each request to determine if the request is
45 | * for a static file to be handled by BrowserSync or a backend request to proxy.
46 | *
47 | * The existing test is a standard check on the files extensions but it may fail
48 | * for your needs. If you can, you could also check on a context in the url which
49 | * may be more reliable but can't be generic.
50 | */
51 | if (/\.(html|css|js|png|jpg|jpeg|gif|ico|xml|rss|txt|eot|svg|ttf|woff|woff2|cur)(\?((r|v|rel|rev)=[\-\.\w]*)?)?$/.test(req.url)) {
52 | next();
53 | } else {
54 | proxy.web(req, res);
55 | }
56 | }
57 |
58 | /*
59 | * This is where you activate or not your proxy.
60 | *
61 | * The first line activate if and the second one ignored it
62 | */
63 |
64 | //module.exports = [proxyMiddleware];
65 | module.exports = function() {
66 | return [];
67 | };
68 |
--------------------------------------------------------------------------------
/app/styles/_infoPanel.scss:
--------------------------------------------------------------------------------
1 | .info-panel-wrapper {
2 | height: 100%;
3 | background: white;
4 | }
5 | .info-panel {
6 | height: calc(100% - 70px); // TODO convert to flex vertical layout
7 | }
8 |
9 | .info-panel-wrapper {
10 | position: fixed;
11 | right: 0;
12 | top: 0;
13 | width: $info-panel-width;
14 | }
15 |
16 | .info-panel {
17 | color: #444;
18 | overflow-y: scroll;
19 | font-size: 13px;
20 |
21 | .info {
22 | margin: 5px 0px;
23 | display: flex;
24 | align-items: center;
25 |
26 | & > span {
27 | vertical-align: middle;
28 | padding: 5px;
29 | }
30 | .node-module-name {
31 | color: $color-module;
32 | }
33 | }
34 |
35 | .node-title {
36 | font-weight: bold;
37 | font-size: 1.2em;
38 | padding: 5px;
39 | }
40 |
41 | .title {
42 | background: #eee;
43 | border-bottom: 1px solid #ccc;
44 | border-top: 1px solid #ddd;
45 | }
46 |
47 | .info-panel-list {
48 | & > div {
49 | padding: 3px;
50 | margin-bottom: 3px;
51 | }
52 |
53 |
54 | @each $type in $components {
55 | $i: index($components, $type);
56 | &.#{$type} {
57 | & > .title {
58 | background: lighten(nth($components-colors, $i), 10%);
59 | color: white;
60 | }
61 | }
62 | }
63 |
64 | }
65 |
66 |
67 | .node-module-title {
68 | }
69 |
70 | .choose-node {
71 | cursor: pointer;
72 | color: $blue;
73 |
74 | &:hover {
75 | color: lighten($blue, 30%);
76 | }
77 | }
78 |
79 | }
80 |
81 | .options {
82 | position: absolute;
83 | bottom: 0px;
84 | right: 0px;
85 | background: white;
86 | width: $info-panel-width;
87 | .title {
88 | padding: 5px;
89 | font-weight: bold;
90 | }
91 |
92 | & > div {
93 | padding: 5px;
94 | label {
95 | display: flex;
96 | align-items: center;
97 | }
98 | span {
99 | width: 50px;
100 | }
101 | input[type=checkbox] {
102 | margin-right: 8px;
103 | }
104 | input[type=text] {
105 | font-size: 12px;
106 | flex: 1;
107 | // width: 200px;
108 | vertical-align: middle;
109 | }
110 |
111 | &:first-child {
112 |
113 | }
114 | }
115 |
116 | }
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 |
3 | var concat = require('gulp-concat');
4 | var stylish = require('jshint-stylish');
5 | var jshint = require('gulp-jshint');
6 | var clean = require('gulp-clean');
7 | var cache = require('gulp-cached');
8 | var runSequence = require('run-sequence');
9 | var gutil = require('gulp-util');
10 | var plumber = require('gulp-plumber');
11 | var _ = require('lodash-node');
12 | var browserSync = require('browser-sync');
13 |
14 |
15 | var paths = {
16 | scripts: ['app/scripts/**/*.js'],
17 | appScripts: ['app/scripts/**/*.js', '!app/scripts/**/*.spec.js', '!app/scripts/inject/inject.js'],
18 | images: 'app/images/**/*',
19 | html: 'app/**/*.html',
20 | styles: {
21 | sass: 'app/styles/**/*.scss',
22 | css: 'app/styles/*.css'
23 | },
24 | notLinted: ['!app/scripts/templates.js']
25 | };
26 |
27 | var options = {
28 | errorHandler: function(title) {
29 | return function(err) {
30 | gutil.log(gutil.colors.red('[' + title + ']'), err.toString());
31 | this.emit('end');
32 | };
33 | },
34 | paths: paths
35 | };
36 |
37 | require('./gulp/release-tasks');
38 | // require('./gulp/versioning');
39 |
40 | require('./gulp/styles')(options);
41 | require('./gulp/inject')(options);
42 | require('./gulp/server')(options);
43 | require('./gulp/unit-tests')(options);
44 | require('./gulp/changelog')(options);
45 | require('./gulp/unit-tests')(options);
46 |
47 |
48 | /**
49 | * Development tasks
50 | */
51 | var developTasks = ['preprocess', 'watch', 'serve', 'test:auto'];
52 | gulp.task('develop', developTasks);
53 |
54 | gulp.task('no-karma', function() {
55 | _.remove(developTasks, 'karma');
56 | gulp.start('develop');
57 | });
58 |
59 |
60 | gulp.task('scripts', ['preprocess', 'lint']);
61 |
62 | gulp.task('preprocess', ['inject', 'sass']);
63 |
64 | // The default task
65 | gulp.task('default', ['develop']);
66 |
67 |
68 | gulp.task('lint', function() {
69 | return gulp.src(paths.scripts.concat(paths.notLinted))
70 | .pipe(cache('lint'))
71 | .pipe(jshint('.jshintrc'))
72 | .pipe(jshint.reporter('jshint-stylish'));
73 | });
74 |
75 | // Rerun the task when a file changes
76 | gulp.task('watch', function() {
77 |
78 | gulp.watch(paths.styles.sass, ['sass']);
79 |
80 | gulp.watch(paths.scripts).on('change', browserSync.reload);
81 | gulp.watch(paths.html).on('change', browserSync.reload);
82 |
83 | gulp.watch(paths.appScripts, function(event) {
84 | if (event.type === 'added' || event.type === 'deleted') {
85 | gulp.start('inject');
86 | }
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/app/scripts/core/appContext.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*jshint -W061 */
4 | // Service for running code in the context of the application being debugged
5 | angular.module('ngDependencyGraph')
6 | .factory('appContext', function(chromeExtension, Const) {
7 |
8 | // Public API
9 | // ==========
10 | return {
11 | refresh: function(cb) {
12 | chromeExtension.eval(function(window) {
13 | window.document.location.reload();
14 | }, cb);
15 | },
16 | /**
17 | * Takes app name. If '', deletes the cookie
18 | */
19 | setCookie: function(appName) {
20 | chromeExtension.eval(function(window, args) {
21 | if (args.value === '') {
22 | document.cookie = '__ngDependencyGraph=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
23 | } else {
24 | document.cookie = '__ngDependencyGraph=' + encodeURIComponent(args.value) + ';';
25 | }
26 | window.location = window.location;
27 | }, {value: appName});
28 | },
29 | getCookie: function(cb) {
30 | chromeExtension.eval(function(window) {
31 | var sKey = '__ngDependencyGraph';
32 | return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
33 | }, cb);
34 | },
35 |
36 | // takes a bool
37 | setLog: function(setting) {
38 | setting = !!setting;
39 | chromeExtension.eval('function (window) {' +
40 | 'window.__ngDependencyGraph.log = ' + setting.toString() + ';' +
41 | '}');
42 | },
43 |
44 | // Registering events
45 | // ------------------
46 |
47 | // TODO: depreciate this; only poll from now on?
48 | // There are some cases where you need to gather data on a once-per-bootstrap basis, for
49 | // instance getting the version of AngularJS
50 |
51 | // TODO: move to chromeExtension?
52 | watchRefresh: function(cb) {
53 | var port = chrome.extension.connect();
54 | port.postMessage({
55 | action: 'register',
56 | inspectedTabId: chrome.devtools.inspectedWindow.tabId
57 | });
58 | port.onMessage.addListener(function(msg) {
59 | if (msg.action === 'refresh' && msg.changeInfo.status === 'complete') {
60 | cb(msg.changeInfo);
61 | }
62 | });
63 | port.onDisconnect.addListener(function(a) {
64 | console.log(a);
65 | });
66 | }
67 |
68 | };
69 | });
70 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/styles/_controls.scss:
--------------------------------------------------------------------------------
1 | .controls {
2 | margin-bottom: 5px;
3 | padding: 5px;
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 |
8 | & > div {
9 | margin-right: 10px;
10 | }
11 |
12 | }
13 |
14 | .choose-scope {
15 |
16 | border-radius: 3px;
17 |
18 | display: inline-block;
19 | background: black;
20 | background-image: linear-gradient(to bottom, darken(#3498db, 10%), #2c84ba);
21 |
22 |
23 |
24 | & > div {
25 | display: inline-block;
26 | padding: 8px;
27 | cursor: pointer;
28 | color: #fff;
29 |
30 |
31 | &.active, &:hover {
32 | border-radius: 3px;
33 |
34 | background-image: linear-gradient(to bottom, #3cb0fd, #3498db);
35 | }
36 |
37 | }
38 | }
39 |
40 | /**
41 | * Search controls - typeahead.js
42 | */
43 | .search {
44 | display: inline-flex;
45 | align-items: center;
46 | label {
47 | margin-right: 5px;
48 | }
49 | }
50 |
51 | .tt-dataset {
52 | background: white;
53 | min-width: 400px;
54 | }
55 |
56 | .tt-menu {
57 | z-index: 10000 !important;
58 | border: 1px solid #444;
59 | }
60 |
61 | .tt-suggestion {
62 | color: #444;
63 | padding: 5px;
64 | border-bottom: 1px solid #aaa;
65 | cursor: pointer;
66 | display: flex;
67 | align-items: center;
68 |
69 | &.tt-cursor {
70 | background: #ddd;
71 | }
72 |
73 | @each $type in $components {
74 | $i: index($components, $type);
75 | &.#{$type} {
76 | .type {
77 | background: lighten(nth($components-colors, $i), 10%);
78 | padding: 3px;
79 | border-radius: 3px;
80 | color: white;
81 | }
82 | }
83 | }
84 |
85 | .type {
86 | margin-left: 5px;
87 | font-style: italic;
88 | font-size: 0.8em;
89 | }
90 | }
91 |
92 | /**
93 | * Bottom left controls
94 | */
95 | .mouse-instructions {
96 | padding: 5px;
97 | }
98 |
99 | .trigger-components {
100 |
101 | label {
102 | cursor: pointer;
103 | padding: 2px;
104 | display: block;
105 | }
106 |
107 | /**
108 | * Node colors
109 | */
110 | @each $type in $components {
111 | $i: index($components, $type);
112 | & > .#{$type} {
113 | background: linear-gradient(to right, nth($components-colors, $i), lighten(nth($components-colors, $i), 8%));
114 | border-width: 0px;
115 | border-style: solid;
116 | font-size: 0.9em;
117 | border-top-right-radius: 5px;
118 | border-bottom-right-radius: 5px;
119 | font-style: italic;
120 | }
121 | }
122 |
123 | }
--------------------------------------------------------------------------------
/app/scripts/infoPanel/infoPanel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Options
36 |
37 |
38 |
40 |
41 |
42 | Show modules
43 |
44 |
45 |
49 |
50 |
51 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/scripts/core/inspectedApp.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*jshint -W061 */
4 | angular.module('ngDependencyGraph')
5 | .factory('inspectedApp', function($q, $timeout, appContext, chromeExtension, sampleAppData, Const) {
6 |
7 | var _data;
8 |
9 | // TODO clear cache on page refresh
10 | // appContext.watchRefresh(function() {
11 | // _data = undefined;
12 | // });
13 |
14 | var service = {
15 | waitingForAppData: false,
16 | getKey: function() {
17 | return this.getData().host + '__' + this.apps[0];
18 | },
19 | getAppsInfo: function() {
20 | var defer = $q.defer();
21 |
22 | chromeExtension.eval(function() {
23 |
24 | var appElms = document.querySelectorAll('[ng-app], [data-ng-app], [x-ng-app]');
25 | var appNames = [];
26 | for (var i = 0; i < appElms.length; ++i) {
27 | var elm = appElms[i];
28 | var appName = elm.getAttribute('ng-app') || elm.getAttribute('data-ng-app') || elm.getAttribute('x-ng-app');
29 | appNames.push(appName);
30 | }
31 |
32 | return {
33 | angularVersion: window.angular.version,
34 | appNames: appNames
35 | };
36 |
37 | }, function(data) {
38 | defer.resolve(data);
39 | });
40 | return defer.promise;
41 | },
42 | // TODO(filip): I don't like this interface... remove getData, pass inspected data instead?
43 | _setData: function(data) {
44 | _data = data;
45 | this.apps = _data.apps;
46 | },
47 | getData: function() {
48 | return _data;
49 | },
50 | loadSampleData: function() {
51 | this._setData(sampleAppData);
52 | },
53 | loadInspectedAppData: function(appNames) {
54 | var defer = $q.defer();
55 | // TODO do sth smarter... maybe load sample app?
56 | if (!chromeExtension.isExtensionContext()) {
57 | defer.reject();
58 | return defer.promise;
59 | }
60 |
61 | var injectedFn = function(window, appNames) {
62 | if (window.__ngDependencyGraph) {
63 | return window.__ngDependencyGraph.getMetadata(appNames);
64 | }
65 | };
66 |
67 | function pollFn() {
68 | chromeExtension.eval(injectedFn, appNames, function(data) {
69 | if ((data === undefined || data.apps.length === 0) && service.waitingForAppData === true) {
70 | $timeout(pollFn, Const.INJECTED_POLL_INTERVAL);
71 | } else {
72 | service._setData(data);
73 | service.waitingForAppData = false;
74 | defer.resolve(_data);
75 | }
76 | });
77 |
78 | }
79 |
80 | service.waitingForAppData = true;
81 | pollFn();
82 |
83 | return defer.promise;
84 | }
85 | };
86 |
87 | return service;
88 | });
89 |
--------------------------------------------------------------------------------
/app/scripts/core/dev.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('dev', function($rootScope, $window, $q) {
5 |
6 | var countWatchers = function() {
7 | var target = $rootScope,
8 | current = target,
9 | next;
10 | var watchers = 0;
11 | do {
12 | watchers += current.$$watchers && current.$$watchers.length;
13 | if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
14 | while (current !== target && !(next = current.$$nextSibling)) {
15 | current = current.$parent;
16 | }
17 | }
18 | } while ((current = next));
19 | return watchers;
20 | };
21 |
22 | var groupCountWatchers = function() {
23 | var target = $rootScope,
24 | current = target,
25 | next,
26 | groups = {};
27 | var watchers = 0;
28 | do {
29 | watchers += current.$$watchers && current.$$watchers.length;
30 | _.each(current.$$watchers, function(w) {
31 | if (typeof(w.exp) === 'string') {
32 | groups[w.exp] = groups[w.exp] || 0;
33 | groups[w.exp] += 1;
34 | }
35 | });
36 | if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
37 | while (current !== target && !(next = current.$$nextSibling)) {
38 | current = current.$parent;
39 | }
40 | }
41 | } while ((current = next));
42 |
43 | return groups;
44 | };
45 |
46 | function consoleRun(fnName, prefix) {
47 | return function(id) {
48 | if (console[fnName]) {
49 | console[fnName](prefix + ' ' + id);
50 | }
51 | };
52 | }
53 |
54 |
55 | var service = {
56 | profile: consoleRun('profile', ''),
57 | profileEnd: consoleRun('profileEnd', ''),
58 | time: consoleRun('time', 'Time: '),
59 | timeEnd: consoleRun('timeEnd', 'Time: '),
60 | gwc: function() {
61 | console.log('Group count watchers', groupCountWatchers());
62 | },
63 | wc: function() {
64 | console.log('Watchers: ' + countWatchers());
65 | },
66 | startLoggingWatchers: function() {
67 |
68 | setInterval(function() {
69 | service.logWatchersCount();
70 | }, 3000);
71 |
72 | },
73 | waitForAngular: function() {
74 | var deferred = $q.defer();
75 |
76 | angular.element($('body')).injector().get('$browser')
77 | .notifyWhenNoOutstandingRequests(deferred.resolve);
78 |
79 | return deferred.promise;
80 | },
81 | getService: function(name) {
82 | return angular.element($('body')).injector().get(name);
83 | },
84 | exposeGlobalObject: function() {
85 | // yes, make it global!
86 | $window.dev = this;
87 | },
88 | clog: function(val) {
89 | var message = JSON.stringify(val).replace(/n/g, " ");
90 | chrome.tabs.sendRequest(tabId,
91 | {"type": "consoleLog", "value": message});
92 | }
93 | };
94 |
95 | return service;
96 | });
--------------------------------------------------------------------------------
/app/scripts/tour/tour.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .factory('tour', function($rootScope, storage) {
5 | var tour = new Shepherd.Tour({
6 | defaults: {
7 | classes: 'shepherd-theme-default shepherd-element shepherd-open',
8 | showCancelLink: true,
9 | scrollTo: false
10 | }
11 | });
12 |
13 | var onDone = function() {
14 | storage.saveTourDone();
15 | $rootScope.$apply();
16 | };
17 |
18 | tour.on('complete', onDone);
19 | tour.on('cancel', onDone);
20 | tour.on('hide', onDone);
21 |
22 | var buttons = {
23 | step: [{
24 | text: 'Next',
25 | action: tour.next
26 | }],
27 | finish: [{
28 | text: 'Finish',
29 | action: tour.next
30 | }]
31 | };
32 |
33 |
34 | var steps = {
35 |
36 | welcome: {
37 | text: 'Welcome to AngularJS dependency graph browser.',
38 | buttons: buttons.step
39 | },
40 |
41 | chooseScope: {
42 | text: 'Switch between modules and components view here.',
43 | attachTo: '.choose-scope bottom',
44 | buttons: buttons.step
45 | },
46 |
47 | ignoreModules: {
48 | text: 'Use \'Ignore\' field to hide modules you don\'t want to see...',
49 | attachTo: '.options__ignore left',
50 | buttons: buttons.step
51 | },
52 |
53 |
54 | filterModules: {
55 | text: '...and/or \'Filter\' field to specify which modules you want to see.',
56 | attachTo: '.options__filter left',
57 | buttons: buttons.step
58 | },
59 |
60 | stickyNodes: {
61 | text: 'If you\'d like your nodes to stay where you drag them - make nodes sticky.
Double click node to unstick.',
62 | attachTo: '.options__sticky-nodes left',
63 | buttons: buttons.step
64 | },
65 |
66 | triggerComponents: {
67 | text: 'You can filter components nodes by component type.',
68 | attachTo: '.trigger-components right',
69 | buttons: buttons.step
70 | },
71 |
72 | search: {
73 | text: 'To focus on particular component or module, use search field.',
74 | attachTo: '.search right',
75 | buttons: buttons.step
76 | },
77 |
78 | saving: {
79 | text: 'The graph is automatically updated with dependencies as you work on your app and refresh the browser.
All options are saved for each of your projects.',
80 | buttons: buttons.step
81 | },
82 |
83 | finish: {
84 | text: 'That\'s it! :) Hope you enjoy.
You can restart this tour by clicking \'Tutorial\' in the bottom right corner.
Please star if you like it:
' +
85 | '',
86 | attachTo: '.search right',
87 | buttons: buttons.finish
88 | }
89 |
90 | };
91 |
92 | _.each(steps, function(step) {
93 | tour.addStep(step);
94 | });
95 |
96 | return tour;
97 |
98 | });
99 |
--------------------------------------------------------------------------------
/app/scripts/main/MainCtrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .controller('MainCtrl', function($scope, $timeout, dev, Graph, Const, currentView, inspectedApp, storage, tour) {
5 | var ctrl = this;
6 | var lastAppKey;
7 | var componentsGraph;
8 | var modulesGraph;
9 |
10 | $scope.currentView = currentView;
11 |
12 | ctrl.startTour = function() {
13 | ga('send', 'event', 'flow', 'action', 'Start tour');
14 | tour.start();
15 | };
16 |
17 | ctrl.isTourActive = function() {
18 | return Shepherd.activeTour !== null && Shepherd.activeTour !== undefined;
19 | };
20 |
21 | // Run this after DOM initialised... post-link directive???
22 | storage.getTourDone().then(function(done) {
23 | if (!done) {
24 | ctrl.startTour();
25 | }
26 | });
27 |
28 |
29 | function init(isTheSameApp) {
30 | ga('send', 'event', 'flow', 'action', 'init ctrl');
31 |
32 | lastAppKey = inspectedApp.getKey();
33 | var rawData = inspectedApp.getData();
34 |
35 | _.each(rawData.modules, function(module) {
36 | module.type = 'module';
37 |
38 | _.each(module.components, function(com) {
39 | com._module = module;
40 | });
41 | });
42 |
43 | var allComponents = [];
44 | _.each(rawData.modules, function(module) {
45 | allComponents = allComponents.concat(module.components);
46 | });
47 |
48 | // Note: if it's the same app, then just update old graph
49 | componentsGraph = Graph.createFromRawNodes(allComponents, Const.Scope.COMPONENTS, isTheSameApp ? componentsGraph : undefined);
50 | modulesGraph = Graph.createFromRawNodes(rawData.modules, Const.Scope.MODULES, isTheSameApp ? modulesGraph : undefined);
51 |
52 | /**
53 | * Connect modules with components
54 | */
55 | _.each(componentsGraph.nodes, function(com) {
56 | var module = _.find(modulesGraph.nodes, {name: com._data._module.name});
57 | com.module = module;
58 |
59 | module.componentsByType = module.componentsByType || {};
60 | if (module.componentsByType[com.type] === undefined) {
61 | module.componentsByType[com.type] = [];
62 | }
63 | module.componentsByType[com.type].push(com);
64 |
65 | });
66 |
67 | currentView.setGraphs(modulesGraph, componentsGraph);
68 |
69 | var appNode = _.find(modulesGraph.nodes, {name: rawData.apps[0]});
70 |
71 | if (isTheSameApp) {
72 | currentView.applyFilters();
73 | } else {
74 | storage.loadCurrentView().then(function() {
75 | currentView.chooseNode(appNode);
76 | currentView.scope = Const.Scope.COMPONENTS;
77 | // TODO meeeh not .setScope here... REFACTOR, setScope should just set scope, not initialise graph
78 | currentView.applyFilters();
79 | }, function() {
80 | currentView.chooseNode(appNode);
81 | currentView.scope = Const.Scope.COMPONENTS;
82 | currentView.applyFilters();
83 | });
84 | }
85 | }
86 |
87 | init(false);
88 |
89 | $scope.$on(Const.Events.INIT_MAIN, function() {
90 | if (inspectedApp.getKey() === lastAppKey) {
91 | init(true);
92 | } else {
93 | init(false);
94 | }
95 | });
96 |
97 | // TODO this seems architecturaly lame
98 | $scope.$on(Const.Events.UPDATE_GRAPH, storage.saveCurrentView);
99 | $scope.$on(Const.Events.CHOOSE_NODE, storage.saveCurrentView);
100 |
101 | });
102 |
--------------------------------------------------------------------------------
/app/scripts/core/currentView.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * TODO this is doing too much: extract filters, serialisation
5 | *
6 | * Responsibilities:
7 | * - singleton holding currentView
8 | * - filtering of modules / components
9 | */
10 | angular.module('ngDependencyGraph')
11 | .factory('currentView', function($rootScope, Const, util) {
12 |
13 | var service = {
14 | selectedNode: undefined,
15 | scope: Const.Scope.MODULES,
16 | filters: {
17 | filterModules: Const.FilterModules.DEFAULT_FILTER,
18 | ignoreModules: Const.FilterModules.DEFAULT_IGNORE,
19 | componentsVisible: {
20 | service: true,
21 | controller: true
22 | }
23 | },
24 | options: {
25 | stickyNodesEnabled: false
26 | },
27 | setGraphs: function(modulesGraph, componentsGraph) {
28 | this.modulesGraph = modulesGraph;
29 | this.componentsGraph = componentsGraph;
30 | },
31 | setScope: function(scope) {
32 | this.scope = scope;
33 | },
34 | chooseNode: function(node, translate) {
35 | if (node.isModule === true) {
36 | this.setScope(Const.Scope.MODULES);
37 | } else {
38 | this.setScope(Const.Scope.COMPONENTS);
39 | }
40 |
41 | this.selectedNode = node;
42 | $rootScope.$broadcast(Const.Events.CHOOSE_NODE, node, translate);
43 | },
44 | applyFilters: _.throttle(function() {
45 | service._applyFilters();
46 | }, 200),
47 | _applyFilters: function() {
48 | if (!this.componentsGraph || !this.modulesGraph) {
49 | return; // not initialised
50 | }
51 |
52 | this.graph = (this.scope === Const.Scope.COMPONENTS ? this.componentsGraph : this.modulesGraph);
53 |
54 | var masks;
55 | this.componentsGraph.resetFilter();
56 | this.modulesGraph.resetFilter();
57 |
58 | if (this.filters.componentsVisible && this.scope === 'components') {
59 | this.componentsGraph.filterNodes(function(node) {
60 | var val = service.filters.componentsVisible[node.type];
61 | return val === true;
62 | });
63 | }
64 |
65 | // Apply ignore and filter masks to modules
66 | if (this.filters.ignoreModules) {
67 | masks = util.extractMasks(this.filters.ignoreModules);
68 |
69 | masks.forEach(function(mask) {
70 | service.modulesGraph.filterNodes(function(node) {
71 | return mask.test(node.name) === false;
72 | });
73 | });
74 | }
75 |
76 | if (this.filters.filterModules) {
77 | masks = util.extractMasks(this.filters.filterModules);
78 |
79 | masks.forEach(function(mask) {
80 | service.modulesGraph.filterNodes(function(node) {
81 | return mask.test(node.name);
82 | });
83 | });
84 | }
85 |
86 | // Now filter all components of excluded modules
87 | this.componentsGraph.filterNodes(function(node) {
88 | return (service.modulesGraph.nodes.indexOf(node.module) !== -1);
89 | });
90 |
91 | $rootScope.$broadcast(Const.Events.UPDATE_GRAPH);
92 | }
93 | };
94 |
95 | function updateView(newVal, oldVal) {
96 | service.applyFilters();
97 | }
98 |
99 | $rootScope.$watch('currentView.filters', updateView, true);
100 | $rootScope.$watch('currentView.scope', updateView, true);
101 |
102 | return service;
103 |
104 | });
105 |
--------------------------------------------------------------------------------
/app/scripts/core/storage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Responsibilities:
5 | * - saving / loading currentView with serialisation
6 | *
7 | * Persist per project:
8 | * - module / component switch
9 | * - focused node
10 | * - ignore + filter modules fields (filters)
11 | * - component types visibility (filters)
12 | */
13 | angular.module('ngDependencyGraph')
14 | .factory('storage', function($q, $rootScope, currentView, inspectedApp, Const) {
15 |
16 | var serializedProps = ['filters', 'options', 'scope'];
17 |
18 |
19 | // this has the same API as StorageArea chrome.sync
20 | var localStorageAdapter = {
21 | get: function(key, cb) {
22 | // Note: run cb outside AngularJS context to mimic chrome.sync behaviour
23 | setTimeout(function() {
24 | var items = {}; items[key] = localStorage.getItem(key);
25 | cb(items);
26 | });
27 | },
28 | set: function(obj, cb) {
29 | _.each(obj, function(val, key) {
30 | localStorage.setItem(key, val);
31 | });
32 | // Note: run cb outside AngularJS context to mimic chrome.sync behaviour
33 | setTimeout(cb);
34 | }
35 | };
36 |
37 | var chromeSync;
38 | if (!chrome.storage) {
39 | chromeSync = localStorageAdapter;
40 | } else {
41 | chromeSync = chrome.storage.sync;
42 | }
43 |
44 | var singleValueAccessor = {
45 | get: function(key) {
46 | var defer = $q.defer();
47 | chromeSync.get(key, function(items) {
48 | defer.resolve(items[key]);
49 | $rootScope.$apply();
50 | });
51 | return defer.promise;
52 | },
53 | set: function(key, val) {
54 | var defer = $q.defer();
55 | var items = {}; items[key] = val;
56 | chromeSync.set(items, function() {
57 | defer.resolve();
58 | $rootScope.$apply();
59 | });
60 | return defer.promise;
61 | }
62 | };
63 |
64 | var service = {
65 |
66 | saveTourDone: function() {
67 | singleValueAccessor.set(Const.TOUR_KEY, true);
68 | },
69 |
70 | getTourDone: function() {
71 | return singleValueAccessor.get(Const.TOUR_KEY);
72 | },
73 |
74 | saveCurrentView: function() {
75 | var defer = $q.defer();
76 |
77 | var key = inspectedApp.getKey();
78 | var obj = _.pick(currentView, serializedProps);
79 | if (currentView.selectedNode) {
80 | obj.selectedNode = currentView.selectedNode.name;
81 | }
82 |
83 | var data = angular.toJson(obj);
84 | var items = {}; items[key] = data;
85 | chromeSync.set(items, function() {
86 | defer.resolve();
87 | $rootScope.$apply();
88 | });
89 |
90 | return defer.promise;
91 | },
92 |
93 | loadCurrentView: function() {
94 | var defer = $q.defer();
95 | var key = inspectedApp.getKey();
96 | var dataLoaded = false;
97 |
98 | chromeSync.get(key, function(items) {
99 | dataLoaded = true;
100 | var serialized = items[key];
101 |
102 | if (serialized) {
103 | var obj = angular.fromJson(serialized);
104 |
105 | _.each(serializedProps, function(key) {
106 | if (obj[key]) {
107 | currentView[key] = obj[key];
108 | }
109 | });
110 | defer.resolve();
111 |
112 | // TODO set previously selected node
113 | } else {
114 | defer.reject();
115 | }
116 |
117 | $rootScope.$apply();
118 | });
119 |
120 | setTimeout(function() {
121 | // HACK: chrome.sync for whatever reasons sometimes doesn't invoke the callback when inspector tab is horizontal
122 | // Make sure that promise is rejected when this happens - wait 200 msec.
123 | if (dataLoaded === false) {
124 | console.log('Warning! syncing data doesn\'t seem to work!');
125 | defer.reject();
126 | $rootScope.$apply();
127 | }
128 | }, 300);
129 |
130 | return defer.promise;
131 | }
132 |
133 | };
134 |
135 | return service;
136 | });
137 |
--------------------------------------------------------------------------------
/app/scripts/graph/dgGraph.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('ngDependencyGraph')
4 | .directive('dgGraph', function($rootScope, $timeout, dev, Component, Const, currentView) {
5 |
6 | return {
7 | link: function(scope, elm, attrs) {
8 |
9 | function update() {
10 | // TODO(filip): profile this, this is run on every node select
11 | var currentGraph = currentView.graph;
12 | force.nodes(currentGraph.nodes)
13 | .links(currentGraph.links);
14 |
15 | links = svg.selectAll('.link')
16 | .data(force.links(), _.property('_id'));
17 |
18 | links.enter()
19 | .insert('line', ':first-child') // this needs to be rendered first -> prepend
20 | .attr('class', 'link')
21 | .attr('marker-end', 'url(#end)');
22 | links.exit().remove();
23 |
24 | nodes = svg.selectAll('.node')
25 | .data(force.nodes(), _.property('_id'));
26 |
27 | /**
28 | * Nodes enter
29 | */
30 | nodesEnter = nodes
31 | .enter()
32 | .append('g')
33 | .attr('class', _.property('type'))
34 | .classed('node', true)
35 | .on('mousedown', nodeClick)
36 | .on('dblclick', dblclick)
37 | .call(drag);
38 |
39 | nodesEnter.append('circle');
40 |
41 | nodesEnter.append('text')
42 | .attr('x', 12)
43 | .attr('dy', '.35em')
44 | .text(function(d) {
45 | return d.name;
46 | });
47 |
48 | /**
49 | * Nodes update
50 | */
51 | if (currentView.options.stickyNodesEnabled === false) {
52 | _.each(force.nodes(), function(node) {
53 | node.fixed = false;
54 | });
55 | }
56 | nodes
57 | .classed('selected', function(d) {
58 | return d === currentView.selectedNode;
59 | })
60 | .classed('fixed', function(d) {
61 | return d.fixed;
62 | });
63 |
64 | /**
65 | * Nodes remove
66 | */
67 | nodes.exit().remove();
68 | force.start();
69 | }
70 |
71 |
72 | function tick() {
73 | links
74 | .attr('x1', function(d) {
75 | return d.source.x;
76 | })
77 | .attr('y1', function(d) {
78 | return d.source.y;
79 | })
80 | .attr('x2', function(d) {
81 | return d.target.x;
82 | })
83 | .attr('y2', function(d) {
84 | return d.target.y;
85 | });
86 |
87 | nodes
88 | .attr('transform', function(d) {
89 | return 'translate(' + d.x + ',' + d.y + ')';
90 | });
91 | }
92 |
93 | function nodeClick(d) {
94 | $rootScope.$apply(function() {
95 | currentView.chooseNode(d);
96 | });
97 | }
98 |
99 | function zoomListener() {
100 | var tmp = svg;
101 |
102 | // add transtion if user is not panning (move) or zooming (wheel)
103 | if (!d3.event.sourceEvent || ['mousemove', 'wheel'].indexOf(d3.event.sourceEvent.type) === -1) {
104 | tmp = tmp.transition();
105 | }
106 | tmp.attr('transform',
107 | 'translate(' + d3.event.translate + ') ' +
108 | ' scale(' + d3.event.scale + ')');
109 | }
110 |
111 |
112 | scope.$on(Const.Events.CHOOSE_NODE, function(event, d, translate) {
113 | if (force.nodes().indexOf(d) === -1) { // if d is not present, it's not visible
114 | return;
115 | }
116 |
117 | if (translate) {
118 | var scale = zoom.scale();
119 | var x = -(scale * d.x - width/2);
120 | var y = -(scale * d.y - height/2);
121 |
122 | zoom.translate([x, y]).event(svg);
123 | }
124 | update();
125 |
126 | });
127 |
128 | var links, nodes, nodesEnter;
129 |
130 | var width = elm.width();
131 | var height = elm.height();
132 |
133 | var force = d3.layout.force()
134 | .size([width, height])
135 | .linkStrength(0.5)
136 | .friction(0.85)
137 | .linkDistance(100)
138 | .charge(-400)
139 | .gravity(0.05)
140 | // .linkDistance(120)
141 | // .charge(-800)
142 | .on('tick', tick);
143 |
144 | var drag = force.drag()
145 | .on("dragstart", dragstart);
146 |
147 |
148 | /**
149 | * Sticky nodes callbacks
150 | */
151 | function dblclick(d) {
152 | d3.select(this).classed('fixed', d.fixed = false);
153 | d3.event.stopImmediatePropagation();
154 | }
155 |
156 | function dragstart(d) {
157 | if (currentView.options.stickyNodesEnabled) {
158 | d3.select(this).classed('fixed', d.fixed = true);
159 | }
160 | }
161 |
162 | scope.$watch('currentView.options.stickyNodesEnabled', function(newVal, oldVal) {
163 | if (newVal !== oldVal) {
164 | if (newVal === false) {
165 | update();
166 | }
167 | }
168 | });
169 |
170 |
171 | var zoom = d3.behavior.zoom()
172 | .scaleExtent([0.5 ,2])
173 | .on('zoom', zoomListener);
174 |
175 | var svg = d3.select(elm[0]).append('svg')
176 | .on('mousedown', function() {
177 | // Allow moving only on svg element,
178 | // otherwise stop propagation to zoom behaviour
179 | if (d3.event.target.tagName !== 'svg') {
180 | d3.event.stopImmediatePropagation();
181 | }
182 | })
183 | .call(zoom)
184 | .append('g');
185 |
186 |
187 | /**
188 | * Definitions of markers
189 | */
190 | svg.append('svg:defs').selectAll('marker')
191 | .data(['end']) // Different link/path types can be defined here
192 | .enter().append('svg:marker') // This section adds in the arrows
193 | .attr('id', String)
194 | .attr('viewBox', '0 -5 10 10')
195 | .attr('refX', 18)
196 | .attr('refY', 0)
197 | .attr('markerWidth', 8)
198 | .attr('markerHeight', 8)
199 | .attr('fill', '#eee')
200 | .attr('orient', 'auto')
201 | .append('svg:path')
202 | .attr('d', 'M0,-3L10,0L0,3');
203 |
204 | var debouncedUpdate = _.debounce(update, 100);
205 | scope.$on(Const.Events.UPDATE_GRAPH, debouncedUpdate);
206 | debouncedUpdate();
207 |
208 | }
209 | };
210 |
211 | });
212 |
--------------------------------------------------------------------------------
/app/scripts/inject/inject.js:
--------------------------------------------------------------------------------
1 | var injectCode = function() {
2 |
3 | document.head.appendChild((function() {
4 | var fn = function bootstrap(window) {
5 |
6 | function disablePlugin(reason) {
7 | console.log(arguments);
8 | console.log(reason);
9 | }
10 |
11 | // Helper to determine if the root 'ng' module has been loaded
12 | // window.angular may be available if the app is bootstrapped asynchronously, but 'ng' might
13 | // finish loading later.
14 | function ngLoaded() {
15 | if (!window.angular) {
16 | return false;
17 | }
18 | try {
19 | window.angular.module('ng');
20 | } catch (e) {
21 | return false;
22 | }
23 | return true;
24 | }
25 |
26 | if (!ngLoaded()) {
27 | (function() {
28 | var isAngularLoaded = function(ev) {
29 |
30 | if (ev.srcElement.tagName === 'SCRIPT') {
31 | var oldOnload = ev.srcElement.onload;
32 | ev.srcElement.onload = function() {
33 | if (ngLoaded()) {
34 |
35 | document.removeEventListener('DOMNodeInserted', isAngularLoaded);
36 | bootstrap(window);
37 | }
38 | if (oldOnload) {
39 | oldOnload.apply(this, arguments);
40 | }
41 | };
42 | }
43 | };
44 | document.addEventListener('DOMNodeInserted', isAngularLoaded);
45 | }());
46 | return;
47 | }
48 |
49 | // do not patch twice
50 | if (window.__ngDependencyGraph) {
51 | return;
52 | }
53 |
54 | var angular = window.angular;
55 |
56 | // helper to extract dependencies from function arguments
57 | // not all versions of AngularJS expose annotate
58 | var annotate; // = angular.injector().annotate;
59 | if (!annotate) {
60 | annotate = (function() {
61 |
62 | var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
63 | var FN_ARG_SPLIT = /,/;
64 | var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
65 | var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
66 |
67 | // TODO: should I keep these assertions?
68 | function assertArg(arg, name, reason) {
69 | if (!arg) {
70 | throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required"));
71 | }
72 | return arg;
73 | }
74 |
75 | function assertArgFn(arg, name, acceptArrayAnnotation) {
76 | if (acceptArrayAnnotation && angular.isArray(arg)) {
77 | arg = arg[arg.length - 1];
78 | }
79 |
80 | assertArg(angular.isFunction(arg), name, 'not a function, got ' +
81 | (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg));
82 | return arg;
83 | }
84 |
85 | return function(fn) {
86 | var $inject,
87 | fnText,
88 | argDecl,
89 | last;
90 |
91 | if (typeof fn === 'function') {
92 | if (!($inject = fn.$inject)) {
93 | $inject = [];
94 | fnText = fn.toString().replace(STRIP_COMMENTS, '');
95 | argDecl = fnText.match(FN_ARGS);
96 | argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) {
97 | arg.replace(FN_ARG, function(all, underscore, name) {
98 | $inject.push(name);
99 | });
100 | });
101 | fn.$inject = $inject;
102 | }
103 | } else if (angular.isArray(fn)) {
104 | last = fn.length - 1;
105 | assertArgFn(fn[last], 'fn');
106 | $inject = fn.slice(0, last);
107 | } else {
108 | assertArgFn(fn, 'fn', true);
109 | }
110 | return $inject;
111 | };
112 | }());
113 | }
114 |
115 | var metadata = {
116 | angularVersion: angular.version,
117 | apps: [],
118 | modules: [],
119 | host: window.location.host
120 | };
121 |
122 | window.__ngDependencyGraph = {
123 | getMetadata: function(appNames) {
124 |
125 | appNames.forEach(function(appName) {
126 | if (metadata.apps.indexOf(appName) === -1) {
127 | metadata.apps.push(appName);
128 | createModule(appName);
129 | }
130 | });
131 |
132 | return metadata;
133 | }
134 | };
135 |
136 | function createModule(name) {
137 | var exist = false;
138 | for (var i = 0; i < metadata.modules.length; i++) {
139 | if (metadata.modules[i].name === name) {
140 | exist = true;
141 | break;
142 | }
143 | }
144 |
145 | if (exist || name === undefined) {
146 | return;
147 | }
148 |
149 | var module = angular.module(name);
150 |
151 | var moduleData = {
152 | name: name,
153 | deps: module.requires,
154 | components: []
155 | };
156 |
157 | processModule(moduleData);
158 | metadata.modules.push(moduleData);
159 |
160 | angular.forEach(module.requires, function(mod) {
161 | createModule(mod);
162 | });
163 |
164 | }
165 |
166 | function addDeps(moduleData, name, depsSrc, type) {
167 | if (typeof depsSrc === 'function') {
168 | moduleData.components.push({
169 | name: name,
170 | deps: annotate(depsSrc),
171 | type: type
172 | });
173 | // Array or empty
174 | } else if (Array.isArray(depsSrc)) {
175 | var deps = depsSrc.slice();
176 | deps.pop();
177 | moduleData.components.push({
178 | name: name,
179 | deps: deps,
180 | type: type
181 | });
182 | } else {
183 | moduleData.components.push({
184 | name: name,
185 | type: type
186 | });
187 | }
188 | }
189 |
190 |
191 | function processModule(moduleData) {
192 | var moduleName = moduleData.name;
193 | var module = angular.module(moduleName);
194 |
195 | // For old versions of AngularJS the property is called 'invokeQueue'
196 | var invokeQueue = module._invokeQueue || module.invokeQueue;
197 |
198 | angular.forEach(invokeQueue, function(item) {
199 | var compArgs = item[2];
200 | switch (item[0]) {
201 | case '$provide':
202 | switch (item[1]) {
203 | case 'value':
204 | case 'constant':
205 | addDeps(moduleData, compArgs[0], compArgs[1], 'value');
206 | break;
207 |
208 | default:
209 | addDeps(moduleData, compArgs[0], compArgs[1], 'service');
210 | break;
211 | }
212 | break;
213 |
214 | case '$filterProvider':
215 | addDeps(moduleData, compArgs[0], compArgs[1], 'filter');
216 | break;
217 | case '$animateProvider':
218 | addDeps(moduleData, compArgs[0], compArgs[1], 'animation');
219 | break;
220 | case '$controllerProvider':
221 | addDeps(moduleData, compArgs[0], compArgs[1], 'controller');
222 | break;
223 | case '$compileProvider':
224 | if (item[1] === 'component') {
225 | if (compArgs[1].controller) {
226 | addDeps(moduleData, compArgs[0], compArgs[1].controller, 'controller');
227 | } else {
228 | addDeps(moduleData, compArgs[0], [], 'controller');
229 | }
230 | break;
231 | }
232 |
233 | if (compArgs[1].constructor === Object) {
234 | angular.forEach(compArgs[1], function(key, value) {
235 | addDeps(moduleData, key, value, 'directive');
236 | });
237 | }
238 |
239 | addDeps(moduleData, compArgs[0], compArgs[1], 'directive');
240 | break;
241 | case '$injector':
242 | // invoke, ignore
243 | break;
244 | default:
245 | disablePlugin('unknown dependency type', item[0]);
246 | break;
247 | }
248 |
249 | });
250 |
251 | }
252 | };
253 |
254 | // Return a script element with the above code embedded in it
255 | var script = window.document.createElement('script');
256 | script.innerHTML = '(' + fn.toString() + '(window))';
257 |
258 | return script;
259 | }()));
260 | };
261 |
262 | // only inject if cookie is set
263 | if (document.cookie.indexOf('__ngDependencyGraph') !== -1) {
264 | document.addEventListener('DOMContentLoaded', injectCode);
265 | }
266 |
--------------------------------------------------------------------------------
/app/vendor/shepherd.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"function"==typeof define&&define.amd?define(["tether"],t):"object"==typeof exports?module.exports=t(require("tether")):e.Shepherd=t(e.Tether)}(this,function(e){"use strict";function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(e.__proto__=t)}function i(e){var t=document.createElement("div");return t.innerHTML=e,t.children[0]}function o(e,t){var n=void 0;return"undefined"!=typeof e.matches?n=e.matches:"undefined"!=typeof e.matchesSelector?n=e.matchesSelector:"undefined"!=typeof e.msMatchesSelector?n=e.msMatchesSelector:"undefined"!=typeof e.webkitMatchesSelector?n=e.webkitMatchesSelector:"undefined"!=typeof e.mozMatchesSelector?n=e.mozMatchesSelector:"undefined"!=typeof e.oMatchesSelector&&(n=e.oMatchesSelector),n.call(e,t)}function r(e,t){if(null===e||"undefined"==typeof e)return e;if("object"==typeof e)return e;var n=e.split(" "),i=n.length,o=t.length;i>o&&(n[0]=n.slice(0,i-o+1).join(" "),n.splice(1,o));for(var r={},s=0;o>s;++s){var h=t[s];r[h]=n[s]}return r}var s=function(){function e(e,t){for(var n=0;n");var t=document.createElement("div");t.className="shepherd-content",this.el.appendChild(t);var n=document.createElement("header");if(t.appendChild(n),"undefined"!=typeof this.options.title&&(n.innerHTML+=""+this.options.title+"
",this.el.className+=" shepherd-has-title"),this.options.showCancelLink){var o=i("X");n.appendChild(o),this.el.className+=" shepherd-has-cancel-link",this.bindCancelLink(o)}"undefined"!=typeof this.options.text&&!function(){var n=i(""),o=e.options.text;"function"==typeof o&&(o=o.call(e,n)),o instanceof HTMLElement?n.appendChild(o):("string"==typeof o&&(o=[o]),o.map(function(e){n.innerHTML+=""+e+"
"})),t.appendChild(n)}();var r=document.createElement("footer");this.options.buttons&&!function(){var t=i("");e.options.buttons.map(function(n){var o=i(""+n.text+"");t.appendChild(o),e.bindButtonEvents(n,o.querySelector("a"))}),r.appendChild(t)}(),t.appendChild(r),document.body.appendChild(this.el),this.setupTether(),this.options.advanceOn&&this.bindAdvance()}},{key:"bindCancelLink",value:function(e){var t=this;e.addEventListener("click",function(e){e.preventDefault(),t.cancel()})}},{key:"bindButtonEvents",value:function(e,t){var n=this;e.events=e.events||{},"undefined"!=typeof e.action&&(e.events.click=e.action);for(var i in e.events)if({}.hasOwnProperty.call(e.events,i)){var o=e.events[i];"string"==typeof o&&!function(){var e=o;o=function(){return n.tour.show(e)}}(),t.addEventListener(i,o)}this.on("destroy",function(){for(var n in e.events)if({}.hasOwnProperty.call(e.events,n)){var i=e.events[n];t.removeEventListener(n,i)}})}}]),c}(c),g=function(e){function i(){var e=this,n=void 0===arguments[0]?{}:arguments[0];t(this,i),h(Object.getPrototypeOf(i.prototype),"constructor",this).call(this,n),this.bindMethods(),this.options=n,this.steps=this.options.steps||[];var o=["complete","cancel","hide","start","show","active","inactive"];return o.map(function(t){!function(t){e.on(t,function(n){n=n||{},n.tour=e,v.trigger(t,n)})}(t)}),this}return n(i,e),s(i,[{key:"bindMethods",value:function(){var e=this,t=["next","back","cancel","complete","hide"];t.map(function(t){e[t]=e[t].bind(e)})}},{key:"addStep",value:function(e,t){return"undefined"==typeof t&&(t=e),t instanceof m?t.tour=this:(("string"==typeof e||"number"==typeof e)&&(t.id=e.toString()),t=d({},this.options.defaults,t),t=new m(this,t)),this.steps.push(t),this}},{key:"getById",value:function(e){for(var t=0;t=0))return i}return document.body}function r(t){var e=void 0;t===document?(e=document,t=document.documentElement):e=t.ownerDocument;var o=e.documentElement,i={},n=t.getBoundingClientRect();for(var r in n)i[r]=n[r];var s=A(e);return i.top-=s.top,i.left-=s.left,"undefined"==typeof i.width&&(i.width=document.body.scrollWidth-i.left-i.right),"undefined"==typeof i.height&&(i.height=document.body.scrollHeight-i.top-i.bottom),i.top=i.top-o.clientTop,i.left=i.left-o.clientLeft,i.right=e.body.clientWidth-i.width-i.left,i.bottom=e.body.clientHeight-i.height-i.top,i}function s(t){return t.offsetParent||document.documentElement}function a(){var t=document.createElement("div");t.style.width="100%",t.style.height="200px";var e=document.createElement("div");f(e.style,{position:"absolute",top:0,left:0,pointerEvents:"none",visibility:"hidden",width:"200px",height:"150px",overflow:"hidden"}),e.appendChild(t),document.body.appendChild(e);var o=t.offsetWidth;e.style.overflow="scroll";var i=t.offsetWidth;o===i&&(i=e.clientWidth),document.body.removeChild(e);var n=o-i;return{width:n,height:n}}function f(){var t=void 0===arguments[0]?{}:arguments[0],e=[];return Array.prototype.push.apply(e,arguments),e.slice(1).forEach(function(e){if(e)for(var o in e)({}).hasOwnProperty.call(e,o)&&(t[o]=e[o])}),t}function h(t,e){if("undefined"!=typeof t.classList)e.split(" ").forEach(function(e){e.trim()&&t.classList.remove(e)});else{var o=new RegExp("(^| )"+e.split(" ").join("|")+"( |$)","gi"),i=p(t).replace(o," ");u(t,i)}}function l(t,e){if("undefined"!=typeof t.classList)e.split(" ").forEach(function(e){e.trim()&&t.classList.add(e)});else{h(t,e);var o=p(t)+" #{name}";u(t,o)}}function d(t,e){if("undefined"!=typeof t.classList)return t.classList.contains(e);var o=p(t);return new RegExp("(^| )"+e+"( |$)","gi").test(o)}function p(t){return t.className instanceof SVGAnimatedString?t.className.baseVal:t.className}function u(t,e){t.setAttribute("class",e)}function c(t,e,o){o.forEach(function(o){-1===e.indexOf(o)&&d(t,o)&&h(t,o)}),e.forEach(function(e){d(t,e)||l(t,e)})}function g(t,e){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t)){var o=[],i=!0,n=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done)&&(o.push(s.value),!e||o.length!==e);i=!0);}catch(f){n=!0,r=f}finally{try{!i&&a["return"]&&a["return"]()}finally{if(n)throw r}}return o}throw new TypeError("Invalid attempt to destructure non-iterable instance")}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function m(t,e){var o=void 0===arguments[2]?1:arguments[2];return t+o>=e&&e>=t-o}function v(){return"undefined"!=typeof performance&&"undefined"!=typeof performance.now?performance.now():+new Date}function y(){for(var t=arguments.length,e=Array(t),o=0;t>o;o++)e[o]=arguments[o];var i={top:0,left:0};return e.forEach(function(t){var e=t.top,o=t.left;"string"==typeof e&&(e=parseFloat(e,10)),"string"==typeof o&&(o=parseFloat(o,10)),i.top+=e,i.left+=o}),i}function b(t,e){return"string"==typeof t.left&&-1!==t.left.indexOf("%")&&(t.left=parseFloat(t.left,10)/100*e.width),"string"==typeof t.top&&-1!==t.top.indexOf("%")&&(t.top=parseFloat(t.top,10)/100*e.height),t}function g(t,e){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t)){var o=[],i=!0,n=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done)&&(o.push(s.value),!e||o.length!==e);i=!0);}catch(f){n=!0,r=f}finally{try{!i&&a["return"]&&a["return"]()}finally{if(n)throw r}}return o}throw new TypeError("Invalid attempt to destructure non-iterable instance")}function w(t,e){return"scrollParent"===e?e=t.scrollParent:"window"===e&&(e=[pageXOffset,pageYOffset,innerWidth+pageXOffset,innerHeight+pageYOffset]),e===document&&(e=e.documentElement),"undefined"!=typeof e.nodeType&&!function(){var t=r(e),o=t,i=getComputedStyle(e);e=[o.left,o.top,t.width+o.left,t.height+o.top],U.forEach(function(t,o){t=t[0].toUpperCase()+t.substr(1),"Top"===t||"Left"===t?e[o]+=parseFloat(i["border"+t+"Width"]):e[o]-=parseFloat(i["border"+t+"Width"])})}(),e}function g(t,e){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t)){var o=[],i=!0,n=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done)&&(o.push(s.value),!e||o.length!==e);i=!0);}catch(f){n=!0,r=f}finally{try{!i&&a["return"]&&a["return"]()}finally{if(n)throw r}}return o}throw new TypeError("Invalid attempt to destructure non-iterable instance")}var C=function(){function t(t,e){for(var o=0;o1?e-1:0),i=1;e>i;i++)o[i-1]=arguments[i];if("undefined"!=typeof this.bindings&&this.bindings[t])for(var n=0;n16?(e=Math.min(e-16,250),void(o=setTimeout(n,250))):void("undefined"!=typeof t&&v()-t<10||("undefined"!=typeof o&&(clearTimeout(o),o=null),t=v(),_(),e=v()-t))};["resize","scroll","touchmove"].forEach(function(t){window.addEventListener(t,i)})}();var z={center:"center",left:"right",right:"left"},F={middle:"middle",top:"bottom",bottom:"top"},L={top:0,left:0,middle:"50%",center:"50%",bottom:"100%",right:"100%"},Y=function(t,e){var o=t.left,i=t.top;return"auto"===o&&(o=z[e.left]),"auto"===i&&(i=F[e.top]),{left:o,top:i}},H=function(t){var e=t.left,o=t.top;return"undefined"!=typeof L[t.left]&&(e=L[t.left]),"undefined"!=typeof L[t.top]&&(o=L[t.top]),{left:e,top:o}},X=function(t){var e=t.split(" "),o=g(e,2),i=o[0],n=o[1];return{top:i,left:n}},j=X,N=function(){function t(e){var o=this;i(this,t),this.position=this.position.bind(this),B.push(this),this.history=[],this.setOptions(e,!1),O.modules.forEach(function(t){"undefined"!=typeof t.initialize&&t.initialize.call(o)}),this.position()}return C(t,[{key:"getClass",value:function(){var t=void 0===arguments[0]?"":arguments[0],e=this.options.classes;return"undefined"!=typeof e&&e[t]?this.options.classes[t]:this.options.classPrefix?""+this.options.classPrefix+"-"+t:t}},{key:"setOptions",value:function(t){var e=this,o=void 0===arguments[1]?!0:arguments[1],i={offset:"0 0",targetOffset:"0 0",targetAttachment:"auto auto",classPrefix:"tether"};this.options=f(i,t);var r=this.options,s=r.element,a=r.target,h=r.targetModifier;if(this.element=s,this.target=a,this.targetModifier=h,"viewport"===this.target?(this.target=document.body,this.targetModifier="visible"):"scroll-handle"===this.target&&(this.target=document.body,this.targetModifier="scroll-handle"),["element","target"].forEach(function(t){if("undefined"==typeof e[t])throw new Error("Tether Error: Both element and target must be defined");"undefined"!=typeof e[t].jquery?e[t]=e[t][0]:"string"==typeof e[t]&&(e[t]=document.querySelector(e[t]))}),l(this.element,this.getClass("element")),this.options.addTargetClasses!==!1&&l(this.target,this.getClass("target")),!this.options.attachment)throw new Error("Tether Error: You must provide an attachment");this.targetAttachment=j(this.options.targetAttachment),this.attachment=j(this.options.attachment),this.offset=X(this.options.offset),this.targetOffset=X(this.options.targetOffset),"undefined"!=typeof this.scrollParent&&this.disable(),this.scrollParent="scroll-handle"===this.targetModifier?this.target:n(this.target),this.options.enabled!==!1&&this.enable(o)}},{key:"getTargetBounds",value:function(){if("undefined"==typeof this.targetModifier)return r(this.target);if("visible"===this.targetModifier){if(this.target===document.body)return{top:pageYOffset,left:pageXOffset,height:innerHeight,width:innerWidth};var t=r(this.target),e={height:t.height,width:t.width,top:t.top,left:t.left};return e.height=Math.min(e.height,t.height-(pageYOffset-t.top)),e.height=Math.min(e.height,t.height-(t.top+t.height-(pageYOffset+innerHeight))),e.height=Math.min(innerHeight,e.height),e.height-=2,e.width=Math.min(e.width,t.width-(pageXOffset-t.left)),e.width=Math.min(e.width,t.width-(t.left+t.width-(pageXOffset+innerWidth))),e.width=Math.min(innerWidth,e.width),e.width-=2,e.topo.clientWidth||[i.overflow,i.overflowX].indexOf("scroll")>=0||this.target!==document.body,s=0;n&&(s=15);var a=t.height-parseFloat(i.borderTopWidth)-parseFloat(i.borderBottomWidth)-s,e={width:15,height:.975*a*(a/o.scrollHeight),left:t.left+t.width-parseFloat(i.borderLeftWidth)-15},f=0;408>a&&this.target===document.body&&(f=-11e-5*Math.pow(a,2)-.00727*a+22.58),this.target!==document.body&&(e.height=Math.max(e.height,24));var h=this.target.scrollTop/(o.scrollHeight-a);return e.top=h*(a-e.height-f)+t.top+parseFloat(i.borderTopWidth),this.target===document.body&&(e.height=Math.max(e.height,24)),e}}},{key:"clearCache",value:function(){this._cache={}}},{key:"cache",value:function(t,e){return"undefined"==typeof this._cache&&(this._cache={}),"undefined"==typeof this._cache[t]&&(this._cache[t]=e.call(this)),this._cache[t]}},{key:"enable",value:function(){var t=void 0===arguments[0]?!0:arguments[0];this.options.addTargetClasses!==!1&&l(this.target,this.getClass("enabled")),l(this.element,this.getClass("enabled")),this.enabled=!0,this.scrollParent!==document&&this.scrollParent.addEventListener("scroll",this.position),t&&this.position()}},{key:"disable",value:function(){h(this.target,this.getClass("enabled")),h(this.element,this.getClass("enabled")),this.enabled=!1,"undefined"!=typeof this.scrollParent&&this.scrollParent.removeEventListener("scroll",this.position)}},{key:"destroy",value:function(){var t=this;this.disable(),B.forEach(function(e,o){return e===t?void B.splice(o,1):void 0})}},{key:"updateAttachClasses",value:function(t,e){var o=this;t=t||this.attachment,e=e||this.targetAttachment;var i=["left","top","bottom","right","middle","center"];"undefined"!=typeof this._addAttachClasses&&this._addAttachClasses.length&&this._addAttachClasses.splice(0,this._addAttachClasses.length),"undefined"==typeof this._addAttachClasses&&(this._addAttachClasses=[]);var n=this._addAttachClasses;t.top&&n.push(""+this.getClass("element-attached")+"-"+t.top),t.left&&n.push(""+this.getClass("element-attached")+"-"+t.left),e.top&&n.push(""+this.getClass("target-attached")+"-"+e.top),e.left&&n.push(""+this.getClass("target-attached")+"-"+e.left);var r=[];i.forEach(function(t){r.push(""+o.getClass("element-attached")+"-"+t),r.push(""+o.getClass("target-attached")+"-"+t)}),S(function(){"undefined"!=typeof o._addAttachClasses&&(c(o.element,o._addAttachClasses,r),o.options.addTargetClasses!==!1&&c(o.target,o._addAttachClasses,r),delete o._addAttachClasses)})}},{key:"position",value:function(){var t=this,e=void 0===arguments[0]?!0:arguments[0];if(this.enabled){this.clearCache();var o=Y(this.targetAttachment,this.attachment);this.updateAttachClasses(this.attachment,o);var i=this.cache("element-bounds",function(){return r(t.element)}),n=i.width,f=i.height;if(0===n&&0===f&&"undefined"!=typeof this.lastSize){var h=this.lastSize;n=h.width,f=h.height}else this.lastSize={width:n,height:f};var l=this.cache("target-bounds",function(){return t.getTargetBounds()}),d=l,p=b(H(this.attachment),{width:n,height:f}),u=b(H(o),d),c=b(this.offset,{width:n,height:f}),g=b(this.targetOffset,d);p=y(p,c),u=y(u,g);for(var m=l.left+u.left-p.left,v=l.top+u.top-p.top,w=0;wwindow.innerWidth&&(A=this.cache("scrollbar-size",a),x.viewport.bottom-=A.height),document.body.scrollHeight>window.innerHeight&&(A=this.cache("scrollbar-size",a),x.viewport.right-=A.width),(-1===["","static"].indexOf(document.body.style.position)||-1===["","static"].indexOf(document.body.parentElement.style.position))&&(x.page.bottom=document.body.scrollHeight-v-f,x.page.right=document.body.scrollWidth-m-n),"undefined"!=typeof this.options.optimizations&&this.options.optimizations.moveElement!==!1&&"undefined"==typeof this.targetModifier&&!function(){var e=t.cache("target-offsetparent",function(){return s(t.target)}),o=t.cache("target-offsetparent-bounds",function(){return r(e)}),i=getComputedStyle(e),n=o,a={};if(["Top","Left","Bottom","Right"].forEach(function(t){a[t.toLowerCase()]=parseFloat(i["border"+t+"Width"])}),o.right=document.body.scrollWidth-o.left-n.width+a.right,o.bottom=document.body.scrollHeight-o.top-n.height+a.bottom,x.page.top>=o.top+a.top&&x.page.bottom>=o.bottom&&x.page.left>=o.left+a.left&&x.page.right>=o.right){var f=e.scrollTop,h=e.scrollLeft;x.offset={top:x.page.top-o.top+f-a.top,left:x.page.left-o.left+h-a.left}}}(),this.move(x),this.history.unshift(x),this.history.length>3&&this.history.pop(),e&&W(),!0}}},{key:"move",value:function(t){var e=this;if("undefined"!=typeof this.element.parentNode){var o={};for(var i in t){o[i]={};for(var n in t[i]){for(var r=!1,a=0;a=0&&(v=parseFloat(v),g=parseFloat(g)),v!==g&&(c=!0,u[n]=g)}c&&S(function(){f(e.element.style,u)})}}}]),t}();N.modules=[],O.position=_;var R=f(N,O),P=O.Utils,r=P.getBounds,f=P.extend,c=P.updateClasses,S=P.defer,U=["left","top","right","bottom"];O.modules.push({position:function(t){var e=this,o=t.top,i=t.left,n=t.targetAttachment;if(!this.options.constraints)return!0;var s=this.cache("element-bounds",function(){return r(e.element)}),a=s.height,h=s.width;if(0===h&&0===a&&"undefined"!=typeof this.lastSize){var l=this.lastSize;h=l.width,a=l.height}var d=this.cache("target-bounds",function(){return e.getTargetBounds()}),p=d.height,u=d.width,m=[this.getClass("pinned"),this.getClass("out-of-bounds")];this.options.constraints.forEach(function(t){var e=t.outOfBoundsClass,o=t.pinnedClass;e&&m.push(e),o&&m.push(o)}),m.forEach(function(t){["left","top","right","bottom"].forEach(function(e){m.push(""+t+"-"+e)})});var v=[],y=f({},n),b=f({},this.attachment);return this.options.constraints.forEach(function(t){var r=t.to,s=t.attachment,f=t.pin;"undefined"==typeof s&&(s="");var l=void 0,d=void 0;if(s.indexOf(" ")>=0){var c=s.split(" "),m=g(c,2);d=m[0],l=m[1]}else l=d=s;var C=w(e,r);("target"===d||"both"===d)&&(oC[3]&&"bottom"===y.top&&(o-=p,y.top="top")),"together"===d&&(oC[3]&&"bottom"===y.top&&("top"===b.top?(o-=p,y.top="top",o-=a,b.top="bottom"):"bottom"===b.top&&(o-=p,y.top="top",o+=a,b.top="top")),"middle"===y.top&&(o+a>C[3]&&"top"===b.top?(o-=a,b.top="bottom"):oC[2]&&"right"===y.left&&(i-=u,y.left="left")),"together"===l&&(iC[2]&&"right"===y.left?"left"===b.left?(i-=u,y.left="left",i-=h,b.left="right"):"right"===b.left&&(i-=u,y.left="left",i+=h,b.left="left"):"center"===y.left&&(i+h>C[2]&&"left"===b.left?(i-=h,b.left="right"):iC[3]&&"top"===b.top&&(o-=a,b.top="bottom")),("element"===l||"both"===l)&&(iC[2]&&"left"===b.left&&(i-=h,b.left="right")),"string"==typeof f?f=f.split(",").map(function(t){return t.trim()}):f===!0&&(f=["top","left","right","bottom"]),f=f||[];var O=[],E=[];o=0?(o=C[1],O.push("top")):E.push("top")),o+a>C[3]&&(f.indexOf("bottom")>=0?(o=C[3]-a,O.push("bottom")):E.push("bottom")),i=0?(i=C[0],O.push("left")):E.push("left")),i+h>C[2]&&(f.indexOf("right")>=0?(i=C[2]-h,O.push("right")):E.push("right")),O.length&&!function(){var t=void 0;t="undefined"!=typeof e.options.pinnedClass?e.options.pinnedClass:e.getClass("pinned"),v.push(t),O.forEach(function(e){v.push(""+t+"-"+e)})}(),E.length&&!function(){var t=void 0;t="undefined"!=typeof e.options.outOfBoundsClass?e.options.outOfBoundsClass:e.getClass("out-of-bounds"),v.push(t),E.forEach(function(e){v.push(""+t+"-"+e)})}(),(O.indexOf("left")>=0||O.indexOf("right")>=0)&&(b.left=y.left=!1),(O.indexOf("top")>=0||O.indexOf("bottom")>=0)&&(b.top=y.top=!1),(y.top!==n.top||y.left!==n.left||b.top!==e.attachment.top||b.left!==e.attachment.left)&&e.updateAttachClasses(b,y)}),S(function(){e.options.addTargetClasses!==!1&&c(e.target,v,m),c(e.element,v,m)}),{top:o,left:i}}});var P=O.Utils,r=P.getBounds,c=P.updateClasses,S=P.defer;return O.modules.push({position:function(t){var e=this,o=t.top,i=t.left,n=this.cache("element-bounds",function(){return r(e.element)}),s=n.height,a=n.width,f=this.getTargetBounds(),h=o+s,l=i+a,d=[];o<=f.bottom&&h>=f.top&&["left","right"].forEach(function(t){var e=f[t];(e===i||e===l)&&d.push(t)}),i<=f.right&&l>=f.left&&["top","bottom"].forEach(function(t){var e=f[t];(e===o||e===h)&&d.push(t)});var p=[],u=[],g=["left","top","right","bottom"];return p.push(this.getClass("abutted")),g.forEach(function(t){p.push(""+e.getClass("abutted")+"-"+t)}),d.length&&u.push(this.getClass("abutted")),d.forEach(function(t){u.push(""+e.getClass("abutted")+"-"+t)}),S(function(){e.options.addTargetClasses!==!1&&c(e.target,u,p),c(e.element,u,p)}),!0}}),O.modules.push({position:function(t){var e=t.top,o=t.left;if(this.options.shift){var i=this.options.shift;"function"==typeof this.options.shift&&(i=this.options.shift.call(this,{top:e,left:o}));var n=void 0,r=void 0;if("string"==typeof i){i=i.split(" "),i[1]=i[1]||i[0];var s=g(i,2);n=s[0],r=s[1],n=parseFloat(n,10),r=parseFloat(r,10)}else n=i.top,r=i.left;return e+=n,o+=r,{top:e,left:o}}}}),R});
--------------------------------------------------------------------------------