├── logs └── .gitkeep ├── app ├── css │ ├── .gitkeep │ └── app.css ├── img │ ├── .gitkeep │ ├── expanded.png │ └── collapsed.png ├── partials │ ├── .gitkeep │ ├── recursive.html │ ├── treerepeat.html │ └── treerepeat-drag.html ├── lib │ └── angular │ │ ├── version.txt │ │ ├── version.json │ │ ├── angular-cookies.min.js │ │ ├── angular-loader.min.js │ │ ├── angular-resource.min.js │ │ ├── angular-touch.min.js │ │ ├── angular-route.min.js │ │ ├── angular-sanitize.min.js │ │ ├── angular-animate.min.js │ │ ├── errors.json │ │ ├── angular-cookies.js │ │ ├── angular-loader.js │ │ ├── angular-sanitize.js │ │ ├── angular-touch.js │ │ ├── angular-resource.js │ │ └── angular-route.js ├── js │ ├── filters.js │ ├── services.js │ ├── app.js │ ├── controllers.js │ └── directives.js ├── index.html └── data │ ├── smalltree.js │ └── mediumtree.js ├── test ├── lib │ └── angular │ │ └── version.txt └── e2e │ ├── scenarios.js │ └── runner.html ├── .gitignore ├── scripts ├── test.sh ├── e2e-test.sh ├── test.bat ├── e2e-test.bat ├── watchr.rb └── web-server.js ├── LICENSE ├── config ├── karma.conf.js └── karma-e2e.conf.js └── README.md /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/partials/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.2.0-rc.3 -------------------------------------------------------------------------------- /test/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.2.0-rc.3 -------------------------------------------------------------------------------- /app/js/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.filters', []); 4 | -------------------------------------------------------------------------------- /app/img/expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchatel/angular-treeRepeat/HEAD/app/img/expanded.png -------------------------------------------------------------------------------- /app/img/collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchatel/angular-treeRepeat/HEAD/app/img/collapsed.png -------------------------------------------------------------------------------- /app/js/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services', []) 4 | .constant('menu', []); 5 | -------------------------------------------------------------------------------- /test/e2e/scenarios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | nbproject 3 | manifest.mf 4 | build.xml 5 | 6 | .project 7 | .settings 8 | .idea/* 9 | -------------------------------------------------------------------------------- /app/lib/angular/version.json: -------------------------------------------------------------------------------- 1 | {"full":"1.2.0-rc.3","major":"1","minor":"2","dot":"0","codename":"ferocious-twitch","cdn":"1.2.0-rc.2"} -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | 5 | echo "" 6 | echo "Starting Karma Server (http://karma-runner.github.io)" 7 | echo "-------------------------------------------------------------------" 8 | 9 | karma start $BASE_DIR/../config/karma.conf.js $* 10 | -------------------------------------------------------------------------------- /scripts/e2e-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | 5 | echo "" 6 | echo "Starting Karma Server (http://karma-runner.github.io)" 7 | echo "-------------------------------------------------------------------" 8 | 9 | karma start $BASE_DIR/../config/karma-e2e.conf.js $* 10 | -------------------------------------------------------------------------------- /test/e2e/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /scripts/test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for running unit tests 4 | REM You have to run server and capture some browser first 5 | REM 6 | REM Requirements: 7 | REM - NodeJS (http://nodejs.org/) 8 | REM - Karma (npm install -g karma) 9 | 10 | set BASE_DIR=%~dp0 11 | karma start "%BASE_DIR%\..\config\karma.conf.js" %* 12 | -------------------------------------------------------------------------------- /scripts/e2e-test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for running e2e tests 4 | REM You have to run server and capture some browser first 5 | REM 6 | REM Requirements: 7 | REM - NodeJS (http://nodejs.org/) 8 | REM - Karma (npm install -g karma) 9 | 10 | set BASE_DIR=%~dp0 11 | karma start "%BASE_DIR%\..\config\karma-e2e.conf.js" %* 12 | -------------------------------------------------------------------------------- /app/partials/recursive.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /scripts/watchr.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env watchr 2 | 3 | # config file for watchr http://github.com/mynyml/watchr 4 | # install: gem install watchr 5 | # run: watch watchr.rb 6 | # note: make sure that you have jstd server running (server.sh) and a browser captured 7 | 8 | log_file = File.expand_path(File.dirname(__FILE__) + '/../logs/jstd.log') 9 | 10 | `cd ..` 11 | `touch #{log_file}` 12 | 13 | puts "String watchr... log file: #{log_file}" 14 | 15 | watch( '(app/js|test/unit)' ) do 16 | `echo "\n\ntest run started @ \`date\`" > #{log_file}` 17 | `scripts/test.sh &> #{log_file}` 18 | end 19 | 20 | -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | basePath : '../', 4 | 5 | files : [ 6 | 'app/lib/angular/angular.js', 7 | 'app/lib/angular/angular-*.js', 8 | 'test/lib/angular/angular-mocks.js', 9 | 'app/js/**/*.js', 10 | 'test/unit/**/*.js' 11 | ], 12 | 13 | autoWatch : true, 14 | 15 | frameworks: ['jasmine'], 16 | 17 | browsers : ['Chrome'], 18 | 19 | plugins : [ 20 | 'karma-junit-reporter', 21 | 'karma-chrome-launcher', 22 | 'karma-firefox-launcher', 23 | 'karma-jasmine' 24 | ], 25 | 26 | junitReporter : { 27 | outputFile: 'test_out/unit.xml', 28 | suite: 'unit' 29 | } 30 | 31 | })} 32 | -------------------------------------------------------------------------------- /config/karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | 4 | 5 | basePath : '../', 6 | 7 | files : [ 8 | 'test/e2e/**/*.js' 9 | ], 10 | 11 | autoWatch : false, 12 | 13 | browsers : ['Chrome'], 14 | 15 | frameworks: ['ng-scenario'], 16 | 17 | singleRun : true, 18 | 19 | proxies : { 20 | '/': 'http://localhost:8000/' 21 | }, 22 | 23 | plugins : [ 24 | 'karma-junit-reporter', 25 | 'karma-chrome-launcher', 26 | 'karma-firefox-launcher', 27 | 'karma-jasmine', 28 | 'karma-ng-scenario' 29 | ], 30 | 31 | junitReporter : { 32 | outputFile: 'test_out/e2e.xml', 33 | suite: 'e2e' 34 | } 35 | 36 | })} 37 | 38 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | #menu ul li { 2 | display: inline-block; 3 | padding: 5px 15px 5px 15px; 4 | border-left: 2px solid grey; 5 | } 6 | #menu ul li:first-child { 7 | border-left: none; 8 | } 9 | 10 | li { 11 | list-style-type: none; 12 | } 13 | 14 | .icon { 15 | display: block; 16 | float: left; 17 | width: 16px; 18 | height: 16px; 19 | } 20 | .expanded { 21 | background-image: url(../img/expanded.png); 22 | } 23 | .collapsed { 24 | background-image: url(../img/collapsed.png); 25 | } 26 | 27 | .label { 28 | color: green; 29 | } 30 | .label.folder { 31 | color: darkmagenta; 32 | } 33 | 34 | ul .tree-drag > div > span.label { 35 | background-color: lightsalmon; 36 | } 37 | .tree-drag-over > div > span.label { 38 | background-color: darkseagreen; 39 | } -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(p,f,n){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,b){var c={},g={},h,k=!1,l=f.copy,m=f.isUndefined;b.addPollFn(function(){var a=b.cookies();h!=a&&(h=a,l(a,g),l(a,c),k&&d.$apply())})();k=!0;d.$watch(function(){var a,e,d;for(a in g)m(c[a])&&b.cookies(a,n);for(a in c)(e=c[a],f.isString(e))?e!==g[a]&&(b.cookies(a,e),d=!0):f.isDefined(g[a])?c[a]=g[a]:delete c[a];if(d)for(a in e=b.cookies(),c)c[a]!==e[a]&&(m(e[a])?delete c[a]:c[a]=e[a])}); 7 | return c}]).factory("$cookieStore",["$cookies",function(d){return{get:function(b){return(b=d[b])?f.fromJson(b):b},put:function(b,c){d[b]=f.toJson(c)},remove:function(b){delete d[b]}}}])})(window,window.angular); 8 | //# sourceMappingURL=angular-cookies.min.js.map 9 | -------------------------------------------------------------------------------- /app/partials/treerepeat.html: -------------------------------------------------------------------------------- 1 |

2 | Expand all 3 | Collapse all 4 |

5 | 6 | 21 | 22 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | treeRepeat demo 6 | 7 | 8 | 9 | 10 | 11 |

treeRepeat

