├── 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 |
4 | none 5 | , 6 |
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 | 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 | [![Join the chat at https://gitter.im/filso/ng-dependency-graph](https://badges.gitter.im/Join%20Chat.svg)](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 |
Detected AngularJS version {{ app.appsInfo.angularVersion.full }}   15 | 16 | Open '{{ app.appsInfo.appNames[0] }}' 17 | 18 | 19 |
20 |
It looks like you don't use [ng-app] tag.
21 | Please enter app name: 22 | 23 |
24 |
25 |
26 | 27 |
28 | Waiting for app '{{ app.appName }}' to load... Cancel 29 |
30 | 31 |
32 | Project site: https://github.com/filso/ng-dependency-graph 33 |
Please star if you like it: 34 | 35 |
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 |
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 | 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}); --------------------------------------------------------------------------------