├── GruntFile.js ├── README.md ├── components └── angular │ ├── angular.js │ ├── angular.min.js │ └── bower.json ├── demo-app ├── index.html ├── main.js └── mainStyle.css ├── flexy-layout.debug.js ├── flexy-layout.min.js ├── package.json ├── src ├── Block.js ├── Directives.js ├── MediatorController.js └── flexyLayout.css ├── test ├── config │ ├── karma-e2e.conf.js │ └── karma.conf.js ├── e2e │ ├── runner.html │ └── scenarios.js ├── lib │ └── angular │ │ ├── angular-mocks.js │ │ └── angular-scenario.js └── unit │ └── BlockSpec.js └── web-server.js /GruntFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | src: { 7 | js: ['src/*.js'] 8 | }, 9 | concat: { 10 | options: { 11 | }, 12 | dist: { 13 | src: ['<%= src.js %>'], 14 | dest: './<%= pkg.name %>.debug.js' 15 | } 16 | }, 17 | uglify: { 18 | main: { 19 | src: ['<%= pkg.name %>.debug.js'], 20 | dest: '<%= pkg.name %>.min.js' 21 | } 22 | } 23 | }); 24 | 25 | // Load the plugin that provides the "uglify" task. 26 | grunt.loadNpmTasks('grunt-contrib-concat'); 27 | grunt.loadNpmTasks('grunt-contrib-uglify'); 28 | }; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flexy-layout 2 | ============ 3 | 4 | An angular module to set up flexible layout. 5 | See [demo website](http://lorenzofox3.github.io/flexy-layout/src/index.html) for mor information 6 | 7 | ## License 8 | 9 | flexy-layout module is under MIT license: 10 | 11 | > Copyright (C) 2013 Laurent Renard. 12 | > 13 | > Permission is hereby granted, free of charge, to any person 14 | > obtaining a copy of this software and associated documentation files 15 | > (the "Software"), to deal in the Software without restriction, 16 | > including without limitation the rights to use, copy, modify, merge, 17 | > publish, distribute, sublicense, and/or sell copies of the Software, 18 | > and to permit persons to whom the Software is furnished to do so, 19 | > subject to the following conditions: 20 | > 21 | > The above copyright notice and this permission notice shall be 22 | > included in all copies or substantial portions of the Software. 23 | > 24 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 27 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 28 | > BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 29 | > ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 30 | > CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | > SOFTWARE. 32 | -------------------------------------------------------------------------------- /components/angular/angular.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.7 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(P,T,q){'use strict';function m(b,a,c){var d;if(b)if(H(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(!b||typeof b.length!=="number"?0:typeof b.hasOwnProperty!="function"&&typeof b.constructor!="function"||b instanceof K||ca&&b instanceof ca||wa.call(b)!=="[object Object]"||typeof b.callee==="function")for(d=0;d=0&&b.splice(c,1);return a}function U(b,a){if(oa(b)||b&&b.$evalAsync&&b.$watch)throw Error("Can't copy Window or Scope");if(a){if(b===a)throw Error("Can't copy equivalent objects or arrays");if(E(b))for(var c=a.length=0;c2?ha.call(arguments,2):[];return H(a)&&!(a instanceof RegExp)?c.length?function(){return arguments.length?a.apply(b,c.concat(ha.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function ic(b,a){var c=a;/^\$+/.test(b)?c=q:oa(a)?c="$WINDOW":a&&T===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function da(b,a){return JSON.stringify(b, 13 | ic,a?" ":null)}function pb(b){return B(b)?JSON.parse(b):b}function Ua(b){b&&b.length!==0?(b=z(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1;return b}function pa(b){b=u(b).clone();try{b.html("")}catch(a){}var c=u("
").append(b).html();try{return b[0].nodeType===3?z(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+z(b)})}catch(d){return z(c)}}function Va(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]), 14 | a[d]=y(c[1])?decodeURIComponent(c[1]):!0)});return a}function qb(b){var a=[];m(b,function(b,d){a.push(Wa(d,!0)+(b===!0?"":"="+Wa(b,!0)))});return a.length?a.join("&"):""}function Xa(b){return Wa(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Wa(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function jc(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,h=["ng:app","ng-app","x-ng-app", 15 | "data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(h,function(a){h[a]=!0;c(T.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&h[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function rb(b,a){var c=function(){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement", 16 | b)}]);a.unshift("ng");var c=sb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},d=/^NG_DEFER_BOOTSTRAP!/;if(P&&!d.test(P.name))return c();P.name=P.name.replace(d,"");Ya.resumeBootstrap=function(b){m(b,function(b){a.push(b)});c()}}function Za(b,a){a=a||"_";return b.replace(kc,function(b,d){return(d?a:"")+b.toLowerCase()})}function $a(b,a,c){if(!b)throw Error("Argument '"+(a||"?")+"' is "+(c||"required")); 17 | return b}function qa(b,a,c){c&&E(b)&&(b=b[b.length-1]);$a(H(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function lc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c,d,e){return function(){b[e||"push"]([c,d,arguments]);return k}}if(!e)throw Error("No module: "+d);var b=[],c=[],j=a("$injector", 18 | "invoke"),k={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:j,run:function(a){c.push(a);return this}};g&&j(g);return k})}})}function tb(b){return b.replace(mc,function(a,b,d,e){return e?d.toUpperCase(): 19 | d}).replace(nc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,h,f,i,j,k,l;b.length;){h=b.shift();f=0;for(i=h.length;f-1}function xb(b,a){a&&m(a.split(" "),function(a){b.className=Q((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+Q(a)+" "," "))})} 22 | function yb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=Q(b.className+" "+Q(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&y(a.length)&&!oa(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!")}else{if(i.cookie!==$){$=i.cookie;d=$.split("; ");r={};for(f=0;f0&&(a=unescape(e.substring(0,j)),r[a]===q&&(r[a]=unescape(e.substring(j+1))))}return r}};f.defer=function(a,b){var c; 34 | p++;c=l(function(){delete o[c];e(a)},b||0);o[c]=!0;return c};f.defer.cancel=function(a){return o[a]?(delete o[a],n(a),e(C),!0):!1}}function wc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new vc(b,d,a,c)}]}function xc(){this.$get=function(){function b(b,d){function e(a){if(a!=l){if(n){if(n==a)n=a.n}else n=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw Error("cacheId "+b+" taken");var h=0,f=v({},d,{id:b}),i={},j=d&& 35 | d.capacity||Number.MAX_VALUE,k={},l=null,n=null;return a[b]={put:function(a,b){var c=k[a]||(k[a]={key:a});e(c);w(b)||(a in i||h++,i[a]=b,h>j&&this.remove(n.key))},get:function(a){var b=k[a];if(b)return e(b),i[a]},remove:function(a){var b=k[a];if(b){if(b==l)l=b.p;if(b==n)n=b.n;g(b.n,b.p);delete k[a];delete i[a];h--}},removeAll:function(){i={};h=0;k={};l=n=null},destroy:function(){k=f=i=null;delete a[b]},info:function(){return v({},f,{size:h})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]= 36 | a.info()});return b};b.get=function(b){return a[b]};return b}}function yc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Db(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: ",h=/^\s*(https?|ftp|mailto|file):/;this.directive=function i(d,e){B(d)?($a(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d], 37 | function(a){try{var g=b.invoke(a);if(H(g))g={compile:I(g)};else if(!g.compile&&g.link)g.compile=I(g.link);g.priority=g.priority||0;g.name=g.name||d;g.require=g.require||g.controller&&g.name;g.restrict=g.restrict||"A";e.push(g)}catch(h){c(h)}});return e}])),a[d].push(e)):m(d,nb(i));return this};this.urlSanitizationWhitelist=function(a){return y(a)?(h=a,this):h};this.$get=["$injector","$interpolate","$exceptionHandler","$http","$templateCache","$parse","$controller","$rootScope","$document",function(b, 38 | j,k,l,n,o,p,s,t){function x(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&b.nodeValue.match(/\S+/)&&(a[c]=u(b).wrap("").parent()[0])});var d=A(a,b,a,c);return function(b,c){$a(b,"scope");for(var e=c?ua.clone.call(a):a,j=0,g=e.length;jr.priority)break;if(Y=r.scope)ta("isolated scope",J,r,D),L(Y)&&(M(D,"ng-isolate-scope"),J=r),M(D,"ng-scope"),s=s||r;F=r.name;if(Y=r.controller)y=y||{},ta("'"+F+"' controller",y[F],r,D),y[F]=r;if(Y=r.transclude)ta("transclusion",ja,r,D),ja=r,l=r.priority,Y=="element"?(W=u(b),D=c.$$element=u(T.createComment(" "+ 45 | F+": "+c[F]+" ")),b=D[0],C(e,u(W[0]),b),V=x(W,d,l)):(W=u(cb(b)).contents(),D.html(""),V=x(W,d));if(Y=r.template)if(ta("template",A,r,D),A=r,Y=Fb(Y),r.replace){W=u("
"+Q(Y)+"
").contents();b=W[0];if(W.length!=1||b.nodeType!==1)throw Error(g+Y);C(e,D,b);F={$attr:{}};a=a.concat(N(b,a.splice(v+1,a.length-(v+1)),F));$(c,F);z=a.length}else D.html(Y);if(r.templateUrl)ta("template",A,r,D),A=r,i=R(a.splice(v,a.length-v),i,D,c,e,r.replace,V),z=a.length;else if(r.compile)try{w=r.compile(D,c,V),H(w)? 46 | j(null,w):w&&j(w.pre,w.post)}catch(G){k(G,pa(D))}if(r.terminal)i.terminal=!0,l=Math.max(l,r.priority)}i.scope=s&&s.scope;i.transclude=ja&&V;return i}function r(d,e,g,j){var h=!1;if(a.hasOwnProperty(e))for(var o,e=b.get(e+c),l=0,p=e.length;lo.priority)&&o.restrict.indexOf(g)!=-1)d.push(o),h=!0}catch(n){k(n)}return h}function $(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b, 47 | function(b,g){g=="class"?(M(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):g=="style"?e.attr("style",e.attr("style")+";"+b):g.charAt(0)!="$"&&!a.hasOwnProperty(g)&&(a[g]=b,d[g]=c[g])})}function R(a,b,c,d,e,j,h){var i=[],k,o,p=c[0],t=a.shift(),s=v({},t,{controller:null,templateUrl:null,transclude:null,scope:null});c.html("");l.get(t.templateUrl,{cache:n}).success(function(l){var n,t,l=Fb(l);if(j){t=u("
"+Q(l)+"
").contents();n=t[0];if(t.length!=1||n.nodeType!==1)throw Error(g+l);l={$attr:{}}; 48 | C(e,c,n);N(n,a,l);$(d,l)}else n=p,c.html(l);a.unshift(s);k=J(a,n,d,h);for(o=A(c[0].childNodes,h);i.length;){var r=i.pop(),l=i.pop();t=i.pop();var ia=i.pop(),D=n;t!==p&&(D=cb(n),C(l,u(t),D));k(function(){b(o,ia,D,e,r)},ia,D,e,r)}i=null}).error(function(a,b,c,d){throw Error("Failed to load template: "+d.url);});return function(a,c,d,e,g){i?(i.push(c),i.push(d),i.push(e),i.push(g)):k(function(){b(o,c,d,e,g)},c,d,e,g)}}function F(a,b){return b.priority-a.priority}function ta(a,b,c,d){if(b)throw Error("Multiple directives ["+ 49 | b.name+", "+c.name+"] asking for "+a+" on: "+pa(d));}function y(a,b){var c=j(b,!0);c&&a.push({priority:0,compile:I(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);M(d.data("$binding",e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue=a})})})}function V(a,b,c,d){var e=j(c,!0);e&&b.push({priority:100,compile:I(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=j(c[d],!0));c[d]=q;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e, 50 | function(a){c.$set(d,a)})})})}function C(a,b,c){var d=b[0],e=d.parentNode,g,j;if(a){g=0;for(j=a.length;g 68 | 0){var e=R[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=h(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),R.shift(),b):!1}function i(a){f(a)||e("is unexpected, expecting ["+a+"]",h())}function j(a,b){return function(c,d){return a(c,d,b)}}function k(a,b,c){return function(d,e){return b(d,e,a,c)}}function l(){for(var a=[];;)if(R.length>0&&!h("}",")",";","]")&&a.push(w()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d,e=0;e","<=",">="))a=k(a,b.fn,t());return a}function x(){for(var a=m(),b;b=f("*","/","%");)a=k(a,b.fn,m());return a}function m(){var a;return f("+")?A():(a=f("-"))?k(r,a.fn,m()):(a=f("!"))?j(a.fn,m()):A()}function A(){var a;if(f("("))a=w(),i(")");else if(f("["))a=N();else if(f("{"))a=J();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=y(a,c),c=null):b.text==="["?(c=a,a=V(a)):b.text==="."?(c=a,a=u(a)):e("IMPOSSIBLE");return a}function N(){var a= 71 | [];if(g().text!="]"){do a.push(F());while(f(","))}i("]");return function(b,c){for(var d=[],e=0;e1;d++){var e=a.shift(),g=b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]= 74 | c}function gb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,h=0;h7),hasEvent:function(c){if(c=="input"&&Z==9)return!1;if(w(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Vc(){this.$get=I(P)}function Ob(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=z(Q(b.substr(0, 92 | e)));d=Q(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Pb(b){var a=L(b)?b:q;return function(c){a||(a=Ob(b));return c?a[z(c)]||null:a}}function Qb(b,a,c){if(H(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Wc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){B(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=pb(d,!0)));return d}],transformRequest:[function(a){return L(a)&&wa.apply(a)!=="[object File]"?da(a):a}], 93 | headers:{common:{Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,i,j,k){function l(a){function c(a){var b=v({},a,{data:Qb(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:j.reject(b)}a.method=la(a.method);var e=a.transformRequest|| 94 | d.transformRequest,f=a.transformResponse||d.transformResponse,g=d.headers,g=v({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},g.common,g[z(a.method)],a.headers),e=Qb(a.data,Pb(g),e),i;w(a.data)&&delete g["Content-Type"];i=n(a,e,g);i=i.then(c,c);m(s,function(a){i=a(i)});i.success=function(b){i.then(function(c){b(c.data,c.status,c.headers,a)});return i};i.error=function(b){i.then(null,function(c){b(c.data,c.status,c.headers,a)});return i};return i}function n(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(q, 95 | [a,b,Ob(c)]):m.remove(q));f(b,a,c);i.$apply()}function f(a,c,d){c=Math.max(c,0);(200<=c&&c<300?k.resolve:k.reject)({data:a,status:c,headers:Pb(d),config:b})}function h(){var a=za(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var k=j.defer(),n=k.promise,m,s,q=o(b.url,b.params);l.pendingRequests.push(b);n.then(h,h);b.cache&&b.method=="GET"&&(m=L(b.cache)?b.cache:p);if(m)if(s=m.get(q))if(s.then)return s.then(h,h),s;else E(s)?f(s[1],s[0],U(s[2])):f(s,200,{});else m.put(q,n);s||a(b.method, 96 | q,c,e,d,b.timeout,b.withCredentials);return n}function o(a,b){if(!b)return a;var c=[];fc(b,function(a,b){a==null||a==q||(L(a)&&(a=da(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var p=c("$http"),s=[];m(e,function(a){s.push(B(a)?k.get(a):k.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(v(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]= 97 | function(b,c,d){return l(v(d||{},{method:a,url:b,data:c}))}})})("post","put");l.defaults=d;return l}]}function Xc(){this.$get=["$browser","$window","$document",function(b,a,c){return Yc(b,Zc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Yc(b,a,c,d,e,g){function h(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;Z?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror= 98 | d;e.body.appendChild(c)}return function(e,i,j,k,l,n,o){function p(a,c,d,e){c=(i.match(Hb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(C)}b.$$incOutstandingRequestCount();i=i||b.url();if(z(e)=="jsonp"){var s="_"+(d.counter++).toString(36);d[s]=function(a){d[s].data=a};h(i.replace("JSON_CALLBACK","angular.callbacks."+s),function(){d[s].data?p(k,200,d[s].data):p(k,-2);delete d[s]})}else{var t=new a;t.open(e,i,!0);m(l,function(a,b){a&&t.setRequestHeader(b,a)}); 99 | var q;t.onreadystatechange=function(){if(t.readyState==4){var a=t.getAllResponseHeaders(),b=["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"];a||(a="",m(b,function(b){var c=t.getResponseHeader(b);c&&(a+=b+": "+c+"\n")}));p(k,q||t.status,t.responseText,a)}};if(o)t.withCredentials=!0;t.send(j||"");n>0&&c(function(){q=-1;t.abort()},n)}}}function $c(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0, 100 | maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","),SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","), 101 | AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function ad(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,f,i){var j=c.defer(),k=j.promise,l=y(i)&&!i,f=a.defer(function(){try{j.resolve(e())}catch(a){j.reject(a),d(a)}l||b.$apply()},f),i=function(){delete g[k.$$timeoutId]}; 102 | k.$$timeoutId=f;g[f]=j;k.then(i,i);return k}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)):!1};return e}]}function Rb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+c)}}];a("currency",Sb);a("date",Tb);a("filter",bd);a("json",cd);a("limitTo",dd);a("lowercase",ed);a("number",Ub);a("orderBy",Vb);a("uppercase",fd)}function bd(){return function(b, 103 | a){if(!E(b))return b;var c=[];c.check=function(a){for(var b=0;b-1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;ce+1?h="0":(f=h,j=!0)}if(!j){h=(h.split(Xb)[1]||"").length;w(e)&&(e=Math.min(Math.max(a.minFrac,h),a.maxFrac));var h=Math.pow(10,e),b=Math.round(b*h)/h,b=(""+b).split(Xb),h=b[0],b=b[1]||"",j=0,k=a.lgSize, 106 | l=a.gSize;if(h.length>=k+l)for(var j=h.length-k,n=0;n0||e> 107 | -c)e+=c;e===0&&c==-12&&(e=12);return jb(e,a,d)}}function Ja(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Tb(b){function a(a){var b;if(b=a.match(c)){var a=new Date(0),g=0,h=0;b[9]&&(g=G(b[9]+b[10]),h=G(b[9]+b[11]));a.setUTCFullYear(G(b[1]),G(b[2])-1,G(b[3]));a.setUTCHours(G(b[4]||0)-g,G(b[5]||0)-h,G(b[6]||0),G(b[7]||0))}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c, 108 | e){var g="",h=[],f,i,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;B(c)&&(c=gd.test(c)?G(c):a(c));Qa(c)&&(c=new Date(c));if(!na(c))return c;for(;e;)(i=hd.exec(e))?(h=h.concat(ha.call(i,1)),e=h.pop()):(h.push(e),e=null);m(h,function(a){f=id[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function cd(){return function(b){return da(b,!0)}}function dd(){return function(b,a){if(!(b instanceof Array))return b;var a=G(a),c=[],d,e;if(!b||!(b instanceof Array))return c; 109 | a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dn?(d.$setValidity("maxlength",!1),q):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function kb(b,a){b="ngClass"+b;return S(function(c,d,e){function g(b){if(a===!0||c.$index%2===a)i&&!fa(b,i)&&h(i),f(b);i=U(b)}function h(a){L(a)&& 115 | !E(a)&&(a=Ra(a,function(a,b){if(a)return b}));d.removeClass(E(a)?a.join(" "):a)}function f(a){L(a)&&!E(a)&&(a=Ra(a,function(a,b){if(a)return b}));a&&d.addClass(E(a)?a.join(" "):a)}var i=q;c.$watch(e[b],g,!0);e.$observe("class",function(){var a=c.$eval(e[b]);g(a,a)});b!=="ngClass"&&c.$watch("$index",function(d,g){var i=d&1;i!==g&1&&(i===a?f(c.$eval(e[b])):h(c.$eval(e[b])))})})}var z=function(b){return B(b)?b.toLowerCase():b},la=function(b){return B(b)?b.toUpperCase():b},Z=G((/msie (\d+)/.exec(z(navigator.userAgent))|| 116 | [])[1]),u,ca,ha=[].slice,Pa=[].push,wa=Object.prototype.toString,Ya=P.angular||(P.angular={}),sa,fb,aa=["0","0","0"];C.$inject=[];ma.$inject=[];fb=Z<9?function(b){b=b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var kc=/[A-Z]/g,jd={full:"1.0.7",major:1,minor:0,dot:7,codeName:"monochromatic-rainbow"},Ba=K.cache={},Aa=K.expando="ng-"+(new Date).getTime(),oc=1,$b=P.document.addEventListener? 117 | function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},db=P.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)},mc=/([\:\-\_]+(.))/g,nc=/^moz([A-Z])/,ua=K.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;this.bind("DOMContentLoaded",a);K(P).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+ 118 | b])},length:0,push:Pa,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[z(b)]=b});var Bb={};m("input,select,option,textarea,button,form".split(","),function(b){Bb[la(b)]=!0});m({data:wb,inheritedData:Da,scope:function(b){return Da(b,"$scope")},controller:zb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=tb(a);if(y(c))b.style[a]=c;else{var d;Z<=8&&(d= 119 | b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];Z<=8&&(d=d===""?q:d);return d}},attr:function(b,a,c){var d=z(a);if(Ea[d])if(y(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||C).specified?d:q;else if(y(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?q:b},prop:function(b,a,c){if(y(c))b[a]=c;else return b[a]},text:v(Z<9?function(b,a){if(b.nodeType==1){if(w(a))return b.innerText; 120 | b.innerText=a}else{if(w(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(w(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(w(a))return b.value;b.value=a},html:function(b,a){if(w(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Mc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},ib={},Zc=P.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw Error("This browser does not support XMLHttpRequest."); 130 | };Rb.$inject=["$provide"];Sb.$inject=["$locale"];Ub.$inject=["$locale"];var Xb=".",id={yyyy:O("FullYear",4),yy:O("FullYear",2,0,!0),y:O("FullYear",1),MMMM:Ja("Month"),MMM:Ja("Month",!0),MM:O("Month",2,1),M:O("Month",1,1),dd:O("Date",2),d:O("Date",1),HH:O("Hours",2),H:O("Hours",1),hh:O("Hours",2,-12),h:O("Hours",1,-12),mm:O("Minutes",2),m:O("Minutes",1),ss:O("Seconds",2),s:O("Seconds",1),EEEE:Ja("Day"),EEE:Ja("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){var a= 131 | -1*a.getTimezoneOffset(),c=a>=0?"+":"";c+=jb(Math[a>0?"floor":"ceil"](a/60),2)+jb(Math.abs(a%60),2);return c}},hd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,gd=/^\d+$/;Tb.$inject=["$locale"];var ed=I(z),fd=I(la);Vb.$inject=["$parse"];var kd=I({restrict:"E",compile:function(a,c){Z<=8&&(!c.href&&!c.name&&c.$set("href",""),a.append(T.createComment("IE fix")));return function(a,c){c.bind("click",function(a){c.attr("href")||a.preventDefault()})}}}),lb={};m(Ea,function(a, 132 | c){var d=ea("ng-"+c);lb[d]=function(){return{priority:100,compile:function(){return function(a,g,h){a.$watch(h[d],function(a){h.$set(c,!!a)})}}}}});m(["src","href"],function(a){var c=ea("ng-"+a);lb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){c&&(g.$set(a,c),Z&&e.prop(a,g[a]))})}}}});var Ma={$addControl:C,$removeControl:C,$setValidity:C,$setDirty:C};Yb.$inject=["$element","$attrs","$scope"];var Pa=function(a){return["$timeout",function(c){var d={name:"form",restrict:"E", 133 | controller:Yb,compile:function(){return{pre:function(a,d,h,f){if(!h.action){var i=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};$b(d[0],"submit",i);d.bind("$destroy",function(){c(function(){db(d[0],"submit",i)},0,!1)})}var j=d.parent().controller("form"),k=h.name||h.ngForm;k&&(a[k]=f);j&&d.bind("$destroy",function(){j.$removeControl(f);k&&(a[k]=q);v(f,Ma)})}}}};return a?v(U(d),{restrict:"EAC"}):d}]},ld=Pa(),md=Pa(!0),nd=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, 134 | od=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,pd=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,bc={text:Oa,number:function(a,c,d,e,g,h){Oa(a,c,d,e,g,h);e.$parsers.push(function(a){var c=X(a);return c||pd.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number",!1),q)});e.$formatters.push(function(a){return X(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!X(a)&&ai?(e.$setValidity("max",!1),q):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return X(a)||Qa(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1),q)})},url:function(a,c,d,e,g,h){Oa(a,c,d,e,g,h);a=function(a){return X(a)||nd.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),q)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a, 136 | c,d,e,g,h){Oa(a,c,d,e,g,h);a=function(a){return X(a)||od.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email",!1),q)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){w(d.name)&&c.attr("name",xa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,h=d.ngFalseValue;B(g)||(g=!0);B(h)||(h=!1);c.bind("click", 137 | function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a===g});e.$parsers.push(function(a){return a?g:h})},hidden:C,button:C,submit:C,reset:C},cc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,e,g,h){h&&(bc[z(g.type)]||bc.text)(d,e,g,h,c,a)}}}],La="ng-valid",Ka="ng-invalid",Na="ng-pristine",Zb="ng-dirty",qd=["$scope","$exceptionHandler","$attrs","$element","$parse", 138 | function(a,c,d,e,g){function h(a,c){c=c?"-"+Za(c,"-"):"";e.removeClass((a?Ka:La)+c).addClass((a?La:Ka)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var f=g(d.ngModel),i=f.assign;if(!i)throw Error(Eb+d.ngModel+" ("+pa(e)+")");this.$render=C;var j=e.inheritedData("$formController")||Ma,k=0,l=this.$error={};e.addClass(Na);h(!0);this.$setValidity=function(a, 139 | c){if(l[a]!==!c){if(c){if(l[a]&&k--,!k)h(!0),this.$valid=!0,this.$invalid=!1}else h(!1),this.$invalid=!0,this.$valid=!1,k++;l[a]=!c;h(c,a);j.$setValidity(a,c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Na).addClass(Zb),j.$setDirty();m(this.$parsers,function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,i(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var n=this;a.$watch(function(){var c= 140 | f(a);if(n.$modelValue!==c){var d=n.$formatters,e=d.length;for(n.$modelValue=c;e--;)c=d[e](c);if(n.$viewValue!==c)n.$viewValue=c,n.$render()}})}],rd=function(){return{require:["ngModel","^?form"],controller:qd,link:function(a,c,d,e){var g=e[0],h=e[1]||Ma;h.$addControl(g);c.bind("$destroy",function(){h.$removeControl(g)})}}},sd=I({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),dc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required= 141 | !0;var g=function(a){if(d.required&&(X(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g);d.$observe("required",function(){g(e.$viewValue)})}}}},td=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(Q(a))});return c});e.$formatters.push(function(a){return E(a)?a.join(", "): 142 | q})}}},ud=/^(true|false|\d+)$/,vd=function(){return{priority:100,compile:function(a,c){return ud.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a,c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},wd=S(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==q?"":a)})}),xd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate", 143 | function(a){d.text(a)})}}],yd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe,function(a){c.html(a||"")})}}],zd=kb("",!0),Ad=kb("Odd",0),Bd=kb("Even",1),Cd=S({compile:function(a,c){c.$set("ngCloak",q);a.removeClass("ng-cloak")}}),Dd=[function(){return{scope:!0,controller:"@"}}],Ed=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],ec={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "), 144 | function(a){var c=ea("ng-"+a);ec[c]=["$parse",function(d){return function(e,g,h){var f=d(h[c]);g.bind(z(a),function(a){e.$apply(function(){f(e,{$event:a})})})}}]});var Fd=S(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Gd=["$http","$templateCache","$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,h){var f=h.ngInclude||h.src,i=h.onload||"",j=h.autoscroll;return function(g,h){var n=0,o,p=function(){o&&(o.$destroy(),o=null);h.html("")}; 145 | g.$watch(f,function(f){var m=++n;f?a.get(f,{cache:c}).success(function(a){m===n&&(o&&o.$destroy(),o=g.$new(),h.html(a),e(h.contents())(o),y(j)&&(!j||g.$eval(j))&&d(),o.$emit("$includeContentLoaded"),g.$eval(i))}).error(function(){m===n&&p()}):p()})}}}}],Hd=S({compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Id=S({terminal:!0,priority:1E3}),Jd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,h){var f=h.count,i=g.attr(h.$attr.when),j=h.offset|| 146 | 0,k=e.$eval(i),l={},n=c.startSymbol(),o=c.endSymbol();m(k,function(a,e){l[e]=c(a.replace(d,n+f+"-"+j+o))});e.$watch(function(){var c=parseFloat(e.$eval(f));return isNaN(c)?"":(c in k||(c=a.pluralCat(c-j)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Kd=S({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a,c,h){var f=h.ngRepeat,h=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),i,j,k;if(!h)throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f= 147 | h[1];i=h[2];h=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!h)throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '"+f+"'.");j=h[3]||h[1];k=h[2];var l=new eb;a.$watch(function(a){var e,f,h=a.$eval(i),m=c,q=new eb,y,A,u,w,r,v;if(E(h))r=h||[];else{r=[];for(u in h)h.hasOwnProperty(u)&&u.charAt(0)!="$"&&r.push(u);r.sort()}y=r.length-1;e=0;for(f=r.length;ez;)u.pop().element.remove()}for(;r.length> 157 | x;)r.pop()[0].element.remove()}var i;if(!(i=s.match(d)))throw Error("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+s+"'.");var j=c(i[2]||i[1]),k=i[4]||i[6],l=i[5],m=c(i[3]||""),n=c(i[2]?i[1]:k),o=c(i[7]),r=[[{element:f,label:""}]];t&&(a(t)(e),t.removeClass("ng-scope"),t.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a,c=o(e)||[],d={},h,i,j,m,s,t;if(p){i=[];m=0;for(t=r.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); 164 | -------------------------------------------------------------------------------- /components/angular/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular", 3 | "version": "1.0.7", 4 | "main": "./angular.js", 5 | "dependencies": {}, 6 | "gitHead": "6c0e81da2073f3831e32ed486d5aabe17bfc915f", 7 | "_id": "angular@1.0.7", 8 | "readme": "ERROR: No README.md file found!", 9 | "description": "ERROR: No README.md file found!", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/angular/bower-angular.git" 13 | } 14 | } -------------------------------------------------------------------------------- /demo-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

inside the block

20 |
21 |
22 | 23 |
24 |

inside the block

25 |
26 |
27 | 28 | 29 |
30 |

inside the block

31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 |

inside the block

39 |
40 |
41 | 42 |
43 |

inside the block

44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /demo-app/main.js: -------------------------------------------------------------------------------- 1 | var app=angular.module('app',['flexyLayout']); 2 | -------------------------------------------------------------------------------- /demo-app/mainStyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: fixed; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | color: rgb(80, 80, 80); 8 | 9 | } 10 | 11 | .block { 12 | transition: all 0.2s; 13 | } 14 | 15 | .block-content{ 16 | border:1px solid #000000; 17 | } 18 | 19 | .splitter{ 20 | background: orange; 21 | } 22 | 23 | .ghost { 24 | background: orange; 25 | opacity: 0.5; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /flexy-layout.debug.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | "use strict"; 3 | angular.module('flexyLayout.block', []) 4 | .provider('Block', function () { 5 | 6 | /** 7 | * A composite block made of different types of blocks that must implement the structural interface 8 | * 9 | * moveLength->change the lengthValue according to specific block rules 10 | * canMoveLength->tells whether the block can change his lengthValue in the current state 11 | * getAvailableLength->return the length the block can be reduced of 12 | * 13 | * , canMoveLength, getAvailableLength 14 | * @param composingBlocks 15 | * @constructor 16 | */ 17 | function CompositeBlock(composingBlocks) { 18 | this.blocks = []; 19 | 20 | if (angular.isArray(composingBlocks)) { 21 | for (var i = 0, l = composingBlocks.length; i < l; i++) { 22 | //should implement structural interface 23 | if (composingBlocks[i].moveLength && composingBlocks[i].canMoveLength && composingBlocks[i].getAvailableLength) { 24 | this.blocks.push(composingBlocks[i]); 25 | } 26 | } 27 | } 28 | } 29 | 30 | CompositeBlock.prototype.moveLength = function (length) { 31 | 32 | var 33 | divider = 0, 34 | initialLength = length, 35 | blockLength; 36 | 37 | for (var i = 0, l = this.blocks.length; i < l; i++) { 38 | if (this.blocks[i].canMoveLength(length) === true) { 39 | divider++; 40 | } 41 | } 42 | 43 | for (var j = 0; divider > 0; j++) { 44 | blockLength = this.blocks[j].moveLength(length / divider); 45 | length -= blockLength; 46 | if (Math.abs(blockLength) > 0) { 47 | divider--; 48 | } 49 | } 50 | 51 | return initialLength - length; 52 | }; 53 | 54 | CompositeBlock.prototype.canMoveLength = function (length) { 55 | 56 | for (var i = 0, l = this.blocks.length; i < l; i++) { 57 | if (this.blocks[i].canMoveLength(length) === true) { 58 | return true; 59 | } 60 | } 61 | 62 | return false; 63 | }; 64 | 65 | CompositeBlock.prototype.getAvailableLength = function () { 66 | var length = 0; 67 | for (var i = 0, l = this.blocks.length; i < l; i++) { 68 | length += this.blocks[i].getAvailableLength(); 69 | } 70 | 71 | return length; 72 | }; 73 | 74 | CompositeBlock.prototype.clean = function () { 75 | delete this.blocks; 76 | }; 77 | 78 | /** 79 | * A Blokc which can be locked (ie its lengthValue can not change) this is the standard composing block 80 | * @constructor 81 | */ 82 | function Block(initial) { 83 | this.initialLength = initial > 0 ? initial : 0; 84 | this.isLocked = false; 85 | this.lengthValue = 0; 86 | this.minLength = 0; 87 | } 88 | 89 | Block.prototype.moveLength = function (length) { 90 | 91 | if (this.isLocked === true) { 92 | return 0; 93 | } 94 | 95 | var oldLength = this.lengthValue; 96 | if (angular.isNumber(length)) { 97 | this.lengthValue = Math.max(0, this.lengthValue + length); 98 | } 99 | return this.lengthValue - oldLength; 100 | }; 101 | 102 | Block.prototype.canMoveLength = function (length) { 103 | return !(this.isLocked === true || (length < 0 && (this.getAvailableLength()) === 0)); 104 | }; 105 | 106 | Block.prototype.getAvailableLength = function () { 107 | return this.isLocked === true ? 0 : this.lengthValue - this.minLength; 108 | }; 109 | 110 | /** 111 | * Splitter a splitter block which split a set of blocks into two separate set 112 | * @constructor 113 | */ 114 | function Splitter() { 115 | this.lengthValue = 5; 116 | this.initialPosition = { x: 0, y: 0}; 117 | this.availableLength = {before: 0, after: 0}; 118 | this.ghostPosition = { x: 0, y: 0}; 119 | 120 | } 121 | 122 | Splitter.prototype.canMoveLength = function () { 123 | return false; 124 | }; 125 | 126 | Splitter.prototype.moveLength = function () { 127 | return 0; 128 | }; 129 | 130 | Splitter.prototype.getAvailableLength = function () { 131 | return 0; 132 | }; 133 | 134 | this.$get = function () { 135 | return { 136 | //variadic -> can call getNewComposite([block1, block2, ...]) or getNewComposite(block1, block2, ...) 137 | getNewComposite: function () { 138 | var args = [].slice.call(arguments); 139 | if (args.length === 1 && angular.isArray(args[0])) { 140 | args = args[0]; 141 | } 142 | return new CompositeBlock(args); 143 | }, 144 | getNewBlock: function (initialLength) { 145 | return new Block(initialLength); 146 | }, 147 | getNewSplitter: function () { 148 | return new Splitter(); 149 | }, 150 | 151 | isSplitter: function (block) { 152 | return block instanceof Splitter; 153 | } 154 | }; 155 | } 156 | }); 157 | })(angular); 158 | (function (angular) { 159 | "use strict"; 160 | angular.module('flexyLayout.directives', ['flexyLayout.mediator']) 161 | .directive('flexyLayout', function () { 162 | return { 163 | restrict: 'E', 164 | scope: {}, 165 | template: '
', 166 | replace: true, 167 | transclude: true, 168 | controller: 'mediatorCtrl', 169 | link: function (scope, element, attrs, ctrl) { 170 | scope.$watch(function () { 171 | return element[0][ctrl.lengthProperties.offsetName]; 172 | }, function () { 173 | ctrl.init(); 174 | }); 175 | } 176 | }; 177 | }) 178 | .directive('blockContainer', ['Block', function (Block) { 179 | return{ 180 | restrict: 'E', 181 | require: '^flexyLayout', 182 | transclude: true, 183 | replace: true, 184 | scope: {}, 185 | template: '
' + 186 | '
' + 187 | '
' + 188 | '
', 189 | link: function (scope, element, attrs, ctrl) { 190 | var initialLength = scope.$eval(attrs.init); 191 | scope.block = Block.getNewBlock(initialLength); 192 | scope.$watch('block.lengthValue', function (newValue, oldValue) { 193 | element.css(ctrl.lengthProperties.lengthName, Math.floor(newValue) + 'px'); 194 | }); 195 | 196 | ctrl.addBlock(scope.block); 197 | } 198 | }; 199 | }]) 200 | .directive('blockSplitter', ['Block', function (Block) { 201 | return{ 202 | restrict: 'E', 203 | require: '^flexyLayout', 204 | replace: true, 205 | scope: {}, 206 | template: '
' + 207 | '
' + 208 | '
', 209 | link: function (scope, element, attrs, ctrl) { 210 | scope.splitter = Block.getNewSplitter(); 211 | 212 | var ghost = element.children()[0]; 213 | var mouseDownHandler = function (event) { 214 | this.initialPosition.x = event.clientX; 215 | this.initialPosition.y = event.clientY; 216 | this.availableLength = ctrl.getSplitterRange(this); 217 | ctrl.movingSplitter = this; 218 | 219 | //to avoid the block content to be selected when dragging the splitter 220 | event.preventDefault(); 221 | }; 222 | 223 | ctrl.addBlock(scope.splitter); 224 | 225 | element.bind('mousedown', angular.bind(scope.splitter, mouseDownHandler)); 226 | 227 | scope.$watch('splitter.ghostPosition.' + ctrl.lengthProperties.position, function (newValue, oldValue) { 228 | if (newValue !== oldValue) { 229 | ghost.style[ctrl.lengthProperties.positionName] = newValue + 'px'; 230 | } 231 | }); 232 | 233 | } 234 | }; 235 | }]); 236 | 237 | angular.module('flexyLayout', ['flexyLayout.directives']); 238 | 239 | })(angular); 240 | (function (angular) { 241 | "use strict"; 242 | //TODO this guy is now big, split it, maybe the part for event handling should be moved somewhere else 243 | angular.module('flexyLayout.mediator', ['flexyLayout.block']). 244 | controller('mediatorCtrl', ['$scope', '$element', '$attrs', 'Block', function (scope, element, attrs, Block) { 245 | 246 | var blocks = [], 247 | pendingSplitter = null, 248 | splitterCount = 0, 249 | self = this, 250 | possibleOrientations = ['vertical', 'horizontal'], 251 | orientation = possibleOrientations.indexOf(attrs.orientation) !== -1 ? attrs.orientation : 'horizontal', 252 | className = orientation === 'horizontal' ? 'flexy-layout-column' : 'flexy-layout-row'; 253 | 254 | element.addClass(className); 255 | 256 | this.lengthProperties = orientation === 'horizontal' ? {lengthName: 'width', offsetName: 'offsetWidth', positionName: 'left', position: 'x', eventProperty: 'clientX'} : 257 | {lengthName: 'height', offsetName: 'offsetHeight', positionName: 'top', position: 'y', eventProperty: 'clientY'}; 258 | 259 | ///// mouse event handler ///// 260 | 261 | this.movingSplitter = null; 262 | 263 | var mouseMoveHandler = function (event) { 264 | var length = 0, 265 | eventProperty = this.lengthProperties.eventProperty, 266 | position = this.lengthProperties.position; 267 | 268 | if (this.movingSplitter !== null) { 269 | length = event[eventProperty] - this.movingSplitter.initialPosition[position]; 270 | if (length < 0) { 271 | this.movingSplitter.ghostPosition[position] = (-1) * Math.min(Math.abs(length), this.movingSplitter.availableLength.before); 272 | } else { 273 | this.movingSplitter.ghostPosition[position] = Math.min(length, this.movingSplitter.availableLength.after); 274 | } 275 | } 276 | }; 277 | 278 | var mouseUpHandler = function (event) { 279 | var length = 0, 280 | eventProperty = this.lengthProperties.eventProperty, 281 | position = this.lengthProperties.position; 282 | 283 | if (this.movingSplitter !== null) { 284 | length = event[eventProperty] - this.movingSplitter.initialPosition[position]; 285 | this.moveSplitterLength(this.movingSplitter, length); 286 | this.movingSplitter.ghostPosition[position] = 0; 287 | this.movingSplitter = null; 288 | } 289 | }; 290 | 291 | element.bind('mouseup', function (event) { 292 | scope.$apply(angular.bind(self, mouseUpHandler, event)); 293 | }); 294 | 295 | //todo should do some throttle before calling apply 296 | element.bind('mousemove', function (event) { 297 | scope.$apply(angular.bind(self, mouseMoveHandler, event)); 298 | }); 299 | 300 | ///// adding blocks //// 301 | 302 | this.addBlock = function (block) { 303 | 304 | if (!Block.isSplitter(block)) { 305 | if (pendingSplitter !== null) { 306 | blocks.push(pendingSplitter); 307 | splitterCount++; 308 | pendingSplitter = null; 309 | } 310 | 311 | blocks.push(block); 312 | this.init(); 313 | } else { 314 | pendingSplitter = block; 315 | } 316 | }; 317 | 318 | /** 319 | * to be called when flexy-layout container has been resized 320 | */ 321 | this.init = function () { 322 | 323 | var i, 324 | l = blocks.length, 325 | elementLength = element[0][this.lengthProperties.offsetName], 326 | block, 327 | bufferBlock = Block.getNewBlock();//temporary buffer block 328 | 329 | blocks.push(bufferBlock); 330 | 331 | //reset all blocks 332 | for (i = 0; i < l; i++) { 333 | block = blocks[i]; 334 | block.isLocked = false; 335 | if (!Block.isSplitter(block)) { 336 | block.moveLength(-10000); 337 | } 338 | } 339 | //buffer block takes all available space 340 | bufferBlock.moveLength(elementLength - splitterCount * 5); 341 | 342 | for (i = 0; i < l; i++) { 343 | block = blocks[i]; 344 | if (block.initialLength > 0) { 345 | this.moveBlockLength(block, block.initialLength); 346 | block.isLocked=true; 347 | } 348 | } 349 | 350 | //buffer block free space for non fixed block 351 | this.moveBlockLength(bufferBlock, -10000); 352 | 353 | for (i = 0; i < l; i++) { 354 | blocks[i].isLocked = false; 355 | } 356 | 357 | blocks.splice(l, 1); 358 | 359 | }; 360 | 361 | ///// public api ///// 362 | 363 | /** 364 | * Will move a given block length from @length 365 | * 366 | * @param block can be a block or an index (likely index of the block) 367 | * @param length < 0 or > 0 : decrease/increase block size of abs(length) px 368 | */ 369 | this.moveBlockLength = function (block, length) { 370 | 371 | var 372 | blockIndex = typeof block !== 'object' ? block : blocks.indexOf(block), 373 | composingBlocks, 374 | composite, 375 | availableLength, 376 | blockToMove; 377 | 378 | 379 | if (blockIndex < 0 || length === 0 || blockIndex >= blocks.length) { 380 | return; 381 | } 382 | 383 | blockToMove = blocks[blockIndex]; 384 | 385 | composingBlocks = (blocks.slice(0, blockIndex)).concat(blocks.slice(blockIndex + 1, blocks.length)); 386 | composite = Block.getNewComposite(composingBlocks); 387 | 388 | if (composite.canMoveLength(-length) !== true || blockToMove.canMoveLength(length) !== true) { 389 | return; 390 | } 391 | 392 | if (length < 0) { 393 | availableLength = (-1) * blockToMove.moveLength(length); 394 | composite.moveLength(availableLength); 395 | } else { 396 | availableLength = (-1) * composite.moveLength(-length); 397 | blockToMove.moveLength(availableLength); 398 | } 399 | 400 | //free memory 401 | composite.clean(); 402 | }; 403 | 404 | /** 405 | * move splitter it will affect all the blocks before until the previous/next splitter or the edge of area 406 | * @param splitter 407 | * @param length 408 | */ 409 | //todo mutualise with moveBlockLength 410 | this.moveSplitterLength = function (splitter, length) { 411 | 412 | var 413 | splitterIndex = blocks.indexOf(splitter), 414 | beforeComposite, 415 | afterComposite, 416 | availableLength; 417 | 418 | if (!Block.isSplitter(splitter) || splitterIndex === -1) { 419 | return; 420 | } 421 | 422 | beforeComposite = Block.getNewComposite(fromSplitterToSplitter(splitter, true)); 423 | afterComposite = Block.getNewComposite(fromSplitterToSplitter(splitter, false)); 424 | 425 | if (!beforeComposite.canMoveLength(length) || !afterComposite.canMoveLength(-length)) { 426 | return; 427 | } 428 | 429 | if (length < 0) { 430 | availableLength = (-1) * beforeComposite.moveLength(length); 431 | afterComposite.moveLength(availableLength); 432 | } else { 433 | availableLength = (-1) * afterComposite.moveLength(-length); 434 | beforeComposite.moveLength(availableLength); 435 | } 436 | 437 | afterComposite.clean(); 438 | beforeComposite.clean(); 439 | 440 | }; 441 | 442 | /** 443 | * return an object with the available length before the splitter and after the splitter 444 | * @param splitter 445 | * @returns {{before: *, after: *}} 446 | */ 447 | this.getSplitterRange = function (splitter) { 448 | 449 | var 450 | beforeSplitter = fromSplitterToSplitter(splitter, true), 451 | afterSplitter = fromSplitterToSplitter(splitter, false), 452 | toReturn = { 453 | before: beforeSplitter.getAvailableLength(), 454 | after: afterSplitter.getAvailableLength() 455 | }; 456 | 457 | beforeSplitter.clean(); 458 | afterSplitter.clean(); 459 | 460 | return toReturn; 461 | }; 462 | 463 | /** 464 | * lock/unlock a given block 465 | * @param block block or blockIndex 466 | * @param lock new value for block.isLocked 467 | */ 468 | this.toggleLockBlock = function (block, lock) { 469 | var 470 | blockIndex = typeof block !== 'object' ? block : blocks.indexOf(block), 471 | blockToLock; 472 | 473 | if (blockIndex >= 0 && blockIndex < blocks.length) { 474 | blockToLock = blocks[blockIndex]; 475 | blockToLock.isLocked = lock; 476 | } 477 | 478 | }; 479 | 480 | var fromSplitterToSplitter = function (splitter, before) { 481 | 482 | var 483 | splitterIndex = blocks.indexOf(splitter), 484 | blockGroup = before === true ? blocks.slice(0, splitterIndex) : blocks.slice(splitterIndex + 1, blocks.length), 485 | fn = before === true ? Array.prototype.pop : Array.prototype.shift, 486 | composite = [], 487 | testedBlock; 488 | 489 | while (testedBlock = fn.apply(blockGroup)) { 490 | if (Block.isSplitter(testedBlock)) { 491 | break; 492 | } else { 493 | composite.push(testedBlock); 494 | } 495 | } 496 | return Block.getNewComposite(composite); 497 | }; 498 | }]); 499 | })(angular); -------------------------------------------------------------------------------- /flexy-layout.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";a.module("flexyLayout.block",[]).provider("Block",function(){function b(b){if(this.blocks=[],a.isArray(b))for(var c=0,d=b.length;d>c;c++)b[c].moveLength&&b[c].canMoveLength&&b[c].getAvailableLength&&this.blocks.push(b[c])}function c(a){this.initialLength=a>0?a:0,this.isLocked=!1,this.lengthValue=0,this.minLength=0}function d(){this.lengthValue=5,this.initialPosition={x:0,y:0},this.availableLength={before:0,after:0},this.ghostPosition={x:0,y:0}}b.prototype.moveLength=function(a){for(var b,c=0,d=a,e=0,f=this.blocks.length;f>e;e++)this.blocks[e].canMoveLength(a)===!0&&c++;for(var g=0;c>0;g++)b=this.blocks[g].moveLength(a/c),a-=b,Math.abs(b)>0&&c--;return d-a},b.prototype.canMoveLength=function(a){for(var b=0,c=this.blocks.length;c>b;b++)if(this.blocks[b].canMoveLength(a)===!0)return!0;return!1},b.prototype.getAvailableLength=function(){for(var a=0,b=0,c=this.blocks.length;c>b;b++)a+=this.blocks[b].getAvailableLength();return a},b.prototype.clean=function(){delete this.blocks},c.prototype.moveLength=function(b){if(this.isLocked===!0)return 0;var c=this.lengthValue;return a.isNumber(b)&&(this.lengthValue=Math.max(0,this.lengthValue+b)),this.lengthValue-c},c.prototype.canMoveLength=function(a){return!(this.isLocked===!0||0>a&&0===this.getAvailableLength())},c.prototype.getAvailableLength=function(){return this.isLocked===!0?0:this.lengthValue-this.minLength},d.prototype.canMoveLength=function(){return!1},d.prototype.moveLength=function(){return 0},d.prototype.getAvailableLength=function(){return 0},this.$get=function(){return{getNewComposite:function(){var c=[].slice.call(arguments);return 1===c.length&&a.isArray(c[0])&&(c=c[0]),new b(c)},getNewBlock:function(a){return new c(a)},getNewSplitter:function(){return new d},isSplitter:function(a){return a instanceof d}}}})}(angular),function(a){"use strict";a.module("flexyLayout.directives",["flexyLayout.mediator"]).directive("flexyLayout",function(){return{restrict:"E",scope:{},template:'
',replace:!0,transclude:!0,controller:"mediatorCtrl",link:function(a,b,c,d){a.$watch(function(){return b[0][d.lengthProperties.offsetName]},function(){d.init()})}}}).directive("blockContainer",["Block",function(a){return{restrict:"E",require:"^flexyLayout",transclude:!0,replace:!0,scope:{},template:'
',link:function(b,c,d,e){var f=b.$eval(d.init);b.block=a.getNewBlock(f),b.$watch("block.lengthValue",function(a){c.css(e.lengthProperties.lengthName,Math.floor(a)+"px")}),e.addBlock(b.block)}}}]).directive("blockSplitter",["Block",function(b){return{restrict:"E",require:"^flexyLayout",replace:!0,scope:{},template:'
',link:function(c,d,e,f){c.splitter=b.getNewSplitter();var g=d.children()[0],h=function(a){this.initialPosition.x=a.clientX,this.initialPosition.y=a.clientY,this.availableLength=f.getSplitterRange(this),f.movingSplitter=this,a.preventDefault()};f.addBlock(c.splitter),d.bind("mousedown",a.bind(c.splitter,h)),c.$watch("splitter.ghostPosition."+f.lengthProperties.position,function(a,b){a!==b&&(g.style[f.lengthProperties.positionName]=a+"px")})}}}]),a.module("flexyLayout",["flexyLayout.directives"])}(angular),function(a){"use strict";a.module("flexyLayout.mediator",["flexyLayout.block"]).controller("mediatorCtrl",["$scope","$element","$attrs","Block",function(b,c,d,e){var f=[],g=null,h=0,i=this,j=["vertical","horizontal"],k=-1!==j.indexOf(d.orientation)?d.orientation:"horizontal",l="horizontal"===k?"flexy-layout-column":"flexy-layout-row";c.addClass(l),this.lengthProperties="horizontal"===k?{lengthName:"width",offsetName:"offsetWidth",positionName:"left",position:"x",eventProperty:"clientX"}:{lengthName:"height",offsetName:"offsetHeight",positionName:"top",position:"y",eventProperty:"clientY"},this.movingSplitter=null;var m=function(a){var b=0,c=this.lengthProperties.eventProperty,d=this.lengthProperties.position;null!==this.movingSplitter&&(b=a[c]-this.movingSplitter.initialPosition[d],this.movingSplitter.ghostPosition[d]=0>b?-1*Math.min(Math.abs(b),this.movingSplitter.availableLength.before):Math.min(b,this.movingSplitter.availableLength.after))},n=function(a){var b=0,c=this.lengthProperties.eventProperty,d=this.lengthProperties.position;null!==this.movingSplitter&&(b=a[c]-this.movingSplitter.initialPosition[d],this.moveSplitterLength(this.movingSplitter,b),this.movingSplitter.ghostPosition[d]=0,this.movingSplitter=null)};c.bind("mouseup",function(c){b.$apply(a.bind(i,n,c))}),c.bind("mousemove",function(c){b.$apply(a.bind(i,m,c))}),this.addBlock=function(a){e.isSplitter(a)?g=a:(null!==g&&(f.push(g),h++,g=null),f.push(a),this.init())},this.init=function(){var a,b,d=f.length,g=c[0][this.lengthProperties.offsetName],i=e.getNewBlock();for(f.push(i),a=0;d>a;a++)b=f[a],b.isLocked=!1,e.isSplitter(b)||b.moveLength(-1e4);for(i.moveLength(g-5*h),a=0;d>a;a++)b=f[a],b.initialLength>0&&(this.moveBlockLength(b,b.initialLength),b.isLocked=!0);for(this.moveBlockLength(i,-1e4),a=0;d>a;a++)f[a].isLocked=!1;f.splice(d,1)},this.moveBlockLength=function(a,b){var c,d,g,h,i="object"!=typeof a?a:f.indexOf(a);0>i||0===b||i>=f.length||(h=f[i],c=f.slice(0,i).concat(f.slice(i+1,f.length)),d=e.getNewComposite(c),d.canMoveLength(-b)===!0&&h.canMoveLength(b)===!0&&(0>b?(g=-1*h.moveLength(b),d.moveLength(g)):(g=-1*d.moveLength(-b),h.moveLength(g)),d.clean()))},this.moveSplitterLength=function(a,b){var c,d,g,h=f.indexOf(a);e.isSplitter(a)&&-1!==h&&(c=e.getNewComposite(o(a,!0)),d=e.getNewComposite(o(a,!1)),c.canMoveLength(b)&&d.canMoveLength(-b)&&(0>b?(g=-1*c.moveLength(b),d.moveLength(g)):(g=-1*d.moveLength(-b),c.moveLength(g)),d.clean(),c.clean()))},this.getSplitterRange=function(a){var b=o(a,!0),c=o(a,!1),d={before:b.getAvailableLength(),after:c.getAvailableLength()};return b.clean(),c.clean(),d},this.toggleLockBlock=function(a,b){var c,d="object"!=typeof a?a:f.indexOf(a);d>=0&&dchange the lengthValue according to specific block rules 10 | * canMoveLength->tells whether the block can change his lengthValue in the current state 11 | * getAvailableLength->return the length the block can be reduced of 12 | * 13 | * , canMoveLength, getAvailableLength 14 | * @param composingBlocks 15 | * @constructor 16 | */ 17 | function CompositeBlock(composingBlocks) { 18 | this.blocks = []; 19 | 20 | if (angular.isArray(composingBlocks)) { 21 | for (var i = 0, l = composingBlocks.length; i < l; i++) { 22 | //should implement structural interface 23 | if (composingBlocks[i].moveLength && composingBlocks[i].canMoveLength && composingBlocks[i].getAvailableLength) { 24 | this.blocks.push(composingBlocks[i]); 25 | } 26 | } 27 | } 28 | } 29 | 30 | CompositeBlock.prototype.moveLength = function (length) { 31 | 32 | var 33 | divider = 0, 34 | initialLength = length, 35 | blockLength; 36 | 37 | for (var i = 0, l = this.blocks.length; i < l; i++) { 38 | if (this.blocks[i].canMoveLength(length) === true) { 39 | divider++; 40 | } 41 | } 42 | 43 | for (var j = 0; divider > 0; j++) { 44 | blockLength = this.blocks[j].moveLength(length / divider); 45 | length -= blockLength; 46 | if (Math.abs(blockLength) > 0) { 47 | divider--; 48 | } 49 | } 50 | 51 | return initialLength - length; 52 | }; 53 | 54 | CompositeBlock.prototype.canMoveLength = function (length) { 55 | 56 | for (var i = 0, l = this.blocks.length; i < l; i++) { 57 | if (this.blocks[i].canMoveLength(length) === true) { 58 | return true; 59 | } 60 | } 61 | 62 | return false; 63 | }; 64 | 65 | CompositeBlock.prototype.getAvailableLength = function () { 66 | var length = 0; 67 | for (var i = 0, l = this.blocks.length; i < l; i++) { 68 | length += this.blocks[i].getAvailableLength(); 69 | } 70 | 71 | return length; 72 | }; 73 | 74 | CompositeBlock.prototype.clean = function () { 75 | delete this.blocks; 76 | }; 77 | 78 | /** 79 | * A Blokc which can be locked (ie its lengthValue can not change) this is the standard composing block 80 | * @constructor 81 | */ 82 | function Block(initial) { 83 | this.initialLength = initial > 0 ? initial : 0; 84 | this.isLocked = false; 85 | this.lengthValue = 0; 86 | this.minLength = 0; 87 | } 88 | 89 | Block.prototype.moveLength = function (length) { 90 | 91 | if (this.isLocked === true) { 92 | return 0; 93 | } 94 | 95 | var oldLength = this.lengthValue; 96 | if (angular.isNumber(length)) { 97 | this.lengthValue = Math.max(0, this.lengthValue + length); 98 | } 99 | return this.lengthValue - oldLength; 100 | }; 101 | 102 | Block.prototype.canMoveLength = function (length) { 103 | return !(this.isLocked === true || (length < 0 && (this.getAvailableLength()) === 0)); 104 | }; 105 | 106 | Block.prototype.getAvailableLength = function () { 107 | return this.isLocked === true ? 0 : this.lengthValue - this.minLength; 108 | }; 109 | 110 | /** 111 | * Splitter a splitter block which split a set of blocks into two separate set 112 | * @constructor 113 | */ 114 | function Splitter() { 115 | this.lengthValue = 5; 116 | this.initialPosition = { x: 0, y: 0}; 117 | this.availableLength = {before: 0, after: 0}; 118 | this.ghostPosition = { x: 0, y: 0}; 119 | 120 | } 121 | 122 | Splitter.prototype.canMoveLength = function () { 123 | return false; 124 | }; 125 | 126 | Splitter.prototype.moveLength = function () { 127 | return 0; 128 | }; 129 | 130 | Splitter.prototype.getAvailableLength = function () { 131 | return 0; 132 | }; 133 | 134 | this.$get = function () { 135 | return { 136 | //variadic -> can call getNewComposite([block1, block2, ...]) or getNewComposite(block1, block2, ...) 137 | getNewComposite: function () { 138 | var args = [].slice.call(arguments); 139 | if (args.length === 1 && angular.isArray(args[0])) { 140 | args = args[0]; 141 | } 142 | return new CompositeBlock(args); 143 | }, 144 | getNewBlock: function (initialLength) { 145 | return new Block(initialLength); 146 | }, 147 | getNewSplitter: function () { 148 | return new Splitter(); 149 | }, 150 | 151 | isSplitter: function (block) { 152 | return block instanceof Splitter; 153 | } 154 | }; 155 | } 156 | }); 157 | })(angular); -------------------------------------------------------------------------------- /src/Directives.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | "use strict"; 3 | angular.module('flexyLayout.directives', ['flexyLayout.mediator']) 4 | .directive('flexyLayout', function () { 5 | return { 6 | restrict: 'E', 7 | scope: {}, 8 | template: '
', 9 | replace: true, 10 | transclude: true, 11 | controller: 'mediatorCtrl', 12 | link: function (scope, element, attrs, ctrl) { 13 | scope.$watch(function () { 14 | return element[0][ctrl.lengthProperties.offsetName]; 15 | }, function () { 16 | ctrl.init(); 17 | }); 18 | } 19 | }; 20 | }) 21 | .directive('blockContainer', ['Block', function (Block) { 22 | return{ 23 | restrict: 'E', 24 | require: '^flexyLayout', 25 | transclude: true, 26 | replace: true, 27 | scope: {}, 28 | template: '
' + 29 | '
' + 30 | '
' + 31 | '
', 32 | link: function (scope, element, attrs, ctrl) { 33 | var initialLength = scope.$eval(attrs.init); 34 | scope.block = Block.getNewBlock(initialLength); 35 | scope.$watch('block.lengthValue', function (newValue, oldValue) { 36 | element.css(ctrl.lengthProperties.lengthName, Math.floor(newValue) + 'px'); 37 | }); 38 | 39 | ctrl.addBlock(scope.block); 40 | } 41 | }; 42 | }]) 43 | .directive('blockSplitter', ['Block', function (Block) { 44 | return{ 45 | restrict: 'E', 46 | require: '^flexyLayout', 47 | replace: true, 48 | scope: {}, 49 | template: '
' + 50 | '
' + 51 | '
', 52 | link: function (scope, element, attrs, ctrl) { 53 | scope.splitter = Block.getNewSplitter(); 54 | 55 | var ghost = element.children()[0]; 56 | var mouseDownHandler = function (event) { 57 | this.initialPosition.x = event.clientX; 58 | this.initialPosition.y = event.clientY; 59 | this.availableLength = ctrl.getSplitterRange(this); 60 | ctrl.movingSplitter = this; 61 | 62 | //to avoid the block content to be selected when dragging the splitter 63 | event.preventDefault(); 64 | }; 65 | 66 | ctrl.addBlock(scope.splitter); 67 | 68 | element.bind('mousedown', angular.bind(scope.splitter, mouseDownHandler)); 69 | 70 | scope.$watch('splitter.ghostPosition.' + ctrl.lengthProperties.position, function (newValue, oldValue) { 71 | if (newValue !== oldValue) { 72 | ghost.style[ctrl.lengthProperties.positionName] = newValue + 'px'; 73 | } 74 | }); 75 | 76 | } 77 | }; 78 | }]); 79 | 80 | angular.module('flexyLayout', ['flexyLayout.directives']); 81 | 82 | })(angular); -------------------------------------------------------------------------------- /src/MediatorController.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | "use strict"; 3 | //TODO this guy is now big, split it, maybe the part for event handling should be moved somewhere else 4 | angular.module('flexyLayout.mediator', ['flexyLayout.block']). 5 | controller('mediatorCtrl', ['$scope', '$element', '$attrs', 'Block', function (scope, element, attrs, Block) { 6 | 7 | var blocks = [], 8 | pendingSplitter = null, 9 | splitterCount = 0, 10 | self = this, 11 | possibleOrientations = ['vertical', 'horizontal'], 12 | orientation = possibleOrientations.indexOf(attrs.orientation) !== -1 ? attrs.orientation : 'horizontal', 13 | className = orientation === 'horizontal' ? 'flexy-layout-column' : 'flexy-layout-row'; 14 | 15 | element.addClass(className); 16 | 17 | this.lengthProperties = orientation === 'horizontal' ? {lengthName: 'width', offsetName: 'offsetWidth', positionName: 'left', position: 'x', eventProperty: 'clientX'} : 18 | {lengthName: 'height', offsetName: 'offsetHeight', positionName: 'top', position: 'y', eventProperty: 'clientY'}; 19 | 20 | ///// mouse event handler ///// 21 | 22 | this.movingSplitter = null; 23 | 24 | var mouseMoveHandler = function (event) { 25 | var length = 0, 26 | eventProperty = this.lengthProperties.eventProperty, 27 | position = this.lengthProperties.position; 28 | 29 | if (this.movingSplitter !== null) { 30 | length = event[eventProperty] - this.movingSplitter.initialPosition[position]; 31 | if (length < 0) { 32 | this.movingSplitter.ghostPosition[position] = (-1) * Math.min(Math.abs(length), this.movingSplitter.availableLength.before); 33 | } else { 34 | this.movingSplitter.ghostPosition[position] = Math.min(length, this.movingSplitter.availableLength.after); 35 | } 36 | } 37 | }; 38 | 39 | var mouseUpHandler = function (event) { 40 | var length = 0, 41 | eventProperty = this.lengthProperties.eventProperty, 42 | position = this.lengthProperties.position; 43 | 44 | if (this.movingSplitter !== null) { 45 | length = event[eventProperty] - this.movingSplitter.initialPosition[position]; 46 | this.moveSplitterLength(this.movingSplitter, length); 47 | this.movingSplitter.ghostPosition[position] = 0; 48 | this.movingSplitter = null; 49 | } 50 | }; 51 | 52 | element.bind('mouseup', function (event) { 53 | scope.$apply(angular.bind(self, mouseUpHandler, event)); 54 | }); 55 | 56 | //todo should do some throttle before calling apply 57 | element.bind('mousemove', function (event) { 58 | scope.$apply(angular.bind(self, mouseMoveHandler, event)); 59 | }); 60 | 61 | ///// adding blocks //// 62 | 63 | this.addBlock = function (block) { 64 | 65 | if (!Block.isSplitter(block)) { 66 | if (pendingSplitter !== null) { 67 | blocks.push(pendingSplitter); 68 | splitterCount++; 69 | pendingSplitter = null; 70 | } 71 | 72 | blocks.push(block); 73 | this.init(); 74 | } else { 75 | pendingSplitter = block; 76 | } 77 | }; 78 | 79 | /** 80 | * to be called when flexy-layout container has been resized 81 | */ 82 | this.init = function () { 83 | 84 | var i, 85 | l = blocks.length, 86 | elementLength = element[0][this.lengthProperties.offsetName], 87 | block, 88 | bufferBlock = Block.getNewBlock();//temporary buffer block 89 | 90 | blocks.push(bufferBlock); 91 | 92 | //reset all blocks 93 | for (i = 0; i < l; i++) { 94 | block = blocks[i]; 95 | block.isLocked = false; 96 | if (!Block.isSplitter(block)) { 97 | block.moveLength(-10000); 98 | } 99 | } 100 | //buffer block takes all available space 101 | bufferBlock.moveLength(elementLength - splitterCount * 5); 102 | 103 | for (i = 0; i < l; i++) { 104 | block = blocks[i]; 105 | if (block.initialLength > 0) { 106 | this.moveBlockLength(block, block.initialLength); 107 | block.isLocked=true; 108 | } 109 | } 110 | 111 | //buffer block free space for non fixed block 112 | this.moveBlockLength(bufferBlock, -10000); 113 | 114 | for (i = 0; i < l; i++) { 115 | blocks[i].isLocked = false; 116 | } 117 | 118 | blocks.splice(l, 1); 119 | 120 | }; 121 | 122 | ///// public api ///// 123 | 124 | /** 125 | * Will move a given block length from @length 126 | * 127 | * @param block can be a block or an index (likely index of the block) 128 | * @param length < 0 or > 0 : decrease/increase block size of abs(length) px 129 | */ 130 | this.moveBlockLength = function (block, length) { 131 | 132 | var 133 | blockIndex = typeof block !== 'object' ? block : blocks.indexOf(block), 134 | composingBlocks, 135 | composite, 136 | availableLength, 137 | blockToMove; 138 | 139 | 140 | if (blockIndex < 0 || length === 0 || blockIndex >= blocks.length) { 141 | return; 142 | } 143 | 144 | blockToMove = blocks[blockIndex]; 145 | 146 | composingBlocks = (blocks.slice(0, blockIndex)).concat(blocks.slice(blockIndex + 1, blocks.length)); 147 | composite = Block.getNewComposite(composingBlocks); 148 | 149 | if (composite.canMoveLength(-length) !== true || blockToMove.canMoveLength(length) !== true) { 150 | return; 151 | } 152 | 153 | if (length < 0) { 154 | availableLength = (-1) * blockToMove.moveLength(length); 155 | composite.moveLength(availableLength); 156 | } else { 157 | availableLength = (-1) * composite.moveLength(-length); 158 | blockToMove.moveLength(availableLength); 159 | } 160 | 161 | //free memory 162 | composite.clean(); 163 | }; 164 | 165 | /** 166 | * move splitter it will affect all the blocks before until the previous/next splitter or the edge of area 167 | * @param splitter 168 | * @param length 169 | */ 170 | //todo mutualise with moveBlockLength 171 | this.moveSplitterLength = function (splitter, length) { 172 | 173 | var 174 | splitterIndex = blocks.indexOf(splitter), 175 | beforeComposite, 176 | afterComposite, 177 | availableLength; 178 | 179 | if (!Block.isSplitter(splitter) || splitterIndex === -1) { 180 | return; 181 | } 182 | 183 | beforeComposite = Block.getNewComposite(fromSplitterToSplitter(splitter, true)); 184 | afterComposite = Block.getNewComposite(fromSplitterToSplitter(splitter, false)); 185 | 186 | if (!beforeComposite.canMoveLength(length) || !afterComposite.canMoveLength(-length)) { 187 | return; 188 | } 189 | 190 | if (length < 0) { 191 | availableLength = (-1) * beforeComposite.moveLength(length); 192 | afterComposite.moveLength(availableLength); 193 | } else { 194 | availableLength = (-1) * afterComposite.moveLength(-length); 195 | beforeComposite.moveLength(availableLength); 196 | } 197 | 198 | afterComposite.clean(); 199 | beforeComposite.clean(); 200 | 201 | }; 202 | 203 | /** 204 | * return an object with the available length before the splitter and after the splitter 205 | * @param splitter 206 | * @returns {{before: *, after: *}} 207 | */ 208 | this.getSplitterRange = function (splitter) { 209 | 210 | var 211 | beforeSplitter = fromSplitterToSplitter(splitter, true), 212 | afterSplitter = fromSplitterToSplitter(splitter, false), 213 | toReturn = { 214 | before: beforeSplitter.getAvailableLength(), 215 | after: afterSplitter.getAvailableLength() 216 | }; 217 | 218 | beforeSplitter.clean(); 219 | afterSplitter.clean(); 220 | 221 | return toReturn; 222 | }; 223 | 224 | /** 225 | * lock/unlock a given block 226 | * @param block block or blockIndex 227 | * @param lock new value for block.isLocked 228 | */ 229 | this.toggleLockBlock = function (block, lock) { 230 | var 231 | blockIndex = typeof block !== 'object' ? block : blocks.indexOf(block), 232 | blockToLock; 233 | 234 | if (blockIndex >= 0 && blockIndex < blocks.length) { 235 | blockToLock = blocks[blockIndex]; 236 | blockToLock.isLocked = lock; 237 | } 238 | 239 | }; 240 | 241 | var fromSplitterToSplitter = function (splitter, before) { 242 | 243 | var 244 | splitterIndex = blocks.indexOf(splitter), 245 | blockGroup = before === true ? blocks.slice(0, splitterIndex) : blocks.slice(splitterIndex + 1, blocks.length), 246 | fn = before === true ? Array.prototype.pop : Array.prototype.shift, 247 | composite = [], 248 | testedBlock; 249 | 250 | while (testedBlock = fn.apply(blockGroup)) { 251 | if (Block.isSplitter(testedBlock)) { 252 | break; 253 | } else { 254 | composite.push(testedBlock); 255 | } 256 | } 257 | return Block.getNewComposite(composite); 258 | }; 259 | }]); 260 | })(angular); -------------------------------------------------------------------------------- /src/flexyLayout.css: -------------------------------------------------------------------------------- 1 | .flexy-layout { 2 | display: inline-block; 3 | width: 100%; 4 | height: 100%; 5 | -moz-box-sizing: border-box; 6 | -webkit-box-sizing: border-box; 7 | box-sizing: border-box; 8 | overflow: hidden; 9 | } 10 | 11 | 12 | .flexy-layout > .block { 13 | display: inline-block; 14 | float: left; 15 | overflow: hidden; 16 | } 17 | 18 | .flexy-layout.flexy-layout-column > .block { 19 | height: 100%; 20 | } 21 | 22 | .flexy-layout.flexy-layout-row > .block { 23 | width: 100%; 24 | } 25 | 26 | .block.splitter { 27 | position: relative; 28 | overflow: visible; 29 | } 30 | 31 | .flexy-layout.flexy-layout-column > .splitter{ 32 | width: 5px; 33 | } 34 | 35 | .flexy-layout.flexy-layout-row > .splitter{ 36 | height: 5px; 37 | } 38 | 39 | .flexy-layout.flexy-layout-column > .splitter > .ghost{ 40 | cursor: col-resize; 41 | } 42 | 43 | .flexy-layout.flexy-layout-row > .splitter > .ghost{ 44 | cursor: row-resize; 45 | } 46 | 47 | .ghost { 48 | float: left; 49 | position: relative; 50 | display: inline-block; 51 | height: 100%; 52 | width: 100%; 53 | z-index: 99; 54 | } 55 | 56 | .block-content { 57 | display: inline-block; 58 | width: 100%; 59 | height: 100%; 60 | box-sizing: border-box; 61 | -moz-box-sizing: border-box; 62 | -webkit-box-sizing: border-box; 63 | } -------------------------------------------------------------------------------- /test/config/karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | basePath = '../'; 2 | 3 | files = [ 4 | ANGULAR_SCENARIO, 5 | ANGULAR_SCENARIO_ADAPTER, 6 | 'test/e2e/**/*.js' 7 | ]; 8 | 9 | autoWatch = false; 10 | 11 | browsers = ['Chrome']; 12 | 13 | singleRun = true; 14 | 15 | proxies = { 16 | '/': 'http://localhost:8000/' 17 | }; 18 | 19 | junitReporter = { 20 | outputFile: 'test_out/e2e.xml', 21 | suite: 'e2e' 22 | }; 23 | -------------------------------------------------------------------------------- /test/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | basePath = '../..'; 2 | 3 | files = [ 4 | JASMINE, 5 | JASMINE_ADAPTER, 6 | 'components/angular/angular.js', 7 | 'test/lib/angular/angular-mocks.js', 8 | 'src/*.js', 9 | 'test/unit/*.js' 10 | ]; 11 | 12 | autoWatch = false; 13 | 14 | browsers = ['Chrome']; 15 | 16 | 17 | preprocessors = { 18 | '**/*.js': 'coverage' 19 | // 'smart-table-module/js/Table.js': 'coverage', 20 | // 'smart-table-module/js/Utilities.js': 'coverage', 21 | // 'smart-table-module/js/Filters.js': 'coverage', 22 | // 'smart-table-module/js/Directives.js': 'coverage' 23 | }; 24 | 25 | reporters = ['junit', 'progress','coverage']; 26 | 27 | 28 | junitReporter = { 29 | outputFile: 'test_out/unit.xml', 30 | suite: 'unit' 31 | }; 32 | 33 | coverageReporter = { 34 | type: 'html', 35 | dir: 'test_out/' 36 | }; 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/e2e/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/e2e/scenarios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ 4 | 5 | describe('my app', function() { 6 | 7 | beforeEach(function() { 8 | browser().navigateTo('../../app/index.html'); 9 | }); 10 | 11 | 12 | it('should automatically redirect to /view1 when location hash/fragment is empty', function() { 13 | expect(browser().location().url()).toBe("/view1"); 14 | }); 15 | 16 | 17 | describe('view1', function() { 18 | 19 | beforeEach(function() { 20 | browser().navigateTo('#/view1'); 21 | }); 22 | 23 | 24 | it('should render view1 when user navigates to /view1', function() { 25 | expect(element('[ng-view] p:first').text()). 26 | toMatch(/partial for view 1/); 27 | }); 28 | 29 | }); 30 | 31 | 32 | describe('view2', function() { 33 | 34 | beforeEach(function() { 35 | browser().navigateTo('#/view2'); 36 | }); 37 | 38 | 39 | it('should render view2 when user navigates to /view2', function() { 40 | expect(element('[ng-view] p:first').text()). 41 | toMatch(/partial for view 2/); 42 | }); 43 | 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/lib/angular/angular-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | * 6 | * TODO(vojta): wrap whole file into closure during build 7 | */ 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name angular.mock 12 | * @description 13 | * 14 | * Namespace from 'angular-mocks.js' which contains testing related code. 15 | */ 16 | angular.mock = {}; 17 | 18 | /** 19 | * ! This is a private undocumented service ! 20 | * 21 | * @name ngMock.$browser 22 | * 23 | * @description 24 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 25 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 26 | * cookies, etc... 27 | * 28 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 29 | * that there are several helper methods available which can be used in tests. 30 | */ 31 | angular.mock.$BrowserProvider = function() { 32 | this.$get = function(){ 33 | return new angular.mock.$Browser(); 34 | }; 35 | }; 36 | 37 | angular.mock.$Browser = function() { 38 | var self = this; 39 | 40 | this.isMock = true; 41 | self.$$url = "http://server/"; 42 | self.$$lastUrl = self.$$url; // used by url polling fn 43 | self.pollFns = []; 44 | 45 | // TODO(vojta): remove this temporary api 46 | self.$$completeOutstandingRequest = angular.noop; 47 | self.$$incOutstandingRequestCount = angular.noop; 48 | 49 | 50 | // register url polling fn 51 | 52 | self.onUrlChange = function(listener) { 53 | self.pollFns.push( 54 | function() { 55 | if (self.$$lastUrl != self.$$url) { 56 | self.$$lastUrl = self.$$url; 57 | listener(self.$$url); 58 | } 59 | } 60 | ); 61 | 62 | return listener; 63 | }; 64 | 65 | self.cookieHash = {}; 66 | self.lastCookieHash = {}; 67 | self.deferredFns = []; 68 | self.deferredNextId = 0; 69 | 70 | self.defer = function(fn, delay) { 71 | delay = delay || 0; 72 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 73 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 74 | return self.deferredNextId++; 75 | }; 76 | 77 | 78 | self.defer.now = 0; 79 | 80 | 81 | self.defer.cancel = function(deferId) { 82 | var fnIndex; 83 | 84 | angular.forEach(self.deferredFns, function(fn, index) { 85 | if (fn.id === deferId) fnIndex = index; 86 | }); 87 | 88 | if (fnIndex !== undefined) { 89 | self.deferredFns.splice(fnIndex, 1); 90 | return true; 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | 97 | /** 98 | * @name ngMock.$browser#defer.flush 99 | * @methodOf ngMock.$browser 100 | * 101 | * @description 102 | * Flushes all pending requests and executes the defer callbacks. 103 | * 104 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 105 | */ 106 | self.defer.flush = function(delay) { 107 | if (angular.isDefined(delay)) { 108 | self.defer.now += delay; 109 | } else { 110 | if (self.deferredFns.length) { 111 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 112 | } else { 113 | throw Error('No deferred tasks to be flushed'); 114 | } 115 | } 116 | 117 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 118 | self.deferredFns.shift().fn(); 119 | } 120 | }; 121 | /** 122 | * @name ngMock.$browser#defer.now 123 | * @propertyOf ngMock.$browser 124 | * 125 | * @description 126 | * Current milliseconds mock time. 127 | */ 128 | 129 | self.$$baseHref = ''; 130 | self.baseHref = function() { 131 | return this.$$baseHref; 132 | }; 133 | }; 134 | angular.mock.$Browser.prototype = { 135 | 136 | /** 137 | * @name ngMock.$browser#poll 138 | * @methodOf ngMock.$browser 139 | * 140 | * @description 141 | * run all fns in pollFns 142 | */ 143 | poll: function poll() { 144 | angular.forEach(this.pollFns, function(pollFn){ 145 | pollFn(); 146 | }); 147 | }, 148 | 149 | addPollFn: function(pollFn) { 150 | this.pollFns.push(pollFn); 151 | return pollFn; 152 | }, 153 | 154 | url: function(url, replace) { 155 | if (url) { 156 | this.$$url = url; 157 | return this; 158 | } 159 | 160 | return this.$$url; 161 | }, 162 | 163 | cookies: function(name, value) { 164 | if (name) { 165 | if (value == undefined) { 166 | delete this.cookieHash[name]; 167 | } else { 168 | if (angular.isString(value) && //strings only 169 | value.length <= 4096) { //strict cookie storage limits 170 | this.cookieHash[name] = value; 171 | } 172 | } 173 | } else { 174 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 175 | this.lastCookieHash = angular.copy(this.cookieHash); 176 | this.cookieHash = angular.copy(this.cookieHash); 177 | } 178 | return this.cookieHash; 179 | } 180 | }, 181 | 182 | notifyWhenNoOutstandingRequests: function(fn) { 183 | fn(); 184 | } 185 | }; 186 | 187 | 188 | /** 189 | * @ngdoc object 190 | * @name ngMock.$exceptionHandlerProvider 191 | * 192 | * @description 193 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed 194 | * into the `$exceptionHandler`. 195 | */ 196 | 197 | /** 198 | * @ngdoc object 199 | * @name ngMock.$exceptionHandler 200 | * 201 | * @description 202 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 203 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 204 | * information. 205 | * 206 | * 207 | *
 208 |  *   describe('$exceptionHandlerProvider', function() {
 209 |  *
 210 |  *     it('should capture log messages and exceptions', function() {
 211 |  *
 212 |  *       module(function($exceptionHandlerProvider) {
 213 |  *         $exceptionHandlerProvider.mode('log');
 214 |  *       });
 215 |  *
 216 |  *       inject(function($log, $exceptionHandler, $timeout) {
 217 |  *         $timeout(function() { $log.log(1); });
 218 |  *         $timeout(function() { $log.log(2); throw 'banana peel'; });
 219 |  *         $timeout(function() { $log.log(3); });
 220 |  *         expect($exceptionHandler.errors).toEqual([]);
 221 |  *         expect($log.assertEmpty());
 222 |  *         $timeout.flush();
 223 |  *         expect($exceptionHandler.errors).toEqual(['banana peel']);
 224 |  *         expect($log.log.logs).toEqual([[1], [2], [3]]);
 225 |  *       });
 226 |  *     });
 227 |  *   });
 228 |  * 
229 | */ 230 | 231 | angular.mock.$ExceptionHandlerProvider = function() { 232 | var handler; 233 | 234 | /** 235 | * @ngdoc method 236 | * @name ngMock.$exceptionHandlerProvider#mode 237 | * @methodOf ngMock.$exceptionHandlerProvider 238 | * 239 | * @description 240 | * Sets the logging mode. 241 | * 242 | * @param {string} mode Mode of operation, defaults to `rethrow`. 243 | * 244 | * - `rethrow`: If any errors are are passed into the handler in tests, it typically 245 | * means that there is a bug in the application or test, so this mock will 246 | * make these tests fail. 247 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` mode stores an 248 | * array of errors in `$exceptionHandler.errors`, to allow later assertion of them. 249 | * See {@link ngMock.$log#assertEmpty assertEmpty()} and 250 | * {@link ngMock.$log#reset reset()} 251 | */ 252 | this.mode = function(mode) { 253 | switch(mode) { 254 | case 'rethrow': 255 | handler = function(e) { 256 | throw e; 257 | }; 258 | break; 259 | case 'log': 260 | var errors = []; 261 | 262 | handler = function(e) { 263 | if (arguments.length == 1) { 264 | errors.push(e); 265 | } else { 266 | errors.push([].slice.call(arguments, 0)); 267 | } 268 | }; 269 | 270 | handler.errors = errors; 271 | break; 272 | default: 273 | throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 274 | } 275 | }; 276 | 277 | this.$get = function() { 278 | return handler; 279 | }; 280 | 281 | this.mode('rethrow'); 282 | }; 283 | 284 | 285 | /** 286 | * @ngdoc service 287 | * @name ngMock.$log 288 | * 289 | * @description 290 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 291 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 292 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 293 | * 294 | */ 295 | angular.mock.$LogProvider = function() { 296 | 297 | function concat(array1, array2, index) { 298 | return array1.concat(Array.prototype.slice.call(array2, index)); 299 | } 300 | 301 | 302 | this.$get = function () { 303 | var $log = { 304 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 305 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 306 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 307 | error: function() { $log.error.logs.push(concat([], arguments, 0)); } 308 | }; 309 | 310 | /** 311 | * @ngdoc method 312 | * @name ngMock.$log#reset 313 | * @methodOf ngMock.$log 314 | * 315 | * @description 316 | * Reset all of the logging arrays to empty. 317 | */ 318 | $log.reset = function () { 319 | /** 320 | * @ngdoc property 321 | * @name ngMock.$log#log.logs 322 | * @propertyOf ngMock.$log 323 | * 324 | * @description 325 | * Array of logged messages. 326 | */ 327 | $log.log.logs = []; 328 | /** 329 | * @ngdoc property 330 | * @name ngMock.$log#warn.logs 331 | * @propertyOf ngMock.$log 332 | * 333 | * @description 334 | * Array of logged messages. 335 | */ 336 | $log.warn.logs = []; 337 | /** 338 | * @ngdoc property 339 | * @name ngMock.$log#info.logs 340 | * @propertyOf ngMock.$log 341 | * 342 | * @description 343 | * Array of logged messages. 344 | */ 345 | $log.info.logs = []; 346 | /** 347 | * @ngdoc property 348 | * @name ngMock.$log#error.logs 349 | * @propertyOf ngMock.$log 350 | * 351 | * @description 352 | * Array of logged messages. 353 | */ 354 | $log.error.logs = []; 355 | }; 356 | 357 | /** 358 | * @ngdoc method 359 | * @name ngMock.$log#assertEmpty 360 | * @methodOf ngMock.$log 361 | * 362 | * @description 363 | * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. 364 | */ 365 | $log.assertEmpty = function() { 366 | var errors = []; 367 | angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { 368 | angular.forEach($log[logLevel].logs, function(log) { 369 | angular.forEach(log, function (logItem) { 370 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); 371 | }); 372 | }); 373 | }); 374 | if (errors.length) { 375 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + 376 | "log message was not checked and removed:"); 377 | errors.push(''); 378 | throw new Error(errors.join('\n---------\n')); 379 | } 380 | }; 381 | 382 | $log.reset(); 383 | return $log; 384 | }; 385 | }; 386 | 387 | 388 | (function() { 389 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 390 | 391 | function jsonStringToDate(string){ 392 | var match; 393 | if (match = string.match(R_ISO8061_STR)) { 394 | var date = new Date(0), 395 | tzHour = 0, 396 | tzMin = 0; 397 | if (match[9]) { 398 | tzHour = int(match[9] + match[10]); 399 | tzMin = int(match[9] + match[11]); 400 | } 401 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 402 | date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); 403 | return date; 404 | } 405 | return string; 406 | } 407 | 408 | function int(str) { 409 | return parseInt(str, 10); 410 | } 411 | 412 | function padNumber(num, digits, trim) { 413 | var neg = ''; 414 | if (num < 0) { 415 | neg = '-'; 416 | num = -num; 417 | } 418 | num = '' + num; 419 | while(num.length < digits) num = '0' + num; 420 | if (trim) 421 | num = num.substr(num.length - digits); 422 | return neg + num; 423 | } 424 | 425 | 426 | /** 427 | * @ngdoc object 428 | * @name angular.mock.TzDate 429 | * @description 430 | * 431 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 432 | * 433 | * Mock of the Date type which has its timezone specified via constructor arg. 434 | * 435 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 436 | * offset, so that we can test code that depends on local timezone settings without dependency on 437 | * the time zone settings of the machine where the code is running. 438 | * 439 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 440 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 441 | * 442 | * @example 443 | * !!!! WARNING !!!!! 444 | * This is not a complete Date object so only methods that were implemented can be called safely. 445 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 446 | * 447 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 448 | * incomplete we might be missing some non-standard methods. This can result in errors like: 449 | * "Date.prototype.foo called on incompatible Object". 450 | * 451 | *
 452 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 453 |    * newYearInBratislava.getTimezoneOffset() => -60;
 454 |    * newYearInBratislava.getFullYear() => 2010;
 455 |    * newYearInBratislava.getMonth() => 0;
 456 |    * newYearInBratislava.getDate() => 1;
 457 |    * newYearInBratislava.getHours() => 0;
 458 |    * newYearInBratislava.getMinutes() => 0;
 459 |    * 
460 | * 461 | */ 462 | angular.mock.TzDate = function (offset, timestamp) { 463 | var self = new Date(0); 464 | if (angular.isString(timestamp)) { 465 | var tsStr = timestamp; 466 | 467 | self.origDate = jsonStringToDate(timestamp); 468 | 469 | timestamp = self.origDate.getTime(); 470 | if (isNaN(timestamp)) 471 | throw { 472 | name: "Illegal Argument", 473 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 474 | }; 475 | } else { 476 | self.origDate = new Date(timestamp); 477 | } 478 | 479 | var localOffset = new Date(timestamp).getTimezoneOffset(); 480 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 481 | self.date = new Date(timestamp + self.offsetDiff); 482 | 483 | self.getTime = function() { 484 | return self.date.getTime() - self.offsetDiff; 485 | }; 486 | 487 | self.toLocaleDateString = function() { 488 | return self.date.toLocaleDateString(); 489 | }; 490 | 491 | self.getFullYear = function() { 492 | return self.date.getFullYear(); 493 | }; 494 | 495 | self.getMonth = function() { 496 | return self.date.getMonth(); 497 | }; 498 | 499 | self.getDate = function() { 500 | return self.date.getDate(); 501 | }; 502 | 503 | self.getHours = function() { 504 | return self.date.getHours(); 505 | }; 506 | 507 | self.getMinutes = function() { 508 | return self.date.getMinutes(); 509 | }; 510 | 511 | self.getSeconds = function() { 512 | return self.date.getSeconds(); 513 | }; 514 | 515 | self.getTimezoneOffset = function() { 516 | return offset * 60; 517 | }; 518 | 519 | self.getUTCFullYear = function() { 520 | return self.origDate.getUTCFullYear(); 521 | }; 522 | 523 | self.getUTCMonth = function() { 524 | return self.origDate.getUTCMonth(); 525 | }; 526 | 527 | self.getUTCDate = function() { 528 | return self.origDate.getUTCDate(); 529 | }; 530 | 531 | self.getUTCHours = function() { 532 | return self.origDate.getUTCHours(); 533 | }; 534 | 535 | self.getUTCMinutes = function() { 536 | return self.origDate.getUTCMinutes(); 537 | }; 538 | 539 | self.getUTCSeconds = function() { 540 | return self.origDate.getUTCSeconds(); 541 | }; 542 | 543 | self.getUTCMilliseconds = function() { 544 | return self.origDate.getUTCMilliseconds(); 545 | }; 546 | 547 | self.getDay = function() { 548 | return self.date.getDay(); 549 | }; 550 | 551 | // provide this method only on browsers that already have it 552 | if (self.toISOString) { 553 | self.toISOString = function() { 554 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 555 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 556 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 557 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 558 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 559 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 560 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' 561 | } 562 | } 563 | 564 | //hide all methods not implemented in this mock that the Date prototype exposes 565 | var unimplementedMethods = ['getMilliseconds', 'getUTCDay', 566 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 567 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 568 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 569 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 570 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 571 | 572 | angular.forEach(unimplementedMethods, function(methodName) { 573 | self[methodName] = function() { 574 | throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 575 | }; 576 | }); 577 | 578 | return self; 579 | }; 580 | 581 | //make "tzDateInstance instanceof Date" return true 582 | angular.mock.TzDate.prototype = Date.prototype; 583 | })(); 584 | 585 | 586 | /** 587 | * @ngdoc function 588 | * @name angular.mock.dump 589 | * @description 590 | * 591 | * *NOTE*: this is not an injectable instance, just a globally available function. 592 | * 593 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. 594 | * 595 | * This method is also available on window, where it can be used to display objects on debug console. 596 | * 597 | * @param {*} object - any object to turn into string. 598 | * @return {string} a serialized string of the argument 599 | */ 600 | angular.mock.dump = function(object) { 601 | return serialize(object); 602 | 603 | function serialize(object) { 604 | var out; 605 | 606 | if (angular.isElement(object)) { 607 | object = angular.element(object); 608 | out = angular.element('
'); 609 | angular.forEach(object, function(element) { 610 | out.append(angular.element(element).clone()); 611 | }); 612 | out = out.html(); 613 | } else if (angular.isArray(object)) { 614 | out = []; 615 | angular.forEach(object, function(o) { 616 | out.push(serialize(o)); 617 | }); 618 | out = '[ ' + out.join(', ') + ' ]'; 619 | } else if (angular.isObject(object)) { 620 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 621 | out = serializeScope(object); 622 | } else if (object instanceof Error) { 623 | out = object.stack || ('' + object.name + ': ' + object.message); 624 | } else { 625 | out = angular.toJson(object, true); 626 | } 627 | } else { 628 | out = String(object); 629 | } 630 | 631 | return out; 632 | } 633 | 634 | function serializeScope(scope, offset) { 635 | offset = offset || ' '; 636 | var log = [offset + 'Scope(' + scope.$id + '): {']; 637 | for ( var key in scope ) { 638 | if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { 639 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 640 | } 641 | } 642 | var child = scope.$$childHead; 643 | while(child) { 644 | log.push(serializeScope(child, offset + ' ')); 645 | child = child.$$nextSibling; 646 | } 647 | log.push('}'); 648 | return log.join('\n' + offset); 649 | } 650 | }; 651 | 652 | /** 653 | * @ngdoc object 654 | * @name ngMock.$httpBackend 655 | * @description 656 | * Fake HTTP backend implementation suitable for unit testing application that use the 657 | * {@link ng.$http $http service}. 658 | * 659 | * *Note*: For fake http backend implementation suitable for end-to-end testing or backend-less 660 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 661 | * 662 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 663 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 664 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 665 | * to verify whether a certain request has been sent or not, or alternatively just let the 666 | * application make requests, respond with pre-trained responses and assert that the end result is 667 | * what we expect it to be. 668 | * 669 | * This mock implementation can be used to respond with static or dynamic responses via the 670 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 671 | * 672 | * When an Angular application needs some data from a server, it calls the $http service, which 673 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 674 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 675 | * the requests and respond with some testing data without sending a request to real server. 676 | * 677 | * There are two ways to specify what test data should be returned as http responses by the mock 678 | * backend when the code under test makes http requests: 679 | * 680 | * - `$httpBackend.expect` - specifies a request expectation 681 | * - `$httpBackend.when` - specifies a backend definition 682 | * 683 | * 684 | * # Request Expectations vs Backend Definitions 685 | * 686 | * Request expectations provide a way to make assertions about requests made by the application and 687 | * to define responses for those requests. The test will fail if the expected requests are not made 688 | * or they are made in the wrong order. 689 | * 690 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 691 | * if a particular request was made or not, it just returns a trained response if a request is made. 692 | * The test will pass whether or not the request gets made during testing. 693 | * 694 | * 695 | * 696 | * 697 | * 698 | * 699 | * 700 | * 701 | * 702 | * 703 | * 704 | * 705 | * 706 | * 707 | * 708 | * 709 | * 710 | * 711 | * 712 | * 713 | * 714 | * 715 | * 716 | * 717 | * 718 | * 719 | * 720 | * 721 | * 722 | * 723 | * 724 | * 725 | * 726 | * 727 | *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
728 | * 729 | * In cases where both backend definitions and request expectations are specified during unit 730 | * testing, the request expectations are evaluated first. 731 | * 732 | * If a request expectation has no response specified, the algorithm will search your backend 733 | * definitions for an appropriate response. 734 | * 735 | * If a request didn't match any expectation or if the expectation doesn't have the response 736 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 737 | * the request. The response from the first matched definition is returned. 738 | * 739 | * 740 | * # Flushing HTTP requests 741 | * 742 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 743 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 744 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 745 | * synchronously because that would change the execution of the code under test. For this reason the 746 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 747 | * requests and thus preserving the async api of the backend, while allowing the test to execute 748 | * synchronously. 749 | * 750 | * 751 | * # Unit testing with mock $httpBackend 752 | * 753 | *
 754 |    // controller
 755 |    function MyController($scope, $http) {
 756 |      $http.get('/auth.py').success(function(data) {
 757 |        $scope.user = data;
 758 |      });
 759 | 
 760 |      this.saveMessage = function(message) {
 761 |        $scope.status = 'Saving...';
 762 |        $http.post('/add-msg.py', message).success(function(response) {
 763 |          $scope.status = '';
 764 |        }).error(function() {
 765 |          $scope.status = 'ERROR!';
 766 |        });
 767 |      };
 768 |    }
 769 | 
 770 |    // testing controller
 771 |    var $httpBackend;
 772 | 
 773 |    beforeEach(inject(function($injector) {
 774 |      $httpBackend = $injector.get('$httpBackend');
 775 | 
 776 |      // backend definition common for all tests
 777 |      $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 778 |    }));
 779 | 
 780 | 
 781 |    afterEach(function() {
 782 |      $httpBackend.verifyNoOutstandingExpectation();
 783 |      $httpBackend.verifyNoOutstandingRequest();
 784 |    });
 785 | 
 786 | 
 787 |    it('should fetch authentication token', function() {
 788 |      $httpBackend.expectGET('/auth.py');
 789 |      var controller = scope.$new(MyController);
 790 |      $httpBackend.flush();
 791 |    });
 792 | 
 793 | 
 794 |    it('should send msg to server', function() {
 795 |      // now you don’t care about the authentication, but
 796 |      // the controller will still send the request and
 797 |      // $httpBackend will respond without you having to
 798 |      // specify the expectation and response for this request
 799 |      $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 800 | 
 801 |      var controller = scope.$new(MyController);
 802 |      $httpBackend.flush();
 803 |      controller.saveMessage('message content');
 804 |      expect(controller.status).toBe('Saving...');
 805 |      $httpBackend.flush();
 806 |      expect(controller.status).toBe('');
 807 |    });
 808 | 
 809 | 
 810 |    it('should send auth header', function() {
 811 |      $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
 812 |        // check if the header was send, if it wasn't the expectation won't
 813 |        // match the request and the test will fail
 814 |        return headers['Authorization'] == 'xxx';
 815 |      }).respond(201, '');
 816 | 
 817 |      var controller = scope.$new(MyController);
 818 |      controller.saveMessage('whatever');
 819 |      $httpBackend.flush();
 820 |    });
 821 |    
822 | */ 823 | angular.mock.$HttpBackendProvider = function() { 824 | this.$get = [createHttpBackendMock]; 825 | }; 826 | 827 | /** 828 | * General factory function for $httpBackend mock. 829 | * Returns instance for unit testing (when no arguments specified): 830 | * - passing through is disabled 831 | * - auto flushing is disabled 832 | * 833 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 834 | * - passing through (delegating request to real backend) is enabled 835 | * - auto flushing is enabled 836 | * 837 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 838 | * @param {Object=} $browser Auto-flushing enabled if specified 839 | * @return {Object} Instance of $httpBackend mock 840 | */ 841 | function createHttpBackendMock($delegate, $browser) { 842 | var definitions = [], 843 | expectations = [], 844 | responses = [], 845 | responsesPush = angular.bind(responses, responses.push); 846 | 847 | function createResponse(status, data, headers) { 848 | if (angular.isFunction(status)) return status; 849 | 850 | return function() { 851 | return angular.isNumber(status) 852 | ? [status, data, headers] 853 | : [200, status, data]; 854 | }; 855 | } 856 | 857 | // TODO(vojta): change params to: method, url, data, headers, callback 858 | function $httpBackend(method, url, data, callback, headers) { 859 | var xhr = new MockXhr(), 860 | expectation = expectations[0], 861 | wasExpected = false; 862 | 863 | function prettyPrint(data) { 864 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 865 | ? data 866 | : angular.toJson(data); 867 | } 868 | 869 | if (expectation && expectation.match(method, url)) { 870 | if (!expectation.matchData(data)) 871 | throw Error('Expected ' + expectation + ' with different data\n' + 872 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 873 | 874 | if (!expectation.matchHeaders(headers)) 875 | throw Error('Expected ' + expectation + ' with different headers\n' + 876 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 877 | prettyPrint(headers)); 878 | 879 | expectations.shift(); 880 | 881 | if (expectation.response) { 882 | responses.push(function() { 883 | var response = expectation.response(method, url, data, headers); 884 | xhr.$$respHeaders = response[2]; 885 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 886 | }); 887 | return; 888 | } 889 | wasExpected = true; 890 | } 891 | 892 | var i = -1, definition; 893 | while ((definition = definitions[++i])) { 894 | if (definition.match(method, url, data, headers || {})) { 895 | if (definition.response) { 896 | // if $browser specified, we do auto flush all requests 897 | ($browser ? $browser.defer : responsesPush)(function() { 898 | var response = definition.response(method, url, data, headers); 899 | xhr.$$respHeaders = response[2]; 900 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 901 | }); 902 | } else if (definition.passThrough) { 903 | $delegate(method, url, data, callback, headers); 904 | } else throw Error('No response defined !'); 905 | return; 906 | } 907 | } 908 | throw wasExpected ? 909 | Error('No response defined !') : 910 | Error('Unexpected request: ' + method + ' ' + url + '\n' + 911 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 912 | } 913 | 914 | /** 915 | * @ngdoc method 916 | * @name ngMock.$httpBackend#when 917 | * @methodOf ngMock.$httpBackend 918 | * @description 919 | * Creates a new backend definition. 920 | * 921 | * @param {string} method HTTP method. 922 | * @param {string|RegExp} url HTTP url. 923 | * @param {(string|RegExp)=} data HTTP request body. 924 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 925 | * object and returns true if the headers match the current definition. 926 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 927 | * request is handled. 928 | * 929 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 930 | * – The respond method takes a set of static data to be returned or a function that can return 931 | * an array containing response status (number), response data (string) and response headers 932 | * (Object). 933 | */ 934 | $httpBackend.when = function(method, url, data, headers) { 935 | var definition = new MockHttpExpectation(method, url, data, headers), 936 | chain = { 937 | respond: function(status, data, headers) { 938 | definition.response = createResponse(status, data, headers); 939 | } 940 | }; 941 | 942 | if ($browser) { 943 | chain.passThrough = function() { 944 | definition.passThrough = true; 945 | }; 946 | } 947 | 948 | definitions.push(definition); 949 | return chain; 950 | }; 951 | 952 | /** 953 | * @ngdoc method 954 | * @name ngMock.$httpBackend#whenGET 955 | * @methodOf ngMock.$httpBackend 956 | * @description 957 | * Creates a new backend definition for GET requests. For more info see `when()`. 958 | * 959 | * @param {string|RegExp} url HTTP url. 960 | * @param {(Object|function(Object))=} headers HTTP headers. 961 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 962 | * request is handled. 963 | */ 964 | 965 | /** 966 | * @ngdoc method 967 | * @name ngMock.$httpBackend#whenHEAD 968 | * @methodOf ngMock.$httpBackend 969 | * @description 970 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 971 | * 972 | * @param {string|RegExp} url HTTP url. 973 | * @param {(Object|function(Object))=} headers HTTP headers. 974 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 975 | * request is handled. 976 | */ 977 | 978 | /** 979 | * @ngdoc method 980 | * @name ngMock.$httpBackend#whenDELETE 981 | * @methodOf ngMock.$httpBackend 982 | * @description 983 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 984 | * 985 | * @param {string|RegExp} url HTTP url. 986 | * @param {(Object|function(Object))=} headers HTTP headers. 987 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 988 | * request is handled. 989 | */ 990 | 991 | /** 992 | * @ngdoc method 993 | * @name ngMock.$httpBackend#whenPOST 994 | * @methodOf ngMock.$httpBackend 995 | * @description 996 | * Creates a new backend definition for POST requests. For more info see `when()`. 997 | * 998 | * @param {string|RegExp} url HTTP url. 999 | * @param {(string|RegExp)=} data HTTP request body. 1000 | * @param {(Object|function(Object))=} headers HTTP headers. 1001 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1002 | * request is handled. 1003 | */ 1004 | 1005 | /** 1006 | * @ngdoc method 1007 | * @name ngMock.$httpBackend#whenPUT 1008 | * @methodOf ngMock.$httpBackend 1009 | * @description 1010 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1011 | * 1012 | * @param {string|RegExp} url HTTP url. 1013 | * @param {(string|RegExp)=} data HTTP request body. 1014 | * @param {(Object|function(Object))=} headers HTTP headers. 1015 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1016 | * request is handled. 1017 | */ 1018 | 1019 | /** 1020 | * @ngdoc method 1021 | * @name ngMock.$httpBackend#whenJSONP 1022 | * @methodOf ngMock.$httpBackend 1023 | * @description 1024 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1025 | * 1026 | * @param {string|RegExp} url HTTP url. 1027 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1028 | * request is handled. 1029 | */ 1030 | createShortMethods('when'); 1031 | 1032 | 1033 | /** 1034 | * @ngdoc method 1035 | * @name ngMock.$httpBackend#expect 1036 | * @methodOf ngMock.$httpBackend 1037 | * @description 1038 | * Creates a new request expectation. 1039 | * 1040 | * @param {string} method HTTP method. 1041 | * @param {string|RegExp} url HTTP url. 1042 | * @param {(string|RegExp)=} data HTTP request body. 1043 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1044 | * object and returns true if the headers match the current expectation. 1045 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1046 | * request is handled. 1047 | * 1048 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1049 | * – The respond method takes a set of static data to be returned or a function that can return 1050 | * an array containing response status (number), response data (string) and response headers 1051 | * (Object). 1052 | */ 1053 | $httpBackend.expect = function(method, url, data, headers) { 1054 | var expectation = new MockHttpExpectation(method, url, data, headers); 1055 | expectations.push(expectation); 1056 | return { 1057 | respond: function(status, data, headers) { 1058 | expectation.response = createResponse(status, data, headers); 1059 | } 1060 | }; 1061 | }; 1062 | 1063 | 1064 | /** 1065 | * @ngdoc method 1066 | * @name ngMock.$httpBackend#expectGET 1067 | * @methodOf ngMock.$httpBackend 1068 | * @description 1069 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1070 | * 1071 | * @param {string|RegExp} url HTTP url. 1072 | * @param {Object=} headers HTTP headers. 1073 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1074 | * request is handled. See #expect for more info. 1075 | */ 1076 | 1077 | /** 1078 | * @ngdoc method 1079 | * @name ngMock.$httpBackend#expectHEAD 1080 | * @methodOf ngMock.$httpBackend 1081 | * @description 1082 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1083 | * 1084 | * @param {string|RegExp} url HTTP url. 1085 | * @param {Object=} headers HTTP headers. 1086 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1087 | * request is handled. 1088 | */ 1089 | 1090 | /** 1091 | * @ngdoc method 1092 | * @name ngMock.$httpBackend#expectDELETE 1093 | * @methodOf ngMock.$httpBackend 1094 | * @description 1095 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1096 | * 1097 | * @param {string|RegExp} url HTTP url. 1098 | * @param {Object=} headers HTTP headers. 1099 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1100 | * request is handled. 1101 | */ 1102 | 1103 | /** 1104 | * @ngdoc method 1105 | * @name ngMock.$httpBackend#expectPOST 1106 | * @methodOf ngMock.$httpBackend 1107 | * @description 1108 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1109 | * 1110 | * @param {string|RegExp} url HTTP url. 1111 | * @param {(string|RegExp)=} data HTTP request body. 1112 | * @param {Object=} headers HTTP headers. 1113 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1114 | * request is handled. 1115 | */ 1116 | 1117 | /** 1118 | * @ngdoc method 1119 | * @name ngMock.$httpBackend#expectPUT 1120 | * @methodOf ngMock.$httpBackend 1121 | * @description 1122 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1123 | * 1124 | * @param {string|RegExp} url HTTP url. 1125 | * @param {(string|RegExp)=} data HTTP request body. 1126 | * @param {Object=} headers HTTP headers. 1127 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1128 | * request is handled. 1129 | */ 1130 | 1131 | /** 1132 | * @ngdoc method 1133 | * @name ngMock.$httpBackend#expectPATCH 1134 | * @methodOf ngMock.$httpBackend 1135 | * @description 1136 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1137 | * 1138 | * @param {string|RegExp} url HTTP url. 1139 | * @param {(string|RegExp)=} data HTTP request body. 1140 | * @param {Object=} headers HTTP headers. 1141 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1142 | * request is handled. 1143 | */ 1144 | 1145 | /** 1146 | * @ngdoc method 1147 | * @name ngMock.$httpBackend#expectJSONP 1148 | * @methodOf ngMock.$httpBackend 1149 | * @description 1150 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1151 | * 1152 | * @param {string|RegExp} url HTTP url. 1153 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1154 | * request is handled. 1155 | */ 1156 | createShortMethods('expect'); 1157 | 1158 | 1159 | /** 1160 | * @ngdoc method 1161 | * @name ngMock.$httpBackend#flush 1162 | * @methodOf ngMock.$httpBackend 1163 | * @description 1164 | * Flushes all pending requests using the trained responses. 1165 | * 1166 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1167 | * all pending requests will be flushed. If there are no pending requests when the flush method 1168 | * is called an exception is thrown (as this typically a sign of programming error). 1169 | */ 1170 | $httpBackend.flush = function(count) { 1171 | if (!responses.length) throw Error('No pending request to flush !'); 1172 | 1173 | if (angular.isDefined(count)) { 1174 | while (count--) { 1175 | if (!responses.length) throw Error('No more pending request to flush !'); 1176 | responses.shift()(); 1177 | } 1178 | } else { 1179 | while (responses.length) { 1180 | responses.shift()(); 1181 | } 1182 | } 1183 | $httpBackend.verifyNoOutstandingExpectation(); 1184 | }; 1185 | 1186 | 1187 | /** 1188 | * @ngdoc method 1189 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1190 | * @methodOf ngMock.$httpBackend 1191 | * @description 1192 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1193 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1194 | * 1195 | * Typically, you would call this method following each test case that asserts requests using an 1196 | * "afterEach" clause. 1197 | * 1198 | *
1199 |    *   afterEach($httpBackend.verifyExpectations);
1200 |    * 
1201 | */ 1202 | $httpBackend.verifyNoOutstandingExpectation = function() { 1203 | if (expectations.length) { 1204 | throw Error('Unsatisfied requests: ' + expectations.join(', ')); 1205 | } 1206 | }; 1207 | 1208 | 1209 | /** 1210 | * @ngdoc method 1211 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1212 | * @methodOf ngMock.$httpBackend 1213 | * @description 1214 | * Verifies that there are no outstanding requests that need to be flushed. 1215 | * 1216 | * Typically, you would call this method following each test case that asserts requests using an 1217 | * "afterEach" clause. 1218 | * 1219 | *
1220 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1221 |    * 
1222 | */ 1223 | $httpBackend.verifyNoOutstandingRequest = function() { 1224 | if (responses.length) { 1225 | throw Error('Unflushed requests: ' + responses.length); 1226 | } 1227 | }; 1228 | 1229 | 1230 | /** 1231 | * @ngdoc method 1232 | * @name ngMock.$httpBackend#resetExpectations 1233 | * @methodOf ngMock.$httpBackend 1234 | * @description 1235 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1236 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1237 | * $httpBackend mock. 1238 | */ 1239 | $httpBackend.resetExpectations = function() { 1240 | expectations.length = 0; 1241 | responses.length = 0; 1242 | }; 1243 | 1244 | return $httpBackend; 1245 | 1246 | 1247 | function createShortMethods(prefix) { 1248 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1249 | $httpBackend[prefix + method] = function(url, headers) { 1250 | return $httpBackend[prefix](method, url, undefined, headers) 1251 | } 1252 | }); 1253 | 1254 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1255 | $httpBackend[prefix + method] = function(url, data, headers) { 1256 | return $httpBackend[prefix](method, url, data, headers) 1257 | } 1258 | }); 1259 | } 1260 | } 1261 | 1262 | function MockHttpExpectation(method, url, data, headers) { 1263 | 1264 | this.data = data; 1265 | this.headers = headers; 1266 | 1267 | this.match = function(m, u, d, h) { 1268 | if (method != m) return false; 1269 | if (!this.matchUrl(u)) return false; 1270 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1271 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1272 | return true; 1273 | }; 1274 | 1275 | this.matchUrl = function(u) { 1276 | if (!url) return true; 1277 | if (angular.isFunction(url.test)) return url.test(u); 1278 | return url == u; 1279 | }; 1280 | 1281 | this.matchHeaders = function(h) { 1282 | if (angular.isUndefined(headers)) return true; 1283 | if (angular.isFunction(headers)) return headers(h); 1284 | return angular.equals(headers, h); 1285 | }; 1286 | 1287 | this.matchData = function(d) { 1288 | if (angular.isUndefined(data)) return true; 1289 | if (data && angular.isFunction(data.test)) return data.test(d); 1290 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1291 | return data == d; 1292 | }; 1293 | 1294 | this.toString = function() { 1295 | return method + ' ' + url; 1296 | }; 1297 | } 1298 | 1299 | function MockXhr() { 1300 | 1301 | // hack for testing $http, $httpBackend 1302 | MockXhr.$$lastInstance = this; 1303 | 1304 | this.open = function(method, url, async) { 1305 | this.$$method = method; 1306 | this.$$url = url; 1307 | this.$$async = async; 1308 | this.$$reqHeaders = {}; 1309 | this.$$respHeaders = {}; 1310 | }; 1311 | 1312 | this.send = function(data) { 1313 | this.$$data = data; 1314 | }; 1315 | 1316 | this.setRequestHeader = function(key, value) { 1317 | this.$$reqHeaders[key] = value; 1318 | }; 1319 | 1320 | this.getResponseHeader = function(name) { 1321 | // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last 1322 | var header = this.$$respHeaders[name]; 1323 | if (header) return header; 1324 | 1325 | name = angular.lowercase(name); 1326 | header = this.$$respHeaders[name]; 1327 | if (header) return header; 1328 | 1329 | header = undefined; 1330 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1331 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1332 | }); 1333 | return header; 1334 | }; 1335 | 1336 | this.getAllResponseHeaders = function() { 1337 | var lines = []; 1338 | 1339 | angular.forEach(this.$$respHeaders, function(value, key) { 1340 | lines.push(key + ': ' + value); 1341 | }); 1342 | return lines.join('\n'); 1343 | }; 1344 | 1345 | this.abort = angular.noop; 1346 | } 1347 | 1348 | 1349 | /** 1350 | * @ngdoc function 1351 | * @name ngMock.$timeout 1352 | * @description 1353 | * 1354 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1355 | * that adds a "flush" method. 1356 | */ 1357 | 1358 | /** 1359 | * @ngdoc method 1360 | * @name ngMock.$timeout#flush 1361 | * @methodOf ngMock.$timeout 1362 | * @description 1363 | * 1364 | * Flushes the queue of pending tasks. 1365 | */ 1366 | 1367 | /** 1368 | * 1369 | */ 1370 | angular.mock.$RootElementProvider = function() { 1371 | this.$get = function() { 1372 | return angular.element('
'); 1373 | } 1374 | }; 1375 | 1376 | /** 1377 | * @ngdoc overview 1378 | * @name ngMock 1379 | * @description 1380 | * 1381 | * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful 1382 | * mocks to the {@link AUTO.$injector $injector}. 1383 | */ 1384 | angular.module('ngMock', ['ng']).provider({ 1385 | $browser: angular.mock.$BrowserProvider, 1386 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1387 | $log: angular.mock.$LogProvider, 1388 | $httpBackend: angular.mock.$HttpBackendProvider, 1389 | $rootElement: angular.mock.$RootElementProvider 1390 | }).config(function($provide) { 1391 | $provide.decorator('$timeout', function($delegate, $browser) { 1392 | $delegate.flush = function() { 1393 | $browser.defer.flush(); 1394 | }; 1395 | return $delegate; 1396 | }); 1397 | }); 1398 | 1399 | 1400 | /** 1401 | * @ngdoc overview 1402 | * @name ngMockE2E 1403 | * @description 1404 | * 1405 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1406 | * Currently there is only one mock present in this module - 1407 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1408 | */ 1409 | angular.module('ngMockE2E', ['ng']).config(function($provide) { 1410 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1411 | }); 1412 | 1413 | /** 1414 | * @ngdoc object 1415 | * @name ngMockE2E.$httpBackend 1416 | * @description 1417 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1418 | * applications that use the {@link ng.$http $http service}. 1419 | * 1420 | * *Note*: For fake http backend implementation suitable for unit testing please see 1421 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1422 | * 1423 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1424 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1425 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1426 | * templates from a webserver). 1427 | * 1428 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1429 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1430 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1431 | * templates or static files from the webserver). To configure the backend with this behavior 1432 | * use the `passThrough` request handler of `when` instead of `respond`. 1433 | * 1434 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1435 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1436 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1437 | * 1438 | * To setup the application to run with this http backend, you have to create a module that depends 1439 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1440 | * 1441 | *
1442 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1443 |  *   myAppDev.run(function($httpBackend) {
1444 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1445 |  *
1446 |  *     // returns the current list of phones
1447 |  *     $httpBackend.whenGET('/phones').respond(phones);
1448 |  *
1449 |  *     // adds a new phone to the phones array
1450 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1451 |  *       phones.push(angular.fromJSON(data));
1452 |  *     });
1453 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1454 |  *     //...
1455 |  *   });
1456 |  * 
1457 | * 1458 | * Afterwards, bootstrap your app with this new module. 1459 | */ 1460 | 1461 | /** 1462 | * @ngdoc method 1463 | * @name ngMockE2E.$httpBackend#when 1464 | * @methodOf ngMockE2E.$httpBackend 1465 | * @description 1466 | * Creates a new backend definition. 1467 | * 1468 | * @param {string} method HTTP method. 1469 | * @param {string|RegExp} url HTTP url. 1470 | * @param {(string|RegExp)=} data HTTP request body. 1471 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1472 | * object and returns true if the headers match the current definition. 1473 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1474 | * control how a matched request is handled. 1475 | * 1476 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1477 | * – The respond method takes a set of static data to be returned or a function that can return 1478 | * an array containing response status (number), response data (string) and response headers 1479 | * (Object). 1480 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1481 | * handler, will be pass through to the real backend (an XHR request will be made to the 1482 | * server. 1483 | */ 1484 | 1485 | /** 1486 | * @ngdoc method 1487 | * @name ngMockE2E.$httpBackend#whenGET 1488 | * @methodOf ngMockE2E.$httpBackend 1489 | * @description 1490 | * Creates a new backend definition for GET requests. For more info see `when()`. 1491 | * 1492 | * @param {string|RegExp} url HTTP url. 1493 | * @param {(Object|function(Object))=} headers HTTP headers. 1494 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1495 | * control how a matched request is handled. 1496 | */ 1497 | 1498 | /** 1499 | * @ngdoc method 1500 | * @name ngMockE2E.$httpBackend#whenHEAD 1501 | * @methodOf ngMockE2E.$httpBackend 1502 | * @description 1503 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1504 | * 1505 | * @param {string|RegExp} url HTTP url. 1506 | * @param {(Object|function(Object))=} headers HTTP headers. 1507 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1508 | * control how a matched request is handled. 1509 | */ 1510 | 1511 | /** 1512 | * @ngdoc method 1513 | * @name ngMockE2E.$httpBackend#whenDELETE 1514 | * @methodOf ngMockE2E.$httpBackend 1515 | * @description 1516 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1517 | * 1518 | * @param {string|RegExp} url HTTP url. 1519 | * @param {(Object|function(Object))=} headers HTTP headers. 1520 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1521 | * control how a matched request is handled. 1522 | */ 1523 | 1524 | /** 1525 | * @ngdoc method 1526 | * @name ngMockE2E.$httpBackend#whenPOST 1527 | * @methodOf ngMockE2E.$httpBackend 1528 | * @description 1529 | * Creates a new backend definition for POST requests. For more info see `when()`. 1530 | * 1531 | * @param {string|RegExp} url HTTP url. 1532 | * @param {(string|RegExp)=} data HTTP request body. 1533 | * @param {(Object|function(Object))=} headers HTTP headers. 1534 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1535 | * control how a matched request is handled. 1536 | */ 1537 | 1538 | /** 1539 | * @ngdoc method 1540 | * @name ngMockE2E.$httpBackend#whenPUT 1541 | * @methodOf ngMockE2E.$httpBackend 1542 | * @description 1543 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1544 | * 1545 | * @param {string|RegExp} url HTTP url. 1546 | * @param {(string|RegExp)=} data HTTP request body. 1547 | * @param {(Object|function(Object))=} headers HTTP headers. 1548 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1549 | * control how a matched request is handled. 1550 | */ 1551 | 1552 | /** 1553 | * @ngdoc method 1554 | * @name ngMockE2E.$httpBackend#whenPATCH 1555 | * @methodOf ngMockE2E.$httpBackend 1556 | * @description 1557 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1558 | * 1559 | * @param {string|RegExp} url HTTP url. 1560 | * @param {(string|RegExp)=} data HTTP request body. 1561 | * @param {(Object|function(Object))=} headers HTTP headers. 1562 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1563 | * control how a matched request is handled. 1564 | */ 1565 | 1566 | /** 1567 | * @ngdoc method 1568 | * @name ngMockE2E.$httpBackend#whenJSONP 1569 | * @methodOf ngMockE2E.$httpBackend 1570 | * @description 1571 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1572 | * 1573 | * @param {string|RegExp} url HTTP url. 1574 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1575 | * control how a matched request is handled. 1576 | */ 1577 | angular.mock.e2e = {}; 1578 | angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; 1579 | 1580 | 1581 | angular.mock.clearDataCache = function() { 1582 | var key, 1583 | cache = angular.element.cache; 1584 | 1585 | for(key in cache) { 1586 | if (cache.hasOwnProperty(key)) { 1587 | var handle = cache[key].handle; 1588 | 1589 | handle && angular.element(handle.elem).unbind(); 1590 | delete cache[key]; 1591 | } 1592 | } 1593 | }; 1594 | 1595 | 1596 | window.jstestdriver && (function(window) { 1597 | /** 1598 | * Global method to output any number of objects into JSTD console. Useful for debugging. 1599 | */ 1600 | window.dump = function() { 1601 | var args = []; 1602 | angular.forEach(arguments, function(arg) { 1603 | args.push(angular.mock.dump(arg)); 1604 | }); 1605 | jstestdriver.console.log.apply(jstestdriver.console, args); 1606 | if (window.console) { 1607 | window.console.log.apply(window.console, args); 1608 | } 1609 | }; 1610 | })(window); 1611 | 1612 | 1613 | window.jasmine && (function(window) { 1614 | 1615 | afterEach(function() { 1616 | var spec = getCurrentSpec(); 1617 | var injector = spec.$injector; 1618 | 1619 | spec.$injector = null; 1620 | spec.$modules = null; 1621 | 1622 | if (injector) { 1623 | injector.get('$rootElement').unbind(); 1624 | injector.get('$browser').pollFns.length = 0; 1625 | } 1626 | 1627 | angular.mock.clearDataCache(); 1628 | 1629 | // clean up jquery's fragment cache 1630 | angular.forEach(angular.element.fragments, function(val, key) { 1631 | delete angular.element.fragments[key]; 1632 | }); 1633 | 1634 | MockXhr.$$lastInstance = null; 1635 | 1636 | angular.forEach(angular.callbacks, function(val, key) { 1637 | delete angular.callbacks[key]; 1638 | }); 1639 | angular.callbacks.counter = 0; 1640 | }); 1641 | 1642 | function getCurrentSpec() { 1643 | return jasmine.getEnv().currentSpec; 1644 | } 1645 | 1646 | function isSpecRunning() { 1647 | var spec = getCurrentSpec(); 1648 | return spec && spec.queue.running; 1649 | } 1650 | 1651 | /** 1652 | * @ngdoc function 1653 | * @name angular.mock.module 1654 | * @description 1655 | * 1656 | * *NOTE*: This function is also published on window for easy access.
1657 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1658 | * 1659 | * This function registers a module configuration code. It collects the configuration information 1660 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1661 | * 1662 | * See {@link angular.mock.inject inject} for usage example 1663 | * 1664 | * @param {...(string|Function)} fns any number of modules which are represented as string 1665 | * aliases or as anonymous module initialization functions. The modules are used to 1666 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. 1667 | */ 1668 | window.module = angular.mock.module = function() { 1669 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1670 | return isSpecRunning() ? workFn() : workFn; 1671 | ///////////////////// 1672 | function workFn() { 1673 | var spec = getCurrentSpec(); 1674 | if (spec.$injector) { 1675 | throw Error('Injector already created, can not register a module!'); 1676 | } else { 1677 | var modules = spec.$modules || (spec.$modules = []); 1678 | angular.forEach(moduleFns, function(module) { 1679 | modules.push(module); 1680 | }); 1681 | } 1682 | } 1683 | }; 1684 | 1685 | /** 1686 | * @ngdoc function 1687 | * @name angular.mock.inject 1688 | * @description 1689 | * 1690 | <<<<<<< HEAD 1691 | * *NOTE*: This is function is also published on window for easy access.
1692 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1693 | ======= 1694 | * *NOTE*: This function is also published on window for easy access.
1695 | >>>>>>> 8dca056... docs(mocks): fix typos 1696 | * 1697 | * The inject function wraps a function into an injectable function. The inject() creates new 1698 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 1699 | * resolving references. 1700 | * 1701 | * See also {@link angular.mock.module module} 1702 | * 1703 | * Example of what a typical jasmine tests looks like with the inject method. 1704 | *
1705 |    *
1706 |    *   angular.module('myApplicationModule', [])
1707 |    *       .value('mode', 'app')
1708 |    *       .value('version', 'v1.0.1');
1709 |    *
1710 |    *
1711 |    *   describe('MyApp', function() {
1712 |    *
1713 |    *     // You need to load modules that you want to test,
1714 |    *     // it loads only the "ng" module by default.
1715 |    *     beforeEach(module('myApplicationModule'));
1716 |    *
1717 |    *
1718 |    *     // inject() is used to inject arguments of all given functions
1719 |    *     it('should provide a version', inject(function(mode, version) {
1720 |    *       expect(version).toEqual('v1.0.1');
1721 |    *       expect(mode).toEqual('app');
1722 |    *     }));
1723 |    *
1724 |    *
1725 |    *     // The inject and module method can also be used inside of the it or beforeEach
1726 |    *     it('should override a version and test the new version is injected', function() {
1727 |    *       // module() takes functions or strings (module aliases)
1728 |    *       module(function($provide) {
1729 |    *         $provide.value('version', 'overridden'); // override version here
1730 |    *       });
1731 |    *
1732 |    *       inject(function(version) {
1733 |    *         expect(version).toEqual('overridden');
1734 |    *       });
1735 |    *     ));
1736 |    *   });
1737 |    *
1738 |    * 
1739 | * 1740 | * @param {...Function} fns any number of functions which will be injected using the injector. 1741 | */ 1742 | window.inject = angular.mock.inject = function() { 1743 | var blockFns = Array.prototype.slice.call(arguments, 0); 1744 | var errorForStack = new Error('Declaration Location'); 1745 | return isSpecRunning() ? workFn() : workFn; 1746 | ///////////////////// 1747 | function workFn() { 1748 | var spec = getCurrentSpec(); 1749 | var modules = spec.$modules || []; 1750 | modules.unshift('ngMock'); 1751 | modules.unshift('ng'); 1752 | var injector = spec.$injector; 1753 | if (!injector) { 1754 | injector = spec.$injector = angular.injector(modules); 1755 | } 1756 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 1757 | try { 1758 | injector.invoke(blockFns[i] || angular.noop, this); 1759 | } catch (e) { 1760 | if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; 1761 | throw e; 1762 | } finally { 1763 | errorForStack = null; 1764 | } 1765 | } 1766 | } 1767 | }; 1768 | })(window); 1769 | -------------------------------------------------------------------------------- /test/unit/BlockSpec.js: -------------------------------------------------------------------------------- 1 | describe('block module', function () { 2 | 3 | beforeEach(module('flexyLayout.block')); 4 | 5 | describe('composite block', function () { 6 | 7 | var blockMock; 8 | 9 | beforeEach(function () { 10 | blockMock = { 11 | moveLength: function () { 12 | return 200; 13 | }, 14 | canMoveLength: function () { 15 | return true; 16 | }, 17 | getAvailableLength: function () { 18 | return 20; 19 | } 20 | }; 21 | }); 22 | 23 | it('should always return an object (CompositeBlock)', inject(function (Block) { 24 | var composite = Block.getNewComposite(); 25 | expect(typeof composite).toEqual('object'); 26 | expect(composite.constructor.name).toEqual('CompositeBlock'); 27 | })); 28 | 29 | it('should add object that implements structural interface', inject(function (Block) { 30 | var composite = Block.getNewComposite(blockMock); 31 | expect(composite.blocks.length).toBe(1); 32 | })); 33 | 34 | it('should not add object that does not implement interface', inject(function (Block) { 35 | var composite = Block.getNewComposite({}); 36 | expect(composite.blocks.length).toBe(0); 37 | })); 38 | 39 | it('should work with array argument', inject(function (Block) { 40 | var 41 | secondBlock = angular.copy(blockMock), 42 | composite = Block.getNewComposite([blockMock, secondBlock]); 43 | expect(composite.blocks[0]).toBe(blockMock); 44 | expect(composite.blocks[1]).toBe(secondBlock); 45 | })); 46 | 47 | it('should work with variadic arguments', inject(function (Block) { 48 | var 49 | secondBlock = angular.copy(blockMock), 50 | composite = Block.getNewComposite(blockMock, secondBlock); 51 | expect(composite.blocks[0]).toBe(blockMock); 52 | expect(composite.blocks[1]).toBe(secondBlock); 53 | })); 54 | 55 | describe('composite interface', function () { 56 | var blockMock2, composite; 57 | 58 | beforeEach(inject(function (Block) { 59 | blockMock2 = angular.copy(blockMock); 60 | composite = Block.getNewComposite(blockMock, blockMock2); 61 | })); 62 | 63 | it('should sum the consumable length of all members', function () { 64 | blockMock.getAvailableLength = function () { 65 | return 400; 66 | }; 67 | blockMock2.getAvailableLength = function () { 68 | return 222; 69 | }; 70 | spyOn(blockMock, 'getAvailableLength').andCallThrough(); 71 | spyOn(blockMock2, 'getAvailableLength').andCallThrough(); 72 | var available = composite.getAvailableLength(); 73 | expect(available).toEqual(622); 74 | expect(blockMock.getAvailableLength).toHaveBeenCalled() 75 | expect(blockMock2.getAvailableLength).toHaveBeenCalled(); 76 | }); 77 | 78 | it('should call moveLength for all members with appropriate divider', function () { 79 | spyOn(blockMock, 'moveLength').andCallThrough(); 80 | spyOn(blockMock2, 'moveLength').andCallThrough(); 81 | composite.moveLength(500); 82 | expect(blockMock.moveLength).toHaveBeenCalledWith(250) // blockMock will return 200 83 | expect(blockMock2.moveLength).toHaveBeenCalledWith(300); 84 | }); 85 | 86 | it('should call moveLength for all members with appropriate divider', inject(function (Block) { 87 | blockMock2.canMoveLength = function () { 88 | return false; 89 | }; 90 | spyOn(blockMock, 'moveLength').andCallThrough(); 91 | spyOn(blockMock2, 'moveLength').andCallThrough(); 92 | composite.moveLength(500); 93 | expect(blockMock.moveLength).toHaveBeenCalledWith(500); 94 | expect(blockMock2.moveLength).not.toHaveBeenCalled(); 95 | })); 96 | 97 | it('composite can move length whenever at least one block can move', inject(function (Block) { 98 | blockMock2.canMoveLength = function () { 99 | return false; 100 | }; 101 | expect(composite.canMoveLength(200)).toBe(true); 102 | })); 103 | 104 | it('composite can not move length if all composing blocks can not move length', inject(function () { 105 | blockMock.canMoveLength = blockMock2.canMoveLength = function () { 106 | return false; 107 | }; 108 | expect(composite.canMoveLength()).toBe(false); 109 | })); 110 | }); 111 | 112 | describe('Block', function () { 113 | var block; 114 | beforeEach(inject(function (Block) { 115 | block = Block.getNewBlock(); 116 | block.lengthValue = 200; 117 | })); 118 | 119 | it('should not move if isLocked', function () { 120 | block.isLocked = true; 121 | var length = block.moveLength(300); 122 | expect(length).toEqual(0); 123 | expect(block.lengthValue).toEqual(200); 124 | 125 | length = block.moveLength(-200); 126 | expect(length).toEqual(0); 127 | expect(block.lengthValue).toEqual(200); 128 | }); 129 | 130 | it('should move length value and return what it effectively moved from', function () { 131 | var length = block.moveLength(-100); 132 | expect(length).toEqual(-100); 133 | expect(block.lengthValue).toEqual(100); 134 | }); 135 | 136 | it('should move length value and return what it effectively moved from', function () { 137 | var length = block.moveLength(100); 138 | expect(length).toEqual(100); 139 | expect(block.lengthValue).toEqual(300); 140 | }); 141 | 142 | it('should reduce length value to maximum 0', function () { 143 | var length = block.moveLength(-300); 144 | expect(length).toEqual(-200); 145 | expect(block.lengthValue).toEqual(0); 146 | }); 147 | 148 | it('can not move if it is locked', function () { 149 | block.isLocked = true; 150 | expect(block.canMoveLength(-100)).toBe(false); 151 | expect(block.canMoveLength(100)).toBe(false); 152 | }); 153 | 154 | it('should always be able to expand if not locked', function () { 155 | expect(block.canMoveLength(100)).toBe(true); 156 | expect(block.canMoveLength(100000)).toBe(true); 157 | }); 158 | 159 | it('should no be able to move if it is already at 0 and length is negative', function () { 160 | block.lengthValue = 0; 161 | expect(block.canMoveLength(-1)).toBe(false); 162 | }); 163 | 164 | it('should not have any available length if it is locked', function () { 165 | block.isLocked = true; 166 | expect(block.getAvailableLength()).toEqual(0); 167 | }); 168 | 169 | it('the available length should be its current lengthvalue', function () { 170 | expect(block.getAvailableLength()).toEqual(200); 171 | block.lengthValue = 456; 172 | expect(block.getAvailableLength()).toEqual(456); 173 | }); 174 | }); 175 | 176 | describe('Splitter', function () { 177 | var splitter; 178 | beforeEach(inject(function (Block) { 179 | splitter = Block.getNewSplitter(); 180 | })); 181 | 182 | it('should never be able to move length', function () { 183 | expect(splitter.canMoveLength(-100)).toBe(false); 184 | expect(splitter.canMoveLength(100)).toBe(false); 185 | }); 186 | 187 | it('should always return 0 when moving length', function () { 188 | var length = splitter.moveLength(100); 189 | expect(length).toEqual(0); 190 | length = splitter.moveLength(-100); 191 | expect(length).toEqual(0); 192 | }); 193 | 194 | it('should not have any length available', function () { 195 | expect(splitter.getAvailableLength()).toEqual(0); 196 | }); 197 | 198 | it('should return true if instance of splitter',inject(function (Block) { 199 | expect(Block.isSplitter(splitter)).toBe(true); 200 | expect(Block.isSplitter(Block.getNewBlock())).toBe(false); 201 | expect(Block.isSplitter(Block.getNewComposite())).toBe(false); 202 | })); 203 | }); 204 | }); 205 | }); 206 | 207 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------