12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(k){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}var l=minErr("$injector");return d(d(k,"angular",Object),"module",function(){var c={};return function(b,e,f){assertNotHasOwnProperty(b,"module");e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw l("nomod",b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide", 7 | "factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); 8 | //# sourceMappingURL=angular-loader.min.js.map 9 | -------------------------------------------------------------------------------- /app/partials/treerepeat-drag.html: -------------------------------------------------------------------------------- 1 |

2 | Expand all 3 | Collapse all 4 |

5 | 6 | 23 | 24 | -------------------------------------------------------------------------------- /app/data/smalltree.js: -------------------------------------------------------------------------------- 1 | [ 2 | {"label": "root", "children": [ 3 | {"label": "folder A", "collapsed": false, "children": [ 4 | {"label": "folder B", "collapsed": false, "children": [ 5 | {"label": "file B1", "collapsed": false}, 6 | {"label": "file B2", "collapsed": false} 7 | ]}, 8 | {"label": "file A1", "collapsed": false}, 9 | {"label": "file A2", "collapsed": false}, 10 | {"label": "file A3", "collapsed": false}, 11 | {"label": "file A4", "collapsed": false} 12 | ]}, 13 | {"label": "folder C", "collapsed": false, "children": [ 14 | {"label": "folder D", "collapsed": true, "children": [ 15 | {"label": "folder E", "collapsed": false, "children": [ 16 | {"label": "file E1", "collapsed": false}, 17 | {"label": "file E2", "collapsed": false}, 18 | {"label": "file E3", "collapsed": false} 19 | ]} 20 | ]}, 21 | {"label": "folder F", "collapsed": false, "children": [ 22 | {"label": "file F1", "collapsed": false}, 23 | {"label": "file F2", "collapsed": false} 24 | ]}, 25 | {"label": "file C1", "collapsed": false} 26 | ]}, 27 | {"label": "folder G", "collapsed": false, "children": [ 28 | {"label": "file G1", "collapsed": false}, 29 | {"label": "file G2", "collapsed": false}, 30 | {"label": "file G3", "collapsed": false}, 31 | {"label": "file G4", "collapsed": false} 32 | ]}, 33 | {"label": "folder H", "collapsed": false, "children": [ 34 | {"label": "file H1", "collapsed": false}, 35 | {"label": "file H2", "collapsed": false}, 36 | {"label": "file H3", "collapsed": false} 37 | ]} 38 | ]} 39 | ] -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app', [ 4 | 'ngRoute', 5 | 'app.filters', 6 | 'app.services', 7 | 'app.directives', 8 | 'app.controllers' 9 | ]).config(function ($routeProvider, menu) { 10 | 11 | function addRouteMenu(shortLabel, fullLabel, routeDef) { 12 | var menuItem = { 13 | index : menu.length, 14 | shortLabel: shortLabel, 15 | fullLabel: fullLabel 16 | }; 17 | menu.push(menuItem); 18 | routeDef.resolve = { 19 | menuItem: function () { 20 | return menuItem; 21 | } 22 | }; 23 | $routeProvider.when('/' + menuItem.index, routeDef); 24 | } 25 | 26 | addRouteMenu('Small tree', 'Small tree with action on nodes (24 nodes)', { 27 | controller: 'TreeCtrlSmall', 28 | templateUrl: 'partials/treerepeat.html' 29 | }); 30 | addRouteMenu('Medium tree', 'Medium tree with action on nodes (7 levels, 128 nodes)', { 31 | controller: 'TreeCtrlMedium', 32 | templateUrl: 'partials/treerepeat.html' 33 | }); 34 | addRouteMenu('Deep tree', 'Deep tree with action on nodes (10 levels, 1024 nodes)', { 35 | controller: 'TreeCtrlDeep', 36 | templateUrl: 'partials/treerepeat.html' 37 | }); 38 | addRouteMenu('Too big tree expanded', 'Too big tree expanded, with action on nodes (4 levels, 10000 nodes)', { 39 | controller: 'TreeCtrlBig', 40 | templateUrl: 'partials/treerepeat.html' 41 | }); 42 | addRouteMenu('Big tree collapsed', 'Big tree collapsed, with action on nodes (4 levels, 10000 nodes)', { 43 | controller: 'TreeCtrlBigCollapsed', 44 | templateUrl: 'partials/treerepeat.html' 45 | }); 46 | addRouteMenu('Drag&drop : medium tree', 'Medium tree with drag&drop (7 levels, 128 nodes)', { 47 | controller: 'TreeCtrlDragMedium', 48 | templateUrl: 'partials/treerepeat-drag.html' 49 | }); 50 | addRouteMenu('Drag&drop : deep tree', 'Deep tree with drag&drop (10 levels, 1024 nodes)', { 51 | controller: 'TreeCtrlDragDeep', 52 | templateUrl: 'partials/treerepeat-drag.html' 53 | }); 54 | $routeProvider.otherwise({ 55 | redirectTo: '/0' 56 | }); 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(H,h,C){'use strict';var x=h.$$minErr("$resource");h.module("ngResource",["ng"]).factory("$resource",["$http","$parse","$q",function(D,y,E){function n(h,k){this.template=h;this.defaults=k||{};this.urlParams={}}function t(e,k,f){function q(b,c){var d={};c=u({},k,c);r(c,function(a,c){s(a)&&(a=a());var m;a&&a.charAt&&"@"==a.charAt(0)?(m=a.substr(1),m=y(m)(b)):m=a;d[c]=m});return d}function d(b){return b.resource}function g(b){z(b||{},this)}var F=new n(e);f=u({},G,f);r(f,function(b,c){var A= 7 | /^(POST|PUT|PATCH)$/i.test(b.method);g[c]=function(a,c,m,k){var p={},e,f,v;switch(arguments.length){case 4:v=k,f=m;case 3:case 2:if(s(c)){if(s(a)){f=a;v=c;break}f=c;v=m}else{p=a;e=c;f=m;break}case 1:s(a)?f=a:A?e=a:p=a;break;case 0:break;default:throw x("badargs",arguments.length);}var n=e instanceof g,l=n?e:b.isArray?[]:new g(e),w={},t=b.interceptor&&b.interceptor.response||d,y=b.interceptor&&b.interceptor.responseError||C;r(b,function(a,c){"params"!=c&&("isArray"!=c&&"interceptor"!=c)&&(w[c]=z(a))}); 8 | A&&(w.data=e);F.setUrlParams(w,u({},q(e,b.params||{}),p),b.url);p=D(w).then(function(c){var a=c.data,d=l.$promise;if(a){if(h.isArray(a)!=!!b.isArray)throw x("badcfg",b.isArray?"array":"object",h.isArray(a)?"array":"object");b.isArray?(l.length=0,r(a,function(a){l.push(new g(a))})):(z(a,l),l.$promise=d)}l.$resolved=!0;c.resource=l;return c},function(a){l.$resolved=!0;(v||B)(a);return E.reject(a)});p=p.then(function(a){var c=t(a);(f||B)(c,a.headers);return c},y);return n?p:(l.$promise=p,l.$resolved= 9 | !1,l)};g.prototype["$"+c]=function(a,b,d){s(a)&&(d=b,b=a,a={});a=g[c](a,this,b,d);return a.$promise||a}});g.bind=function(b){return t(e,u({},k,b),f)};return g}var G={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},B=h.noop,r=h.forEach,u=h.extend,z=h.copy,s=h.isFunction;n.prototype={setUrlParams:function(e,k,f){var q=this,d=f||q.template,g,n,b=q.urlParams={};r(d.split(/\W/),function(c){if("hasOwnProperty"===c)throw x("badname"); 10 | !/^\d+$/.test(c)&&(c&&RegExp("(^|[^\\\\]):"+c+"(\\W|$)").test(d))&&(b[c]=!0)});d=d.replace(/\\:/g,":");k=k||{};r(q.urlParams,function(c,b){g=k.hasOwnProperty(b)?k[b]:q.defaults[b];h.isDefined(g)&&null!==g?(n=encodeURIComponent(g).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"%20").replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"),d=d.replace(RegExp(":"+b+"(\\W|$)","g"),n+"$1")):d=d.replace(RegExp("(/?):"+b+"(\\W|$)","g"),function(a, 11 | c,b){return"/"==b.charAt(0)?b:c+b})});d=d.replace(/\/+$/,"");d=d.replace(/\/\.(?=\w+($|\?))/,".");e.url=d.replace(/\/\\\./,"/.");r(k,function(c,b){q.urlParams[b]||(e.params=e.params||{},e.params[b]=c)})}};return t}])})(window,window.angular); 12 | //# sourceMappingURL=angular-resource.min.js.map 13 | -------------------------------------------------------------------------------- /app/lib/angular/angular-touch.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(y,v,z){'use strict';function t(g,a,b){q.directive(g,["$parse","$swipe",function(l,n){var r=75,h=0.3,d=30;return function(p,m,k){function e(e){if(!u)return!1;var c=Math.abs(e.y-u.y);e=(e.x-u.x)*a;return f&&cd&&c/el&&10>n|| 8 | (n>l?(d=!1,b.cancel&&b.cancel(a)):(a.preventDefault(),b.move&&b.move(m,a)))}});a.on("touchend mouseup",function(a){d&&(d=!1,b.end&&b.end(g(a),a))})}}}]);q.config(["$provide",function(g){g.decorator("ngClickDirective",["$delegate",function(a){a.shift();return a}])}]);q.directive("ngClick",["$parse","$timeout","$rootElement",function(g,a,b){function l(a,c,b){for(var f=0;fh)){var c= 9 | a.touches&&a.touches.length?a.touches:[a],b=c[0].clientX,c=c[0].clientY;1>b&&1>c||l(k,b,c)||(a.stopPropagation(),a.preventDefault(),a.target&&a.target.blur())}}function r(b){b=b.touches&&b.touches.length?b.touches:[b];var c=b[0].clientX,d=b[0].clientY;k.push(c,d);a(function(){for(var a=0;ah&&12>p)&&(k||(b[0].addEventListener("click",n,!0),b[0].addEventListener("touchstart",r,!0),k=[]),m=Date.now(),l(k,e,g),s&&s.blur(),v.isDefined(d.disabled)&&!1!==d.disabled||c.triggerHandler("click",[a]));f()});c.onclick=function(a){};c.on("click",function(b,c){a.$apply(function(){h(a,{$event:c||b})})});c.on("mousedown",function(a){c.addClass(p)});c.on("mousemove mouseup",function(a){c.removeClass(p)})}}]);t("ngSwipeLeft",-1,"swipeleft");t("ngSwipeRight",1,"swiperight")})(window, 12 | window.angular); 13 | //# sourceMappingURL=angular-touch.min.js.map 14 | -------------------------------------------------------------------------------- /app/lib/angular/angular-route.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(u,c,A){'use strict';function w(c,s,g,b,d){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",compile:function(l,m,y){return function(p,l,m){function k(){h&&(h.$destroy(),h=null);q&&(d.leave(q),q=null)}function x(){var a=c.current&&c.current.locals,e=a&&a.$template;if(e){var r=p.$new();y(r,function(v){k();v.html(e);d.enter(v,null,l);var f=g(v.contents()),n=c.current;h=n.scope=r;q=v;if(n.controller){a.$scope=h;var p=b(n.controller,a);n.controllerAs&&(h[n.controllerAs]=p); 7 | v.data("$ngControllerController",p);v.children().data("$ngControllerController",p)}f(h);h.$emit("$viewContentLoaded");h.$eval(t);s()})}else k()}var h,q,t=m.onload||"";p.$on("$routeChangeSuccess",x);x()}}}}u=c.module("ngRoute",["ng"]).provider("$route",function(){function u(b,d){return c.extend(new (c.extend(function(){},{prototype:b})),d)}function s(b,c){var l=c.caseInsensitiveMatch,m={originalPath:b,regexp:b},g=m.keys=[];b=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?|\*])?/g,function(b, 8 | c,d,k){b="?"===k?k:null;k="*"===k?k:null;g.push({name:d,optional:!!b});c=c||"";return""+(b?"":c)+"(?:"+(b?c:"")+(k&&"(.+?)"||"([^/]+)")+(b||"")+")"+(b||"")}).replace(/([\/$\*])/g,"\\$1");m.regexp=RegExp("^"+b+"$",l?"i":"");return m}var g={};this.when=function(b,d){g[b]=c.extend({reloadOnSearch:!0},d,b&&s(b,d));if(b){var l="/"==b[b.length-1]?b.substr(0,b.length-1):b+"/";g[l]=c.extend({redirectTo:b},s(l,d))}return this};this.otherwise=function(b){this.when(null,b);return this};this.$get=["$rootScope", 9 | "$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(b,d,l,m,s,p,w,z){function k(){var a=x(),e=t.current;if(a&&e&&a.$$route===e.$$route&&c.equals(a.pathParams,e.pathParams)&&!a.reloadOnSearch&&!q)e.params=a.params,c.copy(e.params,l),b.$broadcast("$routeUpdate",e);else if(a||e)q=!1,b.$broadcast("$routeChangeStart",a,e),(t.current=a)&&a.redirectTo&&(c.isString(a.redirectTo)?d.path(h(a.redirectTo,a.params)).search(a.params).replace():d.url(a.redirectTo(a.pathParams,d.path(), 10 | d.search())).replace()),m.when(a).then(function(){if(a){var b=c.extend({},a.resolve),e,f;c.forEach(b,function(a,e){b[e]=c.isString(a)?s.get(a):s.invoke(a)});c.isDefined(e=a.template)?c.isFunction(e)&&(e=e(a.params)):c.isDefined(f=a.templateUrl)&&(c.isFunction(f)&&(f=f(a.params)),f=z.getTrustedResourceUrl(f),c.isDefined(f)&&(a.loadedTemplateUrl=f,e=p.get(f,{cache:w}).then(function(a){return a.data})));c.isDefined(e)&&(b.$template=e);return m.all(b)}}).then(function(d){a==t.current&&(a&&(a.locals=d, 11 | c.copy(a.params,l)),b.$broadcast("$routeChangeSuccess",a,e))},function(c){a==t.current&&b.$broadcast("$routeChangeError",a,e,c)})}function x(){var a,b;c.forEach(g,function(r,k){var f;if(f=!b){var n=d.path();f=r.keys;var h={};if(r.regexp)if(n=r.regexp.exec(n)){for(var g=1,l=n.length;g 35 |
  • 36 |
    40 | 44 |
    45 |
      47 |
    • 48 | 49 | ``` 50 | 51 | 52 | Sample template, with drag and drop support : 53 | 54 | ```HTML 55 |
        56 |
      • 59 |
        63 | 67 |
        68 |
          70 |
        • 71 |
        72 | ``` 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,g,n){'use strict';function h(a){var d={};a=a.split(",");var c;for(c=0;c=c;k--)d.end&&d.end(e[k]);e.length= 7 | c}}var b,f,e=[],l=a;for(e.last=function(){return e[e.length-1]};a;){f=!0;if(e.last()&&v[e.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+e.last()+"[^>]*>","i"),function(a,b){b=b.replace(F,"$1").replace(G,"$1");d.chars&&d.chars(p(b));return""}),k("",e.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&d.comment(a.substring(4,b)),a=a.substring(b+3),f=!1);else if(w.test(a)){if(b=a.match(w))a=a.replace(b[0],""),f=!1}else if(H.test(a)){if(b=a.match(x))a= 8 | a.substring(b[0].length),b[0].replace(x,k),f=!1}else I.test(a)&&(b=a.match(y))&&(a=a.substring(b[0].length),b[0].replace(y,c),f=!1);f&&(b=a.indexOf("<"),f=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(p(f)))}if(a==l)throw J("badparse",a);l=a}k()}function p(a){q.innerHTML=a.replace(//g,">")}function A(a){var d= 9 | !1,c=g.bind(a,a.push);return{start:function(a,b,f){a=g.lowercase(a);!d&&v[a]&&(d=a);d||!0!=B[a]||(c("<"),c(a),g.forEach(b,function(a,b){var d=g.lowercase(b);!0!=L[d]||!0===C[d]&&!a.match(M)||(c(" "),c(b),c('="'),c(z(a)),c('"'))}),c(f?"/>":">"))},end:function(a){a=g.lowercase(a);d||!0!=B[a]||(c(""));a==d&&(d=!1)},chars:function(a){d||c(z(a))}}}var J=g.$$minErr("$sanitize"),y=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,x=/^<\s*\/\s*([\w:-]+)[^>]*>/, 10 | E=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,I=/^]*?)>/i,G=/]/,d=/^mailto:/;return function(c,k){if(!c)return c;var b,f=c,e=[],l=A(e),h,m,n={};g.isDefined(k)&&(n.target=k);for(;b=f.match(a);)h=b[0],b[2]==b[3]&&(h="mailto:"+h),m=b.index,l.chars(f.substr(0,m)),n.href=h,l.start("a",n),l.chars(b[0].replace(d, 13 | "")),l.end("a"),f=f.substring(m+b[0].length);l.chars(f);return e.join("")}})})(window,window.angular); 14 | //# sourceMappingURL=angular-sanitize.min.js.map 15 | -------------------------------------------------------------------------------- /app/lib/angular/angular-animate.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.3 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,f,n){'use strict';f.module("ngAnimate",["ng"]).config(["$provide","$animateProvider",function(A,s){var v=f.noop,w=f.forEach,B=s.$$selectors,k="$$ngAnimateState",x="ng-animate",u={running:!0};A.decorator("$animate",["$delegate","$injector","$sniffer","$rootElement","$timeout","$rootScope",function(q,m,y,n,p,g){function G(a){if(a){var c=[],b={};a=a.substr(1).split(".");(y.transitions||y.animations)&&a.push("");for(var e=0;e=m&&a.elapsedTime>=k&&e()}var h=x(c);if(!(0 to the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more information.","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","imatcher":"Matchers may only be \"self\", string patterns or RegExp objects","iwcard":"Illegal sequence *** in string matcher. String: {0}","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}","unsafe":"Attempting to use an unsafe value in a safe context."},"$controller":{"noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$compile":{"nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","multidir":"Multiple directives [{0}, {1}] asking for {2} on: {3}","nonassign":"Expression '{0}' used with directive '{1}' is non-assignable!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","tpload":"Failed to load template: {0}","iscp":"Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found."},"$injector":{"modulerr":"Failed to instantiate module {0} due to:\n{1}","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}","cdep":"Circular dependency found: {0}","nomod":"Module '{0}' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.","pget":"Provider '{0}' must define $get factory method."},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$interpolate":{"noconcat":"Error while interpolating: {0}\nStrict Contextual Escaping disallows interpolations that concatenate multiple expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce","interr":"Can't interpolate: {0}\n{1}"},"jqLite":{"offargs":"jqLite#off() does not support the `selector` argument","onargs":"jqLite#on() does not support the `selector` or `eventData` parameters","nosel":"Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"ngRepeat":{"iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'."},"ng":{"areq":"Argument '{0}' is {1}","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","badname":"hasOwnProperty is not a valid {0} name","btstrpd":"App Already Bootstrapped with this Element '{0}'","cpi":"Can't copy! Source and destination are identical."},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'."},"ngTransclude":{"orphan":"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: {0}"},"$parse":{"isecfld":"Referencing \"constructor\" field in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","isecdom":"Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","ueoe":"Unexpected end of expression: {0}","isecwindow":"Referencing the Window in Angular expressions is disallowed! Expression: {0}","isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}"},"$httpBackend":{"noxhr":"This browser does not support XMLHttpRequest."},"$location":{"ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","ihshprfx":"Invalid url \"{0}\", missing hash prefix \"{1}\"."},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badcfg":"Error in resource configuration. Expected response to contain an {0} but got an {1}","badname":"hasOwnProperty is not a valid parameter name."},"$sanitize":{"badparse":"The sanitizer was unable to parse the following block of html: {0}"}}} -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.3 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc overview 10 | * @name ngCookies 11 | * @description 12 | * 13 | * # ngCookies 14 | * 15 | * Provides the {@link ngCookies.$cookies `$cookies`} and 16 | * {@link ngCookies.$cookieStore `$cookieStore`} services. 17 | * 18 | * {@installModule cookies} 19 | * 20 | * See {@link ngCookies.$cookies `$cookies`} and 21 | * {@link ngCookies.$cookieStore `$cookieStore`} for usage. 22 | */ 23 | 24 | 25 | angular.module('ngCookies', ['ng']). 26 | /** 27 | * @ngdoc object 28 | * @name ngCookies.$cookies 29 | * @requires $browser 30 | * 31 | * @description 32 | * Provides read/write access to browser's cookies. 33 | * 34 | * Only a simple Object is exposed and by adding or removing properties to/from 35 | * this object, new cookies are created/deleted at the end of current $eval. 36 | * 37 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 38 | * 39 | * @example 40 | 41 | 42 | 50 | 51 | 52 | */ 53 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 54 | var cookies = {}, 55 | lastCookies = {}, 56 | lastBrowserCookies, 57 | runEval = false, 58 | copy = angular.copy, 59 | isUndefined = angular.isUndefined; 60 | 61 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 62 | $browser.addPollFn(function() { 63 | var currentCookies = $browser.cookies(); 64 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 65 | lastBrowserCookies = currentCookies; 66 | copy(currentCookies, lastCookies); 67 | copy(currentCookies, cookies); 68 | if (runEval) $rootScope.$apply(); 69 | } 70 | })(); 71 | 72 | runEval = true; 73 | 74 | //at the end of each eval, push cookies 75 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 76 | // strings or browser refuses to store some cookies, we update the model in the push fn. 77 | $rootScope.$watch(push); 78 | 79 | return cookies; 80 | 81 | 82 | /** 83 | * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. 84 | */ 85 | function push() { 86 | var name, 87 | value, 88 | browserCookies, 89 | updated; 90 | 91 | //delete any cookies deleted in $cookies 92 | for (name in lastCookies) { 93 | if (isUndefined(cookies[name])) { 94 | $browser.cookies(name, undefined); 95 | } 96 | } 97 | 98 | //update all cookies updated in $cookies 99 | for(name in cookies) { 100 | value = cookies[name]; 101 | if (!angular.isString(value)) { 102 | if (angular.isDefined(lastCookies[name])) { 103 | cookies[name] = lastCookies[name]; 104 | } else { 105 | delete cookies[name]; 106 | } 107 | } else if (value !== lastCookies[name]) { 108 | $browser.cookies(name, value); 109 | updated = true; 110 | } 111 | } 112 | 113 | //verify what was actually stored 114 | if (updated){ 115 | updated = false; 116 | browserCookies = $browser.cookies(); 117 | 118 | for (name in cookies) { 119 | if (cookies[name] !== browserCookies[name]) { 120 | //delete or reset all cookies that the browser dropped from $cookies 121 | if (isUndefined(browserCookies[name])) { 122 | delete cookies[name]; 123 | } else { 124 | cookies[name] = browserCookies[name]; 125 | } 126 | updated = true; 127 | } 128 | } 129 | } 130 | } 131 | }]). 132 | 133 | 134 | /** 135 | * @ngdoc object 136 | * @name ngCookies.$cookieStore 137 | * @requires $cookies 138 | * 139 | * @description 140 | * Provides a key-value (string-object) storage, that is backed by session cookies. 141 | * Objects put or retrieved from this storage are automatically serialized or 142 | * deserialized by angular's toJson/fromJson. 143 | * 144 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 145 | * 146 | * @example 147 | */ 148 | factory('$cookieStore', ['$cookies', function($cookies) { 149 | 150 | return { 151 | /** 152 | * @ngdoc method 153 | * @name ngCookies.$cookieStore#get 154 | * @methodOf ngCookies.$cookieStore 155 | * 156 | * @description 157 | * Returns the value of given cookie key 158 | * 159 | * @param {string} key Id to use for lookup. 160 | * @returns {Object} Deserialized cookie value. 161 | */ 162 | get: function(key) { 163 | var value = $cookies[key]; 164 | return value ? angular.fromJson(value) : value; 165 | }, 166 | 167 | /** 168 | * @ngdoc method 169 | * @name ngCookies.$cookieStore#put 170 | * @methodOf ngCookies.$cookieStore 171 | * 172 | * @description 173 | * Sets a value for given cookie key 174 | * 175 | * @param {string} key Id for the `value`. 176 | * @param {Object} value Value to be stored. 177 | */ 178 | put: function(key, value) { 179 | $cookies[key] = angular.toJson(value); 180 | }, 181 | 182 | /** 183 | * @ngdoc method 184 | * @name ngCookies.$cookieStore#remove 185 | * @methodOf ngCookies.$cookieStore 186 | * 187 | * @description 188 | * Remove given cookie 189 | * 190 | * @param {string} key Id of the key-value pair to delete. 191 | */ 192 | remove: function(key) { 193 | delete $cookies[key]; 194 | } 195 | }; 196 | 197 | }]); 198 | 199 | 200 | })(window, window.angular); 201 | -------------------------------------------------------------------------------- /scripts/web-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var util = require('util'), 4 | http = require('http'), 5 | fs = require('fs'), 6 | url = require('url'), 7 | events = require('events'); 8 | 9 | var DEFAULT_PORT = 8000; 10 | 11 | function main(argv) { 12 | new HttpServer({ 13 | 'GET': createServlet(StaticServlet), 14 | 'HEAD': createServlet(StaticServlet) 15 | }).start(Number(argv[2]) || DEFAULT_PORT); 16 | } 17 | 18 | function escapeHtml(value) { 19 | return value.toString(). 20 | replace('<', '<'). 21 | replace('>', '>'). 22 | replace('"', '"'); 23 | } 24 | 25 | function createServlet(Class) { 26 | var servlet = new Class(); 27 | return servlet.handleRequest.bind(servlet); 28 | } 29 | 30 | /** 31 | * An Http server implementation that uses a map of methods to decide 32 | * action routing. 33 | * 34 | * @param {Object} Map of method => Handler function 35 | */ 36 | function HttpServer(handlers) { 37 | this.handlers = handlers; 38 | this.server = http.createServer(this.handleRequest_.bind(this)); 39 | } 40 | 41 | HttpServer.prototype.start = function(port) { 42 | this.port = port; 43 | this.server.listen(port); 44 | util.puts('Http Server running at http://localhost:' + port + '/'); 45 | }; 46 | 47 | HttpServer.prototype.parseUrl_ = function(urlString) { 48 | var parsed = url.parse(urlString); 49 | parsed.pathname = url.resolve('/', parsed.pathname); 50 | return url.parse(url.format(parsed), true); 51 | }; 52 | 53 | HttpServer.prototype.handleRequest_ = function(req, res) { 54 | var logEntry = req.method + ' ' + req.url; 55 | if (req.headers['user-agent']) { 56 | logEntry += ' ' + req.headers['user-agent']; 57 | } 58 | util.puts(logEntry); 59 | req.url = this.parseUrl_(req.url); 60 | var handler = this.handlers[req.method]; 61 | if (!handler) { 62 | res.writeHead(501); 63 | res.end(); 64 | } else { 65 | handler.call(this, req, res); 66 | } 67 | }; 68 | 69 | /** 70 | * Handles static content. 71 | */ 72 | function StaticServlet() {} 73 | 74 | StaticServlet.MimeMap = { 75 | 'txt': 'text/plain', 76 | 'html': 'text/html', 77 | 'css': 'text/css', 78 | 'xml': 'application/xml', 79 | 'json': 'application/json', 80 | 'js': 'application/javascript', 81 | 'jpg': 'image/jpeg', 82 | 'jpeg': 'image/jpeg', 83 | 'gif': 'image/gif', 84 | 'png': 'image/png', 85 |   'svg': 'image/svg+xml' 86 | }; 87 | 88 | StaticServlet.prototype.handleRequest = function(req, res) { 89 | var self = this; 90 | var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/g, function(match, hex){ 91 | return String.fromCharCode(parseInt(hex, 16)); 92 | }); 93 | var parts = path.split('/'); 94 | if (parts[parts.length-1].charAt(0) === '.') 95 | return self.sendForbidden_(req, res, path); 96 | fs.stat(path, function(err, stat) { 97 | if (err) 98 | return self.sendMissing_(req, res, path); 99 | if (stat.isDirectory()) 100 | return self.sendDirectory_(req, res, path); 101 | return self.sendFile_(req, res, path); 102 | }); 103 | } 104 | 105 | StaticServlet.prototype.sendError_ = function(req, res, error) { 106 | res.writeHead(500, { 107 | 'Content-Type': 'text/html' 108 | }); 109 | res.write('\n'); 110 | res.write('Internal Server Error\n'); 111 | res.write('

        Internal Server Error

        '); 112 | res.write('
        ' + escapeHtml(util.inspect(error)) + '
        '); 113 | util.puts('500 Internal Server Error'); 114 | util.puts(util.inspect(error)); 115 | }; 116 | 117 | StaticServlet.prototype.sendMissing_ = function(req, res, path) { 118 | path = path.substring(1); 119 | res.writeHead(404, { 120 | 'Content-Type': 'text/html' 121 | }); 122 | res.write('\n'); 123 | res.write('404 Not Found\n'); 124 | res.write('

        Not Found

        '); 125 | res.write( 126 | '

        The requested URL ' + 127 | escapeHtml(path) + 128 | ' was not found on this server.

        ' 129 | ); 130 | res.end(); 131 | util.puts('404 Not Found: ' + path); 132 | }; 133 | 134 | StaticServlet.prototype.sendForbidden_ = function(req, res, path) { 135 | path = path.substring(1); 136 | res.writeHead(403, { 137 | 'Content-Type': 'text/html' 138 | }); 139 | res.write('\n'); 140 | res.write('403 Forbidden\n'); 141 | res.write('

        Forbidden

        '); 142 | res.write( 143 | '

        You do not have permission to access ' + 144 | escapeHtml(path) + ' on this server.

        ' 145 | ); 146 | res.end(); 147 | util.puts('403 Forbidden: ' + path); 148 | }; 149 | 150 | StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) { 151 | res.writeHead(301, { 152 | 'Content-Type': 'text/html', 153 | 'Location': redirectUrl 154 | }); 155 | res.write('\n'); 156 | res.write('301 Moved Permanently\n'); 157 | res.write('

        Moved Permanently

        '); 158 | res.write( 159 | '

        The document has moved here.

        ' 162 | ); 163 | res.end(); 164 | util.puts('301 Moved Permanently: ' + redirectUrl); 165 | }; 166 | 167 | StaticServlet.prototype.sendFile_ = function(req, res, path) { 168 | var self = this; 169 | var file = fs.createReadStream(path); 170 | res.writeHead(200, { 171 | 'Content-Type': StaticServlet. 172 | MimeMap[path.split('.').pop()] || 'text/plain' 173 | }); 174 | if (req.method === 'HEAD') { 175 | res.end(); 176 | } else { 177 | file.on('data', res.write.bind(res)); 178 | file.on('close', function() { 179 | res.end(); 180 | }); 181 | file.on('error', function(error) { 182 | self.sendError_(req, res, error); 183 | }); 184 | } 185 | }; 186 | 187 | StaticServlet.prototype.sendDirectory_ = function(req, res, path) { 188 | var self = this; 189 | if (path.match(/[^\/]$/)) { 190 | req.url.pathname += '/'; 191 | var redirectUrl = url.format(url.parse(url.format(req.url))); 192 | return self.sendRedirect_(req, res, redirectUrl); 193 | } 194 | fs.readdir(path, function(err, files) { 195 | if (err) 196 | return self.sendError_(req, res, error); 197 | 198 | if (!files.length) 199 | return self.writeDirectoryIndex_(req, res, path, []); 200 | 201 | var remaining = files.length; 202 | files.forEach(function(fileName, index) { 203 | fs.stat(path + '/' + fileName, function(err, stat) { 204 | if (err) 205 | return self.sendError_(req, res, err); 206 | if (stat.isDirectory()) { 207 | files[index] = fileName + '/'; 208 | } 209 | if (!(--remaining)) 210 | return self.writeDirectoryIndex_(req, res, path, files); 211 | }); 212 | }); 213 | }); 214 | }; 215 | 216 | StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) { 217 | path = path.substring(1); 218 | res.writeHead(200, { 219 | 'Content-Type': 'text/html' 220 | }); 221 | if (req.method === 'HEAD') { 222 | res.end(); 223 | return; 224 | } 225 | res.write('\n'); 226 | res.write('' + escapeHtml(path) + '\n'); 227 | res.write('\n'); 230 | res.write('

        Directory: ' + escapeHtml(path) + '

        '); 231 | res.write('
          '); 232 | files.forEach(function(fileName) { 233 | if (fileName.charAt(0) !== '.') { 234 | res.write('
        1. ' + 236 | escapeHtml(fileName) + '
        2. '); 237 | } 238 | }); 239 | res.write('
        '); 240 | res.end(); 241 | }; 242 | 243 | // Must be last, 244 | main(process.argv); 245 | -------------------------------------------------------------------------------- /app/js/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | 5 | angular.module('app.controllers', []) 6 | 7 | .controller('MenuCtrl', ['$scope', '$route', 'menu', function($scope, $route, menu) { 8 | $scope.menu = menu; 9 | $scope.getCurrentMenuItem = function() { 10 | return $route.current && $route.current.locals && $route.current.locals.menuItem; 11 | } 12 | }]) 13 | 14 | 15 | .controller('TreeCtrlSmall', ['$scope', '$http', function($scope, $http) { 16 | addExpandAllCollapseAll($scope); 17 | $scope.treeData = null; 18 | $http.get("data/smalltree.js").success(function (data) { 19 | $scope.treeData = data; 20 | }); 21 | $scope.action = function(node) { 22 | alert("Action on node : " + node.label); 23 | }; 24 | }]) 25 | 26 | .controller('TreeCtrlMedium', ['$scope', '$http', function($scope, $http) { 27 | addExpandAllCollapseAll($scope); 28 | $scope.treeData = null; 29 | $http.get("data/mediumtree.js").success(function (data) { 30 | $scope.treeData = data; 31 | }); 32 | $scope.action = function(node) { 33 | alert("Action on node : " + node.label); 34 | }; 35 | }]) 36 | 37 | .controller('TreeCtrlDeep', ['$scope', '$http', function($scope, $http) { 38 | addExpandAllCollapseAll($scope); 39 | $scope.treeData = null; 40 | $http.get("data/deeptree.js").success(function (data) { 41 | $scope.treeData = data; 42 | }); 43 | $scope.action = function(node) { 44 | alert("Action on node : " + node.label); 45 | }; 46 | }]) 47 | 48 | .controller('TreeCtrlBig', ['$scope', '$http', function($scope, $http) { 49 | addExpandAllCollapseAll($scope); 50 | $scope.treeData = null; 51 | $http.get("data/bigtree.js").success(function (data) { 52 | $scope.treeData = data; 53 | }); 54 | $scope.action = function(node) { 55 | alert("Action on node : " + node.label); 56 | }; 57 | }]) 58 | 59 | .controller('TreeCtrlBigCollapsed', ['$scope', '$http', function($scope, $http) { 60 | addExpandAllCollapseAll($scope); 61 | $scope.treeData = null; 62 | $http.get("data/bigtree.js").success(function (data) { 63 | $scope.treeData = data; 64 | $scope.collapseAll(); 65 | }); 66 | $scope.action = function(node) { 67 | alert("Action on node : " + node.label); 68 | }; 69 | }]) 70 | 71 | .controller('TreeCtrlDragMedium', ['$scope', '$http', function($scope, $http) { 72 | addExpandAllCollapseAll($scope); 73 | $scope.treeData = null; 74 | $http.get("data/mediumtree.js").success(function (data) { 75 | $scope.treeData = data; 76 | }); 77 | $scope.drop = function (targetNode, sourceNode, sourceParentNode) { 78 | var children = sourceParentNode.children; 79 | for (var i = 0 ; i < children.length ; i++) { 80 | if (children[i] == sourceNode) { 81 | children.splice(i, 1); 82 | if (!targetNode.children) { 83 | targetNode.children = []; 84 | } 85 | targetNode.children.push(sourceNode); 86 | break; 87 | } 88 | } 89 | }; 90 | }]) 91 | 92 | .controller('TreeCtrlDragDeep', ['$scope', '$http', function($scope, $http) { 93 | addExpandAllCollapseAll($scope); 94 | $scope.treeData = null; 95 | $http.get("data/deeptree.js").success(function (data) { 96 | $scope.treeData = data; 97 | }); 98 | $scope.drop = function (targetNode, sourceNode, sourceParentNode) { 99 | var children = sourceParentNode.children; 100 | for (var i = 0 ; i < children.length ; i++) { 101 | if (children[i] == sourceNode) { 102 | children.splice(i, 1); 103 | if (!targetNode.children) { 104 | targetNode.children = []; 105 | } 106 | targetNode.children.push(sourceNode); 107 | break; 108 | } 109 | } 110 | }; 111 | }]) 112 | 113 | 114 | function addExpandAllCollapseAll($scope) { 115 | function rec(nodes, action) { 116 | for (var i = 0 ; i < nodes.length ; i++) { 117 | action(nodes[i]); 118 | if (nodes[i].children) { 119 | rec(nodes[i].children, action); 120 | } 121 | } 122 | } 123 | $scope.collapseAll = function () { 124 | rec($scope.treeData, function (node) { 125 | node.collapsed = true; 126 | }); 127 | }; 128 | $scope.expandAll = function () { 129 | rec($scope.treeData, function (node) { 130 | node.collapsed = false; 131 | }); 132 | }; 133 | } 134 | 135 | 136 | /* ---------- functions used to build the sample trees ------------- */ 137 | 138 | function addChild(parent, label) { 139 | if (!parent.children) { 140 | parent.children = []; 141 | } 142 | var newNode = { 143 | label: label, 144 | collapsed: false 145 | }; 146 | parent.children.push(newNode); 147 | return newNode; 148 | } 149 | function addChildren(parent, labels) { 150 | for (var i = 0 ; i < labels.length ; i++) { 151 | addChild(parent, labels[i]); 152 | } 153 | } 154 | 155 | function smallTree() { 156 | var root = { 157 | label: "root" 158 | }; 159 | var folderA = addChild(root, "folder A"); 160 | var folderB = addChild(folderA, "folder B"); 161 | var folderC = addChild(root, "folder C"); 162 | var folderD = addChild(folderC, "folder D"); 163 | folderD.collapsed = true; 164 | var folderE = addChild(folderD, "folder E"); 165 | var folderF = addChild(folderC, "folder F"); 166 | var folderG = addChild(root, "folder G"); 167 | var folderH = addChild(root, "folder H"); 168 | addChildren(folderA, ["file A1", "file A2", "file A3", "file A4"]); 169 | addChildren(folderB, ["file B1", "file B2"]); 170 | addChildren(folderC, ["file C1"]); 171 | addChildren(folderE, ["file E1", "file E2", "file E3"]); 172 | addChildren(folderF, ["file F1", "file F2"]); 173 | addChildren(folderG, ["file G1", "file G2", "file G3", "file G4"]); 174 | addChildren(folderH, ["file H1", "file H2", "file H3"]); 175 | return [root]; 176 | } 177 | 178 | function buildTree() { 179 | var maxLevel = 6; 180 | var size = 2; 181 | var root = {}; 182 | recursiveAddChildren(maxLevel, 0, size, root, "A"); 183 | return root.children; 184 | 185 | function recursiveAddChildren(maxLevel, currentLevel, size, parent, prefix) { 186 | for (var i = 0 ; i < size ; i++) { 187 | var label = (currentLevel < maxLevel ? "Folder" : "File") 188 | + ' ' + prefix + i; 189 | var node = addChild(parent, label); 190 | if (currentLevel < maxLevel) { 191 | recursiveAddChildren(maxLevel, currentLevel + 1, size, node, prefix + i); 192 | } 193 | } 194 | } 195 | } 196 | 197 | })(); -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.3 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | 7 | ( 8 | 9 | /** 10 | * @ngdoc interface 11 | * @name angular.Module 12 | * @description 13 | * 14 | * Interface for configuring angular {@link angular.module modules}. 15 | */ 16 | 17 | function setupModuleLoader(window) { 18 | 19 | var $injectorMinErr = minErr('$injector'); 20 | 21 | function ensure(obj, name, factory) { 22 | return obj[name] || (obj[name] = factory()); 23 | } 24 | 25 | return ensure(ensure(window, 'angular', Object), 'module', function() { 26 | /** @type {Object.} */ 27 | var modules = {}; 28 | 29 | /** 30 | * @ngdoc function 31 | * @name angular.module 32 | * @description 33 | * 34 | * The `angular.module` is a global place for creating, registering and retrieving Angular modules. 35 | * All modules (angular core or 3rd party) that should be available to an application must be 36 | * registered using this mechanism. 37 | * 38 | * When passed two or more arguments, a new module is created. If passed only one argument, an 39 | * existing module (the name passed as the first argument to `module`) is retrieved. 40 | * 41 | * 42 | * # Module 43 | * 44 | * A module is a collection of services, directives, filters, and configuration information. 45 | * `angular.module` is used to configure the {@link AUTO.$injector $injector}. 46 | * 47 | *
         48 |      * // Create a new module
         49 |      * var myModule = angular.module('myModule', []);
         50 |      *
         51 |      * // register a new service
         52 |      * myModule.value('appName', 'MyCoolApp');
         53 |      *
         54 |      * // configure existing services inside initialization blocks.
         55 |      * myModule.config(function($locationProvider) {'use strict';
         56 |      *   // Configure existing providers
         57 |      *   $locationProvider.hashPrefix('!');
         58 |      * });
         59 |      * 
        60 | * 61 | * Then you can create an injector and load your modules like this: 62 | * 63 | *
         64 |      * var injector = angular.injector(['ng', 'MyModule'])
         65 |      * 
        66 | * 67 | * However it's more likely that you'll just use 68 | * {@link ng.directive:ngApp ngApp} or 69 | * {@link angular.bootstrap} to simplify this process for you. 70 | * 71 | * @param {!string} name The name of the module to create or retrieve. 72 | * @param {Array.=} requires If specified then new module is being created. If unspecified then the 73 | * the module is being retrieved for further configuration. 74 | * @param {Function} configFn Optional configuration function for the module. Same as 75 | * {@link angular.Module#config Module#config()}. 76 | * @returns {module} new module with the {@link angular.Module} api. 77 | */ 78 | return function module(name, requires, configFn) { 79 | assertNotHasOwnProperty(name, 'module'); 80 | if (requires && modules.hasOwnProperty(name)) { 81 | modules[name] = null; 82 | } 83 | return ensure(modules, name, function() { 84 | if (!requires) { 85 | throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled the module name " + 86 | "or forgot to load it. If registering a module ensure that you specify the dependencies as the second " + 87 | "argument.", name); 88 | } 89 | 90 | /** @type {!Array.>} */ 91 | var invokeQueue = []; 92 | 93 | /** @type {!Array.} */ 94 | var runBlocks = []; 95 | 96 | var config = invokeLater('$injector', 'invoke'); 97 | 98 | /** @type {angular.Module} */ 99 | var moduleInstance = { 100 | // Private state 101 | _invokeQueue: invokeQueue, 102 | _runBlocks: runBlocks, 103 | 104 | /** 105 | * @ngdoc property 106 | * @name angular.Module#requires 107 | * @propertyOf angular.Module 108 | * @returns {Array.} List of module names which must be loaded before this module. 109 | * @description 110 | * Holds the list of modules which the injector will load before the current module is loaded. 111 | */ 112 | requires: requires, 113 | 114 | /** 115 | * @ngdoc property 116 | * @name angular.Module#name 117 | * @propertyOf angular.Module 118 | * @returns {string} Name of the module. 119 | * @description 120 | */ 121 | name: name, 122 | 123 | 124 | /** 125 | * @ngdoc method 126 | * @name angular.Module#provider 127 | * @methodOf angular.Module 128 | * @param {string} name service name 129 | * @param {Function} providerType Construction function for creating new instance of the service. 130 | * @description 131 | * See {@link AUTO.$provide#provider $provide.provider()}. 132 | */ 133 | provider: invokeLater('$provide', 'provider'), 134 | 135 | /** 136 | * @ngdoc method 137 | * @name angular.Module#factory 138 | * @methodOf angular.Module 139 | * @param {string} name service name 140 | * @param {Function} providerFunction Function for creating new instance of the service. 141 | * @description 142 | * See {@link AUTO.$provide#factory $provide.factory()}. 143 | */ 144 | factory: invokeLater('$provide', 'factory'), 145 | 146 | /** 147 | * @ngdoc method 148 | * @name angular.Module#service 149 | * @methodOf angular.Module 150 | * @param {string} name service name 151 | * @param {Function} constructor A constructor function that will be instantiated. 152 | * @description 153 | * See {@link AUTO.$provide#service $provide.service()}. 154 | */ 155 | service: invokeLater('$provide', 'service'), 156 | 157 | /** 158 | * @ngdoc method 159 | * @name angular.Module#value 160 | * @methodOf angular.Module 161 | * @param {string} name service name 162 | * @param {*} object Service instance object. 163 | * @description 164 | * See {@link AUTO.$provide#value $provide.value()}. 165 | */ 166 | value: invokeLater('$provide', 'value'), 167 | 168 | /** 169 | * @ngdoc method 170 | * @name angular.Module#constant 171 | * @methodOf angular.Module 172 | * @param {string} name constant name 173 | * @param {*} object Constant value. 174 | * @description 175 | * Because the constant are fixed, they get applied before other provide methods. 176 | * See {@link AUTO.$provide#constant $provide.constant()}. 177 | */ 178 | constant: invokeLater('$provide', 'constant', 'unshift'), 179 | 180 | /** 181 | * @ngdoc method 182 | * @name angular.Module#animation 183 | * @methodOf angular.Module 184 | * @param {string} name animation name 185 | * @param {Function} animationFactory Factory function for creating new instance of an animation. 186 | * @description 187 | * 188 | * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. 189 | * 190 | * 191 | * Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and 192 | * directives that use this service. 193 | * 194 | *
        195 |            * module.animation('.animation-name', function($inject1, $inject2) {
        196 |            *   return {
        197 |            *     eventName : function(element, done) {
        198 |            *       //code to run the animation
        199 |            *       //once complete, then run done()
        200 |            *       return function cancellationFunction(element) {
        201 |            *         //code to cancel the animation
        202 |            *       }
        203 |            *     }
        204 |            *   }
        205 |            * })
        206 |            * 
        207 | * 208 | * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and 209 | * {@link ngAnimate ngAnimate module} for more information. 210 | */ 211 | animation: invokeLater('$animateProvider', 'register'), 212 | 213 | /** 214 | * @ngdoc method 215 | * @name angular.Module#filter 216 | * @methodOf angular.Module 217 | * @param {string} name Filter name. 218 | * @param {Function} filterFactory Factory function for creating new instance of filter. 219 | * @description 220 | * See {@link ng.$filterProvider#register $filterProvider.register()}. 221 | */ 222 | filter: invokeLater('$filterProvider', 'register'), 223 | 224 | /** 225 | * @ngdoc method 226 | * @name angular.Module#controller 227 | * @methodOf angular.Module 228 | * @param {string|Object} name Controller name, or an object map of controllers where the 229 | * keys are the names and the values are the constructors. 230 | * @param {Function} constructor Controller constructor function. 231 | * @description 232 | * See {@link ng.$controllerProvider#register $controllerProvider.register()}. 233 | */ 234 | controller: invokeLater('$controllerProvider', 'register'), 235 | 236 | /** 237 | * @ngdoc method 238 | * @name angular.Module#directive 239 | * @methodOf angular.Module 240 | * @param {string|Object} name Directive name, or an object map of directives where the 241 | * keys are the names and the values are the factories. 242 | * @param {Function} directiveFactory Factory function for creating new instance of 243 | * directives. 244 | * @description 245 | * See {@link ng.$compileProvider#directive $compileProvider.directive()}. 246 | */ 247 | directive: invokeLater('$compileProvider', 'directive'), 248 | 249 | /** 250 | * @ngdoc method 251 | * @name angular.Module#config 252 | * @methodOf angular.Module 253 | * @param {Function} configFn Execute this function on module load. Useful for service 254 | * configuration. 255 | * @description 256 | * Use this method to register work which needs to be performed on module loading. 257 | */ 258 | config: config, 259 | 260 | /** 261 | * @ngdoc method 262 | * @name angular.Module#run 263 | * @methodOf angular.Module 264 | * @param {Function} initializationFn Execute this function after injector creation. 265 | * Useful for application initialization. 266 | * @description 267 | * Use this method to register work which should be performed when the injector is done 268 | * loading all modules. 269 | */ 270 | run: function(block) { 271 | runBlocks.push(block); 272 | return this; 273 | } 274 | }; 275 | 276 | if (configFn) { 277 | config(configFn); 278 | } 279 | 280 | return moduleInstance; 281 | 282 | /** 283 | * @param {string} provider 284 | * @param {string} method 285 | * @param {String=} insertMethod 286 | * @returns {angular.Module} 287 | */ 288 | function invokeLater(provider, method, insertMethod) { 289 | return function() { 290 | invokeQueue[insertMethod || 'push']([provider, method, arguments]); 291 | return moduleInstance; 292 | } 293 | } 294 | }); 295 | }; 296 | }); 297 | 298 | } 299 | 300 | )(window); 301 | 302 | /** 303 | * Closure compiler type information 304 | * 305 | * @typedef { { 306 | * requires: !Array., 307 | * invokeQueue: !Array.>, 308 | * 309 | * service: function(string, Function):angular.Module, 310 | * factory: function(string, Function):angular.Module, 311 | * value: function(string, *):angular.Module, 312 | * 313 | * filter: function(string, Function):angular.Module, 314 | * 315 | * init: function(Function):angular.Module 316 | * } } 317 | */ 318 | angular.Module; 319 | 320 | -------------------------------------------------------------------------------- /app/lib/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.3 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | var $sanitizeMinErr = angular.$$minErr('$sanitize'); 9 | 10 | /** 11 | * @ngdoc overview 12 | * @name ngSanitize 13 | * @description 14 | * 15 | * # ngSanitize 16 | * 17 | * The `ngSanitize` module provides functionality to sanitize HTML. 18 | * 19 | * {@installModule sanitize} 20 | * 21 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 22 | */ 23 | 24 | /* 25 | * HTML Parser By Misko Hevery (misko@hevery.com) 26 | * based on: HTML Parser By John Resig (ejohn.org) 27 | * Original code by Erik Arvidsson, Mozilla Public License 28 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 29 | * 30 | * // Use like so: 31 | * htmlParser(htmlString, { 32 | * start: function(tag, attrs, unary) {}, 33 | * end: function(tag) {}, 34 | * chars: function(text) {}, 35 | * comment: function(text) {} 36 | * }); 37 | * 38 | */ 39 | 40 | 41 | /** 42 | * @ngdoc service 43 | * @name ngSanitize.$sanitize 44 | * @function 45 | * 46 | * @description 47 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 48 | * then serialized back to properly escaped html string. This means that no unsafe input can make 49 | * it into the returned string, however, since our parser is more strict than a typical browser 50 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 51 | * browser, won't make it through the sanitizer. 52 | * 53 | * @param {string} html Html input. 54 | * @returns {string} Sanitized html. 55 | * 56 | * @example 57 | 58 | 59 | 70 |
        71 | Snippet: 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
        DirectiveHowSourceRendered
        ng-bind-htmlAutomatically uses $sanitize
        <div ng-bind-html="snippet">
        </div>
        ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value
        <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
        </div>
        ng-bindAutomatically escapes
        <div ng-bind="snippet">
        </div>
        98 |
        99 |
        100 | 101 | it('should sanitize the html snippet by default', function() { 102 | expect(using('#bind-html-with-sanitize').element('div').html()). 103 | toBe('

        an html\nclick here\nsnippet

        '); 104 | }); 105 | 106 | it('should inline raw snippet if bound to a trusted value', function() { 107 | expect(using('#bind-html-with-trust').element("div").html()). 108 | toBe("

        an html\n" + 109 | "click here\n" + 110 | "snippet

        "); 111 | }); 112 | 113 | it('should escape snippet without any filter', function() { 114 | expect(using('#bind-default').element('div').html()). 115 | toBe("<p style=\"color:blue\">an html\n" + 116 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 117 | "snippet</p>"); 118 | }); 119 | 120 | it('should update', function() { 121 | input('snippet').enter('new text'); 122 | expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new text'); 123 | expect(using('#bind-html-with-trust').element('div').html()).toBe('new text'); 124 | expect(using('#bind-default').element('div').html()).toBe("new <b onclick=\"alert(1)\">text</b>"); 125 | }); 126 |
        127 |
        128 | */ 129 | var $sanitize = function(html) { 130 | var buf = []; 131 | htmlParser(html, htmlSanitizeWriter(buf)); 132 | return buf.join(''); 133 | }; 134 | 135 | 136 | // Regular Expressions for parsing tags and attributes 137 | var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 138 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 139 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 140 | BEGIN_TAG_REGEXP = /^/g, 143 | DOCTYPE_REGEXP = /]*?)>/i, 144 | CDATA_REGEXP = //g, 145 | URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/i, 146 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) 147 | 148 | 149 | // Good source of info about elements and attributes 150 | // http://dev.w3.org/html5/spec/Overview.html#semantics 151 | // http://simon.html5.org/html-elements 152 | 153 | // Safe Void Elements - HTML5 154 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 155 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 156 | 157 | // Elements that you can, intentionally, leave open (and which close themselves) 158 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 159 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 160 | optionalEndTagInlineElements = makeMap("rp,rt"), 161 | optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); 162 | 163 | // Safe Block Elements - HTML5 164 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + 165 | "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + 166 | "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 167 | 168 | // Inline Elements - HTML5 169 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + 170 | "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + 171 | "span,strike,strong,sub,sup,time,tt,u,var")); 172 | 173 | 174 | // Special Elements (can contain anything) 175 | var specialElements = makeMap("script,style"); 176 | 177 | var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); 178 | 179 | //Attributes that have href and hence need to be sanitized 180 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 181 | var validAttrs = angular.extend({}, uriAttrs, makeMap( 182 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 183 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 184 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 185 | 'scope,scrolling,shape,span,start,summary,target,title,type,'+ 186 | 'valign,value,vspace,width')); 187 | 188 | function makeMap(str) { 189 | var obj = {}, items = str.split(','), i; 190 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 191 | return obj; 192 | } 193 | 194 | 195 | /** 196 | * @example 197 | * htmlParser(htmlString, { 198 | * start: function(tag, attrs, unary) {}, 199 | * end: function(tag) {}, 200 | * chars: function(text) {}, 201 | * comment: function(text) {} 202 | * }); 203 | * 204 | * @param {string} html string 205 | * @param {object} handler 206 | */ 207 | function htmlParser( html, handler ) { 208 | var index, chars, match, stack = [], last = html; 209 | stack.last = function() { return stack[ stack.length - 1 ]; }; 210 | 211 | while ( html ) { 212 | chars = true; 213 | 214 | // Make sure we're not in a script or style element 215 | if ( !stack.last() || !specialElements[ stack.last() ] ) { 216 | 217 | // Comment 218 | if ( html.indexOf("", index) === index) { 223 | if (handler.comment) handler.comment( html.substring( 4, index ) ); 224 | html = html.substring( index + 3 ); 225 | chars = false; 226 | } 227 | // DOCTYPE 228 | } else if ( DOCTYPE_REGEXP.test(html) ) { 229 | match = html.match( DOCTYPE_REGEXP ); 230 | 231 | if ( match ) { 232 | html = html.replace( match[0] , ''); 233 | chars = false; 234 | } 235 | // end tag 236 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 237 | match = html.match( END_TAG_REGEXP ); 238 | 239 | if ( match ) { 240 | html = html.substring( match[0].length ); 241 | match[0].replace( END_TAG_REGEXP, parseEndTag ); 242 | chars = false; 243 | } 244 | 245 | // start tag 246 | } else if ( BEGIN_TAG_REGEXP.test(html) ) { 247 | match = html.match( START_TAG_REGEXP ); 248 | 249 | if ( match ) { 250 | html = html.substring( match[0].length ); 251 | match[0].replace( START_TAG_REGEXP, parseStartTag ); 252 | chars = false; 253 | } 254 | } 255 | 256 | if ( chars ) { 257 | index = html.indexOf("<"); 258 | 259 | var text = index < 0 ? html : html.substring( 0, index ); 260 | html = index < 0 ? "" : html.substring( index ); 261 | 262 | if (handler.chars) handler.chars( decodeEntities(text) ); 263 | } 264 | 265 | } else { 266 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ 267 | text = text. 268 | replace(COMMENT_REGEXP, "$1"). 269 | replace(CDATA_REGEXP, "$1"); 270 | 271 | if (handler.chars) handler.chars( decodeEntities(text) ); 272 | 273 | return ""; 274 | }); 275 | 276 | parseEndTag( "", stack.last() ); 277 | } 278 | 279 | if ( html == last ) { 280 | throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block of html: {0}", html); 281 | } 282 | last = html; 283 | } 284 | 285 | // Clean up any remaining tags 286 | parseEndTag(); 287 | 288 | function parseStartTag( tag, tagName, rest, unary ) { 289 | tagName = angular.lowercase(tagName); 290 | if ( blockElements[ tagName ] ) { 291 | while ( stack.last() && inlineElements[ stack.last() ] ) { 292 | parseEndTag( "", stack.last() ); 293 | } 294 | } 295 | 296 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 297 | parseEndTag( "", tagName ); 298 | } 299 | 300 | unary = voidElements[ tagName ] || !!unary; 301 | 302 | if ( !unary ) 303 | stack.push( tagName ); 304 | 305 | var attrs = {}; 306 | 307 | rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { 308 | var value = doubleQuotedValue 309 | || singleQuotedValue 310 | || unquotedValue 311 | || ''; 312 | 313 | attrs[name] = decodeEntities(value); 314 | }); 315 | if (handler.start) handler.start( tagName, attrs, unary ); 316 | } 317 | 318 | function parseEndTag( tag, tagName ) { 319 | var pos = 0, i; 320 | tagName = angular.lowercase(tagName); 321 | if ( tagName ) 322 | // Find the closest opened tag of the same type 323 | for ( pos = stack.length - 1; pos >= 0; pos-- ) 324 | if ( stack[ pos ] == tagName ) 325 | break; 326 | 327 | if ( pos >= 0 ) { 328 | // Close all the open elements, up the stack 329 | for ( i = stack.length - 1; i >= pos; i-- ) 330 | if (handler.end) handler.end( stack[ i ] ); 331 | 332 | // Remove the open elements from the stack 333 | stack.length = pos; 334 | } 335 | } 336 | } 337 | 338 | /** 339 | * decodes all entities into regular string 340 | * @param value 341 | * @returns {string} A string with decoded entities. 342 | */ 343 | var hiddenPre=document.createElement("pre"); 344 | function decodeEntities(value) { 345 | hiddenPre.innerHTML=value.replace(//g, '>'); 364 | } 365 | 366 | /** 367 | * create an HTML/XML writer which writes to buffer 368 | * @param {Array} buf use buf.jain('') to get out sanitized html string 369 | * @returns {object} in the form of { 370 | * start: function(tag, attrs, unary) {}, 371 | * end: function(tag) {}, 372 | * chars: function(text) {}, 373 | * comment: function(text) {} 374 | * } 375 | */ 376 | function htmlSanitizeWriter(buf){ 377 | var ignore = false; 378 | var out = angular.bind(buf, buf.push); 379 | return { 380 | start: function(tag, attrs, unary){ 381 | tag = angular.lowercase(tag); 382 | if (!ignore && specialElements[tag]) { 383 | ignore = tag; 384 | } 385 | if (!ignore && validElements[tag] == true) { 386 | out('<'); 387 | out(tag); 388 | angular.forEach(attrs, function(value, key){ 389 | var lkey=angular.lowercase(key); 390 | if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { 391 | out(' '); 392 | out(key); 393 | out('="'); 394 | out(encodeEntities(value)); 395 | out('"'); 396 | } 397 | }); 398 | out(unary ? '/>' : '>'); 399 | } 400 | }, 401 | end: function(tag){ 402 | tag = angular.lowercase(tag); 403 | if (!ignore && validElements[tag] == true) { 404 | out(''); 407 | } 408 | if (tag == ignore) { 409 | ignore = false; 410 | } 411 | }, 412 | chars: function(chars){ 413 | if (!ignore) { 414 | out(encodeEntities(chars)); 415 | } 416 | } 417 | }; 418 | } 419 | 420 | 421 | // define ngSanitize module and register $sanitize service 422 | angular.module('ngSanitize', []).value('$sanitize', $sanitize); 423 | 424 | /** 425 | * @ngdoc filter 426 | * @name ngSanitize.filter:linky 427 | * @function 428 | * 429 | * @description 430 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 431 | * plain email address links. 432 | * 433 | * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 434 | * 435 | * @param {string} text Input text. 436 | * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. 437 | * @returns {string} Html-linkified text. 438 | * 439 | * @usage 440 | 441 | * 442 | * @example 443 | 444 | 445 | 456 |
        457 | Snippet: 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 469 | 472 | 473 | 474 | 475 | 478 | 481 | 482 | 483 | 484 | 485 | 486 | 487 |
        FilterSourceRendered
        linky filter 467 |
        <div ng-bind-html="snippet | linky">
        </div>
        468 |
        470 |
        471 |
        linky target 476 |
        <div ng-bind-html="snippetWithTarget | linky:'_blank'">
        </div>
        477 |
        479 |
        480 |
        no filter
        <div ng-bind="snippet">
        </div>
        488 | 489 | 490 | it('should linkify the snippet with urls', function() { 491 | expect(using('#linky-filter').binding('snippet | linky')). 492 | toBe('Pretty text with some links: ' + 493 | 'http://angularjs.org/, ' + 494 | 'us@somewhere.org, ' + 495 | 'another@somewhere.org, ' + 496 | 'and one more: ftp://127.0.0.1/.'); 497 | }); 498 | 499 | it ('should not linkify snippet without the linky filter', function() { 500 | expect(using('#escaped-html').binding('snippet')). 501 | toBe("Pretty text with some links:\n" + 502 | "http://angularjs.org/,\n" + 503 | "mailto:us@somewhere.org,\n" + 504 | "another@somewhere.org,\n" + 505 | "and one more: ftp://127.0.0.1/."); 506 | }); 507 | 508 | it('should update', function() { 509 | input('snippet').enter('new http://link.'); 510 | expect(using('#linky-filter').binding('snippet | linky')). 511 | toBe('new http://link.'); 512 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 513 | }); 514 | 515 | it('should work with the target property', function() { 516 | expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")). 517 | toBe('http://angularjs.org/'); 518 | }); 519 | 520 | 521 | */ 522 | angular.module('ngSanitize').filter('linky', function() { 523 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 524 | MAILTO_REGEXP = /^mailto:/; 525 | 526 | return function(text, target) { 527 | if (!text) return text; 528 | var match; 529 | var raw = text; 530 | var html = []; 531 | // TODO(vojta): use $sanitize instead 532 | var writer = htmlSanitizeWriter(html); 533 | var url; 534 | var i; 535 | var properties = {}; 536 | if (angular.isDefined(target)) { 537 | properties.target = target; 538 | } 539 | while ((match = raw.match(LINKY_URL_REGEXP))) { 540 | // We can not end in these as they are sometimes found at the end of the sentence 541 | url = match[0]; 542 | // if we did not match ftp/http/mailto then assume mailto 543 | if (match[2] == match[3]) url = 'mailto:' + url; 544 | i = match.index; 545 | writer.chars(raw.substr(0, i)); 546 | properties.href = url; 547 | writer.start('a', properties); 548 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 549 | writer.end('a'); 550 | raw = raw.substring(i + match[0].length); 551 | } 552 | writer.chars(raw); 553 | return html.join(''); 554 | }; 555 | }); 556 | 557 | 558 | })(window, window.angular); 559 | -------------------------------------------------------------------------------- /app/js/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.directives', []) 4 | 5 | // Main directive, that just publish a controller 6 | .directive('frangTree', function ($parse, $animate) { 7 | return { 8 | restrict: 'EA', 9 | controller: function($scope, $element) { 10 | this.insertChildren = null; 11 | this.init = function(insertChildren) { 12 | this.insertChildren = insertChildren; 13 | }; 14 | } 15 | }; 16 | }) 17 | 18 | .directive('frangTreeRepeat', function ($parse, $animate) { 19 | 20 | // ---------- Some necessary internal functions from angular.js ---------- 21 | 22 | function hashKey(obj) { 23 | var objType = typeof obj, 24 | key; 25 | 26 | if (objType == 'object' && obj !== null) { 27 | if (typeof (key = obj.$$hashKey) == 'function') { 28 | // must invoke on object to keep the right this 29 | key = obj.$$hashKey(); 30 | } else if (key === undefined) { 31 | key = obj.$$hashKey = nextUid(); 32 | } 33 | } else { 34 | key = obj; 35 | } 36 | 37 | return objType + ':' + key; 38 | } 39 | function isArrayLike(obj) { 40 | if (obj == null || isWindow(obj)) { 41 | return false; 42 | } 43 | 44 | var length = obj.length; 45 | 46 | if (obj.nodeType === 1 && length) { 47 | return true; 48 | } 49 | 50 | return isString(obj) || isArray(obj) || length === 0 || 51 | typeof length === 'number' && length > 0 && (length - 1) in obj; 52 | } 53 | function isWindow(obj) { 54 | return obj && obj.document && obj.location && obj.alert && obj.setInterval; 55 | } 56 | function isString(value){return typeof value == 'string';} 57 | function isArray(value) { 58 | return toString.apply(value) == '[object Array]'; 59 | } 60 | var uid = ['0', '0', '0']; 61 | function nextUid() { 62 | var index = uid.length; 63 | var digit; 64 | 65 | while(index) { 66 | index--; 67 | digit = uid[index].charCodeAt(0); 68 | if (digit == 57 /*'9'*/) { 69 | uid[index] = 'A'; 70 | return uid.join(''); 71 | } 72 | if (digit == 90 /*'Z'*/) { 73 | uid[index] = '0'; 74 | } else { 75 | uid[index] = String.fromCharCode(digit + 1); 76 | return uid.join(''); 77 | } 78 | } 79 | uid.unshift('0'); 80 | return uid.join(''); 81 | } 82 | function assertNotHasOwnProperty(name, context) { 83 | if (name === 'hasOwnProperty') { 84 | throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); 85 | } 86 | } 87 | var jqLite = angular.element; 88 | var forEach = angular.forEach; 89 | 90 | function minErr(module) { 91 | return function () { 92 | var code = arguments[0], 93 | prefix = '[' + (module ? module + ':' : '') + code + '] ', 94 | template = arguments[1], 95 | templateArgs = arguments, 96 | stringify = function (obj) { 97 | if (isFunction(obj)) { 98 | return obj.toString().replace(/ \{[\s\S]*$/, ''); 99 | } else if (isUndefined(obj)) { 100 | return 'undefined'; 101 | } else if (!isString(obj)) { 102 | return JSON.stringify(obj); 103 | } 104 | return obj; 105 | }, 106 | message, i; 107 | 108 | message = prefix + template.replace(/\{\d+\}/g, function (match) { 109 | var index = +match.slice(1, -1), arg; 110 | 111 | if (index + 2 < templateArgs.length) { 112 | arg = templateArgs[index + 2]; 113 | if (isFunction(arg)) { 114 | return arg.toString().replace(/ ?\{[\s\S]*$/, ''); 115 | } else if (isUndefined(arg)) { 116 | return 'undefined'; 117 | } else if (!isString(arg)) { 118 | return toJson(arg); 119 | } 120 | return arg; 121 | } 122 | return match; 123 | }); 124 | 125 | message = message + '\nhttp://errors.angularjs.org/' + version.full + '/' + 126 | (module ? module + '/' : '') + code; 127 | for (i = 2; i < arguments.length; i++) { 128 | message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + 129 | encodeURIComponent(stringify(arguments[i])); 130 | } 131 | 132 | return new Error(message); 133 | }; 134 | } 135 | 136 | 137 | // ---------- Some initializations at the beginning of ngRepeat factory ---------- 138 | 139 | var NG_REMOVED = '$$NG_REMOVED'; 140 | var ngRepeatMinErr = minErr('ngRepeat'); 141 | var ngMinErr = minErr('ng'); 142 | var toString = Object.prototype.toString; 143 | var isFunction = angular.isFunction; 144 | var isUndefined = angular.isUndefined; 145 | var toJson = angular.toJson; 146 | 147 | // ---------- Internal function at the end of ngRepeat factory ---------- 148 | 149 | function getBlockElements(block) { 150 | if (block.startNode === block.endNode) { 151 | return jqLite(block.startNode); 152 | } 153 | 154 | var element = block.startNode; 155 | var elements = [element]; 156 | 157 | do { 158 | element = element.nextSibling; 159 | if (!element) break; 160 | elements.push(element); 161 | } while (element !== block.endNode); 162 | 163 | return jqLite(elements); 164 | } 165 | 166 | 167 | // ---------- Add watch, extracted into a function to call it not only on the element but also on its children ---------- 168 | 169 | function addRepeatWatch($scope, $element, _lastBlockMap, valueIdentifier, keyIdentifier, 170 | rhs, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, linker, expression) { 171 | var lastBlockMap = _lastBlockMap; 172 | 173 | //watch props 174 | $scope.$watchCollection(rhs, function ngRepeatAction(collection){ 175 | var index, length, 176 | previousNode = $element[0], // current position of the node 177 | nextNode, 178 | // Same as lastBlockMap but it has the current state. It will become the 179 | // lastBlockMap on the next iteration. 180 | nextBlockMap = {}, 181 | arrayLength, 182 | childScope, 183 | key, value, // key/value of iteration 184 | trackById, 185 | trackByIdFn, 186 | collectionKeys, 187 | block, // last object information {scope, element, id} 188 | nextBlockOrder = [], 189 | elementsToRemove; 190 | 191 | 192 | if (isArrayLike(collection)) { 193 | collectionKeys = collection; 194 | trackByIdFn = trackByIdExpFn || trackByIdArrayFn; 195 | } else { 196 | trackByIdFn = trackByIdExpFn || trackByIdObjFn; 197 | // if object, extract keys, sort them and use to determine order of iteration over obj props 198 | collectionKeys = []; 199 | for (key in collection) { 200 | if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { 201 | collectionKeys.push(key); 202 | } 203 | } 204 | collectionKeys.sort(); 205 | } 206 | 207 | arrayLength = collectionKeys.length; 208 | 209 | // locate existing items 210 | length = nextBlockOrder.length = collectionKeys.length; 211 | for(index = 0; index < length; index++) { 212 | key = (collection === collectionKeys) ? index : collectionKeys[index]; 213 | value = collection[key]; 214 | trackById = trackByIdFn(key, value, index); 215 | assertNotHasOwnProperty(trackById, '`track by` id'); 216 | if(lastBlockMap.hasOwnProperty(trackById)) { 217 | block = lastBlockMap[trackById] 218 | delete lastBlockMap[trackById]; 219 | nextBlockMap[trackById] = block; 220 | nextBlockOrder[index] = block; 221 | } else if (nextBlockMap.hasOwnProperty(trackById)) { 222 | // restore lastBlockMap 223 | forEach(nextBlockOrder, function(block) { 224 | if (block && block.startNode) lastBlockMap[block.id] = block; 225 | }); 226 | // This is a duplicate and we need to throw an error 227 | throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", 228 | expression, trackById); 229 | } else { 230 | // new never before seen block 231 | nextBlockOrder[index] = { id: trackById }; 232 | nextBlockMap[trackById] = false; 233 | } 234 | } 235 | 236 | // remove existing items 237 | for (key in lastBlockMap) { 238 | // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn 239 | if (lastBlockMap.hasOwnProperty(key)) { 240 | block = lastBlockMap[key]; 241 | elementsToRemove = getBlockElements(block); 242 | $animate.leave(elementsToRemove); 243 | forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); 244 | block.scope.$destroy(); 245 | } 246 | } 247 | 248 | // we are not using forEach for perf reasons (trying to avoid #call) 249 | for (index = 0, length = collectionKeys.length; index < length; index++) { 250 | key = (collection === collectionKeys) ? index : collectionKeys[index]; 251 | value = collection[key]; 252 | block = nextBlockOrder[index]; 253 | if (nextBlockOrder[index - 1]) previousNode = nextBlockOrder[index - 1].endNode; 254 | 255 | if (block.startNode) { 256 | // if we have already seen this object, then we need to reuse the 257 | // associated scope/element 258 | childScope = block.scope; 259 | 260 | nextNode = previousNode; 261 | do { 262 | nextNode = nextNode.nextSibling; 263 | } while(nextNode && nextNode[NG_REMOVED]); 264 | 265 | if (block.startNode == nextNode) { 266 | // do nothing 267 | } else { 268 | // existing item which got moved 269 | $animate.move(getBlockElements(block), null, jqLite(previousNode)); 270 | } 271 | previousNode = block.endNode; 272 | } else { 273 | // new item which we don't know about 274 | childScope = $scope.$new(); 275 | } 276 | 277 | childScope[valueIdentifier] = value; 278 | if (keyIdentifier) childScope[keyIdentifier] = key; 279 | childScope.$index = index; 280 | childScope.$first = (index === 0); 281 | childScope.$last = (index === (arrayLength - 1)); 282 | childScope.$middle = !(childScope.$first || childScope.$last); 283 | childScope.$odd = !(childScope.$even = index%2==0); 284 | 285 | if (!block.startNode) { 286 | linker(childScope, function(clone) { 287 | clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); 288 | $animate.enter(clone, null, jqLite(previousNode)); 289 | previousNode = clone; 290 | block.scope = childScope; 291 | block.startNode = previousNode && previousNode.endNode ? previousNode.endNode : clone[0]; 292 | block.endNode = clone[clone.length - 1]; 293 | nextBlockMap[block.id] = block; 294 | }); 295 | } 296 | } 297 | lastBlockMap = nextBlockMap; 298 | }); 299 | } 300 | 301 | 302 | return { 303 | restrict: 'A', 304 | transclude: 'element', 305 | priority: 1000, 306 | terminal: true, 307 | require: '^frangTree', 308 | compile: function(element, attr, linker) { 309 | return function($scope, $element, $attr, ctrl){ 310 | var expression = $attr.frangTreeRepeat; 311 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/), 312 | trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, 313 | lhs, rhs, valueIdentifier, keyIdentifier, 314 | hashFnLocals = {$id: hashKey}; 315 | 316 | if (!match) { 317 | throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", 318 | expression); 319 | } 320 | 321 | lhs = match[1]; 322 | rhs = match[2]; 323 | trackByExp = match[4]; 324 | 325 | if (trackByExp) { 326 | trackByExpGetter = $parse(trackByExp); 327 | trackByIdExpFn = function(key, value, index) { 328 | // assign key, value, and $index to the locals so that they can be used in hash functions 329 | if (keyIdentifier) hashFnLocals[keyIdentifier] = key; 330 | hashFnLocals[valueIdentifier] = value; 331 | hashFnLocals.$index = index; 332 | return trackByExpGetter($scope, hashFnLocals); 333 | }; 334 | } else { 335 | trackByIdArrayFn = function(key, value) { 336 | return hashKey(value); 337 | } 338 | trackByIdObjFn = function(key) { 339 | return key; 340 | } 341 | } 342 | 343 | match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); 344 | if (!match) { 345 | throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", 346 | lhs); 347 | } 348 | valueIdentifier = match[3] || match[1]; 349 | keyIdentifier = match[2]; 350 | 351 | // Store a list of elements from previous run. This is a hash where key is the item from the 352 | // iterator, and the value is objects with following properties. 353 | // - scope: bound scope 354 | // - element: previous element. 355 | // - index: position 356 | var lastBlockMap = {}; 357 | 358 | 359 | addRepeatWatch($scope, $element, /*lastBlockMap*/ {}, valueIdentifier, keyIdentifier, 360 | rhs, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, linker, expression); 361 | 362 | ctrl.init(function ($scope, $element, collection) { 363 | addRepeatWatch($scope, $element, /*lastBlockMap*/ {}, valueIdentifier, keyIdentifier, 364 | collection, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, linker, expression) 365 | }); 366 | }; 367 | 368 | } 369 | }; 370 | }) 371 | 372 | .directive('frangTreeInsertChildren', function () { 373 | return { 374 | restrict: 'EA', 375 | require: '^frangTree', 376 | link: function (scope, element, attrs, ctrl) { 377 | var comment = document.createComment('treeRepeat'); 378 | element.append(comment); 379 | 380 | ctrl.insertChildren(scope, angular.element(comment), attrs.frangTreeInsertChildren); 381 | } 382 | }; 383 | }) 384 | 385 | .directive('frangTreeDrag', function($parse) { 386 | return { 387 | restrict: 'A', 388 | require: '^frangTree', 389 | link: function(scope, element, attrs, ctrl) { 390 | var el = element[0]; 391 | var parsedDrag = $parse(attrs.frangTreeDrag); 392 | el.draggable = true; 393 | el.addEventListener( 394 | 'dragstart', 395 | function(e) { 396 | if (e.stopPropagation) e.stopPropagation(); 397 | e.dataTransfer.effectAllowed = 'move'; 398 | e.dataTransfer.setData('Text', 'nothing'); // Firefox requires some data 399 | element.addClass('tree-drag'); 400 | ctrl.dragData = parsedDrag(scope); 401 | return false; 402 | }, 403 | false 404 | ); 405 | el.addEventListener( 406 | 'dragend', 407 | function(e) { 408 | if (e.stopPropagation) e.stopPropagation(); 409 | element.removeClass('tree-drag'); 410 | ctrl.dragData = null; 411 | return false; 412 | }, 413 | false 414 | ); 415 | } 416 | }; 417 | }) 418 | 419 | .directive('frangTreeDrop', function($parse) { 420 | return { 421 | restrict: 'A', 422 | require: '^frangTree', 423 | link: function(scope, element, attrs, ctrl) { 424 | var el = element[0]; 425 | var parsedDrop = $parse(attrs.frangTreeDrop); 426 | var parsedAllowDrop = $parse(attrs.frangTreeAllowDrop || 'true'); 427 | el.addEventListener( 428 | 'dragover', 429 | function(e) { 430 | if (parsedAllowDrop(scope, {dragData: ctrl.dragData})) { 431 | if (e.stopPropagation) { e.stopPropagation(); } 432 | e.dataTransfer.dropEffect = 'move'; 433 | element.addClass('tree-drag-over'); 434 | // allow drop 435 | if (e.preventDefault) { e.preventDefault(); } 436 | } 437 | return false; 438 | }, 439 | false 440 | ); 441 | el.addEventListener( 442 | 'dragenter', 443 | function(e) { 444 | if (parsedAllowDrop(scope, {dragData: ctrl.dragData})) { 445 | if (e.stopPropagation) { e.stopPropagation(); } 446 | element.addClass('tree-drag-over'); 447 | // allow drop 448 | if (e.preventDefault) { e.preventDefault(); } 449 | } 450 | return false; 451 | }, 452 | false 453 | ); 454 | el.addEventListener( 455 | 'dragleave', 456 | function(e) { 457 | if (parsedAllowDrop(scope, {dragData: ctrl.dragData})) { 458 | if (e.stopPropagation) { e.stopPropagation(); } 459 | element.removeClass('tree-drag-over'); 460 | } 461 | return false; 462 | }, 463 | false 464 | ); 465 | el.addEventListener( 466 | 'drop', 467 | function(e) { 468 | if (parsedAllowDrop(scope, {dragData: ctrl.dragData})) { 469 | if (e.stopPropagation) { e.stopPropagation(); } 470 | element.removeClass('tree-drag-over'); 471 | scope.$apply(function () { 472 | parsedDrop(scope, {dragData: ctrl.dragData}); 473 | }); 474 | ctrl.dragData = null; 475 | if (e.preventDefault) { e.preventDefault(); } 476 | } 477 | return false; 478 | }, 479 | false 480 | ); 481 | } 482 | } 483 | }); 484 | 485 | -------------------------------------------------------------------------------- /app/lib/angular/angular-touch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.3 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc overview 10 | * @name ngTouch 11 | * @description 12 | * 13 | * # ngTouch 14 | * 15 | * `ngTouch` is the name of the optional Angular module that provides touch events and other 16 | * helpers for touch-enabled devices. 17 | * The implementation is based on jQuery Mobile touch event handling 18 | * ([jquerymobile.com](http://jquerymobile.com/)) 19 | * 20 | * {@installModule touch} 21 | * 22 | * See {@link ngTouch.$swipe `$swipe`} for usage. 23 | */ 24 | 25 | // define ngTouch module 26 | var ngTouch = angular.module('ngTouch', []); 27 | 28 | /** 29 | * @ngdoc object 30 | * @name ngTouch.$swipe 31 | * 32 | * @description 33 | * The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe 34 | * behavior, to make implementing swipe-related directives more convenient. 35 | * 36 | * Requires the {@link ngTouch `ngTouch`} module to be installed. 37 | * 38 | * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`, and by 39 | * `ngCarousel` in a separate component. 40 | * 41 | * # Usage 42 | * The `$swipe` service is an object with a single method: `bind`. `bind` takes an element 43 | * which is to be watched for swipes, and an object with four handler functions. See the 44 | * documentation for `bind` below. 45 | */ 46 | 47 | ngTouch.factory('$swipe', [function() { 48 | // The total distance in any direction before we make the call on swipe vs. scroll. 49 | var MOVE_BUFFER_RADIUS = 10; 50 | 51 | function getCoordinates(event) { 52 | var touches = event.touches && event.touches.length ? event.touches : [event]; 53 | var e = (event.changedTouches && event.changedTouches[0]) || 54 | (event.originalEvent && event.originalEvent.changedTouches && 55 | event.originalEvent.changedTouches[0]) || 56 | touches[0].originalEvent || touches[0]; 57 | 58 | return { 59 | x: e.clientX, 60 | y: e.clientY 61 | }; 62 | } 63 | 64 | return { 65 | /** 66 | * @ngdoc method 67 | * @name ngTouch.$swipe#bind 68 | * @methodOf ngTouch.$swipe 69 | * 70 | * @description 71 | * The main method of `$swipe`. It takes an element to be watched for swipe motions, and an 72 | * object containing event handlers. 73 | * 74 | * The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end` 75 | * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`. 76 | * 77 | * `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is 78 | * watching for `touchmove` or `mousemove` events. These events are ignored until the total 79 | * distance moved in either dimension exceeds a small threshold. 80 | * 81 | * Once this threshold is exceeded, either the horizontal or vertical delta is greater. 82 | * - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow. 83 | * - If the vertical distance is greater, this is a scroll, and we let the browser take over. 84 | * A `cancel` event is sent. 85 | * 86 | * `move` is called on `mousemove` and `touchmove` after the above logic has determined that 87 | * a swipe is in progress. 88 | * 89 | * `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`. 90 | * 91 | * `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling 92 | * as described above. 93 | * 94 | */ 95 | bind: function(element, eventHandlers) { 96 | // Absolute total movement, used to control swipe vs. scroll. 97 | var totalX, totalY; 98 | // Coordinates of the start position. 99 | var startCoords; 100 | // Last event's position. 101 | var lastPos; 102 | // Whether a swipe is active. 103 | var active = false; 104 | 105 | element.on('touchstart mousedown', function(event) { 106 | startCoords = getCoordinates(event); 107 | active = true; 108 | totalX = 0; 109 | totalY = 0; 110 | lastPos = startCoords; 111 | eventHandlers['start'] && eventHandlers['start'](startCoords, event); 112 | }); 113 | 114 | element.on('touchcancel', function(event) { 115 | active = false; 116 | eventHandlers['cancel'] && eventHandlers['cancel'](event); 117 | }); 118 | 119 | element.on('touchmove mousemove', function(event) { 120 | if (!active) return; 121 | 122 | // Android will send a touchcancel if it thinks we're starting to scroll. 123 | // So when the total distance (+ or - or both) exceeds 10px in either direction, 124 | // we either: 125 | // - On totalX > totalY, we send preventDefault() and treat this as a swipe. 126 | // - On totalY > totalX, we let the browser handle it as a scroll. 127 | 128 | if (!startCoords) return; 129 | var coords = getCoordinates(event); 130 | 131 | totalX += Math.abs(coords.x - lastPos.x); 132 | totalY += Math.abs(coords.y - lastPos.y); 133 | 134 | lastPos = coords; 135 | 136 | if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) { 137 | return; 138 | } 139 | 140 | // One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll. 141 | if (totalY > totalX) { 142 | // Allow native scrolling to take over. 143 | active = false; 144 | eventHandlers['cancel'] && eventHandlers['cancel'](event); 145 | return; 146 | } else { 147 | // Prevent the browser from scrolling. 148 | event.preventDefault(); 149 | eventHandlers['move'] && eventHandlers['move'](coords, event); 150 | } 151 | }); 152 | 153 | element.on('touchend mouseup', function(event) { 154 | if (!active) return; 155 | active = false; 156 | eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); 157 | }); 158 | } 159 | }; 160 | }]); 161 | 162 | /** 163 | * @ngdoc directive 164 | * @name ngTouch.directive:ngClick 165 | * 166 | * @description 167 | * A more powerful replacement for the default ngClick designed to be used on touchscreen 168 | * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending 169 | * the click event. This version handles them immediately, and then prevents the 170 | * following click event from propagating. 171 | * 172 | * Requires the {@link ngTouch `ngTouch`} module to be installed. 173 | * 174 | * This directive can fall back to using an ordinary click event, and so works on desktop 175 | * browsers as well as mobile. 176 | * 177 | * This directive also sets the CSS class `ng-click-active` while the element is being held 178 | * down (by a mouse click or touch) so you can restyle the depressed element if you wish. 179 | * 180 | * @element ANY 181 | * @param {expression} ngClick {@link guide/expression Expression} to evaluate 182 | * upon tap. (Event object is available as `$event`) 183 | * 184 | * @example 185 | 186 | 187 | 190 | count: {{ count }} 191 | 192 | 193 | */ 194 | 195 | ngTouch.config(['$provide', function($provide) { 196 | $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { 197 | // drop the default ngClick directive 198 | $delegate.shift(); 199 | return $delegate; 200 | }]); 201 | }]); 202 | 203 | ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', 204 | function($parse, $timeout, $rootElement) { 205 | var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag. 206 | var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers. 207 | var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click 208 | var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks. 209 | 210 | var ACTIVE_CLASS_NAME = 'ng-click-active'; 211 | var lastPreventedTime; 212 | var touchCoordinates; 213 | 214 | 215 | // TAP EVENTS AND GHOST CLICKS 216 | // 217 | // Why tap events? 218 | // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're 219 | // double-tapping, and then fire a click event. 220 | // 221 | // This delay sucks and makes mobile apps feel unresponsive. 222 | // So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when 223 | // the user has tapped on something. 224 | // 225 | // What happens when the browser then generates a click event? 226 | // The browser, of course, also detects the tap and fires a click after a delay. This results in 227 | // tapping/clicking twice. So we do "clickbusting" to prevent it. 228 | // 229 | // How does it work? 230 | // We attach global touchstart and click handlers, that run during the capture (early) phase. 231 | // So the sequence for a tap is: 232 | // - global touchstart: Sets an "allowable region" at the point touched. 233 | // - element's touchstart: Starts a touch 234 | // (- touchmove or touchcancel ends the touch, no click follows) 235 | // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold 236 | // too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). 237 | // - preventGhostClick() removes the allowable region the global touchstart created. 238 | // - The browser generates a click event. 239 | // - The global click handler catches the click, and checks whether it was in an allowable region. 240 | // - If preventGhostClick was called, the region will have been removed, the click is busted. 241 | // - If the region is still there, the click proceeds normally. Therefore clicks on links and 242 | // other elements without ngTap on them work normally. 243 | // 244 | // This is an ugly, terrible hack! 245 | // Yeah, tell me about it. The alternatives are using the slow click events, or making our users 246 | // deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular 247 | // encapsulates this ugly logic away from the user. 248 | // 249 | // Why not just put click handlers on the element? 250 | // We do that too, just to be sure. The problem is that the tap event might have caused the DOM 251 | // to change, so that the click fires in the same position but something else is there now. So 252 | // the handlers are global and care only about coordinates and not elements. 253 | 254 | // Checks if the coordinates are close enough to be within the region. 255 | function hit(x1, y1, x2, y2) { 256 | return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD; 257 | } 258 | 259 | // Checks a list of allowable regions against a click location. 260 | // Returns true if the click should be allowed. 261 | // Splices out the allowable region from the list after it has been used. 262 | function checkAllowableRegions(touchCoordinates, x, y) { 263 | for (var i = 0; i < touchCoordinates.length; i += 2) { 264 | if (hit(touchCoordinates[i], touchCoordinates[i+1], x, y)) { 265 | touchCoordinates.splice(i, i + 2); 266 | return true; // allowable region 267 | } 268 | } 269 | return false; // No allowable region; bust it. 270 | } 271 | 272 | // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick 273 | // was called recently. 274 | function onClick(event) { 275 | if (Date.now() - lastPreventedTime > PREVENT_DURATION) { 276 | return; // Too old. 277 | } 278 | 279 | var touches = event.touches && event.touches.length ? event.touches : [event]; 280 | var x = touches[0].clientX; 281 | var y = touches[0].clientY; 282 | // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label 283 | // and on the input element). Depending on the exact browser, this second click we don't want 284 | // to bust has either (0,0) or negative coordinates. 285 | if (x < 1 && y < 1) { 286 | return; // offscreen 287 | } 288 | 289 | // Look for an allowable region containing this click. 290 | // If we find one, that means it was created by touchstart and not removed by 291 | // preventGhostClick, so we don't bust it. 292 | if (checkAllowableRegions(touchCoordinates, x, y)) { 293 | return; 294 | } 295 | 296 | // If we didn't find an allowable region, bust the click. 297 | event.stopPropagation(); 298 | event.preventDefault(); 299 | 300 | // Blur focused form elements 301 | event.target && event.target.blur(); 302 | } 303 | 304 | 305 | // Global touchstart handler that creates an allowable region for a click event. 306 | // This allowable region can be removed by preventGhostClick if we want to bust it. 307 | function onTouchStart(event) { 308 | var touches = event.touches && event.touches.length ? event.touches : [event]; 309 | var x = touches[0].clientX; 310 | var y = touches[0].clientY; 311 | touchCoordinates.push(x, y); 312 | 313 | $timeout(function() { 314 | // Remove the allowable region. 315 | for (var i = 0; i < touchCoordinates.length; i += 2) { 316 | if (touchCoordinates[i] == x && touchCoordinates[i+1] == y) { 317 | touchCoordinates.splice(i, i + 2); 318 | return; 319 | } 320 | } 321 | }, PREVENT_DURATION, false); 322 | } 323 | 324 | // On the first call, attaches some event handlers. Then whenever it gets called, it creates a 325 | // zone around the touchstart where clicks will get busted. 326 | function preventGhostClick(x, y) { 327 | if (!touchCoordinates) { 328 | $rootElement[0].addEventListener('click', onClick, true); 329 | $rootElement[0].addEventListener('touchstart', onTouchStart, true); 330 | touchCoordinates = []; 331 | } 332 | 333 | lastPreventedTime = Date.now(); 334 | 335 | checkAllowableRegions(touchCoordinates, x, y); 336 | } 337 | 338 | // Actual linking function. 339 | return function(scope, element, attr) { 340 | var clickHandler = $parse(attr.ngClick), 341 | tapping = false, 342 | tapElement, // Used to blur the element after a tap. 343 | startTime, // Used to check if the tap was held too long. 344 | touchStartX, 345 | touchStartY; 346 | 347 | function resetState() { 348 | tapping = false; 349 | element.removeClass(ACTIVE_CLASS_NAME); 350 | } 351 | 352 | element.on('touchstart', function(event) { 353 | tapping = true; 354 | tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. 355 | // Hack for Safari, which can target text nodes instead of containers. 356 | if(tapElement.nodeType == 3) { 357 | tapElement = tapElement.parentNode; 358 | } 359 | 360 | element.addClass(ACTIVE_CLASS_NAME); 361 | 362 | startTime = Date.now(); 363 | 364 | var touches = event.touches && event.touches.length ? event.touches : [event]; 365 | var e = touches[0].originalEvent || touches[0]; 366 | touchStartX = e.clientX; 367 | touchStartY = e.clientY; 368 | }); 369 | 370 | element.on('touchmove', function(event) { 371 | resetState(); 372 | }); 373 | 374 | element.on('touchcancel', function(event) { 375 | resetState(); 376 | }); 377 | 378 | element.on('touchend', function(event) { 379 | var diff = Date.now() - startTime; 380 | 381 | var touches = (event.changedTouches && event.changedTouches.length) ? event.changedTouches : 382 | ((event.touches && event.touches.length) ? event.touches : [event]); 383 | var e = touches[0].originalEvent || touches[0]; 384 | var x = e.clientX; 385 | var y = e.clientY; 386 | var dist = Math.sqrt( Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2) ); 387 | 388 | if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { 389 | // Call preventGhostClick so the clickbuster will catch the corresponding click. 390 | preventGhostClick(x, y); 391 | 392 | // Blur the focused element (the button, probably) before firing the callback. 393 | // This doesn't work perfectly on Android Chrome, but seems to work elsewhere. 394 | // I couldn't get anything to work reliably on Android Chrome. 395 | if (tapElement) { 396 | tapElement.blur(); 397 | } 398 | 399 | if (!angular.isDefined(attr.disabled) || attr.disabled === false) { 400 | element.triggerHandler('click', [event]); 401 | } 402 | } 403 | 404 | resetState(); 405 | }); 406 | 407 | // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click 408 | // something else nearby. 409 | element.onclick = function(event) { }; 410 | 411 | // Actual click handler. 412 | // There are three different kinds of clicks, only two of which reach this point. 413 | // - On desktop browsers without touch events, their clicks will always come here. 414 | // - On mobile browsers, the simulated "fast" click will call this. 415 | // - But the browser's follow-up slow click will be "busted" before it reaches this handler. 416 | // Therefore it's safe to use this directive on both mobile and desktop. 417 | element.on('click', function(event, touchend) { 418 | scope.$apply(function() { 419 | clickHandler(scope, {$event: (touchend || event)}); 420 | }); 421 | }); 422 | 423 | element.on('mousedown', function(event) { 424 | element.addClass(ACTIVE_CLASS_NAME); 425 | }); 426 | 427 | element.on('mousemove mouseup', function(event) { 428 | element.removeClass(ACTIVE_CLASS_NAME); 429 | }); 430 | 431 | }; 432 | }]); 433 | 434 | /** 435 | * @ngdoc directive 436 | * @name ngTouch.directive:ngSwipeLeft 437 | * 438 | * @description 439 | * Specify custom behavior when an element is swiped to the left on a touchscreen device. 440 | * A leftward swipe is a quick, right-to-left slide of the finger. 441 | * Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag too. 442 | * 443 | * Requires the {@link ngTouch `ngTouch`} module to be installed. 444 | * 445 | * @element ANY 446 | * @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate 447 | * upon left swipe. (Event object is available as `$event`) 448 | * 449 | * @example 450 | 451 | 452 |
        453 | Some list content, like an email in the inbox 454 |
        455 |
        456 | 457 | 458 |
        459 |
        460 |
        461 | */ 462 | 463 | /** 464 | * @ngdoc directive 465 | * @name ngTouch.directive:ngSwipeRight 466 | * 467 | * @description 468 | * Specify custom behavior when an element is swiped to the right on a touchscreen device. 469 | * A rightward swipe is a quick, left-to-right slide of the finger. 470 | * Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag too. 471 | * 472 | * Requires the {@link ngTouch `ngTouch`} module to be installed. 473 | * 474 | * @element ANY 475 | * @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate 476 | * upon right swipe. (Event object is available as `$event`) 477 | * 478 | * @example 479 | 480 | 481 |
        482 | Some list content, like an email in the inbox 483 |
        484 |
        485 | 486 | 487 |
        488 |
        489 |
        490 | */ 491 | 492 | function makeSwipeDirective(directiveName, direction, eventName) { 493 | ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) { 494 | // The maximum vertical delta for a swipe should be less than 75px. 495 | var MAX_VERTICAL_DISTANCE = 75; 496 | // Vertical distance should not be more than a fraction of the horizontal distance. 497 | var MAX_VERTICAL_RATIO = 0.3; 498 | // At least a 30px lateral motion is necessary for a swipe. 499 | var MIN_HORIZONTAL_DISTANCE = 30; 500 | 501 | return function(scope, element, attr) { 502 | var swipeHandler = $parse(attr[directiveName]); 503 | 504 | var startCoords, valid; 505 | 506 | function validSwipe(coords) { 507 | // Check that it's within the coordinates. 508 | // Absolute vertical distance must be within tolerances. 509 | // Horizontal distance, we take the current X - the starting X. 510 | // This is negative for leftward swipes and positive for rightward swipes. 511 | // After multiplying by the direction (-1 for left, +1 for right), legal swipes 512 | // (ie. same direction as the directive wants) will have a positive delta and 513 | // illegal ones a negative delta. 514 | // Therefore this delta must be positive, and larger than the minimum. 515 | if (!startCoords) return false; 516 | var deltaY = Math.abs(coords.y - startCoords.y); 517 | var deltaX = (coords.x - startCoords.x) * direction; 518 | return valid && // Short circuit for already-invalidated swipes. 519 | deltaY < MAX_VERTICAL_DISTANCE && 520 | deltaX > 0 && 521 | deltaX > MIN_HORIZONTAL_DISTANCE && 522 | deltaY / deltaX < MAX_VERTICAL_RATIO; 523 | } 524 | 525 | $swipe.bind(element, { 526 | 'start': function(coords, event) { 527 | startCoords = coords; 528 | valid = true; 529 | }, 530 | 'cancel': function(event) { 531 | valid = false; 532 | }, 533 | 'end': function(coords, event) { 534 | if (validSwipe(coords)) { 535 | scope.$apply(function() { 536 | element.triggerHandler(eventName); 537 | swipeHandler(scope, {$event: event}); 538 | }); 539 | } 540 | } 541 | }); 542 | }; 543 | }]); 544 | } 545 | 546 | // Left is negative X-coordinate, right is positive. 547 | makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft'); 548 | makeSwipeDirective('ngSwipeRight', 1, 'swiperight'); 549 | 550 | 551 | 552 | })(window, window.angular); 553 | -------------------------------------------------------------------------------- /app/data/mediumtree.js: -------------------------------------------------------------------------------- 1 | [ 2 | {"label": "Folder A0", "collapsed": false, "children": [ 3 | {"label": "Folder A00", "collapsed": false, "children": [ 4 | {"label": "Folder A000", "collapsed": false, "children": [ 5 | {"label": "Folder A0000", "collapsed": false, "children": [ 6 | {"label": "Folder A00000", "collapsed": false, "children": [ 7 | {"label": "Folder A000000", "collapsed": false, "children": [ 8 | {"label": "File A0000000", "collapsed": false}, 9 | {"label": "File A0000001", "collapsed": false} 10 | ]}, 11 | {"label": "Folder A000001", "collapsed": false, "children": [ 12 | {"label": "File A0000010", "collapsed": false}, 13 | {"label": "File A0000011", "collapsed": false} 14 | ]} 15 | ]}, 16 | {"label": "Folder A00001", "collapsed": false, "children": [ 17 | {"label": "Folder A000010", "collapsed": false, "children": [ 18 | {"label": "File A0000100", "collapsed": false}, 19 | {"label": "File A0000101", "collapsed": false} 20 | ]}, 21 | {"label": "Folder A000011", "collapsed": false, "children": [ 22 | {"label": "File A0000110", "collapsed": false}, 23 | {"label": "File A0000111", "collapsed": false} 24 | ]} 25 | ]} 26 | ]}, 27 | {"label": "Folder A0001", "collapsed": false, "children": [ 28 | {"label": "Folder A00010", "collapsed": false, "children": [ 29 | {"label": "Folder A000100", "collapsed": false, "children": [ 30 | {"label": "File A0001000", "collapsed": false}, 31 | {"label": "File A0001001", "collapsed": false} 32 | ]}, 33 | {"label": "Folder A000101", "collapsed": false, "children": [ 34 | {"label": "File A0001010", "collapsed": false}, 35 | {"label": "File A0001011", "collapsed": false} 36 | ]} 37 | ]}, 38 | {"label": "Folder A00011", "collapsed": false, "children": [ 39 | {"label": "Folder A000110", "collapsed": false, "children": [ 40 | {"label": "File A0001100", "collapsed": false}, 41 | {"label": "File A0001101", "collapsed": false} 42 | ]}, 43 | {"label": "Folder A000111", "collapsed": false, "children": [ 44 | {"label": "File A0001110", "collapsed": false}, 45 | {"label": "File A0001111", "collapsed": false} 46 | ]} 47 | ]} 48 | ]} 49 | ]}, 50 | {"label": "Folder A001", "collapsed": false, "children": [ 51 | {"label": "Folder A0010", "collapsed": false, "children": [ 52 | {"label": "Folder A00100", "collapsed": false, "children": [ 53 | {"label": "Folder A001000", "collapsed": false, "children": [ 54 | {"label": "File A0010000", "collapsed": false}, 55 | {"label": "File A0010001", "collapsed": false} 56 | ]}, 57 | {"label": "Folder A001001", "collapsed": false, "children": [ 58 | {"label": "File A0010010", "collapsed": false}, 59 | {"label": "File A0010011", "collapsed": false} 60 | ]} 61 | ]}, 62 | {"label": "Folder A00101", "collapsed": false, "children": [ 63 | {"label": "Folder A001010", "collapsed": false, "children": [ 64 | {"label": "File A0010100", "collapsed": false}, 65 | {"label": "File A0010101", "collapsed": false} 66 | ]}, 67 | {"label": "Folder A001011", "collapsed": false, "children": [ 68 | {"label": "File A0010110", "collapsed": false}, 69 | {"label": "File A0010111", "collapsed": false} 70 | ]} 71 | ]} 72 | ]}, 73 | {"label": "Folder A0011", "collapsed": false, "children": [ 74 | {"label": "Folder A00110", "collapsed": false, "children": [ 75 | {"label": "Folder A001100", "collapsed": false, "children": [ 76 | {"label": "File A0011000", "collapsed": false}, 77 | {"label": "File A0011001", "collapsed": false} 78 | ]}, 79 | {"label": "Folder A001101", "collapsed": false, "children": [ 80 | {"label": "File A0011010", "collapsed": false}, 81 | {"label": "File A0011011", "collapsed": false} 82 | ]} 83 | ]}, 84 | {"label": "Folder A00111", "collapsed": false, "children": [ 85 | {"label": "Folder A001110", "collapsed": false, "children": [ 86 | {"label": "File A0011100", "collapsed": false}, 87 | {"label": "File A0011101", "collapsed": false} 88 | ]}, 89 | {"label": "Folder A001111", "collapsed": false, "children": [ 90 | {"label": "File A0011110", "collapsed": false}, 91 | {"label": "File A0011111", "collapsed": false} 92 | ]} 93 | ]} 94 | ]} 95 | ]} 96 | ]}, 97 | {"label": "Folder A01", "collapsed": false, "children": [ 98 | {"label": "Folder A010", "collapsed": false, "children": [ 99 | {"label": "Folder A0100", "collapsed": false, "children": [ 100 | {"label": "Folder A01000", "collapsed": false, "children": [ 101 | {"label": "Folder A010000", "collapsed": false, "children": [ 102 | {"label": "File A0100000", "collapsed": false}, 103 | {"label": "File A0100001", "collapsed": false} 104 | ]}, 105 | {"label": "Folder A010001", "collapsed": false, "children": [ 106 | {"label": "File A0100010", "collapsed": false}, 107 | {"label": "File A0100011", "collapsed": false} 108 | ]} 109 | ]}, 110 | {"label": "Folder A01001", "collapsed": false, "children": [ 111 | {"label": "Folder A010010", "collapsed": false, "children": [ 112 | {"label": "File A0100100", "collapsed": false}, 113 | {"label": "File A0100101", "collapsed": false} 114 | ]}, 115 | {"label": "Folder A010011", "collapsed": false, "children": [ 116 | {"label": "File A0100110", "collapsed": false}, 117 | {"label": "File A0100111", "collapsed": false} 118 | ]} 119 | ]} 120 | ]}, 121 | {"label": "Folder A0101", "collapsed": false, "children": [ 122 | {"label": "Folder A01010", "collapsed": false, "children": [ 123 | {"label": "Folder A010100", "collapsed": false, "children": [ 124 | {"label": "File A0101000", "collapsed": false}, 125 | {"label": "File A0101001", "collapsed": false} 126 | ]}, 127 | {"label": "Folder A010101", "collapsed": false, "children": [ 128 | {"label": "File A0101010", "collapsed": false}, 129 | {"label": "File A0101011", "collapsed": false} 130 | ]} 131 | ]}, 132 | {"label": "Folder A01011", "collapsed": false, "children": [ 133 | {"label": "Folder A010110", "collapsed": false, "children": [ 134 | {"label": "File A0101100", "collapsed": false}, 135 | {"label": "File A0101101", "collapsed": false} 136 | ]}, 137 | {"label": "Folder A010111", "collapsed": false, "children": [ 138 | {"label": "File A0101110", "collapsed": false}, 139 | {"label": "File A0101111", "collapsed": false} 140 | ]} 141 | ]} 142 | ]} 143 | ]}, 144 | {"label": "Folder A011", "collapsed": false, "children": [ 145 | {"label": "Folder A0110", "collapsed": false, "children": [ 146 | {"label": "Folder A01100", "collapsed": false, "children": [ 147 | {"label": "Folder A011000", "collapsed": false, "children": [ 148 | {"label": "File A0110000", "collapsed": false}, 149 | {"label": "File A0110001", "collapsed": false} 150 | ]}, 151 | {"label": "Folder A011001", "collapsed": false, "children": [ 152 | {"label": "File A0110010", "collapsed": false}, 153 | {"label": "File A0110011", "collapsed": false} 154 | ]} 155 | ]}, 156 | {"label": "Folder A01101", "collapsed": false, "children": [ 157 | {"label": "Folder A011010", "collapsed": false, "children": [ 158 | {"label": "File A0110100", "collapsed": false}, 159 | {"label": "File A0110101", "collapsed": false} 160 | ]}, 161 | {"label": "Folder A011011", "collapsed": false, "children": [ 162 | {"label": "File A0110110", "collapsed": false}, 163 | {"label": "File A0110111", "collapsed": false} 164 | ]} 165 | ]} 166 | ]}, 167 | {"label": "Folder A0111", "collapsed": false, "children": [ 168 | {"label": "Folder A01110", "collapsed": false, "children": [ 169 | {"label": "Folder A011100", "collapsed": false, "children": [ 170 | {"label": "File A0111000", "collapsed": false}, 171 | {"label": "File A0111001", "collapsed": false} 172 | ]}, 173 | {"label": "Folder A011101", "collapsed": false, "children": [ 174 | {"label": "File A0111010", "collapsed": false}, 175 | {"label": "File A0111011", "collapsed": false} 176 | ]} 177 | ]}, 178 | {"label": "Folder A01111", "collapsed": false, "children": [ 179 | {"label": "Folder A011110", "collapsed": false, "children": [ 180 | {"label": "File A0111100", "collapsed": false}, 181 | {"label": "File A0111101", "collapsed": false} 182 | ]}, 183 | {"label": "Folder A011111", "collapsed": false, "children": [ 184 | {"label": "File A0111110", "collapsed": false}, 185 | {"label": "File A0111111", "collapsed": false} 186 | ]} 187 | ]} 188 | ]} 189 | ]} 190 | ]} 191 | ]}, 192 | {"label": "Folder A1", "collapsed": false, "children": [ 193 | {"label": "Folder A10", "collapsed": false, "children": [ 194 | {"label": "Folder A100", "collapsed": false, "children": [ 195 | {"label": "Folder A1000", "collapsed": false, "children": [ 196 | {"label": "Folder A10000", "collapsed": false, "children": [ 197 | {"label": "Folder A100000", "collapsed": false, "children": [ 198 | {"label": "File A1000000", "collapsed": false}, 199 | {"label": "File A1000001", "collapsed": false} 200 | ]}, 201 | {"label": "Folder A100001", "collapsed": false, "children": [ 202 | {"label": "File A1000010", "collapsed": false}, 203 | {"label": "File A1000011", "collapsed": false} 204 | ]} 205 | ]}, 206 | {"label": "Folder A10001", "collapsed": false, "children": [ 207 | {"label": "Folder A100010", "collapsed": false, "children": [ 208 | {"label": "File A1000100", "collapsed": false}, 209 | {"label": "File A1000101", "collapsed": false} 210 | ]}, 211 | {"label": "Folder A100011", "collapsed": false, "children": [ 212 | {"label": "File A1000110", "collapsed": false}, 213 | {"label": "File A1000111", "collapsed": false} 214 | ]} 215 | ]} 216 | ]}, 217 | {"label": "Folder A1001", "collapsed": false, "children": [ 218 | {"label": "Folder A10010", "collapsed": false, "children": [ 219 | {"label": "Folder A100100", "collapsed": false, "children": [ 220 | {"label": "File A1001000", "collapsed": false}, 221 | {"label": "File A1001001", "collapsed": false} 222 | ]}, 223 | {"label": "Folder A100101", "collapsed": false, "children": [ 224 | {"label": "File A1001010", "collapsed": false}, 225 | {"label": "File A1001011", "collapsed": false} 226 | ]} 227 | ]}, 228 | {"label": "Folder A10011", "collapsed": false, "children": [ 229 | {"label": "Folder A100110", "collapsed": false, "children": [ 230 | {"label": "File A1001100", "collapsed": false}, 231 | {"label": "File A1001101", "collapsed": false} 232 | ]}, 233 | {"label": "Folder A100111", "collapsed": false, "children": [ 234 | {"label": "File A1001110", "collapsed": false}, 235 | {"label": "File A1001111", "collapsed": false} 236 | ]} 237 | ]} 238 | ]} 239 | ]}, 240 | {"label": "Folder A101", "collapsed": false, "children": [ 241 | {"label": "Folder A1010", "collapsed": false, "children": [ 242 | {"label": "Folder A10100", "collapsed": false, "children": [ 243 | {"label": "Folder A101000", "collapsed": false, "children": [ 244 | {"label": "File A1010000", "collapsed": false}, 245 | {"label": "File A1010001", "collapsed": false} 246 | ]}, 247 | {"label": "Folder A101001", "collapsed": false, "children": [ 248 | {"label": "File A1010010", "collapsed": false}, 249 | {"label": "File A1010011", "collapsed": false} 250 | ]} 251 | ]}, 252 | {"label": "Folder A10101", "collapsed": false, "children": [ 253 | {"label": "Folder A101010", "collapsed": false, "children": [ 254 | {"label": "File A1010100", "collapsed": false}, 255 | {"label": "File A1010101", "collapsed": false} 256 | ]}, 257 | {"label": "Folder A101011", "collapsed": false, "children": [ 258 | {"label": "File A1010110", "collapsed": false}, 259 | {"label": "File A1010111", "collapsed": false} 260 | ]} 261 | ]} 262 | ]}, 263 | {"label": "Folder A1011", "collapsed": false, "children": [ 264 | {"label": "Folder A10110", "collapsed": false, "children": [ 265 | {"label": "Folder A101100", "collapsed": false, "children": [ 266 | {"label": "File A1011000", "collapsed": false}, 267 | {"label": "File A1011001", "collapsed": false} 268 | ]}, 269 | {"label": "Folder A101101", "collapsed": false, "children": [ 270 | {"label": "File A1011010", "collapsed": false}, 271 | {"label": "File A1011011", "collapsed": false} 272 | ]} 273 | ]}, 274 | {"label": "Folder A10111", "collapsed": false, "children": [ 275 | {"label": "Folder A101110", "collapsed": false, "children": [ 276 | {"label": "File A1011100", "collapsed": false}, 277 | {"label": "File A1011101", "collapsed": false} 278 | ]}, 279 | {"label": "Folder A101111", "collapsed": false, "children": [ 280 | {"label": "File A1011110", "collapsed": false}, 281 | {"label": "File A1011111", "collapsed": false} 282 | ]} 283 | ]} 284 | ]} 285 | ]} 286 | ]}, 287 | {"label": "Folder A11", "collapsed": false, "children": [ 288 | {"label": "Folder A110", "collapsed": false, "children": [ 289 | {"label": "Folder A1100", "collapsed": false, "children": [ 290 | {"label": "Folder A11000", "collapsed": false, "children": [ 291 | {"label": "Folder A110000", "collapsed": false, "children": [ 292 | {"label": "File A1100000", "collapsed": false}, 293 | {"label": "File A1100001", "collapsed": false} 294 | ]}, 295 | {"label": "Folder A110001", "collapsed": false, "children": [ 296 | {"label": "File A1100010", "collapsed": false}, 297 | {"label": "File A1100011", "collapsed": false} 298 | ]} 299 | ]}, 300 | {"label": "Folder A11001", "collapsed": false, "children": [ 301 | {"label": "Folder A110010", "collapsed": false, "children": [ 302 | {"label": "File A1100100", "collapsed": false}, 303 | {"label": "File A1100101", "collapsed": false} 304 | ]}, 305 | {"label": "Folder A110011", "collapsed": false, "children": [ 306 | {"label": "File A1100110", "collapsed": false}, 307 | {"label": "File A1100111", "collapsed": false} 308 | ]} 309 | ]} 310 | ]}, 311 | {"label": "Folder A1101", "collapsed": false, "children": [ 312 | {"label": "Folder A11010", "collapsed": false, "children": [ 313 | {"label": "Folder A110100", "collapsed": false, "children": [ 314 | {"label": "File A1101000", "collapsed": false}, 315 | {"label": "File A1101001", "collapsed": false} 316 | ]}, 317 | {"label": "Folder A110101", "collapsed": false, "children": [ 318 | {"label": "File A1101010", "collapsed": false}, 319 | {"label": "File A1101011", "collapsed": false} 320 | ]} 321 | ]}, 322 | {"label": "Folder A11011", "collapsed": false, "children": [ 323 | {"label": "Folder A110110", "collapsed": false, "children": [ 324 | {"label": "File A1101100", "collapsed": false}, 325 | {"label": "File A1101101", "collapsed": false} 326 | ]}, 327 | {"label": "Folder A110111", "collapsed": false, "children": [ 328 | {"label": "File A1101110", "collapsed": false}, 329 | {"label": "File A1101111", "collapsed": false} 330 | ]} 331 | ]} 332 | ]} 333 | ]}, 334 | {"label": "Folder A111", "collapsed": false, "children": [ 335 | {"label": "Folder A1110", "collapsed": false, "children": [ 336 | {"label": "Folder A11100", "collapsed": false, "children": [ 337 | {"label": "Folder A111000", "collapsed": false, "children": [ 338 | {"label": "File A1110000", "collapsed": false}, 339 | {"label": "File A1110001", "collapsed": false} 340 | ]}, 341 | {"label": "Folder A111001", "collapsed": false, "children": [ 342 | {"label": "File A1110010", "collapsed": false}, 343 | {"label": "File A1110011", "collapsed": false} 344 | ]} 345 | ]}, 346 | {"label": "Folder A11101", "collapsed": false, "children": [ 347 | {"label": "Folder A111010", "collapsed": false, "children": [ 348 | {"label": "File A1110100", "collapsed": false}, 349 | {"label": "File A1110101", "collapsed": false} 350 | ]}, 351 | {"label": "Folder A111011", "collapsed": false, "children": [ 352 | {"label": "File A1110110", "collapsed": false}, 353 | {"label": "File A1110111", "collapsed": false} 354 | ]} 355 | ]} 356 | ]}, 357 | {"label": "Folder A1111", "collapsed": false, "children": [ 358 | {"label": "Folder A11110", "collapsed": false, "children": [ 359 | {"label": "Folder A111100", "collapsed": false, "children": [ 360 | {"label": "File A1111000", "collapsed": false}, 361 | {"label": "File A1111001", "collapsed": false} 362 | ]}, 363 | {"label": "Folder A111101", "collapsed": false, "children": [ 364 | {"label": "File A1111010", "collapsed": false}, 365 | {"label": "File A1111011", "collapsed": false} 366 | ]} 367 | ]}, 368 | {"label": "Folder A11111", "collapsed": false, "children": [ 369 | {"label": "Folder A111110", "collapsed": false, "children": [ 370 | {"label": "File A1111100", "collapsed": false}, 371 | {"label": "File A1111101", "collapsed": false} 372 | ]}, 373 | {"label": "Folder A111111", "collapsed": false, "children": [ 374 | {"label": "File A1111110", "collapsed": false}, 375 | {"label": "File A1111111", "collapsed": false} 376 | ]} 377 | ]} 378 | ]} 379 | ]} 380 | ]} 381 | ]} 382 | ] -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.3 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | var $resourceMinErr = angular.$$minErr('$resource'); 9 | 10 | /** 11 | * @ngdoc overview 12 | * @name ngResource 13 | * @description 14 | * 15 | * # ngResource 16 | * 17 | * `ngResource` is the name of the optional Angular module that adds support for interacting with 18 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 19 | * `ngResource` provides the {@link ngResource.$resource `$resource`} service. 20 | * 21 | * {@installModule resource} 22 | * 23 | * See {@link ngResource.$resource `$resource`} for usage. 24 | */ 25 | 26 | /** 27 | * @ngdoc object 28 | * @name ngResource.$resource 29 | * @requires $http 30 | * 31 | * @description 32 | * A factory which creates a resource object that lets you interact with 33 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 34 | * 35 | * The returned resource object has action methods which provide high-level behaviors without 36 | * the need to interact with the low level {@link ng.$http $http} service. 37 | * 38 | * Requires the {@link ngResource `ngResource`} module to be installed. 39 | * 40 | * @param {string} url A parametrized URL template with parameters prefixed by `:` as in 41 | * `/user/:username`. If you are using a URL with a port number (e.g. 42 | * `http://example.com:8080/api`), it will be respected. 43 | * 44 | * If you are using a url with a suffix, just add the suffix, like this: 45 | * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` 46 | * or even `$resource('http://example.com/resource/:resource_id.:format')` 47 | * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be 48 | * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you 49 | * can escape it with `/\.`. 50 | * 51 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 52 | * `actions` methods. If any of the parameter value is a function, it will be executed every time 53 | * when a param value needs to be obtained for a request (unless the param was overridden). 54 | * 55 | * Each key value in the parameter object is first bound to url template if present and then any 56 | * excess keys are appended to the url search query after the `?`. 57 | * 58 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 59 | * URL `/path/greet?salutation=Hello`. 60 | * 61 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 62 | * the data object (useful for non-GET operations). 63 | * 64 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 65 | * default set of resource actions. The declaration should be created in the format of {@link 66 | * ng.$http#Parameters $http.config}: 67 | * 68 | * {action1: {method:?, params:?, isArray:?, headers:?, ...}, 69 | * action2: {method:?, params:?, isArray:?, headers:?, ...}, 70 | * ...} 71 | * 72 | * Where: 73 | * 74 | * - **`action`** – {string} – The name of action. This name becomes the name of the method on your 75 | * resource object. 76 | * - **`method`** – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 77 | * and `JSONP`. 78 | * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of the 79 | * parameter value is a function, it will be executed every time when a param value needs to be 80 | * obtained for a request (unless the param was overridden). 81 | * - **`url`** – {string} – action specific `url` override. The url templating is supported just like 82 | * for the resource-level urls. 83 | * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, see 84 | * `returns` section. 85 | * - **`transformRequest`** – `{function(data, headersGetter)|Array.}` – 86 | * transform function or an array of such functions. The transform function takes the http 87 | * request body and headers and returns its transformed (typically serialized) version. 88 | * - **`transformResponse`** – `{function(data, headersGetter)|Array.}` – 89 | * transform function or an array of such functions. The transform function takes the http 90 | * response body and headers and returns its transformed (typically deserialized) version. 91 | * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the 92 | * GET request, otherwise if a cache instance built with 93 | * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for 94 | * caching. 95 | * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that 96 | * should abort the request when resolved. 97 | * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the 98 | * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 99 | * requests with credentials} for more information. 100 | * - **`responseType`** - `{string}` - see {@link 101 | * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. 102 | * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - 103 | * `response` and `responseError`. Both `response` and `responseError` interceptors get called 104 | * with `http response` object. See {@link ng.$http $http interceptors}. 105 | * 106 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 107 | * optionally extended with custom `actions`. The default set contains these actions: 108 | * 109 | * { 'get': {method:'GET'}, 110 | * 'save': {method:'POST'}, 111 | * 'query': {method:'GET', isArray:true}, 112 | * 'remove': {method:'DELETE'}, 113 | * 'delete': {method:'DELETE'} }; 114 | * 115 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 116 | * destination and parameters. When the data is returned from the server then the object is an 117 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 118 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 119 | * read, update, delete) on server-side data like this: 120 | *
        121 |         var User = $resource('/user/:userId', {userId:'@id'});
        122 |         var user = User.get({userId:123}, function() {
        123 |           user.abc = true;
        124 |           user.$save();
        125 |         });
        126 |      
        127 | * 128 | * It is important to realize that invoking a $resource object method immediately returns an 129 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 130 | * server the existing reference is populated with the actual data. This is a useful trick since 131 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 132 | * object results in no rendering, once the data arrives from the server then the object is 133 | * populated with the data and the view automatically re-renders itself showing the new data. This 134 | * means that in most case one never has to write a callback function for the action methods. 135 | * 136 | * The action methods on the class object or instance object can be invoked with the following 137 | * parameters: 138 | * 139 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 140 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 141 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 142 | * 143 | * Success callback is called with (value, responseHeaders) arguments. Error callback is called 144 | * with (httpResponse) argument. 145 | * 146 | * Class actions return empty instance (with additional properties below). 147 | * Instance actions return promise of the action. 148 | * 149 | * The Resource instances and collection have these additional properties: 150 | * 151 | * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this 152 | * instance or collection. 153 | * 154 | * On success, the promise is resolved with the same resource instance or collection object, 155 | * updated with data from server. This makes it easy to use in 156 | * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view rendering 157 | * until the resource(s) are loaded. 158 | * 159 | * On failure, the promise is resolved with the {@link ng.$http http response} object, 160 | * without the `resource` property. 161 | * 162 | * - `$resolved`: `true` after first server interaction is completed (either with success or rejection), 163 | * `false` before that. Knowing if the Resource has been resolved is useful in data-binding. 164 | * 165 | * @example 166 | * 167 | * # Credit card resource 168 | * 169 | *
        170 |      // Define CreditCard class
        171 |      var CreditCard = $resource('/user/:userId/card/:cardId',
        172 |       {userId:123, cardId:'@id'}, {
        173 |        charge: {method:'POST', params:{charge:true}}
        174 |       });
        175 | 
        176 |      // We can retrieve a collection from the server
        177 |      var cards = CreditCard.query(function() {
        178 |        // GET: /user/123/card
        179 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
        180 | 
        181 |        var card = cards[0];
        182 |        // each item is an instance of CreditCard
        183 |        expect(card instanceof CreditCard).toEqual(true);
        184 |        card.name = "J. Smith";
        185 |        // non GET methods are mapped onto the instances
        186 |        card.$save();
        187 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
        188 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
        189 | 
        190 |        // our custom method is mapped as well.
        191 |        card.$charge({amount:9.99});
        192 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
        193 |      });
        194 | 
        195 |      // we can create an instance as well
        196 |      var newCard = new CreditCard({number:'0123'});
        197 |      newCard.name = "Mike Smith";
        198 |      newCard.$save();
        199 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
        200 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
        201 |      expect(newCard.id).toEqual(789);
        202 |  * 
        203 | * 204 | * The object returned from this function execution is a resource "class" which has "static" method 205 | * for each action in the definition. 206 | * 207 | * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and `headers`. 208 | * When the data is returned from the server then the object is an instance of the resource type and 209 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 210 | * operations (create, read, update, delete) on server-side data. 211 | 212 |
        213 |      var User = $resource('/user/:userId', {userId:'@id'});
        214 |      var user = User.get({userId:123}, function() {
        215 |        user.abc = true;
        216 |        user.$save();
        217 |      });
        218 |    
        219 | * 220 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 221 | * in the response that came from the server as well as $http header getter function, so one 222 | * could rewrite the above example and get access to http headers as: 223 | * 224 |
        225 |      var User = $resource('/user/:userId', {userId:'@id'});
        226 |      User.get({userId:123}, function(u, getResponseHeaders){
        227 |        u.abc = true;
        228 |        u.$save(function(u, putResponseHeaders) {
        229 |          //u => saved user object
        230 |          //putResponseHeaders => $http header getter
        231 |        });
        232 |      });
        233 |    
        234 | 235 | * # Buzz client 236 | 237 | Let's look at what a buzz client created with the `$resource` service looks like: 238 | 239 | 240 | 260 | 261 |
        262 | 263 | 264 |
        265 |
        266 |

        267 | 268 | {{item.actor.name}} 269 | Expand replies: {{item.links.replies[0].count}} 270 |

        271 | {{item.object.content | html}} 272 |
        273 | 274 | {{reply.actor.name}}: {{reply.content | html}} 275 |
        276 |
        277 |
        278 |
        279 | 280 | 281 |
        282 | */ 283 | angular.module('ngResource', ['ng']). 284 | factory('$resource', ['$http', '$parse', '$q', function($http, $parse, $q) { 285 | var DEFAULT_ACTIONS = { 286 | 'get': {method:'GET'}, 287 | 'save': {method:'POST'}, 288 | 'query': {method:'GET', isArray:true}, 289 | 'remove': {method:'DELETE'}, 290 | 'delete': {method:'DELETE'} 291 | }; 292 | var noop = angular.noop, 293 | forEach = angular.forEach, 294 | extend = angular.extend, 295 | copy = angular.copy, 296 | isFunction = angular.isFunction, 297 | getter = function(obj, path) { 298 | return $parse(path)(obj); 299 | }; 300 | 301 | /** 302 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 303 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 304 | * segments: 305 | * segment = *pchar 306 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 307 | * pct-encoded = "%" HEXDIG HEXDIG 308 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 309 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 310 | * / "*" / "+" / "," / ";" / "=" 311 | */ 312 | function encodeUriSegment(val) { 313 | return encodeUriQuery(val, true). 314 | replace(/%26/gi, '&'). 315 | replace(/%3D/gi, '='). 316 | replace(/%2B/gi, '+'); 317 | } 318 | 319 | 320 | /** 321 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 322 | * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be 323 | * encoded per http://tools.ietf.org/html/rfc3986: 324 | * query = *( pchar / "/" / "?" ) 325 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 326 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 327 | * pct-encoded = "%" HEXDIG HEXDIG 328 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 329 | * / "*" / "+" / "," / ";" / "=" 330 | */ 331 | function encodeUriQuery(val, pctEncodeSpaces) { 332 | return encodeURIComponent(val). 333 | replace(/%40/gi, '@'). 334 | replace(/%3A/gi, ':'). 335 | replace(/%24/g, '$'). 336 | replace(/%2C/gi, ','). 337 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 338 | } 339 | 340 | function Route(template, defaults) { 341 | this.template = template; 342 | this.defaults = defaults || {}; 343 | this.urlParams = {}; 344 | } 345 | 346 | Route.prototype = { 347 | setUrlParams: function(config, params, actionUrl) { 348 | var self = this, 349 | url = actionUrl || self.template, 350 | val, 351 | encodedVal; 352 | 353 | var urlParams = self.urlParams = {}; 354 | forEach(url.split(/\W/), function(param){ 355 | if (param === 'hasOwnProperty') { 356 | throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); 357 | } 358 | if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { 359 | urlParams[param] = true; 360 | } 361 | }); 362 | url = url.replace(/\\:/g, ':'); 363 | 364 | params = params || {}; 365 | forEach(self.urlParams, function(_, urlParam){ 366 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 367 | if (angular.isDefined(val) && val !== null) { 368 | encodedVal = encodeUriSegment(val); 369 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), encodedVal + "$1"); 370 | } else { 371 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, 372 | leadingSlashes, tail) { 373 | if (tail.charAt(0) == '/') { 374 | return tail; 375 | } else { 376 | return leadingSlashes + tail; 377 | } 378 | }); 379 | } 380 | }); 381 | 382 | // strip trailing slashes and set the url 383 | url = url.replace(/\/+$/, ''); 384 | // then replace collapse `/.` if found in the last URL path segment before the query 385 | // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` 386 | url = url.replace(/\/\.(?=\w+($|\?))/, '.'); 387 | // replace escaped `/\.` with `/.` 388 | config.url = url.replace(/\/\\\./, '/.'); 389 | 390 | 391 | // set params - delegate param encoding to $http 392 | forEach(params, function(value, key){ 393 | if (!self.urlParams[key]) { 394 | config.params = config.params || {}; 395 | config.params[key] = value; 396 | } 397 | }); 398 | } 399 | }; 400 | 401 | 402 | function ResourceFactory(url, paramDefaults, actions) { 403 | var route = new Route(url); 404 | 405 | actions = extend({}, DEFAULT_ACTIONS, actions); 406 | 407 | function extractParams(data, actionParams){ 408 | var ids = {}; 409 | actionParams = extend({}, paramDefaults, actionParams); 410 | forEach(actionParams, function(value, key){ 411 | if (isFunction(value)) { value = value(); } 412 | ids[key] = value && value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 413 | }); 414 | return ids; 415 | } 416 | 417 | function defaultResponseInterceptor(response) { 418 | return response.resource; 419 | } 420 | 421 | function Resource(value){ 422 | copy(value || {}, this); 423 | } 424 | 425 | forEach(actions, function(action, name) { 426 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 427 | 428 | Resource[name] = function(a1, a2, a3, a4) { 429 | var params = {}, data, success, error; 430 | 431 | switch(arguments.length) { 432 | case 4: 433 | error = a4; 434 | success = a3; 435 | //fallthrough 436 | case 3: 437 | case 2: 438 | if (isFunction(a2)) { 439 | if (isFunction(a1)) { 440 | success = a1; 441 | error = a2; 442 | break; 443 | } 444 | 445 | success = a2; 446 | error = a3; 447 | //fallthrough 448 | } else { 449 | params = a1; 450 | data = a2; 451 | success = a3; 452 | break; 453 | } 454 | case 1: 455 | if (isFunction(a1)) success = a1; 456 | else if (hasBody) data = a1; 457 | else params = a1; 458 | break; 459 | case 0: break; 460 | default: 461 | throw $resourceMinErr('badargs', 462 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments", arguments.length); 463 | } 464 | 465 | var isInstanceCall = data instanceof Resource; 466 | var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); 467 | var httpConfig = {}; 468 | var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; 469 | var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || undefined; 470 | 471 | forEach(action, function(value, key) { 472 | if (key != 'params' && key != 'isArray' && key != 'interceptor') { 473 | httpConfig[key] = copy(value); 474 | } 475 | }); 476 | 477 | if (hasBody) httpConfig.data = data; 478 | route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url); 479 | 480 | var promise = $http(httpConfig).then(function(response) { 481 | var data = response.data, 482 | promise = value.$promise; 483 | 484 | if (data) { 485 | if ( angular.isArray(data) != !!action.isArray ) { 486 | throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected response' + 487 | ' to contain an {0} but got an {1}', 488 | action.isArray?'array':'object', angular.isArray(data)?'array':'object'); 489 | } 490 | if (action.isArray) { 491 | value.length = 0; 492 | forEach(data, function(item) { 493 | value.push(new Resource(item)); 494 | }); 495 | } else { 496 | copy(data, value); 497 | value.$promise = promise; 498 | } 499 | } 500 | 501 | value.$resolved = true; 502 | 503 | response.resource = value; 504 | 505 | return response; 506 | }, function(response) { 507 | value.$resolved = true; 508 | 509 | (error||noop)(response); 510 | 511 | return $q.reject(response); 512 | }); 513 | 514 | promise = promise.then( 515 | function(response) { 516 | var value = responseInterceptor(response); 517 | (success||noop)(value, response.headers); 518 | return value; 519 | }, 520 | responseErrorInterceptor); 521 | 522 | if (!isInstanceCall) { 523 | // we are creating instance / collection 524 | // - set the initial promise 525 | // - return the instance / collection 526 | value.$promise = promise; 527 | value.$resolved = false; 528 | 529 | return value; 530 | } 531 | 532 | // instance call 533 | return promise; 534 | }; 535 | 536 | 537 | Resource.prototype['$' + name] = function(params, success, error) { 538 | if (isFunction(params)) { 539 | error = success; success = params; params = {}; 540 | } 541 | var result = Resource[name](params, this, success, error); 542 | return result.$promise || result; 543 | }; 544 | }); 545 | 546 | Resource.bind = function(additionalParamDefaults){ 547 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 548 | }; 549 | 550 | return Resource; 551 | } 552 | 553 | return ResourceFactory; 554 | }]); 555 | 556 | 557 | })(window, window.angular); 558 | -------------------------------------------------------------------------------- /app/lib/angular/angular-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.3 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc overview 10 | * @name ngRoute 11 | * @description 12 | * 13 | * # ngRoute 14 | * 15 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. 16 | * 17 | * {@installModule route} 18 | * 19 | */ 20 | 21 | var ngRouteModule = angular.module('ngRoute', ['ng']). 22 | provider('$route', $RouteProvider); 23 | 24 | /** 25 | * @ngdoc object 26 | * @name ngRoute.$routeProvider 27 | * @function 28 | * 29 | * @description 30 | * 31 | * Used for configuring routes. See {@link ngRoute.$route $route} for an example. 32 | * 33 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 34 | */ 35 | function $RouteProvider(){ 36 | function inherit(parent, extra) { 37 | return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); 38 | } 39 | 40 | var routes = {}; 41 | 42 | /** 43 | * @ngdoc method 44 | * @name ngRoute.$routeProvider#when 45 | * @methodOf ngRoute.$routeProvider 46 | * 47 | * @param {string} path Route path (matched against `$location.path`). If `$location.path` 48 | * contains redundant trailing slash or is missing one, the route will still match and the 49 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the 50 | * route definition. 51 | * 52 | * * `path` can contain named groups starting with a colon (`:name`). All characters up 53 | * to the next slash are matched and stored in `$routeParams` under the given `name` 54 | * when the route matches. 55 | * * `path` can contain named groups starting with a colon and ending with a star (`:name*`). 56 | * All characters are eagerly stored in `$routeParams` under the given `name` 57 | * when the route matches. 58 | * * `path` can contain optional named groups with a question mark (`:name?`). 59 | * 60 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match 61 | * `/color/brown/largecode/code/with/slashs/edit` and extract: 62 | * 63 | * * `color: brown` 64 | * * `largecode: code/with/slashs`. 65 | * 66 | * 67 | * @param {Object} route Mapping information to be assigned to `$route.current` on route 68 | * match. 69 | * 70 | * Object properties: 71 | * 72 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly 73 | * created scope or the name of a {@link angular.Module#controller registered controller} 74 | * if passed as a string. 75 | * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be 76 | * published to scope under the `controllerAs` name. 77 | * - `template` – `{string=|function()=}` – html template as a string or a function that 78 | * returns an html template as a string which should be used by {@link 79 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. 80 | * This property takes precedence over `templateUrl`. 81 | * 82 | * If `template` is a function, it will be called with the following parameters: 83 | * 84 | * - `{Array.}` - route parameters extracted from the current 85 | * `$location.path()` by applying the current route 86 | * 87 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html 88 | * template that should be used by {@link ngRoute.directive:ngView ngView}. 89 | * 90 | * If `templateUrl` is a function, it will be called with the following parameters: 91 | * 92 | * - `{Array.}` - route parameters extracted from the current 93 | * `$location.path()` by applying the current route 94 | * 95 | * - `resolve` - `{Object.=}` - An optional map of dependencies which should 96 | * be injected into the controller. If any of these dependencies are promises, they will be 97 | * resolved and converted to a value before the controller is instantiated and the 98 | * `$routeChangeSuccess` event is fired. The map object is: 99 | * 100 | * - `key` – `{string}`: a name of a dependency to be injected into the controller. 101 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service. 102 | * Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected} 103 | * and the return value is treated as the dependency. If the result is a promise, it is resolved 104 | * before its value is injected into the controller. Be aware that `ngRoute.$routeParams` will 105 | * still refer to the previous route within these resolve functions. Use `$route.current.params` 106 | * to access the new route parameters, instead. 107 | * 108 | * - `redirectTo` – {(string|function())=} – value to update 109 | * {@link ng.$location $location} path with and trigger route redirection. 110 | * 111 | * If `redirectTo` is a function, it will be called with the following parameters: 112 | * 113 | * - `{Object.}` - route parameters extracted from the current 114 | * `$location.path()` by applying the current route templateUrl. 115 | * - `{string}` - current `$location.path()` 116 | * - `{Object}` - current `$location.search()` 117 | * 118 | * The custom `redirectTo` function is expected to return a string which will be used 119 | * to update `$location.path()` and `$location.search()`. 120 | * 121 | * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` 122 | * or `$location.hash()` changes. 123 | * 124 | * If the option is set to `false` and url in the browser changes, then 125 | * `$routeUpdate` event is broadcasted on the root scope. 126 | * 127 | * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive 128 | * 129 | * If the option is set to `true`, then the particular route can be matched without being 130 | * case sensitive 131 | * 132 | * @returns {Object} self 133 | * 134 | * @description 135 | * Adds a new route definition to the `$route` service. 136 | */ 137 | this.when = function(path, route) { 138 | routes[path] = angular.extend( 139 | {reloadOnSearch: true}, 140 | route, 141 | path && pathRegExp(path, route) 142 | ); 143 | 144 | // create redirection for trailing slashes 145 | if (path) { 146 | var redirectPath = (path[path.length-1] == '/') 147 | ? path.substr(0, path.length-1) 148 | : path +'/'; 149 | 150 | routes[redirectPath] = angular.extend( 151 | {redirectTo: path}, 152 | pathRegExp(redirectPath, route) 153 | ); 154 | } 155 | 156 | return this; 157 | }; 158 | 159 | /** 160 | * @param path {string} path 161 | * @param opts {Object} options 162 | * @return {?Object} 163 | * 164 | * @description 165 | * Normalizes the given path, returning a regular expression 166 | * and the original path. 167 | * 168 | * Inspired by pathRexp in visionmedia/express/lib/utils.js. 169 | */ 170 | function pathRegExp(path, opts) { 171 | var insensitive = opts.caseInsensitiveMatch, 172 | ret = { 173 | originalPath: path, 174 | regexp: path 175 | }, 176 | keys = ret.keys = []; 177 | 178 | path = path 179 | .replace(/([().])/g, '\\$1') 180 | .replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){ 181 | var optional = option === '?' ? option : null; 182 | var star = option === '*' ? option : null; 183 | keys.push({ name: key, optional: !!optional }); 184 | slash = slash || ''; 185 | return '' 186 | + (optional ? '' : slash) 187 | + '(?:' 188 | + (optional ? slash : '') 189 | + (star && '(.+?)' || '([^/]+)') 190 | + (optional || '') 191 | + ')' 192 | + (optional || ''); 193 | }) 194 | .replace(/([\/$\*])/g, '\\$1'); 195 | 196 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); 197 | return ret; 198 | } 199 | 200 | /** 201 | * @ngdoc method 202 | * @name ngRoute.$routeProvider#otherwise 203 | * @methodOf ngRoute.$routeProvider 204 | * 205 | * @description 206 | * Sets route definition that will be used on route change when no other route definition 207 | * is matched. 208 | * 209 | * @param {Object} params Mapping information to be assigned to `$route.current`. 210 | * @returns {Object} self 211 | */ 212 | this.otherwise = function(params) { 213 | this.when(null, params); 214 | return this; 215 | }; 216 | 217 | 218 | this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', '$sce', 219 | function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { 220 | 221 | /** 222 | * @ngdoc object 223 | * @name ngRoute.$route 224 | * @requires $location 225 | * @requires $routeParams 226 | * 227 | * @property {Object} current Reference to the current route definition. 228 | * The route definition contains: 229 | * 230 | * - `controller`: The controller constructor as define in route definition. 231 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for 232 | * controller instantiation. The `locals` contain 233 | * the resolved values of the `resolve` map. Additionally the `locals` also contain: 234 | * 235 | * - `$scope` - The current route scope. 236 | * - `$template` - The current route template HTML. 237 | * 238 | * @property {Array.} routes Array of all configured routes. 239 | * 240 | * @description 241 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials). 242 | * It watches `$location.url()` and tries to map the path to an existing route definition. 243 | * 244 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 245 | * 246 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. 247 | * 248 | * The `$route` service is typically used in conjunction with the {@link ngRoute.directive:ngView `ngView`} 249 | * directive and the {@link ngRoute.$routeParams `$routeParams`} service. 250 | * 251 | * @example 252 | This example shows how changing the URL hash causes the `$route` to match a route against the 253 | URL, and the `ngView` pulls in the partial. 254 | 255 | Note that this example is using {@link ng.directive:script inlined templates} 256 | to get it working on jsfiddle as well. 257 | 258 | 259 | 260 |
        261 | Choose: 262 | Moby | 263 | Moby: Ch1 | 264 | Gatsby | 265 | Gatsby: Ch4 | 266 | Scarlet Letter
        267 | 268 |
        269 |
        270 | 271 |
        $location.path() = {{$location.path()}}
        272 |
        $route.current.templateUrl = {{$route.current.templateUrl}}
        273 |
        $route.current.params = {{$route.current.params}}
        274 |
        $route.current.scope.name = {{$route.current.scope.name}}
        275 |
        $routeParams = {{$routeParams}}
        276 |
        277 |
        278 | 279 | 280 | controller: {{name}}
        281 | Book Id: {{params.bookId}}
        282 |
        283 | 284 | 285 | controller: {{name}}
        286 | Book Id: {{params.bookId}}
        287 | Chapter Id: {{params.chapterId}} 288 |
        289 | 290 | 291 | angular.module('ngView', ['ngRoute']).config(function($routeProvider, $locationProvider) { 292 | $routeProvider.when('/Book/:bookId', { 293 | templateUrl: 'book.html', 294 | controller: BookCntl, 295 | resolve: { 296 | // I will cause a 1 second delay 297 | delay: function($q, $timeout) { 298 | var delay = $q.defer(); 299 | $timeout(delay.resolve, 1000); 300 | return delay.promise; 301 | } 302 | } 303 | }); 304 | $routeProvider.when('/Book/:bookId/ch/:chapterId', { 305 | templateUrl: 'chapter.html', 306 | controller: ChapterCntl 307 | }); 308 | 309 | // configure html5 to get links working on jsfiddle 310 | $locationProvider.html5Mode(true); 311 | }); 312 | 313 | function MainCntl($scope, $route, $routeParams, $location) { 314 | $scope.$route = $route; 315 | $scope.$location = $location; 316 | $scope.$routeParams = $routeParams; 317 | } 318 | 319 | function BookCntl($scope, $routeParams) { 320 | $scope.name = "BookCntl"; 321 | $scope.params = $routeParams; 322 | } 323 | 324 | function ChapterCntl($scope, $routeParams) { 325 | $scope.name = "ChapterCntl"; 326 | $scope.params = $routeParams; 327 | } 328 | 329 | 330 | 331 | it('should load and compile correct template', function() { 332 | element('a:contains("Moby: Ch1")').click(); 333 | var content = element('.doc-example-live [ng-view]').text(); 334 | expect(content).toMatch(/controller\: ChapterCntl/); 335 | expect(content).toMatch(/Book Id\: Moby/); 336 | expect(content).toMatch(/Chapter Id\: 1/); 337 | 338 | element('a:contains("Scarlet")').click(); 339 | sleep(2); // promises are not part of scenario waiting 340 | content = element('.doc-example-live [ng-view]').text(); 341 | expect(content).toMatch(/controller\: BookCntl/); 342 | expect(content).toMatch(/Book Id\: Scarlet/); 343 | }); 344 | 345 |
        346 | */ 347 | 348 | /** 349 | * @ngdoc event 350 | * @name ngRoute.$route#$routeChangeStart 351 | * @eventOf ngRoute.$route 352 | * @eventType broadcast on root scope 353 | * @description 354 | * Broadcasted before a route change. At this point the route services starts 355 | * resolving all of the dependencies needed for the route change to occurs. 356 | * Typically this involves fetching the view template as well as any dependencies 357 | * defined in `resolve` route property. Once all of the dependencies are resolved 358 | * `$routeChangeSuccess` is fired. 359 | * 360 | * @param {Object} angularEvent Synthetic event object. 361 | * @param {Route} next Future route information. 362 | * @param {Route} current Current route information. 363 | */ 364 | 365 | /** 366 | * @ngdoc event 367 | * @name ngRoute.$route#$routeChangeSuccess 368 | * @eventOf ngRoute.$route 369 | * @eventType broadcast on root scope 370 | * @description 371 | * Broadcasted after a route dependencies are resolved. 372 | * {@link ngRoute.directive:ngView ngView} listens for the directive 373 | * to instantiate the controller and render the view. 374 | * 375 | * @param {Object} angularEvent Synthetic event object. 376 | * @param {Route} current Current route information. 377 | * @param {Route|Undefined} previous Previous route information, or undefined if current is first route entered. 378 | */ 379 | 380 | /** 381 | * @ngdoc event 382 | * @name ngRoute.$route#$routeChangeError 383 | * @eventOf ngRoute.$route 384 | * @eventType broadcast on root scope 385 | * @description 386 | * Broadcasted if any of the resolve promises are rejected. 387 | * 388 | * @param {Object} angularEvent Synthetic event object 389 | * @param {Route} current Current route information. 390 | * @param {Route} previous Previous route information. 391 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. 392 | */ 393 | 394 | /** 395 | * @ngdoc event 396 | * @name ngRoute.$route#$routeUpdate 397 | * @eventOf ngRoute.$route 398 | * @eventType broadcast on root scope 399 | * @description 400 | * 401 | * The `reloadOnSearch` property has been set to false, and we are reusing the same 402 | * instance of the Controller. 403 | */ 404 | 405 | var forceReload = false, 406 | $route = { 407 | routes: routes, 408 | 409 | /** 410 | * @ngdoc method 411 | * @name ngRoute.$route#reload 412 | * @methodOf ngRoute.$route 413 | * 414 | * @description 415 | * Causes `$route` service to reload the current route even if 416 | * {@link ng.$location $location} hasn't changed. 417 | * 418 | * As a result of that, {@link ngRoute.directive:ngView ngView} 419 | * creates new scope, reinstantiates the controller. 420 | */ 421 | reload: function() { 422 | forceReload = true; 423 | $rootScope.$evalAsync(updateRoute); 424 | } 425 | }; 426 | 427 | $rootScope.$on('$locationChangeSuccess', updateRoute); 428 | 429 | return $route; 430 | 431 | ///////////////////////////////////////////////////// 432 | 433 | /** 434 | * @param on {string} current url 435 | * @param route {Object} route regexp to match the url against 436 | * @return {?Object} 437 | * 438 | * @description 439 | * Check if the route matches the current url. 440 | * 441 | * Inspired by match in 442 | * visionmedia/express/lib/router/router.js. 443 | */ 444 | function switchRouteMatcher(on, route) { 445 | var keys = route.keys, 446 | params = {}; 447 | 448 | if (!route.regexp) return null; 449 | 450 | var m = route.regexp.exec(on); 451 | if (!m) return null; 452 | 453 | for (var i = 1, len = m.length; i < len; ++i) { 454 | var key = keys[i - 1]; 455 | 456 | var val = 'string' == typeof m[i] 457 | ? decodeURIComponent(m[i]) 458 | : m[i]; 459 | 460 | if (key && val) { 461 | params[key.name] = val; 462 | } 463 | } 464 | return params; 465 | } 466 | 467 | function updateRoute() { 468 | var next = parseRoute(), 469 | last = $route.current; 470 | 471 | if (next && last && next.$$route === last.$$route 472 | && angular.equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { 473 | last.params = next.params; 474 | angular.copy(last.params, $routeParams); 475 | $rootScope.$broadcast('$routeUpdate', last); 476 | } else if (next || last) { 477 | forceReload = false; 478 | $rootScope.$broadcast('$routeChangeStart', next, last); 479 | $route.current = next; 480 | if (next) { 481 | if (next.redirectTo) { 482 | if (angular.isString(next.redirectTo)) { 483 | $location.path(interpolate(next.redirectTo, next.params)).search(next.params) 484 | .replace(); 485 | } else { 486 | $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) 487 | .replace(); 488 | } 489 | } 490 | } 491 | 492 | $q.when(next). 493 | then(function() { 494 | if (next) { 495 | var locals = angular.extend({}, next.resolve), 496 | template, templateUrl; 497 | 498 | angular.forEach(locals, function(value, key) { 499 | locals[key] = angular.isString(value) ? $injector.get(value) : $injector.invoke(value); 500 | }); 501 | 502 | if (angular.isDefined(template = next.template)) { 503 | if (angular.isFunction(template)) { 504 | template = template(next.params); 505 | } 506 | } else if (angular.isDefined(templateUrl = next.templateUrl)) { 507 | if (angular.isFunction(templateUrl)) { 508 | templateUrl = templateUrl(next.params); 509 | } 510 | templateUrl = $sce.getTrustedResourceUrl(templateUrl); 511 | if (angular.isDefined(templateUrl)) { 512 | next.loadedTemplateUrl = templateUrl; 513 | template = $http.get(templateUrl, {cache: $templateCache}). 514 | then(function(response) { return response.data; }); 515 | } 516 | } 517 | if (angular.isDefined(template)) { 518 | locals['$template'] = template; 519 | } 520 | return $q.all(locals); 521 | } 522 | }). 523 | // after route change 524 | then(function(locals) { 525 | if (next == $route.current) { 526 | if (next) { 527 | next.locals = locals; 528 | angular.copy(next.params, $routeParams); 529 | } 530 | $rootScope.$broadcast('$routeChangeSuccess', next, last); 531 | } 532 | }, function(error) { 533 | if (next == $route.current) { 534 | $rootScope.$broadcast('$routeChangeError', next, last, error); 535 | } 536 | }); 537 | } 538 | } 539 | 540 | 541 | /** 542 | * @returns the current active route, by matching it against the URL 543 | */ 544 | function parseRoute() { 545 | // Match a route 546 | var params, match; 547 | angular.forEach(routes, function(route, path) { 548 | if (!match && (params = switchRouteMatcher($location.path(), route))) { 549 | match = inherit(route, { 550 | params: angular.extend({}, $location.search(), params), 551 | pathParams: params}); 552 | match.$$route = route; 553 | } 554 | }); 555 | // No route matched; fallback to "otherwise" route 556 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); 557 | } 558 | 559 | /** 560 | * @returns interpolation of the redirect path with the parameters 561 | */ 562 | function interpolate(string, params) { 563 | var result = []; 564 | angular.forEach((string||'').split(':'), function(segment, i) { 565 | if (i === 0) { 566 | result.push(segment); 567 | } else { 568 | var segmentMatch = segment.match(/(\w+)(.*)/); 569 | var key = segmentMatch[1]; 570 | result.push(params[key]); 571 | result.push(segmentMatch[2] || ''); 572 | delete params[key]; 573 | } 574 | }); 575 | return result.join(''); 576 | } 577 | }]; 578 | } 579 | 580 | ngRouteModule.provider('$routeParams', $RouteParamsProvider); 581 | 582 | 583 | /** 584 | * @ngdoc object 585 | * @name ngRoute.$routeParams 586 | * @requires $route 587 | * 588 | * @description 589 | * The `$routeParams` service allows you to retrieve the current set of route parameters. 590 | * 591 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 592 | * 593 | * The route parameters are a combination of {@link ng.$location `$location`}'s 594 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. 595 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. 596 | * 597 | * In case of parameter name collision, `path` params take precedence over `search` params. 598 | * 599 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged 600 | * (but its properties will likely change) even when a route change occurs. 601 | * 602 | * Note that the `$routeParams` are only updated *after* a route change completes successfully. 603 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions. 604 | * Instead you can use `$route.current.params` to access the new route's parameters. 605 | * 606 | * @example 607 | *
        608 |  *  // Given:
        609 |  *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
        610 |  *  // Route: /Chapter/:chapterId/Section/:sectionId
        611 |  *  //
        612 |  *  // Then
        613 |  *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
        614 |  * 
        615 | */ 616 | function $RouteParamsProvider() { 617 | this.$get = function() { return {}; }; 618 | } 619 | 620 | ngRouteModule.directive('ngView', ngViewFactory); 621 | 622 | /** 623 | * @ngdoc directive 624 | * @name ngRoute.directive:ngView 625 | * @restrict ECA 626 | * 627 | * @description 628 | * # Overview 629 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by 630 | * including the rendered template of the current route into the main layout (`index.html`) file. 631 | * Every time the current route changes, the included view changes with it according to the 632 | * configuration of the `$route` service. 633 | * 634 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 635 | * 636 | * @animations 637 | * enter - animation is used to bring new content into the browser. 638 | * leave - animation is used to animate existing content away. 639 | * 640 | * The enter and leave animation occur concurrently. 641 | * 642 | * @scope 643 | * @priority 400 644 | * @example 645 | 646 | 647 |
        648 | Choose: 649 | Moby | 650 | Moby: Ch1 | 651 | Gatsby | 652 | Gatsby: Ch4 | 653 | Scarlet Letter
        654 | 655 |
        656 |
        657 |
        658 |
        659 | 660 |
        $location.path() = {{main.$location.path()}}
        661 |
        $route.current.templateUrl = {{main.$route.current.templateUrl}}
        662 |
        $route.current.params = {{main.$route.current.params}}
        663 |
        $route.current.scope.name = {{main.$route.current.scope.name}}
        664 |
        $routeParams = {{main.$routeParams}}
        665 |
        666 |
        667 | 668 | 669 |
        670 | controller: {{book.name}}
        671 | Book Id: {{book.params.bookId}}
        672 |
        673 |
        674 | 675 | 676 |
        677 | controller: {{chapter.name}}
        678 | Book Id: {{chapter.params.bookId}}
        679 | Chapter Id: {{chapter.params.chapterId}} 680 |
        681 |
        682 | 683 | 684 | .example-animate-container { 685 | position:relative; 686 | background:white; 687 | border:1px solid black; 688 | height:40px; 689 | overflow:hidden; 690 | } 691 | 692 | .example-animate-container > div { 693 | padding:10px; 694 | } 695 | 696 | .view-example.ng-enter, .view-example.ng-leave { 697 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 698 | -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 699 | -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 700 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 701 | 702 | display:block; 703 | width:100%; 704 | border-left:1px solid black; 705 | 706 | position:absolute; 707 | top:0; 708 | left:0; 709 | right:0; 710 | bottom:0; 711 | padding:10px; 712 | } 713 | 714 | .example-animate-container { 715 | position:relative; 716 | height:100px; 717 | } 718 | 719 | .view-example.ng-enter { 720 | left:100%; 721 | } 722 | .view-example.ng-enter.ng-enter-active { 723 | left:0; 724 | } 725 | 726 | .view-example.ng-leave { } 727 | .view-example.ng-leave.ng-leave-active { 728 | left:-100%; 729 | } 730 | 731 | 732 | 733 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate'], function($routeProvider, $locationProvider) { 734 | $routeProvider.when('/Book/:bookId', { 735 | templateUrl: 'book.html', 736 | controller: BookCntl, 737 | controllerAs: 'book' 738 | }); 739 | $routeProvider.when('/Book/:bookId/ch/:chapterId', { 740 | templateUrl: 'chapter.html', 741 | controller: ChapterCntl, 742 | controllerAs: 'chapter' 743 | }); 744 | 745 | // configure html5 to get links working on jsfiddle 746 | $locationProvider.html5Mode(true); 747 | }); 748 | 749 | function MainCntl($route, $routeParams, $location) { 750 | this.$route = $route; 751 | this.$location = $location; 752 | this.$routeParams = $routeParams; 753 | } 754 | 755 | function BookCntl($routeParams) { 756 | this.name = "BookCntl"; 757 | this.params = $routeParams; 758 | } 759 | 760 | function ChapterCntl($routeParams) { 761 | this.name = "ChapterCntl"; 762 | this.params = $routeParams; 763 | } 764 | 765 | 766 | 767 | it('should load and compile correct template', function() { 768 | element('a:contains("Moby: Ch1")').click(); 769 | var content = element('.doc-example-live [ng-view]').text(); 770 | expect(content).toMatch(/controller\: ChapterCntl/); 771 | expect(content).toMatch(/Book Id\: Moby/); 772 | expect(content).toMatch(/Chapter Id\: 1/); 773 | 774 | element('a:contains("Scarlet")').click(); 775 | content = element('.doc-example-live [ng-view]').text(); 776 | expect(content).toMatch(/controller\: BookCntl/); 777 | expect(content).toMatch(/Book Id\: Scarlet/); 778 | }); 779 | 780 |
        781 | */ 782 | 783 | 784 | /** 785 | * @ngdoc event 786 | * @name ngRoute.directive:ngView#$viewContentLoaded 787 | * @eventOf ngRoute.directive:ngView 788 | * @eventType emit on the current ngView scope 789 | * @description 790 | * Emitted every time the ngView content is reloaded. 791 | */ 792 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animate']; 793 | function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animate) { 794 | return { 795 | restrict: 'ECA', 796 | terminal: true, 797 | priority: 400, 798 | transclude: 'element', 799 | compile: function(element, attr, linker) { 800 | return function(scope, $element, attr) { 801 | var currentScope, 802 | currentElement, 803 | onloadExp = attr.onload || ''; 804 | 805 | scope.$on('$routeChangeSuccess', update); 806 | update(); 807 | 808 | function cleanupLastView() { 809 | if (currentScope) { 810 | currentScope.$destroy(); 811 | currentScope = null; 812 | } 813 | if(currentElement) { 814 | $animate.leave(currentElement); 815 | currentElement = null; 816 | } 817 | } 818 | 819 | function update() { 820 | var locals = $route.current && $route.current.locals, 821 | template = locals && locals.$template; 822 | 823 | if (template) { 824 | var newScope = scope.$new(); 825 | linker(newScope, function(clone) { 826 | cleanupLastView(); 827 | 828 | clone.html(template); 829 | $animate.enter(clone, null, $element); 830 | 831 | var link = $compile(clone.contents()), 832 | current = $route.current; 833 | 834 | currentScope = current.scope = newScope; 835 | currentElement = clone; 836 | 837 | if (current.controller) { 838 | locals.$scope = currentScope; 839 | var controller = $controller(current.controller, locals); 840 | if (current.controllerAs) { 841 | currentScope[current.controllerAs] = controller; 842 | } 843 | clone.data('$ngControllerController', controller); 844 | clone.children().data('$ngControllerController', controller); 845 | } 846 | 847 | link(currentScope); 848 | currentScope.$emit('$viewContentLoaded'); 849 | currentScope.$eval(onloadExp); 850 | 851 | // $anchorScroll might listen on event... 852 | $anchorScroll(); 853 | }); 854 | } else { 855 | cleanupLastView(); 856 | } 857 | } 858 | } 859 | } 860 | }; 861 | } 862 | 863 | 864 | })(window, window.angular); 865 | --------------------------------------------------------------------------------