diff --git a/app/assets/javascripts/external/handlebars-1.0.rc.4.js b/app/assets/javascripts/external/handlebars.js
old mode 100755
new mode 100644
similarity index 88%
rename from app/assets/javascripts/external/handlebars-1.0.rc.4.js
rename to app/assets/javascripts/external/handlebars.js
index 96d86ea814..c70f09d1de
--- a/app/assets/javascripts/external/handlebars-1.0.rc.4.js
+++ b/app/assets/javascripts/external/handlebars.js
@@ -29,13 +29,14 @@ var Handlebars = {};
;
// lib/handlebars/base.js
-Handlebars.VERSION = "1.0.0-rc.4";
-Handlebars.COMPILER_REVISION = 3;
+Handlebars.VERSION = "1.0.0";
+Handlebars.COMPILER_REVISION = 4;
Handlebars.REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
- 3: '>= 1.0.0-rc.4'
+ 3: '== 1.0.0-rc.4',
+ 4: '>= 1.0.0'
};
Handlebars.helpers = {};
@@ -67,7 +68,7 @@ Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
- throw new Error("Could not find property '" + arg + "'");
+ throw new Error("Missing helper: '" + arg + "'");
}
});
@@ -124,6 +125,9 @@ Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
+ var type = toString.call(context);
+ if(type === functionType) { context = context.call(this); }
+
if (options.data) {
data = Handlebars.createFrame(options.data);
}
@@ -152,22 +156,25 @@ Handlebars.registerHelper('each', function(context, options) {
return ret;
});
-Handlebars.registerHelper('if', function(context, options) {
- var type = toString.call(context);
- if(type === functionType) { context = context.call(this); }
+Handlebars.registerHelper('if', function(conditional, options) {
+ var type = toString.call(conditional);
+ if(type === functionType) { conditional = conditional.call(this); }
- if(!context || Handlebars.Utils.isEmpty(context)) {
+ if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
-Handlebars.registerHelper('unless', function(context, options) {
- return Handlebars.helpers['if'].call(this, context, {fn: options.inverse, inverse: options.fn});
+Handlebars.registerHelper('unless', function(conditional, options) {
+ return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
});
Handlebars.registerHelper('with', function(context, options) {
+ var type = toString.call(context);
+ if(type === functionType) { context = context.call(this); }
+
if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
});
@@ -181,9 +188,9 @@ Handlebars.registerHelper('log', function(context, options) {
var handlebars = (function(){
var parser = {trace: function trace() { },
yy: {},
-symbols_: {"error":2,"root":3,"program":4,"EOF":5,"simpleInverse":6,"statements":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"partialName":25,"params":26,"hash":27,"DATA":28,"param":29,"STRING":30,"INTEGER":31,"BOOLEAN":32,"hashSegments":33,"hashSegment":34,"ID":35,"EQUALS":36,"PARTIAL_NAME":37,"pathSegments":38,"SEP":39,"$accept":0,"$end":1},
-terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",28:"DATA",30:"STRING",31:"INTEGER",32:"BOOLEAN",35:"ID",36:"EQUALS",37:"PARTIAL_NAME",39:"SEP"},
-productions_: [0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[26,2],[26,1],[29,1],[29,1],[29,1],[29,1],[29,1],[27,1],[33,2],[33,1],[34,3],[34,3],[34,3],[34,3],[34,3],[25,1],[21,1],[38,3],[38,1]],
+symbols_: {"error":2,"root":3,"program":4,"EOF":5,"simpleInverse":6,"statements":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"params":27,"hash":28,"dataName":29,"param":30,"STRING":31,"INTEGER":32,"BOOLEAN":33,"hashSegments":34,"hashSegment":35,"ID":36,"EQUALS":37,"DATA":38,"pathSegments":39,"SEP":40,"$accept":0,"$end":1},
+terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",31:"STRING",32:"INTEGER",33:"BOOLEAN",36:"ID",37:"EQUALS",38:"DATA",40:"SEP"},
+productions_: [0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[27,2],[27,1],[30,1],[30,1],[30,1],[30,1],[30,1],[28,1],[34,2],[34,1],[35,3],[35,3],[35,3],[35,3],[35,3],[26,1],[26,1],[26,1],[29,2],[21,1],[39,3],[39,1]],
performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
var $0 = $$.length - 1;
@@ -224,7 +231,10 @@ case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
break;
case 18: this.$ = $$[$0-1];
break;
-case 19: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
+case 19:
+ // Parsing out the '&' escape token at this level saves ~500 bytes after min due to the removal of one parser node.
+ this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2][2] === '&');
+
break;
case 20: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true);
break;
@@ -242,7 +252,7 @@ case 26: this.$ = [[$$[$0-1]], $$[$0]];
break;
case 27: this.$ = [[$$[$0]], null];
break;
-case 28: this.$ = [[new yy.DataNode($$[$0])], null];
+case 28: this.$ = [[$$[$0]], null];
break;
case 29: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
break;
@@ -256,7 +266,7 @@ case 33: this.$ = new yy.IntegerNode($$[$0]);
break;
case 34: this.$ = new yy.BooleanNode($$[$0]);
break;
-case 35: this.$ = new yy.DataNode($$[$0]);
+case 35: this.$ = $$[$0];
break;
case 36: this.$ = new yy.HashNode($$[$0]);
break;
@@ -272,20 +282,26 @@ case 41: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])];
break;
case 42: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])];
break;
-case 43: this.$ = [$$[$0-2], new yy.DataNode($$[$0])];
+case 43: this.$ = [$$[$0-2], $$[$0]];
break;
case 44: this.$ = new yy.PartialNameNode($$[$0]);
break;
-case 45: this.$ = new yy.IdNode($$[$0]);
+case 45: this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0]));
break;
-case 46: $$[$0-2].push($$[$0]); this.$ = $$[$0-2];
+case 46: this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0]));
break;
-case 47: this.$ = [$$[$0]];
+case 47: this.$ = new yy.DataNode($$[$0]);
+break;
+case 48: this.$ = new yy.IdNode($$[$0]);
+break;
+case 49: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2];
+break;
+case 50: this.$ = [{part: $$[$0]}];
break;
}
},
-table: [{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],24:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],24:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],24:[1,16]},{17:23,18:[1,22],21:24,28:[1,25],35:[1,27],38:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{4:28,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],24:[1,16]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],24:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{17:30,21:24,28:[1,25],35:[1,27],38:26},{17:31,21:24,28:[1,25],35:[1,27],38:26},{17:32,21:24,28:[1,25],35:[1,27],38:26},{25:33,37:[1,34]},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],24:[1,16]},{17:23,21:24,28:[1,25],35:[1,27],38:26},{5:[2,4],7:35,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],24:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],24:[2,23]},{18:[1,36]},{18:[2,27],21:41,26:37,27:38,28:[1,45],29:39,30:[1,42],31:[1,43],32:[1,44],33:40,34:46,35:[1,47],38:26},{18:[2,28]},{18:[2,45],28:[2,45],30:[2,45],31:[2,45],32:[2,45],35:[2,45],39:[1,48]},{18:[2,47],28:[2,47],30:[2,47],31:[2,47],32:[2,47],35:[2,47],39:[2,47]},{10:49,20:[1,50]},{10:51,20:[1,50]},{18:[1,52]},{18:[1,53]},{18:[1,54]},{18:[1,55],21:56,35:[1,27],38:26},{18:[2,44],35:[2,44]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],24:[1,16]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{18:[2,25],21:41,27:57,28:[1,45],29:58,30:[1,42],31:[1,43],32:[1,44],33:40,34:46,35:[1,47],38:26},{18:[2,26]},{18:[2,30],28:[2,30],30:[2,30],31:[2,30],32:[2,30],35:[2,30]},{18:[2,36],34:59,35:[1,60]},{18:[2,31],28:[2,31],30:[2,31],31:[2,31],32:[2,31],35:[2,31]},{18:[2,32],28:[2,32],30:[2,32],31:[2,32],32:[2,32],35:[2,32]},{18:[2,33],28:[2,33],30:[2,33],31:[2,33],32:[2,33],35:[2,33]},{18:[2,34],28:[2,34],30:[2,34],31:[2,34],32:[2,34],35:[2,34]},{18:[2,35],28:[2,35],30:[2,35],31:[2,35],32:[2,35],35:[2,35]},{18:[2,38],35:[2,38]},{18:[2,47],28:[2,47],30:[2,47],31:[2,47],32:[2,47],35:[2,47],36:[1,61],39:[2,47]},{35:[1,62]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{21:63,35:[1,27],38:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],24:[2,21]},{18:[1,64]},{18:[2,24]},{18:[2,29],28:[2,29],30:[2,29],31:[2,29],32:[2,29],35:[2,29]},{18:[2,37],35:[2,37]},{36:[1,61]},{21:65,28:[1,69],30:[1,66],31:[1,67],32:[1,68],35:[1,27],38:26},{18:[2,46],28:[2,46],30:[2,46],31:[2,46],32:[2,46],35:[2,46],39:[2,46]},{18:[1,70]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],24:[2,22]},{18:[2,39],35:[2,39]},{18:[2,40],35:[2,40]},{18:[2,41],35:[2,41]},{18:[2,42],35:[2,42]},{18:[2,43],35:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]}],
-defaultActions: {17:[2,1],25:[2,28],38:[2,26],57:[2,24]},
+table: [{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],25:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],25:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],25:[1,16]},{17:23,18:[1,22],21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],25:[2,8]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{4:30,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{17:31,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:32,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:33,21:24,29:25,36:[1,28],38:[1,27],39:26},{21:35,26:34,31:[1,36],32:[1,37],36:[1,28],39:26},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],25:[1,16]},{17:23,21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,4],7:38,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],25:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{18:[1,39]},{18:[2,27],21:44,24:[2,27],27:40,28:41,29:48,30:42,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,28],24:[2,28]},{18:[2,48],24:[2,48],31:[2,48],32:[2,48],33:[2,48],36:[2,48],38:[2,48],40:[1,51]},{21:52,36:[1,28],39:26},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],38:[2,50],40:[2,50]},{10:53,20:[1,54]},{10:55,20:[1,54]},{18:[1,56]},{18:[1,57]},{24:[1,58]},{18:[1,59],21:60,36:[1,28],39:26},{18:[2,44],36:[2,44]},{18:[2,45],36:[2,45]},{18:[2,46],36:[2,46]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],25:[1,16]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{18:[2,25],21:44,24:[2,25],28:61,29:48,30:62,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,26],24:[2,26]},{18:[2,30],24:[2,30],31:[2,30],32:[2,30],33:[2,30],36:[2,30],38:[2,30]},{18:[2,36],24:[2,36],35:63,36:[1,64]},{18:[2,31],24:[2,31],31:[2,31],32:[2,31],33:[2,31],36:[2,31],38:[2,31]},{18:[2,32],24:[2,32],31:[2,32],32:[2,32],33:[2,32],36:[2,32],38:[2,32]},{18:[2,33],24:[2,33],31:[2,33],32:[2,33],33:[2,33],36:[2,33],38:[2,33]},{18:[2,34],24:[2,34],31:[2,34],32:[2,34],33:[2,34],36:[2,34],38:[2,34]},{18:[2,35],24:[2,35],31:[2,35],32:[2,35],33:[2,35],36:[2,35],38:[2,35]},{18:[2,38],24:[2,38],36:[2,38]},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],37:[1,65],38:[2,50],40:[2,50]},{36:[1,66]},{18:[2,47],24:[2,47],31:[2,47],32:[2,47],33:[2,47],36:[2,47],38:[2,47]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{21:67,36:[1,28],39:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,68]},{18:[2,24],24:[2,24]},{18:[2,29],24:[2,29],31:[2,29],32:[2,29],33:[2,29],36:[2,29],38:[2,29]},{18:[2,37],24:[2,37],36:[2,37]},{37:[1,65]},{21:69,29:73,31:[1,70],32:[1,71],33:[1,72],36:[1,28],38:[1,27],39:26},{18:[2,49],24:[2,49],31:[2,49],32:[2,49],33:[2,49],36:[2,49],38:[2,49],40:[2,49]},{18:[1,74]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{18:[2,39],24:[2,39],36:[2,39]},{18:[2,40],24:[2,40],36:[2,40]},{18:[2,41],24:[2,41],36:[2,41]},{18:[2,42],24:[2,42],36:[2,42]},{18:[2,43],24:[2,43],36:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]}],
+defaultActions: {17:[2,1]},
parseError: function parseError(str, hash) {
throw new Error(str);
},
@@ -584,7 +600,7 @@ case 3:
break;
case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15;
break;
-case 5: this.begin("par"); return 24;
+case 5: return 25;
break;
case 6: return 16;
break;
@@ -596,7 +612,7 @@ case 9: return 19;
break;
case 10: return 23;
break;
-case 11: return 23;
+case 11: return 22;
break;
case 12: this.popState(); this.begin('com');
break;
@@ -604,48 +620,44 @@ case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return
break;
case 14: return 22;
break;
-case 15: return 36;
+case 15: return 37;
break;
-case 16: return 35;
+case 16: return 36;
break;
-case 17: return 35;
+case 17: return 36;
break;
-case 18: return 39;
+case 18: return 40;
break;
case 19: /*ignore whitespace*/
break;
-case 20: this.popState(); return 18;
+case 20: this.popState(); return 24;
break;
case 21: this.popState(); return 18;
break;
-case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 30;
+case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31;
break;
-case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30;
+case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31;
break;
-case 24: yy_.yytext = yy_.yytext.substr(1); return 28;
+case 24: return 38;
break;
-case 25: return 32;
+case 25: return 33;
break;
-case 26: return 32;
+case 26: return 33;
break;
-case 27: return 31;
+case 27: return 32;
break;
-case 28: return 35;
+case 28: return 36;
break;
-case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35;
+case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36;
break;
case 30: return 'INVALID';
break;
-case 31: /*ignore whitespace*/
-break;
-case 32: this.popState(); return 37;
-break;
-case 33: return 5;
+case 31: return 5;
break;
}
};
-lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$:\-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:\s+)/,/^(?:[a-zA-Z0-9_$\-\/]+)/,/^(?:$)/];
-lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,33],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"par":{"rules":[31,32],"inclusive":false},"INITIAL":{"rules":[0,1,2,33],"inclusive":true}};
+lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/];
+lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}};
return lexer;})()
parser.lexer = lexer;
function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
@@ -731,21 +743,24 @@ Handlebars.AST.HashNode = function(pairs) {
Handlebars.AST.IdNode = function(parts) {
this.type = "ID";
- this.original = parts.join(".");
- var dig = [], depth = 0;
+ var original = "",
+ dig = [],
+ depth = 0;
for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + this.original); }
+ if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + original); }
else if (part === "..") { depth++; }
else { this.isScoped = true; }
}
else { dig.push(part); }
}
+ this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
@@ -759,7 +774,7 @@ Handlebars.AST.IdNode = function(parts) {
Handlebars.AST.PartialNameNode = function(name) {
this.type = "PARTIAL_NAME";
- this.name = name;
+ this.name = name.original;
};
Handlebars.AST.DataNode = function(id) {
@@ -769,13 +784,15 @@ Handlebars.AST.DataNode = function(id) {
Handlebars.AST.StringNode = function(string) {
this.type = "STRING";
- this.string = string;
- this.stringModeValue = string;
+ this.original =
+ this.string =
+ this.stringModeValue = string;
};
Handlebars.AST.IntegerNode = function(integer) {
this.type = "INTEGER";
- this.integer = integer;
+ this.original =
+ this.integer = integer;
this.stringModeValue = Number(integer);
};
@@ -1162,7 +1179,15 @@ Compiler.prototype = {
DATA: function(data) {
this.options.data = true;
- this.opcode('lookupData', data.id);
+ if (data.id.isScoped || data.id.depth) {
+ throw new Handlebars.Exception('Scoped data references are not supported: ' + data.original);
+ }
+
+ this.opcode('lookupData');
+ var parts = data.id.parts;
+ for(var i=0, l=parts.length; i 0) {
+ method.apply(target, args);
+ } else {
+ method.call(target);
+ }
+ }
+ if (l && after) { after(); }
+
+ // check if new items have been added
+ if (queue.length > l) {
+ this._queue = queue.slice(l);
+ this.flush();
+ } else {
+ this._queue.length = 0;
+ }
+ },
+
+ cancel: function(actionToCancel) {
+ var queue = this._queue, currentTarget, currentMethod, i, l;
+
+ for (i = 0, l = queue.length; i < l; i += 4) {
+ currentTarget = queue[i];
+ currentMethod = queue[i+1];
+
+ if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) {
+ queue.splice(i, 4);
+ return true;
+ }
+ }
+
+ // if not found in current queue
+ // could be in the queue that is being flushed
+ queue = this._queueBeingFlushed;
+ if (!queue) {
+ return;
+ }
+ for (i = 0, l = queue.length; i < l; i += 4) {
+ currentTarget = queue[i];
+ currentMethod = queue[i+1];
+
+ if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) {
+ // don't mess with array during flush
+ // just nullify the method
+ queue[i+1] = null;
+ return true;
+ }
+ }
+ }
+ };
+
+
+ __exports__.Queue = Queue;
+ });
+
+define("backburner/deferred_action_queues",
+ ["backburner/queue","exports"],
+ function(__dependency1__, __exports__) {
+ "use strict";
+ var Queue = __dependency1__.Queue;
+
+ function DeferredActionQueues(queueNames, options) {
+ var queues = this.queues = {};
+ this.queueNames = queueNames = queueNames || [];
+
+ var queueName;
+ for (var i = 0, l = queueNames.length; i < l; i++) {
+ queueName = queueNames[i];
+ queues[queueName] = new Queue(this, queueName, options[queueName]);
+ }
+ }
+
+ DeferredActionQueues.prototype = {
+ queueNames: null,
+ queues: null,
+
+ schedule: function(queueName, target, method, args, onceFlag, stack) {
+ var queues = this.queues,
+ queue = queues[queueName];
+
+ if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); }
+
+ if (onceFlag) {
+ return queue.pushUnique(target, method, args, stack);
+ } else {
+ return queue.push(target, method, args, stack);
+ }
+ },
+
+ flush: function() {
+ var queues = this.queues,
+ queueNames = this.queueNames,
+ queueName, queue, queueItems, priorQueueNameIndex,
+ queueNameIndex = 0, numberOfQueues = queueNames.length;
+
+ outerloop:
+ while (queueNameIndex < numberOfQueues) {
+ queueName = queueNames[queueNameIndex];
+ queue = queues[queueName];
+ queueItems = queue._queueBeingFlushed = queue._queue.slice();
+ queue._queue = [];
+
+ var options = queue.options,
+ before = options && options.before,
+ after = options && options.after,
+ target, method, args, stack,
+ queueIndex = 0, numberOfQueueItems = queueItems.length;
+
+ if (numberOfQueueItems && before) { before(); }
+ while (queueIndex < numberOfQueueItems) {
+ target = queueItems[queueIndex];
+ method = queueItems[queueIndex+1];
+ args = queueItems[queueIndex+2];
+ stack = queueItems[queueIndex+3]; // Debugging assistance
+
+ if (typeof method === 'string') { method = target[method]; }
+
+ // method could have been nullified / canceled during flush
+ if (method) {
+ // TODO: error handling
+ if (args && args.length > 0) {
+ method.apply(target, args);
+ } else {
+ method.call(target);
+ }
+ }
+
+ queueIndex += 4;
+ }
+ queue._queueBeingFlushed = null;
+ if (numberOfQueueItems && after) { after(); }
+
+ if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) {
+ queueNameIndex = priorQueueNameIndex;
+ continue outerloop;
+ }
+
+ queueNameIndex++;
+ }
+ }
+ };
+
+ function indexOfPriorQueueWithActions(daq, currentQueueIndex) {
+ var queueName, queue;
+
+ for (var i = 0, l = currentQueueIndex; i <= l; i++) {
+ queueName = daq.queueNames[i];
+ queue = daq.queues[queueName];
+ if (queue._queue.length) { return i; }
+ }
+
+ return -1;
+ }
+
+
+ __exports__.DeferredActionQueues = DeferredActionQueues;
+ });
+
define("backburner",
["backburner/deferred_action_queues","exports"],
function(__dependency1__, __exports__) {
@@ -4536,9 +4765,11 @@ define("backburner",
var slice = [].slice,
pop = [].pop,
+ throttlers = [],
debouncees = [],
timers = [],
- autorun, laterTimer, laterTimerExpiresAt;
+ autorun, laterTimer, laterTimerExpiresAt,
+ global = this;
function Backburner(queueNames, options) {
this.queueNames = queueNames;
@@ -4695,7 +4926,7 @@ define("backburner",
clearTimeout(laterTimer);
laterTimer = null;
}
- laterTimer = window.setTimeout(function() {
+ laterTimer = global.setTimeout(function() {
executeTimers(self);
laterTimer = null;
laterTimerExpiresAt = null;
@@ -4705,38 +4936,86 @@ define("backburner",
return fn;
},
- debounce: function(target, method /* , args, wait */) {
+ throttle: function(target, method /* , args, wait */) {
var self = this,
args = arguments,
wait = pop.call(args),
- debouncee;
+ throttler;
- for (var i = 0, l = debouncees.length; i < l; i++) {
- debouncee = debouncees[i];
- if (debouncee[0] === target && debouncee[1] === method) { return; } // do nothing
+ for (var i = 0, l = throttlers.length; i < l; i++) {
+ throttler = throttlers[i];
+ if (throttler[0] === target && throttler[1] === method) { return; } // do nothing
}
- var timer = window.setTimeout(function() {
+ var timer = global.setTimeout(function() {
self.run.apply(self, args);
- // remove debouncee
+ // remove throttler
var index = -1;
- for (var i = 0, l = debouncees.length; i < l; i++) {
- debouncee = debouncees[i];
- if (debouncee[0] === target && debouncee[1] === method) {
+ for (var i = 0, l = throttlers.length; i < l; i++) {
+ throttler = throttlers[i];
+ if (throttler[0] === target && throttler[1] === method) {
index = i;
break;
}
}
- if (index > -1) { debouncees.splice(index, 1); }
+ if (index > -1) { throttlers.splice(index, 1); }
}, wait);
+ throttlers.push([target, method, timer]);
+ },
+
+ debounce: function(target, method /* , args, wait, [immediate] */) {
+ var self = this,
+ args = arguments,
+ immediate = pop.call(args),
+ wait,
+ index,
+ debouncee;
+
+ if (typeof immediate === "number") {
+ wait = immediate;
+ immediate = false;
+ } else {
+ wait = pop.call(args);
+ }
+
+ // Remove debouncee
+ index = findDebouncee(target, method);
+
+ if (index !== -1) {
+ debouncee = debouncees[index];
+ debouncees.splice(index, 1);
+ clearTimeout(debouncee[2]);
+ }
+
+ var timer = window.setTimeout(function() {
+ if (!immediate) {
+ self.run.apply(self, args);
+ }
+ index = findDebouncee(target, method);
+ if (index) {
+ debouncees.splice(index, 1);
+ }
+ }, wait);
+
+ if (immediate && index === -1) {
+ self.run.apply(self, args);
+ }
+
debouncees.push([target, method, timer]);
},
cancelTimers: function() {
- for (var i = 0, l = debouncees.length; i < l; i++) {
+ var i, len;
+
+ for (i = 0, len = throttlers.length; i < len; i++) {
+ clearTimeout(throttlers[i][2]);
+ }
+ throttlers = [];
+
+ for (i = 0, len = debouncees.length; i < len; i++) {
clearTimeout(debouncees[i][2]);
}
debouncees = [];
@@ -4779,7 +5058,7 @@ define("backburner",
function createAutorun(backburner) {
backburner.begin();
- autorun = window.setTimeout(function() {
+ autorun = global.setTimeout(function() {
backburner.end();
autorun = null;
});
@@ -4804,7 +5083,7 @@ define("backburner",
});
if (timers.length) {
- laterTimer = window.setTimeout(function() {
+ laterTimer = global.setTimeout(function() {
executeTimers(self);
laterTimer = null;
laterTimerExpiresAt = null;
@@ -4813,199 +5092,24 @@ define("backburner",
}
}
+ function findDebouncee(target, method) {
+ var debouncee,
+ index = -1;
+
+ for (var i = 0, l = debouncees.length; i < l; i++) {
+ debouncee = debouncees[i];
+ if (debouncee[0] === target && debouncee[1] === method) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
__exports__.Backburner = Backburner;
});
-
-define("backburner/deferred_action_queues",
- ["backburner/queue","exports"],
- function(__dependency1__, __exports__) {
- "use strict";
- var Queue = __dependency1__.Queue;
-
- function DeferredActionQueues(queueNames, options) {
- var queues = this.queues = {};
- this.queueNames = queueNames = queueNames || [];
-
- var queueName;
- for (var i = 0, l = queueNames.length; i < l; i++) {
- queueName = queueNames[i];
- queues[queueName] = new Queue(this, queueName, options[queueName]);
- }
- }
-
- DeferredActionQueues.prototype = {
- queueNames: null,
- queues: null,
-
- schedule: function(queueName, target, method, args, onceFlag, stack) {
- var queues = this.queues,
- queue = queues[queueName];
-
- if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); }
-
- if (onceFlag) {
- return queue.pushUnique(target, method, args, stack);
- } else {
- return queue.push(target, method, args, stack);
- }
- },
-
- flush: function() {
- var queues = this.queues,
- queueNames = this.queueNames,
- queueName, queue, queueItems, priorQueueNameIndex,
- queueNameIndex = 0, numberOfQueues = queueNames.length;
-
- outerloop:
- while (queueNameIndex < numberOfQueues) {
- queueName = queueNames[queueNameIndex];
- queue = queues[queueName];
- queueItems = queue._queue.slice();
- queue._queue = [];
-
- var options = queue.options,
- before = options && options.before,
- after = options && options.after,
- target, method, args, stack,
- queueIndex = 0, numberOfQueueItems = queueItems.length;
-
- if (numberOfQueueItems && before) { before(); }
- while (queueIndex < numberOfQueueItems) {
- target = queueItems[queueIndex];
- method = queueItems[queueIndex+1];
- args = queueItems[queueIndex+2];
- stack = queueItems[queueIndex+3]; // Debugging assistance
-
- if (typeof method === 'string') { method = target[method]; }
-
- // TODO: error handling
- if (args && args.length > 0) {
- method.apply(target, args);
- } else {
- method.call(target);
- }
-
- queueIndex += 4;
- }
- if (numberOfQueueItems && after) { after(); }
-
- if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) {
- queueNameIndex = priorQueueNameIndex;
- continue outerloop;
- }
-
- queueNameIndex++;
- }
- }
- };
-
- function indexOfPriorQueueWithActions(daq, currentQueueIndex) {
- var queueName, queue;
-
- for (var i = 0, l = currentQueueIndex; i <= l; i++) {
- queueName = daq.queueNames[i];
- queue = daq.queues[queueName];
- if (queue._queue.length) { return i; }
- }
-
- return -1;
- }
-
- __exports__.DeferredActionQueues = DeferredActionQueues;
- });
-
-define("backburner/queue",
- ["exports"],
- function(__exports__) {
- "use strict";
- function Queue(daq, name, options) {
- this.daq = daq;
- this.name = name;
- this.options = options;
- this._queue = [];
- }
-
- Queue.prototype = {
- daq: null,
- name: null,
- options: null,
- _queue: null,
-
- push: function(target, method, args, stack) {
- var queue = this._queue;
- queue.push(target, method, args, stack);
- return {queue: this, target: target, method: method};
- },
-
- pushUnique: function(target, method, args, stack) {
- var queue = this._queue, currentTarget, currentMethod, i, l;
-
- for (i = 0, l = queue.length; i < l; i += 4) {
- currentTarget = queue[i];
- currentMethod = queue[i+1];
-
- if (currentTarget === target && currentMethod === method) {
- queue[i+2] = args; // replace args
- queue[i+3] = stack; // replace stack
- return {queue: this, target: target, method: method}; // TODO: test this code path
- }
- }
-
- this._queue.push(target, method, args, stack);
- return {queue: this, target: target, method: method};
- },
-
- // TODO: remove me, only being used for Ember.run.sync
- flush: function() {
- var queue = this._queue,
- options = this.options,
- before = options && options.before,
- after = options && options.after,
- target, method, args, stack, i, l = queue.length;
-
- if (l && before) { before(); }
- for (i = 0; i < l; i += 4) {
- target = queue[i];
- method = queue[i+1];
- args = queue[i+2];
- stack = queue[i+3]; // Debugging assistance
-
- // TODO: error handling
- if (args && args.length > 0) {
- method.apply(target, args);
- } else {
- method.call(target);
- }
- }
- if (l && after) { after(); }
-
- // check if new items have been added
- if (queue.length > l) {
- this._queue = queue.slice(l);
- this.flush();
- } else {
- this._queue.length = 0;
- }
- },
-
- cancel: function(actionToCancel) {
- var queue = this._queue, currentTarget, currentMethod, i, l;
-
- for (i = 0, l = queue.length; i < l; i += 4) {
- currentTarget = queue[i];
- currentMethod = queue[i+1];
-
- if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) {
- queue.splice(i, 4);
- return true;
- }
- }
- }
- };
-
- __exports__.Queue = Queue;
- });
})();
@@ -5113,7 +5217,7 @@ Ember.run = function(target, method) {
May be a function or a string. If you pass a string
then it will be looked up on the passed target.
@param {Object} [args*] Any additional arguments you wish to pass to the method.
- @return {Object} return value from invoking the passed function. Please note,
+ @return {Object} return value from invoking the passed function. Please note,
when called within an existing loop, no return value is possible.
*/
Ember.run.join = function(target, method) {
@@ -5249,7 +5353,9 @@ Ember.run.cancelTimers = function () {
@return {void}
*/
Ember.run.sync = function() {
- backburner.currentInstance.queues.sync.flush();
+ if (backburner.currentInstance) {
+ backburner.currentInstance.queues.sync.flush();
+ }
};
/**
@@ -5315,7 +5421,7 @@ Ember.run.once = function(target, method) {
var sayHi = function() { console.log('hi'); }
Ember.run.scheduleOnce('afterRender', myContext, sayHi);
Ember.run.scheduleOnce('afterRender', myContext, sayHi);
- // doFoo will only be executed once, in the afterRender queue of the RunLoop
+ // sayHi will only be executed once, in the afterRender queue of the RunLoop
});
```
@@ -5443,8 +5549,15 @@ Ember.run.cancel = function(timer) {
};
/**
- Execute the passed method in a specified amount of time, reset timer
- upon additional calls.
+ Delay calling the target method until the debounce period has elapsed
+ with no additional debounce calls. If `debounce` is called again before
+ the specified time has elapsed, the timer is reset and the entire period
+ must pass again before the target method is called.
+
+ This method should be used when an event may be called multiple times
+ but the action should only be called once when the event is done firing.
+ A common example is for scroll events where you only want updates to
+ happen once scrolling has ceased.
```javascript
var myFunc = function() { console.log(this.name + ' ran.'); };
@@ -5468,12 +5581,50 @@ Ember.run.cancel = function(timer) {
then it will be looked up on the passed target.
@param {Object} [args*] Optional arguments to pass to the timeout.
@param {Number} wait Number of milliseconds to wait.
+ @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval.
@return {void}
*/
Ember.run.debounce = function() {
return backburner.debounce.apply(backburner, arguments);
};
+/**
+ Ensure that the target method is never called more frequently than
+ the specified spacing period.
+
+ ```javascript
+ var myFunc = function() { console.log(this.name + ' ran.'); };
+ var myContext = {name: 'throttle'};
+
+ Ember.run.throttle(myContext, myFunc, 150);
+
+ // 50ms passes
+ Ember.run.throttle(myContext, myFunc, 150);
+
+ // 50ms passes
+ Ember.run.throttle(myContext, myFunc, 150);
+
+ // 50ms passes
+ Ember.run.throttle(myContext, myFunc, 150);
+
+ // 150ms passes
+ // myFunc is invoked with context myContext
+ // console logs 'throttle ran.' twice, 150ms apart.
+ ```
+
+ @method throttle
+ @param {Object} [target] target of method to invoke
+ @param {Function|String} method The method to invoke.
+ May be a function or a string. If you pass a string
+ then it will be looked up on the passed target.
+ @param {Object} [args*] Optional arguments to pass to the timeout.
+ @param {Number} spacing Number of milliseconds to space out requests.
+ @return {void}
+*/
+Ember.run.throttle = function() {
+ return backburner.throttle.apply(backburner, arguments);
+};
+
// Make sure it's not an autorun during testing
function checkAutoRun() {
if (!Ember.run.currentRunLoop) {
@@ -7263,6 +7414,8 @@ define("rsvp",
__exports__.reject = reject;
});
+
+
})();
(function() {
@@ -7270,12 +7423,43 @@ define("container",
[],
function() {
+ /**
+ A safe and simple inheriting object.
+
+ @class InheritingDict
+ */
function InheritingDict(parent) {
this.parent = parent;
this.dict = {};
}
InheritingDict.prototype = {
+
+ /**
+ @property parent
+ @type InheritingDict
+ @default null
+ */
+
+ parent: null,
+
+ /**
+ Object used to store the current nodes data.
+
+ @property dict
+ @type Object
+ @default Object
+ */
+ dict: null,
+
+ /**
+ Retrieve the value given a key, if the value is present at the current
+ level use it, otherwise walk up the parent hierarchy and try again. If
+ no matching key is found, return undefined.
+
+ @method get
+ @return {any}
+ */
get: function(key) {
var dict = this.dict;
@@ -7288,10 +7472,36 @@ define("container",
}
},
+ /**
+ Set the given value for the given key, at the current level.
+
+ @method set
+ @param {String} key
+ @param {Any} value
+ */
set: function(key, value) {
this.dict[key] = value;
},
+ /**
+ Delete the given key
+
+ @method remove
+ @param {String} key
+ */
+ remove: function(key) {
+ delete this.dict[key];
+ },
+
+ /**
+ Check for the existence of given a key, if the key is present at the current
+ level return true, otherwise walk up the parent hierarchy and try again. If
+ no matching key is found, return false.
+
+ @method has
+ @param {String} key
+ @returns {Boolean}
+ */
has: function(key) {
var dict = this.dict;
@@ -7306,6 +7516,13 @@ define("container",
return false;
},
+ /**
+ Iterate and invoke a callback for each local key-value pair.
+
+ @method eachLocal
+ @param {Function} callback
+ @param {Object} binding
+ */
eachLocal: function(callback, binding) {
var dict = this.dict;
@@ -7317,6 +7534,11 @@ define("container",
}
};
+ /**
+ A lightweight container that helps to assemble and decouple components.
+
+ @class Container
+ */
function Container(parent) {
this.parent = parent;
this.children = [];
@@ -7331,16 +7553,115 @@ define("container",
}
Container.prototype = {
+
+ /**
+ @property parent
+ @type Container
+ @default null
+ */
+ parent: null,
+
+ /**
+ @property children
+ @type Array
+ @default []
+ */
+ children: null,
+
+ /**
+ @property resolver
+ @type function
+ */
+ resolver: null,
+
+ /**
+ @property registry
+ @type InheritingDict
+ */
+ registry: null,
+
+ /**
+ @property cache
+ @type InheritingDict
+ */
+ cache: null,
+
+ /**
+ @property typeInjections
+ @type InheritingDict
+ */
+ typeInjections: null,
+
+ /**
+ @property injections
+ @type Object
+ @default {}
+ */
+ injections: null,
+
+ /**
+ @private
+
+ @property _options
+ @type InheritingDict
+ @default null
+ */
+ _options: null,
+
+ /**
+ @private
+
+ @property _typeOptions
+ @type InheritingDict
+ */
+ _typeOptions: null,
+
+ /**
+ Returns a new child of the current container. These children are configured
+ to correctly inherit from the current container.
+
+ @method child
+ @returns {Container}
+ */
child: function() {
var container = new Container(this);
this.children.push(container);
return container;
},
+ /**
+ Sets a key-value pair on the current container. If a parent container,
+ has the same key, once set on a child, the parent and child will diverge
+ as expected.
+
+ @method set
+ @param {Object} obkect
+ @param {String} key
+ @param {any} value
+ */
set: function(object, key, value) {
object[key] = value;
},
+ /**
+ Registers a factory for later injection.
+
+ Example:
+
+ ```javascript
+ var container = new Container();
+
+ container.register('model:user', Person, {singleton: false });
+ container.register('fruit:favorite', Orange);
+ container.register('communication:main', Email, {singleton: false});
+ ```
+
+ @method register
+ @param {String} type
+ @param {String} name
+ @param {Function} factory
+ @param {Object} options
+ */
register: function(type, name, factory, options) {
var fullName;
@@ -7359,14 +7680,115 @@ define("container",
this._options.set(normalizedName, options || {});
},
+ /**
+ Unregister a fullName
+
+ ```javascript
+ var container = new Container();
+ container.register('model:user', User);
+
+ container.lookup('model:user') instanceof User //=> true
+
+ container.unregister('model:user')
+ container.lookup('model:user') === undefined //=> true
+
+ @method unregister
+ @param {String} fullName
+ */
+ unregister: function(fullName) {
+ var normalizedName = this.normalize(fullName);
+
+ this.registry.remove(normalizedName);
+ this.cache.remove(normalizedName);
+ this._options.remove(normalizedName);
+ },
+
+ /**
+ Given a fullName return the corresponding factory.
+
+ By default `resolve` will retrieve the factory from
+ its container's registry.
+
+ ```javascript
+ var container = new Container();
+ container.register('api:twitter', Twitter);
+
+ container.resolve('api:twitter') // => Twitter
+ ```
+
+ Optionally the container can be provided with a custom resolver.
+ If provided, `resolve` will first provide the custom resolver
+ the oppertunity to resolve the fullName, otherwise it will fallback
+ to the registry.
+
+ ```javascript
+ var container = new Container();
+ container.resolver = function(fullName) {
+ // lookup via the module system of choice
+ };
+
+ // the twitter factory is added to the module system
+ container.resolve('api:twitter') // => Twitter
+ ```
+
+ @method resolve
+ @param {String} fullName
+ @returns {Function} fullName's factory
+ */
resolve: function(fullName) {
return this.resolver(fullName) || this.registry.get(fullName);
},
+ /**
+ A hook to enable custom fullName normalization behaviour
+
+ @method normalize
+ @param {String} fullName
+ @return {string} normalized fullName
+ */
normalize: function(fullName) {
return fullName;
},
+ /**
+ Given a fullName return a corresponding instance.
+
+ The default behaviour is for lookup to return a singleton instance.
+ The singleton is scoped to the container, allowing multiple containers
+ to all have there own locally scoped singletons.
+
+ ```javascript
+ var container = new Container();
+ container.register('api:twitter', Twitter);
+
+ var twitter = container.lookup('api:twitter');
+
+ twitter instanceof Twitter; // => true
+
+ // by default the container will return singletons
+ twitter2 = container.lookup('api:twitter');
+ twitter instanceof Twitter; // => true
+
+ twitter === twitter2; //=> true
+ ```
+
+ If singletons are not wanted an optional flag can be provided at lookup.
+
+ ```javascript
+ var container = new Container();
+ container.register('api:twitter', Twitter);
+
+ var twitter = container.lookup('api:twitter', { singleton: false });
+ var twitter2 = container.lookup('api:twitter', { singleton: false });
+
+ twitter === twitter2; //=> false
+ ```
+
+ @method lookup
+ @param {String} fullName
+ @param {Object} options
+ @return {any}
+ */
lookup: function(fullName, options) {
fullName = this.normalize(fullName);
@@ -7387,6 +7809,25 @@ define("container",
return value;
},
+ /**
+ Given a fullName return the corresponding factory.
+
+ @method lookupFactory
+ @param {String} fullName
+ @return {any}
+ */
+ lookupFactory: function(fullName) {
+ return factoryFor(this, fullName);
+ },
+
+ /**
+ Given a fullName check if the container is aware of its factory
+ or singleton instance.
+
+ @method has
+ @param {String} fullName
+ @return {Boolean}
+ */
has: function(fullName) {
if (this.cache.has(fullName)) {
return true;
@@ -7395,27 +7836,144 @@ define("container",
return !!factoryFor(this, fullName);
},
+ /**
+ Allow registerying options for all factories of a type.
+
+ ```javascript
+ var container = new Container();
+
+ // if all of type `connection` must not be singletons
+ container.optionsForType('connection', { singleton: false });
+
+ container.register('connection:twitter', TwitterConnection);
+ container.register('connection:facebook', FacebookConnection);
+
+ var twitter = container.lookup('connection:twitter');
+ var twitter2 = container.lookup('connection:twitter');
+
+ twitter === twitter2; // => false
+
+ var facebook = container.lookup('connection:facebook');
+ var facebook2 = container.lookup('connection:facebook');
+
+ facebook === facebook2; // => false
+ ```
+
+ @method optionsForType
+ @param {String} type
+ @param {Object} options
+ */
optionsForType: function(type, options) {
if (this.parent) { illegalChildOperation('optionsForType'); }
this._typeOptions.set(type, options);
},
+ /**
+ @method options
+ @param {String} type
+ @param {Object} options
+ */
options: function(type, options) {
this.optionsForType(type, options);
},
+ /*
+ @private
+
+ Used only via `injection`.
+
+ Provides a specialized form of injection, specifically enabling
+ all objects of one type to be injected with a reference to another
+ object.
+
+ For example, provided each object of type `controller` needed a `router`.
+ one would do the following:
+
+ ```javascript
+ var container = new Container();
+
+ container.register('router:main', Router);
+ container.register('controller:user', UserController);
+ container.register('controller:post', PostController);
+
+ container.typeInjection('controller', 'router', 'router:main');
+
+ var user = container.lookup('controller:user');
+ var post = container.lookup('controller:post');
+
+ user.router instanceof Router; //=> true
+ post.router instanceof Router; //=> true
+
+ // both controllers share the same router
+ user.router === post.router; //=> true
+ ```
+
+ @method typeInjection
+ @param {String} type
+ @param {String} property
+ @param {String} fullName
+ */
typeInjection: function(type, property, fullName) {
if (this.parent) { illegalChildOperation('typeInjection'); }
var injections = this.typeInjections.get(type);
+
if (!injections) {
injections = [];
this.typeInjections.set(type, injections);
}
- injections.push({ property: property, fullName: fullName });
+
+ injections.push({
+ property: property,
+ fullName: fullName
+ });
},
+ /*
+ Defines injection rules.
+
+ These rules are used to inject dependencies onto objects when they
+ are instantiated.
+
+ Two forms of injections are possible:
+
+ * Injecting one fullName on another fullName
+ * Injecting one fullName on a type
+
+ Example:
+
+ ```javascript
+ var container = new Container();
+
+ container.register('source:main', Source);
+ container.register('model:user', User);
+ container.register('model:post', PostController);
+
+ // injecting one fullName on another fullName
+ // eg. each user model gets a post model
+ container.injection('model:user', 'post', 'model:post');
+
+ // injecting one fullName on another type
+ container.injection('model', 'source', 'source:main');
+
+ var user = container.lookup('model:user');
+ var post = container.lookup('model:post');
+
+ user.source instanceof Source; //=> true
+ post.source instanceof Source; //=> true
+
+ user.post instanceof Post; //=> true
+
+ // and both models share the same source
+ user.source === post.source; //=> true
+ ```
+
+ @method injection
+ @param {String} factoryName
+ @param {String} property
+ @param {String} injectionName
+ */
injection: function(factoryName, property, injectionName) {
if (this.parent) { illegalChildOperation('injection'); }
@@ -7427,6 +7985,12 @@ define("container",
injections.push({ property: property, fullName: injectionName });
},
+ /**
+ A depth first traversal, destroying the container, its descendant containers and all
+ their managed objects.
+
+ @method destroy
+ */
destroy: function() {
this.isDestroyed = true;
@@ -7444,6 +8008,9 @@ define("container",
this.isDestroyed = true;
},
+ /**
+ @method reset
+ */
reset: function() {
for (var i=0, l=this.children.length; i1) args = a_slice.call(arguments, 1);
this.forEach(function(x, idx) {
@@ -9021,7 +9590,7 @@ Ember.Enumerable = Ember.Mixin.create({
@return {Array} the enumerable as an array.
*/
toArray: function() {
- var ret = Ember.A([]);
+ var ret = Ember.A();
this.forEach(function(o, idx) { ret[idx] = o; });
return ret ;
},
@@ -9057,7 +9626,7 @@ Ember.Enumerable = Ember.Mixin.create({
*/
without: function(value) {
if (!this.contains(value)) return this; // nothing to do
- var ret = Ember.A([]);
+ var ret = Ember.A();
this.forEach(function(k) {
if (k !== value) ret[ret.length] = k;
}) ;
@@ -9077,7 +9646,7 @@ Ember.Enumerable = Ember.Mixin.create({
@return {Ember.Enumerable}
*/
uniq: function() {
- var ret = Ember.A([]);
+ var ret = Ember.A();
this.forEach(function(k){
if (a_indexOf(ret, k)<0) ret.push(k);
});
@@ -9393,7 +9962,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot
@return {Array} New array with specified slice
*/
slice: function(beginIndex, endIndex) {
- var ret = Ember.A([]);
+ var ret = Ember.A();
var length = get(this, 'length') ;
if (isNone(beginIndex)) beginIndex = 0 ;
if (isNone(endIndex) || (endIndex > length)) endIndex = length ;
@@ -10150,7 +10719,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/**
```javascript
var colors = ["red", "green", "blue"];
- colors.pushObjects("black"); // ["red", "green", "blue", "black"]
+ colors.pushObjects(["black"]); // ["red", "green", "blue", "black"]
colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"]
```
@@ -10159,6 +10728,9 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/**
@return {Ember.Array} receiver
*/
pushObjects: function(objects) {
+ if(!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) {
+ throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects");
+ }
this.replace(get(this, 'length'), 0, objects);
return this;
},
@@ -11081,7 +11653,7 @@ Ember.Evented = Ember.Mixin.create({
},
/**
- Cancels subscription for give name, target, and method.
+ Cancels subscription for given name, target, and method.
@method off
@param {String} name The name of the event
@@ -11213,8 +11785,8 @@ Ember.Container.set = Ember.set;
*/
-// NOTE: this object should never be included directly. Instead use Ember.
-// Ember.Object. We only define this separately so that Ember.Set can depend on it
+// NOTE: this object should never be included directly. Instead use `Ember.Object`.
+// We only define this separately so that `Ember.Set` can depend on it.
var set = Ember.set, get = Ember.get,
@@ -12246,6 +12818,9 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array
},
pushObjects: function(objects) {
+ if(!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) {
+ throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects");
+ }
this._replace(get(this, 'length'), 0, objects);
return this;
},
@@ -13498,16 +14073,39 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, {
*/
sortAscending: true,
+ /**
+ The function used to compare two values. You can override this if you
+ want to do custom comparisons.Functions must be of the type expected by
+ Array#sort, i.e.
+ return 0 if the two parameters are equal,
+ return a negative value if the first parameter is smaller than the second or
+ return a positive value otherwise:
+
+ ```javascript
+ function(x,y){ // These are assumed to be integers
+ if(x === y)
+ return 0;
+ return x < y ? -1 : 1;
+ }
+ ```
+
+ @property sortFunction
+ @type {Function}
+ @default Ember.compare
+ */
+ sortFunction: Ember.compare,
+
orderBy: function(item1, item2) {
var result = 0,
sortProperties = get(this, 'sortProperties'),
- sortAscending = get(this, 'sortAscending');
+ sortAscending = get(this, 'sortAscending'),
+ sortFunction = get(this, 'sortFunction');
Ember.assert("you need to define `sortProperties`", !!sortProperties);
forEach(sortProperties, function(propertyName) {
if (result === 0) {
- result = Ember.compare(get(item1, propertyName), get(item2, propertyName));
+ result = sortFunction(get(item1, propertyName), get(item2, propertyName));
if ((result !== 0) && !sortAscending) {
result = (-1) * result;
}
@@ -13945,7 +14543,7 @@ Ember Runtime
*/
var jQuery = Ember.imports.jQuery;
-Ember.assert("Ember Views require jQuery 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY));
+Ember.assert("Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY));
/**
Alias for jQuery
@@ -14643,6 +15241,47 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
*/
Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{
+ /**
+ The set of events names (and associated handler function names) to be setup
+ and dispatched by the `EventDispatcher`. Custom events can added to this list at setup
+ time, generally via the `Ember.Application.customEvents` hash. Only override this
+ default set to prevent the EventDispatcher from listening on some events all together.
+
+ This set will be modified by `setup` to also include any events added at that time.
+
+ @property events
+ @type Object
+ */
+ events: {
+ touchstart : 'touchStart',
+ touchmove : 'touchMove',
+ touchend : 'touchEnd',
+ touchcancel : 'touchCancel',
+ keydown : 'keyDown',
+ keyup : 'keyUp',
+ keypress : 'keyPress',
+ mousedown : 'mouseDown',
+ mouseup : 'mouseUp',
+ contextmenu : 'contextMenu',
+ click : 'click',
+ dblclick : 'doubleClick',
+ mousemove : 'mouseMove',
+ focusin : 'focusIn',
+ focusout : 'focusOut',
+ mouseenter : 'mouseEnter',
+ mouseleave : 'mouseLeave',
+ submit : 'submit',
+ input : 'input',
+ change : 'change',
+ dragstart : 'dragStart',
+ drag : 'drag',
+ dragenter : 'dragEnter',
+ dragleave : 'dragLeave',
+ dragover : 'dragOver',
+ drop : 'drop',
+ dragend : 'dragEnd'
+ },
+
/**
@private
@@ -14674,35 +15313,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro
@param addedEvents {Hash}
*/
setup: function(addedEvents, rootElement) {
- var event, events = {
- touchstart : 'touchStart',
- // touchmove : 'touchMove',
- touchend : 'touchEnd',
- touchcancel : 'touchCancel',
- keydown : 'keyDown',
- keyup : 'keyUp',
- keypress : 'keyPress',
- mousedown : 'mouseDown',
- mouseup : 'mouseUp',
- contextmenu : 'contextMenu',
- click : 'click',
- dblclick : 'doubleClick',
- // mousemove : 'mouseMove',
- focusin : 'focusIn',
- focusout : 'focusOut',
- mouseenter : 'mouseEnter',
- mouseleave : 'mouseLeave',
- submit : 'submit',
- input : 'input',
- change : 'change',
- dragstart : 'dragStart',
- drag : 'drag',
- dragenter : 'dragEnter',
- dragleave : 'dragLeave',
- dragover : 'dragOver',
- drop : 'drop',
- dragend : 'dragEnd'
- };
+ var event, events = get(this, 'events');
Ember.$.extend(events, addedEvents || {});
@@ -14949,6 +15560,15 @@ Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionali
*/
Ember.TEMPLATES = {};
+/**
+ `Ember.CoreView` is
+
+ @class CoreView
+ @namespace Ember
+ @extends Ember.Object
+ @uses Ember.Evented
+*/
+
Ember.CoreView = Ember.Object.extend(Ember.Evented, {
isView: true,
@@ -15652,8 +16272,10 @@ class:
### Event Names
- Possible events names for any of the responding approaches described above
- are:
+ All of the event handling approaches described above respond to the same set
+ of events. The names of the built-in events are listed below. (The hash of
+ built-in events exists in `Ember.EventDispatcher`.) Additional, custom events
+ can be registered by using `Ember.Application.customEvents`.
Touch events:
@@ -15706,8 +16328,7 @@ class:
@class View
@namespace Ember
- @extends Ember.Object
- @uses Ember.Evented
+ @extends Ember.CoreView
*/
Ember.View = Ember.CoreView.extend(
/** @scope Ember.View.prototype */ {
@@ -16018,6 +16639,8 @@ Ember.View = Ember.CoreView.extend(
_parentViewDidChange: Ember.observer(function() {
if (this.isDestroying) { return; }
+ this.trigger('parentViewDidChange');
+
if (get(this, 'parentView.controller') && !get(this, 'controller')) {
this.notifyPropertyChange('controller');
}
@@ -16289,9 +16912,9 @@ Ember.View = Ember.CoreView.extend(
For example, calling `view.$('li')` will return a jQuery object containing
all of the `li` elements inside the DOM element of this view.
- @property $
+ @method $
@param {String} [selector] a jQuery-compatible selector string
- @return {jQuery} the CoreQuery object for the DOM node
+ @return {jQuery} the jQuery object for the DOM node
*/
$: function(sel) {
return this.currentState.$(this, sel);
@@ -16972,31 +17595,32 @@ Ember.View = Ember.CoreView.extend(
@return {Ember.View} new instance
*/
createChildView: function(view, attrs) {
- if (view.isView && view._parentView === this) { return view; }
+ if (view.isView && view._parentView === this && view.container === this.container) {
+ return view;
+ }
+
+ attrs = attrs || {};
+ attrs._parentView = this;
+ attrs.container = this.container;
if (Ember.CoreView.detect(view)) {
- attrs = attrs || {};
- attrs._parentView = this;
- attrs.container = this.container;
attrs.templateData = attrs.templateData || get(this, 'templateData');
view = view.create(attrs);
// don't set the property on a virtual view, as they are invisible to
// consumers of the view API
- if (view.viewName) { set(get(this, 'concreteView'), view.viewName, view); }
+ if (view.viewName) {
+ set(get(this, 'concreteView'), view.viewName, view);
+ }
} else {
Ember.assert('You must pass instance or subclass of View', view.isView);
- if (attrs) {
- view.setProperties(attrs);
- }
+ Ember.setProperties(view, attrs);
if (!get(view, 'templateData')) {
set(view, 'templateData', get(this, 'templateData'));
}
-
- set(view, '_parentView', this);
}
return view;
@@ -17343,8 +17967,8 @@ Ember.View.applyAttributeBindings = function(elem, name, value) {
elem.attr(name, value);
}
} else if (name === 'value' || type === 'boolean') {
- // We can't set properties to undefined
- if (value === undefined) { value = null; }
+ // We can't set properties to undefined or null
+ if (Ember.isNone(value)) { value = ''; }
if (value !== elem.prop(name)) {
// value and booleans should always be properties
@@ -18044,6 +18668,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, {
initializeViews: function(views, parentView, templateData) {
forEach(views, function(view) {
set(view, '_parentView', parentView);
+ set(view, 'container', parentView && parentView.container);
if (!get(view, 'templateData')) {
set(view, 'templateData', templateData);
@@ -18509,6 +19134,102 @@ Ember.CollectionView.CONTAINER_MAP = {
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+/**
+ An `Ember.Component` is a view that is completely
+ isolated. Property access in its templates go
+ to the view object and actions are targeted at
+ the view object. There is no access to the
+ surrounding context or outer controller; all
+ contextual information is passed in.
+
+ The easiest way to create an `Ember.Component` is via
+ a template. If you name a template
+ `components/my-foo`, you will be able to use
+ `{{my-foo}}` in other templates, which will make
+ an instance of the isolated component.
+
+ ```html
+ {{app-profile person=currentUser}}
+ ```
+
+ ```html
+
+
{{person.title}}
+
+
{{person.signature}}
+ ```
+
+ You can also use `yield` inside a template to
+ include the **contents** of the custom tag:
+
+ ```html
+ {{#app-profile person=currentUser}}
+
Admin mode
+ {{/app-profile}}
+ ```
+
+ ```html
+
+
{{person.title}}
+ {{yield}}
+ ```
+
+ If you want to customize the component, in order to
+ handle events or actions, you implement a subclass
+ of `Ember.Component` named after the name of the
+ component.
+
+ For example, you could implement the action
+ `hello` for the `app-profile` component:
+
+ ```js
+ App.AppProfileComponent = Ember.Component.extend({
+ hello: function(name) {
+ console.log("Hello", name)
+ }
+ });
+ ```
+
+ And then use it in the component's template:
+
+ ```html
+
+
+
{{person.title}}
+ {{yield}}
+
+
+ ```
+
+ Components must have a `-` in their name to avoid
+ conflicts with built-in controls that wrap HTML
+ elements. This is consistent with the same
+ requirement in web components.
+
+ @class Component
+ @namespace Ember
+ @extends Ember.View
+*/
+Ember.Component = Ember.View.extend({
+ init: function() {
+ this._super();
+ this.set('context', this);
+ this.set('controller', this);
+ }
+});
+
+})();
+
+
+
(function() {
})();
@@ -19070,8 +19791,8 @@ if(!Handlebars && typeof require === 'function') {
Handlebars = require('handlebars');
}
-Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars)
-Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4, COMPILER_REVISION expected: 3, got: " + Handlebars.COMPILER_REVISION + " – Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 3);
+Ember.assert("Ember Handlebars requires Handlebars version 1.0.0. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars);
+Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + " - Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 4);
/**
Prepares the Handlebars templating library for use inside Ember's view
@@ -19157,6 +19878,17 @@ function makeBindings(options) {
@param {String} dependentKeys*
*/
Ember.Handlebars.helper = function(name, value) {
+ if (Ember.Component.detect(value)) {
+ Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", name.match(/-/));
+
+ var proto = value.proto();
+ if (!proto.layoutName && !proto.templateName) {
+ value.reopen({
+ layoutName: 'components/' + name
+ });
+ }
+ }
+
if (Ember.View.detect(value)) {
Ember.Handlebars.registerHelper(name, function(options) {
Ember.assert("You can only pass attributes as parameters (not values) to a application-defined helper", arguments.length < 2);
@@ -19166,7 +19898,7 @@ Ember.Handlebars.helper = function(name, value) {
} else {
Ember.Handlebars.registerBoundHelper.apply(null, arguments);
}
-}
+};
/**
@class helpers
@@ -19247,7 +19979,7 @@ Ember.Handlebars.Compiler.prototype.mustache = function(mustache) {
} else if (mustache.params.length || mustache.hash) {
// no changes required
} else {
- var id = new Handlebars.AST.IdNode(['_triageMustache']);
+ var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]);
// Update the mustache node to include a hash value indicating whether the original node
// was escaped. This will allow us to properly escape values when the underlying value
@@ -19683,7 +20415,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt
view.appendChild(bindView);
- // Assemble liast of watched properties that'll re-render this helper.
+ // Assemble list of watched properties that'll re-render this helper.
watchedProperties = [];
for (boundOption in boundOptions) {
if (boundOptions.hasOwnProperty(boundOption)) {
@@ -19917,7 +20649,7 @@ Ember._MetamorphView = Ember.View.extend(Ember._Metamorph);
/**
@class _SimpleMetamorphView
@namespace Ember
- @extends Ember.View
+ @extends Ember.CoreView
@uses Ember._Metamorph
@private
*/
@@ -20905,6 +21637,8 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
var get = Ember.get, set = Ember.set;
var EmberHandlebars = Ember.Handlebars;
+var LOWERCASE_A_Z = /^[a-z]/;
+var VIEW_PREFIX = /^view\./;
EmberHandlebars.ViewHelper = Ember.Object.create({
@@ -21016,7 +21750,18 @@ EmberHandlebars.ViewHelper = Ember.Object.create({
newView;
if ('string' === typeof path) {
- newView = EmberHandlebars.get(thisContext, path, options);
+
+ // TODO: this is a lame conditional, this should likely change
+ // but something along these lines will likely need to be added
+ // as deprecation warnings
+ //
+ if (options.types[0] === 'STRING' && LOWERCASE_A_Z.test(path) && !VIEW_PREFIX.test(path)) {
+ Ember.assert("View requires a container", !!data.view.container);
+ newView = data.view.container.lookupFactory('view:' + path);
+ } else {
+ newView = EmberHandlebars.get(thisContext, path, options);
+ }
+
Ember.assert("Unable to find view at path '" + path + "'", !!newView);
} else {
newView = path;
@@ -21385,11 +22130,25 @@ Ember.Handlebars.registerHelper('collection', function(path, options) {
var hash = options.hash, itemHash = {}, match;
// Extract item view class if provided else default to the standard class
- var itemViewClass, itemViewPath = hash.itemViewClass;
- var collectionPrototype = collectionClass.proto();
+ var collectionPrototype = collectionClass.proto(),
+ itemViewClass;
+
+ if (hash.itemView) {
+ var controller = data.keywords.controller;
+ Ember.assert('itemView given, but no container is available', controller && controller.container);
+ var container = controller.container;
+ itemViewClass = container.resolve('view:' + Ember.String.camelize(hash.itemView));
+ Ember.assert('itemView not found in container', !!itemViewClass);
+ } else if (hash.itemViewClass) {
+ itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options);
+ } else {
+ itemViewClass = collectionPrototype.itemViewClass;
+ }
+
+ Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass);
+
delete hash.itemViewClass;
- itemViewClass = itemViewPath ? handlebarsGet(collectionPrototype, itemViewPath, options) : collectionPrototype.itemViewClass;
- Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewPath]), !!itemViewClass);
+ delete hash.itemView;
// Go through options passed to the {{collection}} helper and extract options
// that configure item views instead of the collection itself.
@@ -21805,6 +22564,12 @@ GroupedEach.prototype = {
```
+ If an `itemViewClass` is defined on the helper, and therefore the helper is not
+ being used as a block, an `emptyViewClass` can also be provided optionally.
+ The `emptyViewClass` will match the behavior of the `{{else}}` condition
+ described above. That is, the `emptyViewClass` will render if the collection
+ is empty.
+
### Representing each item with a Controller.
By default the controller lookup within an `{{#each}}` block will be
the controller of the template where the `{{#each}}` was used. If each
@@ -22068,6 +22833,38 @@ Ember.Handlebars.registerHelper('yield', function(options) {
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+/**
+ `loc` looks up the string in the localized strings hash.
+ This is a convenient way to localize text. For example:
+
+ ```html
+
+ ```
+
+ Take note that `welcome` is a string and not an object
+ reference.
+
+ @method loc
+ @for Ember.Handlebars.helpers
+ @param {String} str The string to format
+*/
+
+Ember.Handlebars.registerHelper('loc', function(str) {
+ return Ember.String.loc(str);
+});
+
+})();
+
+
+
(function() {
})();
@@ -22584,6 +23381,7 @@ var set = Ember.set,
get = Ember.get,
indexOf = Ember.EnumerableUtils.indexOf,
indexesOf = Ember.EnumerableUtils.indexesOf,
+ forEach = Ember.EnumerableUtils.forEach,
replace = Ember.EnumerableUtils.replace,
isArray = Ember.isArray,
precompileTemplate = Ember.Handlebars.compile;
@@ -22637,6 +23435,18 @@ Ember.SelectOption = Ember.View.extend({
}, 'parentView.optionValuePath')
});
+Ember.SelectOptgroup = Ember.CollectionView.extend({
+ tagName: 'optgroup',
+ attributeBindings: ['label'],
+
+ selectionBinding: 'parentView.selection',
+ multipleBinding: 'parentView.multiple',
+ optionLabelPathBinding: 'parentView.optionLabelPath',
+ optionValuePathBinding: 'parentView.optionValuePath',
+
+ itemViewClassBinding: 'parentView.optionView'
+});
+
/**
The `Ember.Select` view class renders a
[select](https://developer.mozilla.org/en/HTML/Element/select) HTML element,
@@ -22733,7 +23543,6 @@ Ember.SelectOption = Ember.View.extend({
```html
@@ -22766,7 +23575,6 @@ Ember.SelectOption = Ember.View.extend({
```html
@@ -22804,7 +23612,6 @@ Ember.SelectOption = Ember.View.extend({
```html
@@ -22890,12 +23697,12 @@ Ember.Select = Ember.View.extend(
tagName: 'select',
classNames: ['ember-select'],
defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
-this.compilerInfo = [3,'>= 1.0.0-rc.4'];
-helpers = helpers || Ember.Handlebars.helpers; data = data || {};
+this.compilerInfo = [4,'>= 1.0.0'];
+helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this;
function program1(depth0,data) {
-
+
var buffer = '', hashTypes, hashContexts;
data.buffer.push("
```
+ If an `itemViewClass` is defined on the helper, and therefore the helper is not
+ being used as a block, an `emptyViewClass` can also be provided optionally.
+ The `emptyViewClass` will match the behavior of the `{{else}}` condition
+ described above. That is, the `emptyViewClass` will render if the collection
+ is empty.
+
### Representing each item with a Controller.
By default the controller lookup within an `{{#each}}` block will be
the controller of the template where the `{{#each}}` was used. If each
@@ -21879,6 +22636,38 @@ Ember.Handlebars.registerHelper('yield', function(options) {
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+/**
+ `loc` looks up the string in the localized strings hash.
+ This is a convenient way to localize text. For example:
+
+ ```html
+
+ ```
+
+ Take note that `welcome` is a string and not an object
+ reference.
+
+ @method loc
+ @for Ember.Handlebars.helpers
+ @param {String} str The string to format
+*/
+
+Ember.Handlebars.registerHelper('loc', function(str) {
+ return Ember.String.loc(str);
+});
+
+})();
+
+
+
(function() {
})();
@@ -22395,6 +23184,7 @@ var set = Ember.set,
get = Ember.get,
indexOf = Ember.EnumerableUtils.indexOf,
indexesOf = Ember.EnumerableUtils.indexesOf,
+ forEach = Ember.EnumerableUtils.forEach,
replace = Ember.EnumerableUtils.replace,
isArray = Ember.isArray,
precompileTemplate = Ember.Handlebars.compile;
@@ -22448,6 +23238,18 @@ Ember.SelectOption = Ember.View.extend({
}, 'parentView.optionValuePath')
});
+Ember.SelectOptgroup = Ember.CollectionView.extend({
+ tagName: 'optgroup',
+ attributeBindings: ['label'],
+
+ selectionBinding: 'parentView.selection',
+ multipleBinding: 'parentView.multiple',
+ optionLabelPathBinding: 'parentView.optionLabelPath',
+ optionValuePathBinding: 'parentView.optionValuePath',
+
+ itemViewClassBinding: 'parentView.optionView'
+});
+
/**
The `Ember.Select` view class renders a
[select](https://developer.mozilla.org/en/HTML/Element/select) HTML element,
@@ -22544,7 +23346,6 @@ Ember.SelectOption = Ember.View.extend({
```html
@@ -22577,7 +23378,6 @@ Ember.SelectOption = Ember.View.extend({
```html
@@ -22615,7 +23415,6 @@ Ember.SelectOption = Ember.View.extend({
```html
@@ -22701,8 +23500,8 @@ Ember.Select = Ember.View.extend(
tagName: 'select',
classNames: ['ember-select'],
defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
-this.compilerInfo = [3,'>= 1.0.0-rc.4'];
-helpers = helpers || Ember.Handlebars.helpers; data = data || {};
+this.compilerInfo = [4,'>= 1.0.0'];
+helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this;
function program1(depth0,data) {
@@ -22718,6 +23517,35 @@ function program1(depth0,data) {
function program3(depth0,data) {
+ var stack1, hashTypes, hashContexts;
+ hashTypes = {};
+ hashContexts = {};
+ stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data});
+ if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
+ else { data.buffer.push(''); }
+ }
+function program4(depth0,data) {
+
+ var hashContexts, hashTypes;
+ hashContexts = {'contentBinding': depth0,'labelBinding': depth0};
+ hashTypes = {'contentBinding': "ID",'labelBinding': "ID"};
+ data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{
+ 'contentBinding': ("content"),
+ 'labelBinding': ("label")
+ },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data})));
+ }
+
+function program6(depth0,data) {
+
+ var stack1, hashTypes, hashContexts;
+ hashTypes = {};
+ hashContexts = {};
+ stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data});
+ if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
+ else { data.buffer.push(''); }
+ }
+function program7(depth0,data) {
+
var hashContexts, hashTypes;
hashContexts = {'contentBinding': depth0};
hashTypes = {'contentBinding': "STRING"};
@@ -22732,7 +23560,7 @@ function program3(depth0,data) {
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
hashTypes = {};
hashContexts = {};
- stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data});
+ stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
return buffer;
@@ -22832,6 +23660,45 @@ function program3(depth0,data) {
*/
optionValuePath: 'content',
+ /**
+ The path of the option group.
+ When this property is used, `content` should be sorted by `optionGroupPath`.
+
+ @property optionGroupPath
+ @type String
+ @default null
+ */
+ optionGroupPath: null,
+
+ /**
+ The view class for optgroup.
+
+ @property groupView
+ @type Ember.View
+ @default Ember.SelectOptgroup
+ */
+ groupView: Ember.SelectOptgroup,
+
+ groupedContent: Ember.computed(function() {
+ var groupPath = get(this, 'optionGroupPath');
+ var groupedContent = Ember.A();
+
+ forEach(get(this, 'content'), function(item) {
+ var label = get(item, groupPath);
+
+ if (get(groupedContent, 'lastObject.label') !== label) {
+ groupedContent.pushObject({
+ label: label,
+ content: Ember.A()
+ });
+ }
+
+ get(groupedContent, 'lastObject.content').push(item);
+ });
+
+ return groupedContent;
+ }).property('optionGroupPath', 'content.@each'),
+
/**
The view class for option.
@@ -22883,8 +23750,8 @@ function program3(depth0,data) {
var selection = get(this, 'selection');
var value = get(this, 'value');
- if (selection) { this.selectionDidChange(); }
- if (value) { this.valueDidChange(); }
+ if (!Ember.isNone(selection)) { this.selectionDidChange(); }
+ if (!Ember.isNone(value)) { this.valueDidChange(); }
this._change();
},
@@ -23081,6 +23948,31 @@ function bootstrap() {
Ember.Handlebars.bootstrap( Ember.$(document) );
}
+function registerComponents(container) {
+ var templates = Ember.TEMPLATES, match;
+ if (!templates) { return; }
+
+ for (var prop in templates) {
+ if (match = prop.match(/^components\/(.*)$/)) {
+ registerComponent(container, match[1]);
+ }
+ }
+}
+
+function registerComponent(container, name) {
+
+
+ var className = name.replace(/-/g, '_');
+ var Component = container.lookupFactory('component:' + className) || container.lookupFactory('component:' + name);
+ var View = Component || Ember.Component.extend();
+
+ View.reopen({
+ layoutName: 'components/' + name
+ });
+
+ Ember.Handlebars.helper(name, View);
+}
+
/*
We tie this to application.load to ensure that we've at least
attempted to bootstrap at the point that the application is loaded.
@@ -23092,7 +23984,23 @@ function bootstrap() {
from the DOM after processing.
*/
-Ember.onLoad('application', bootstrap);
+Ember.onLoad('Ember.Application', function(Application) {
+ if (Application.initializer) {
+ Application.initializer({
+ name: 'domTemplates',
+ initialize: bootstrap
+ });
+
+ Application.initializer({
+ name: 'registerComponents',
+ after: 'domTemplates',
+ initialize: registerComponents
+ });
+ } else {
+ // for ember-old-router
+ Ember.onLoad('application', bootstrap);
+ }
+});
})();
@@ -23627,8 +24535,8 @@ define("route-recognizer",
(function() {
define("router",
- ["route-recognizer"],
- function(RouteRecognizer) {
+ ["route-recognizer", "rsvp"],
+ function(RouteRecognizer, RSVP) {
"use strict";
/**
@private
@@ -23649,11 +24557,148 @@ define("router",
*/
+ var slice = Array.prototype.slice;
+
+
+
+ /**
+ @private
+
+ A Transition is a thennable (a promise-like object) that represents
+ an attempt to transition to another route. It can be aborted, either
+ explicitly via `abort` or by attempting another transition while a
+ previous one is still underway. An aborted transition can also
+ be `retry()`d later.
+ */
+
+ function Transition(router, promise) {
+ this.router = router;
+ this.promise = promise;
+ this.data = {};
+ this.resolvedModels = {};
+ this.providedModels = {};
+ this.providedModelsArray = [];
+ this.sequence = ++Transition.currentSequence;
+ this.params = {};
+ }
+
+ Transition.currentSequence = 0;
+
+ Transition.prototype = {
+ targetName: null,
+ urlMethod: 'update',
+ providedModels: null,
+ resolvedModels: null,
+ params: null,
+
+ /**
+ The Transition's internal promise. Calling `.then` on this property
+ is that same as calling `.then` on the Transition object itself, but
+ this property is exposed for when you want to pass around a
+ Transition's promise, but not the Transition object itself, since
+ Transition object can be externally `abort`ed, while the promise
+ cannot.
+ */
+ promise: null,
+
+ /**
+ Custom state can be stored on a Transition's `data` object.
+ This can be useful for decorating a Transition within an earlier
+ hook and shared with a later hook. Properties set on `data` will
+ be copied to new transitions generated by calling `retry` on this
+ transition.
+ */
+ data: null,
+
+ /**
+ A standard promise hook that resolves if the transition
+ succeeds and rejects if it fails/redirects/aborts.
+
+ Forwards to the internal `promise` property which you can
+ use in situations where you want to pass around a thennable,
+ but not the Transition itself.
+
+ @param {Function} success
+ @param {Function} failure
+ */
+ then: function(success, failure) {
+ return this.promise.then(success, failure);
+ },
+
+ /**
+ Aborts the Transition. Note you can also implicitly abort a transition
+ by initiating another transition while a previous one is underway.
+ */
+ abort: function() {
+ if (this.isAborted) { return this; }
+ log(this.router, this.sequence, this.targetName + ": transition was aborted");
+ this.isAborted = true;
+ this.router.activeTransition = null;
+ return this;
+ },
+
+ /**
+ Retries a previously-aborted transition (making sure to abort the
+ transition if it's still active). Returns a new transition that
+ represents the new attempt to transition.
+ */
+ retry: function() {
+ this.abort();
+
+ var recogHandlers = this.router.recognizer.handlersFor(this.targetName),
+ newTransition = performTransition(this.router, recogHandlers, this.providedModelsArray, this.params, this.data);
+
+ return newTransition;
+ },
+
+ /**
+ Sets the URL-changing method to be employed at the end of a
+ successful transition. By default, a new Transition will just
+ use `updateURL`, but passing 'replace' to this method will
+ cause the URL to update using 'replaceWith' instead. Omitting
+ a parameter will disable the URL change, allowing for transitions
+ that don't update the URL at completion (this is also used for
+ handleURL, since the URL has already changed before the
+ transition took place).
+
+ @param {String} method the type of URL-changing method to use
+ at the end of a transition. Accepted values are 'replace',
+ falsy values, or any other non-falsy value (which is
+ interpreted as an updateURL transition).
+
+ @return {Transition} this transition
+ */
+ method: function(method) {
+ this.urlMethod = method;
+ return this;
+ }
+ };
+
function Router() {
this.recognizer = new RouteRecognizer();
}
+
+ /**
+ Promise reject reasons passed to promise rejection
+ handlers for failed transitions.
+ */
+ Router.UnrecognizedURLError = function(message) {
+ this.message = (message || "UnrecognizedURLError");
+ this.name = "UnrecognizedURLError";
+ };
+
+ Router.TransitionAborted = function(message) {
+ this.message = (message || "TransitionAborted");
+ this.name = "TransitionAborted";
+ };
+
+ function errorTransition(router, reason) {
+ return new Transition(router, RSVP.reject(reason));
+ }
+
+
Router.prototype = {
/**
The main entry point into the router. The API is essentially
@@ -23684,7 +24729,8 @@ define("router",
its ancestors.
*/
reset: function() {
- eachHandler(this.currentHandlerInfos || [], function(handler) {
+ eachHandler(this.currentHandlerInfos || [], function(handlerInfo) {
+ var handler = handlerInfo.handler;
if (handler.exit) {
handler.exit();
}
@@ -23693,7 +24739,10 @@ define("router",
this.targetHandlerInfos = null;
},
+ activeTransition: null,
+
/**
+ var handler = handlerInfo.handler;
The entry point for handling a change to the URL (usually
via the back and forward button).
@@ -23705,13 +24754,11 @@ define("router",
@return {Array} an Array of `[handler, parameter]` tuples
*/
handleURL: function(url) {
- var results = this.recognizer.recognize(url);
-
- if (!results) {
- throw new Error("No route matched the URL '" + url + "'");
- }
-
- collectObjects(this, results, 0, []);
+ // Perform a URL-based transition, but don't change
+ // the URL afterward, since it already happened.
+ var args = slice.call(arguments);
+ if (url.charAt(0) !== '/') { args[0] = '/' + url; }
+ return doTransition(this, args).method(null);
},
/**
@@ -23720,7 +24767,7 @@ define("router",
@param {String} url a URL to update to
*/
updateURL: function() {
- throw "updateURL is not implemented";
+ throw new Error("updateURL is not implemented");
},
/**
@@ -23743,8 +24790,7 @@ define("router",
@param {String} name the name of the route
*/
transitionTo: function(name) {
- var args = Array.prototype.slice.call(arguments, 1);
- doTransition(this, name, this.updateURL, args);
+ return doTransition(this, arguments);
},
/**
@@ -23756,8 +24802,7 @@ define("router",
@param {String} name the name of the route
*/
replaceWith: function(name) {
- var args = Array.prototype.slice.call(arguments, 1);
- doTransition(this, name, this.replaceURL, args);
+ return doTransition(this, arguments).method('replace');
},
/**
@@ -23771,8 +24816,7 @@ define("router",
@return {Object} a serialized parameter hash
*/
paramsForHandler: function(handlerName, callback) {
- var output = this._paramsForHandler(handlerName, [].slice.call(arguments, 1));
- return output.params;
+ return paramsForHandler(this, handlerName, slice.call(arguments, 1));
},
/**
@@ -23786,109 +24830,17 @@ define("router",
@return {String} a URL
*/
generate: function(handlerName) {
- var params = this.paramsForHandler.apply(this, arguments);
+ var params = paramsForHandler(this, handlerName, slice.call(arguments, 1));
return this.recognizer.generate(handlerName, params);
},
- /**
- @private
-
- Used internally by `generate` and `transitionTo`.
- */
- _paramsForHandler: function(handlerName, objects, doUpdate) {
- var handlers = this.recognizer.handlersFor(handlerName),
- params = {},
- toSetup = [],
- startIdx = handlers.length,
- objectsToMatch = objects.length,
- object, objectChanged, handlerObj, handler, names, i;
-
- // Find out which handler to start matching at
- for (i=handlers.length-1; i>=0 && objectsToMatch>0; i--) {
- if (handlers[i].names.length) {
- objectsToMatch--;
- startIdx = i;
- }
- }
-
- if (objectsToMatch > 0) {
- throw "More context objects were passed than there are dynamic segments for the route: "+handlerName;
- }
-
- // Connect the objects to the routes
- for (i=0; i= startIdx) {
- object = objects.shift();
- objectChanged = true;
- // Otherwise use existing context
- } else {
- object = handler.context;
- }
-
- // Serialize to generate params
- if (handler.serialize) {
- merge(params, handler.serialize(object, names));
- }
- // If it's not a dynamic segment and we're updating
- } else if (doUpdate) {
- // If we've passed the match point we need to deserialize again
- // or if we never had a context
- if (i > startIdx || !handler.hasOwnProperty('context')) {
- if (handler.deserialize) {
- object = handler.deserialize({});
- objectChanged = true;
- }
- // Otherwise use existing context
- } else {
- object = handler.context;
- }
- }
-
- // Make sure that we update the context here so it's available to
- // subsequent deserialize calls
- if (doUpdate && objectChanged) {
- // TODO: It's a bit awkward to set the context twice, see if we can DRY things up
- setContext(handler, object);
- }
-
- toSetup.push({
- isDynamic: !!handlerObj.names.length,
- name: handlerObj.handler,
- handler: handler,
- context: object
- });
-
- if (i === handlers.length - 1) {
- var lastHandler = toSetup[toSetup.length - 1],
- additionalHandler;
-
- if (additionalHandler = lastHandler.handler.additionalHandler) {
- handlers.push({
- handler: additionalHandler.call(lastHandler.handler),
- names: []
- });
- }
- }
- }
-
- return { params: params, toSetup: toSetup };
- },
-
isActive: function(handlerName) {
- var contexts = [].slice.call(arguments, 1);
+ var contexts = slice.call(arguments, 1);
var targetHandlerInfos = this.targetHandlerInfos,
found = false, names, object, handlerInfo, handlerObj;
- if (!targetHandlerInfos) { return; }
+ if (!targetHandlerInfos) { return false; }
for (var i=targetHandlerInfos.length-1; i>=0; i--) {
handlerInfo = targetHandlerInfos[i];
@@ -23908,164 +24860,187 @@ define("router",
},
trigger: function(name) {
- var args = [].slice.call(arguments);
- trigger(this, args);
- }
+ var args = slice.call(arguments);
+ trigger(this.currentHandlerInfos, false, args);
+ },
+
+ /**
+ Hook point for logging transition status updates.
+
+ @param {String} message The message to log.
+ */
+ log: null
};
+ /**
+ @private
+
+ Used internally for both URL and named transition to determine
+ a shared pivot parent route and other data necessary to perform
+ a transition.
+ */
+ function getMatchPoint(router, handlers, objects, inputParams) {
+
+ var matchPoint = handlers.length,
+ providedModels = {}, i,
+ currentHandlerInfos = router.currentHandlerInfos || [],
+ params = {},
+ oldParams = router.currentParams || {},
+ activeTransition = router.activeTransition,
+ handlerParams = {},
+ obj;
+
+ objects = slice.call(objects);
+ merge(params, inputParams);
+
+ for (i = handlers.length - 1; i >= 0; i--) {
+ var handlerObj = handlers[i],
+ handlerName = handlerObj.handler,
+ oldHandlerInfo = currentHandlerInfos[i],
+ hasChanged = false;
+
+ // Check if handler names have changed.
+ if (!oldHandlerInfo || oldHandlerInfo.name !== handlerObj.handler) { hasChanged = true; }
+
+ if (handlerObj.isDynamic) {
+ // URL transition.
+
+ if (obj = getMatchPointObject(objects, handlerName, activeTransition, true, params)) {
+ hasChanged = true;
+ providedModels[handlerName] = obj;
+ } else {
+ handlerParams[handlerName] = {};
+ for (var prop in handlerObj.params) {
+ if (!handlerObj.params.hasOwnProperty(prop)) { continue; }
+ var newParam = handlerObj.params[prop];
+ if (oldParams[prop] !== newParam) { hasChanged = true; }
+ handlerParams[handlerName][prop] = params[prop] = newParam;
+ }
+ }
+ } else if (handlerObj.hasOwnProperty('names')) {
+ // Named transition.
+
+ if (objects.length) { hasChanged = true; }
+
+ if (obj = getMatchPointObject(objects, handlerName, activeTransition, handlerObj.names[0], params)) {
+ providedModels[handlerName] = obj;
+ } else {
+ var names = handlerObj.names;
+ handlerParams[handlerName] = {};
+ for (var j = 0, len = names.length; j < len; ++j) {
+ var name = names[j];
+ handlerParams[handlerName][name] = params[name] = params[name] || oldParams[name];
+ }
+ }
+ }
+
+ if (hasChanged) { matchPoint = i; }
+ }
+
+ if (objects.length > 0) {
+ throw new Error("More context objects were passed than there are dynamic segments for the route: " + handlers[handlers.length - 1].handler);
+ }
+
+ return { matchPoint: matchPoint, providedModels: providedModels, params: params, handlerParams: handlerParams };
+ }
+
+ function getMatchPointObject(objects, handlerName, activeTransition, paramName, params) {
+
+ if (objects.length && paramName) {
+
+ var object = objects.pop();
+
+ // If provided object is string or number, treat as param.
+ if (isParam(object)) {
+ params[paramName] = object.toString();
+ } else {
+ return object;
+ }
+ } else if (activeTransition) {
+ // Use model from previous transition attempt, preferably the resolved one.
+ return (paramName && activeTransition.providedModels[handlerName]) ||
+ activeTransition.resolvedModels[handlerName];
+ }
+ }
+
+ function isParam(object) {
+ return object && (typeof object === "string" || object instanceof String || !isNaN(object));
+ }
+
+ /**
+ @private
+
+ This method takes a handler name and a list of contexts and returns
+ a serialized parameter hash suitable to pass to `recognizer.generate()`.
+
+ @param {Router} router
+ @param {String} handlerName
+ @param {Array[Object]} objects
+ @return {Object} a serialized parameter hash
+ */
+ function paramsForHandler(router, handlerName, objects) {
+
+ var handlers = router.recognizer.handlersFor(handlerName),
+ params = {},
+ matchPoint = getMatchPoint(router, handlers, objects).matchPoint,
+ object, handlerObj, handler, names, i;
+
+ for (i=0; i= matchPoint) {
+ object = objects.shift();
+ // Otherwise use existing context
+ } else {
+ object = handler.context;
+ }
+
+ // Serialize to generate params
+ merge(params, serialize(handler, object, names));
+ }
+ }
+ return params;
+ }
+
function merge(hash, other) {
for (var prop in other) {
if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; }
}
}
- function isCurrent(currentHandlerInfos, handlerName) {
- return currentHandlerInfos[currentHandlerInfos.length - 1].name === handlerName;
- }
-
/**
@private
-
- This function is called the first time the `collectObjects`
- function encounters a promise while converting URL parameters
- into objects.
-
- It triggers the `enter` and `setup` methods on the `loading`
- handler.
-
- @param {Router} router
*/
- function loading(router) {
- if (!router.isLoading) {
- router.isLoading = true;
- var handler = router.getHandler('loading');
+ function createNamedTransition(router, args) {
+ var handlers = router.recognizer.handlersFor(args[0]);
- if (handler) {
- if (handler.enter) { handler.enter(); }
- if (handler.setup) { handler.setup(); }
- }
- }
- }
+ log(router, "Attempting transition to " + args[0]);
- /**
- @private
-
- This function is called if a promise was previously
- encountered once all promises are resolved.
-
- It triggers the `exit` method on the `loading` handler.
-
- @param {Router} router
- */
- function loaded(router) {
- router.isLoading = false;
- var handler = router.getHandler('loading');
- if (handler && handler.exit) { handler.exit(); }
- }
-
- /**
- @private
-
- This function is called if any encountered promise
- is rejected.
-
- It triggers the `exit` method on the `loading` handler,
- the `enter` method on the `failure` handler, and the
- `setup` method on the `failure` handler with the
- `error`.
-
- @param {Router} router
- @param {Object} error the reason for the promise
- rejection, to pass into the failure handler's
- `setup` method.
- */
- function failure(router, error) {
- loaded(router);
- var handler = router.getHandler('failure');
- if (handler) {
- if (handler.enter) { handler.enter(); }
- if (handler.setup) { handler.setup(error); }
- }
+ return performTransition(router, handlers, slice.call(args, 1), router.currentParams);
}
/**
@private
*/
- function doTransition(router, name, method, args) {
- var output = router._paramsForHandler(name, args, true);
- var params = output.params, toSetup = output.toSetup;
+ function createURLTransition(router, url) {
- var url = router.recognizer.generate(name, params);
- method.call(router, url);
+ var results = router.recognizer.recognize(url),
+ currentHandlerInfos = router.currentHandlerInfos;
- setupContexts(router, toSetup);
+ log(router, "Attempting URL transition to " + url);
+
+ if (!results) {
+ return errorTransition(router, new Router.UnrecognizedURLError(url));
+ }
+
+ return performTransition(router, results, [], {});
}
- /**
- @private
-
- This function is called after a URL change has been handled
- by `router.handleURL`.
-
- Takes an Array of `RecognizedHandler`s, and converts the raw
- params hashes into deserialized objects by calling deserialize
- on the handlers. This process builds up an Array of
- `HandlerInfo`s. It then calls `setupContexts` with the Array.
-
- If the `deserialize` method on a handler returns a promise
- (i.e. has a method called `then`), this function will pause
- building up the `HandlerInfo` Array until the promise is
- resolved. It will use the resolved value as the context of
- `HandlerInfo`.
- */
- function collectObjects(router, results, index, objects) {
- if (results.length === index) {
- var lastObject = objects[objects.length - 1],
- lastHandler = lastObject && lastObject.handler;
-
- if (lastHandler && lastHandler.additionalHandler) {
- var additionalResult = {
- handler: lastHandler.additionalHandler(),
- params: {},
- isDynamic: false
- };
- results.push(additionalResult);
- } else {
- loaded(router);
- setupContexts(router, objects);
- return;
- }
- }
-
- var result = results[index];
- var handler = router.getHandler(result.handler);
- var object = handler.deserialize && handler.deserialize(result.params);
-
- if (object && typeof object.then === 'function') {
- loading(router);
-
- // The chained `then` means that we can also catch errors that happen in `proceed`
- object.then(proceed).then(null, function(error) {
- failure(router, error);
- });
- } else {
- proceed(object);
- }
-
- function proceed(value) {
- if (handler.context !== object) {
- setContext(handler, object);
- }
-
- var updatedObjects = objects.concat([{
- context: value,
- name: result.handler,
- handler: router.getHandler(result.handler),
- isDynamic: result.isDynamic
- }]);
- collectObjects(router, results, index + 1, updatedObjects);
- }
- }
/**
@private
@@ -24089,7 +25064,7 @@ define("router",
Consider the following transitions:
1. A URL transition to `/posts/1`.
- 1. Triggers the `deserialize` callback on the
+ 1. Triggers the `*model` callbacks on the
`index`, `posts`, and `showPost` handlers
2. Triggers the `enter` callback on the same
3. Triggers the `setup` callback on the same
@@ -24105,16 +25080,17 @@ define("router",
3. Triggers the `enter` callback on `about`
4. Triggers the `setup` callback on `about`
- @param {Router} router
+ @param {Transition} transition
@param {Array[HandlerInfo]} handlerInfos
*/
- function setupContexts(router, handlerInfos) {
- var partition =
- partitionHandlers(router.currentHandlerInfos || [], handlerInfos);
+ function setupContexts(transition, handlerInfos) {
+ var router = transition.router,
+ partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos);
router.targetHandlerInfos = handlerInfos;
- eachHandler(partition.exited, function(handler, context) {
+ eachHandler(partition.exited, function(handlerInfo) {
+ var handler = handlerInfo.handler;
delete handler.context;
if (handler.exit) { handler.exit(); }
});
@@ -24122,33 +25098,51 @@ define("router",
var currentHandlerInfos = partition.unchanged.slice();
router.currentHandlerInfos = currentHandlerInfos;
- eachHandler(partition.updatedContext, function(handler, context, handlerInfo) {
- setContext(handler, context);
- if (handler.setup) { handler.setup(context); }
- currentHandlerInfos.push(handlerInfo);
+ eachHandler(partition.updatedContext, function(handlerInfo) {
+ handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, false);
});
- var aborted = false;
- eachHandler(partition.entered, function(handler, context, handlerInfo) {
- if (aborted) { return; }
- if (handler.enter) { handler.enter(); }
- setContext(handler, context);
- if (handler.setup) {
- if (false === handler.setup(context)) {
- aborted = true;
- }
- }
-
- if (!aborted) {
- currentHandlerInfos.push(handlerInfo);
- }
+ eachHandler(partition.entered, function(handlerInfo) {
+ handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true);
});
- if (!aborted && router.didTransition) {
+ if (router.didTransition) {
router.didTransition(handlerInfos);
}
}
+ /**
+ @private
+
+ Helper method used by setupContexts. Handles errors or redirects
+ that may happen in enter/setup.
+ */
+ function handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, enter) {
+ var handler = handlerInfo.handler,
+ context = handlerInfo.context;
+
+ try {
+ if (enter && handler.enter) { handler.enter(); }
+ checkAbort(transition);
+
+ setContext(handler, context);
+
+ if (handler.setup) { handler.setup(context); }
+ checkAbort(transition);
+ } catch(e) {
+ if (!(e instanceof Router.TransitionAborted)) {
+ // Trigger the `error` event starting from this failed handler.
+ trigger(currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]);
+ }
+
+ // Propagate the error so that the transition promise will reject.
+ throw e;
+ }
+
+ currentHandlerInfos.push(handlerInfo);
+ }
+
+
/**
@private
@@ -24160,11 +25154,7 @@ define("router",
*/
function eachHandler(handlerInfos, callback) {
for (var i=0, l=handlerInfos.length; i=0; i--) {
- var handlerInfo = currentHandlerInfos[i],
+ for (var i=handlerInfos.length-1; i>=0; i--) {
+ var handlerInfo = handlerInfos[i],
handler = handlerInfo.handler;
if (handler.events && handler.events[name]) {
@@ -24268,7 +25258,7 @@ define("router",
}
}
- if (!eventWasHandled) {
+ if (!eventWasHandled && !ignoreFailure) {
throw new Error("Nothing handled the event '" + name + "'.");
}
}
@@ -24277,10 +25267,377 @@ define("router",
handler.context = context;
if (handler.contextDidChange) { handler.contextDidChange(); }
}
+
+ /**
+ @private
+
+ Creates, begins, and returns a Transition.
+ */
+ function performTransition(router, recogHandlers, providedModelsArray, params, data) {
+
+ var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params),
+ targetName = recogHandlers[recogHandlers.length - 1].handler,
+ wasTransitioning = false;
+
+ // Check if there's already a transition underway.
+ if (router.activeTransition) {
+ if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray)) {
+ return router.activeTransition;
+ }
+ router.activeTransition.abort();
+ wasTransitioning = true;
+ }
+
+ var deferred = RSVP.defer(),
+ transition = new Transition(router, deferred.promise);
+
+ transition.targetName = targetName;
+ transition.providedModels = matchPointResults.providedModels;
+ transition.providedModelsArray = providedModelsArray;
+ transition.params = matchPointResults.params;
+ transition.data = data || {};
+ router.activeTransition = transition;
+
+ var handlerInfos = generateHandlerInfos(router, recogHandlers);
+
+ // Fire 'willTransition' event on current handlers, but don't fire it
+ // if a transition was already underway.
+ if (!wasTransitioning) {
+ trigger(router.currentHandlerInfos, true, ['willTransition', transition]);
+ }
+
+ log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName);
+ validateEntry(transition, handlerInfos, 0, matchPointResults.matchPoint, matchPointResults.handlerParams)
+ .then(transitionSuccess, transitionFailure);
+
+ return transition;
+
+ function transitionSuccess() {
+ checkAbort(transition);
+
+ try {
+ finalizeTransition(transition, handlerInfos);
+
+ // Resolve with the final handler.
+ deferred.resolve(handlerInfos[handlerInfos.length - 1].handler);
+ } catch(e) {
+ deferred.reject(e);
+ }
+
+ // Don't nullify if another transition is underway (meaning
+ // there was a transition initiated with enter/setup).
+ if (!transition.isAborted) {
+ router.activeTransition = null;
+ }
+ }
+
+ function transitionFailure(reason) {
+ deferred.reject(reason);
+ }
+ }
+
+ /**
+ @private
+
+ Accepts handlers in Recognizer format, either returned from
+ recognize() or handlersFor(), and returns unified
+ `HandlerInfo`s.
+ */
+ function generateHandlerInfos(router, recogHandlers) {
+ var handlerInfos = [];
+ for (var i = 0, len = recogHandlers.length; i < len; ++i) {
+ var handlerObj = recogHandlers[i],
+ isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length);
+
+ handlerInfos.push({
+ isDynamic: !!isDynamic,
+ name: handlerObj.handler,
+ handler: router.getHandler(handlerObj.handler)
+ });
+ }
+ return handlerInfos;
+ }
+
+ /**
+ @private
+ */
+ function transitionsIdentical(oldTransition, targetName, providedModelsArray) {
+
+ if (oldTransition.targetName !== targetName) { return false; }
+
+ var oldModels = oldTransition.providedModelsArray;
+ if (oldModels.length !== providedModelsArray.length) { return false; }
+
+ for (var i = 0, len = oldModels.length; i < len; ++i) {
+ if (oldModels[i] !== providedModelsArray[i]) { return false; }
+ }
+ return true;
+ }
+
+ /**
+ @private
+
+ Updates the URL (if necessary) and calls `setupContexts`
+ to update the router's array of `currentHandlerInfos`.
+ */
+ function finalizeTransition(transition, handlerInfos) {
+
+ var router = transition.router,
+ seq = transition.sequence,
+ handlerName = handlerInfos[handlerInfos.length - 1].name;
+
+ log(router, seq, "Validation succeeded, finalizing transition;");
+
+ // Collect params for URL.
+ var objects = [];
+ for (var i = 0, len = handlerInfos.length; i < len; ++i) {
+ var handlerInfo = handlerInfos[i];
+ if (handlerInfo.isDynamic) {
+ objects.push(handlerInfo.context);
+ }
+ }
+
+ var params = paramsForHandler(router, handlerName, objects);
+
+ transition.providedModelsArray = [];
+ transition.providedContexts = {};
+ router.currentParams = params;
+
+ var urlMethod = transition.urlMethod;
+ if (urlMethod) {
+ var url = router.recognizer.generate(handlerName, params);
+
+ if (urlMethod === 'replace') {
+ router.replaceURL(url);
+ } else {
+ // Assume everything else is just a URL update for now.
+ router.updateURL(url);
+ }
+ }
+
+ setupContexts(transition, handlerInfos);
+ log(router, seq, "TRANSITION COMPLETE.");
+ }
+
+ /**
+ @private
+
+ Internal function used to construct the chain of promises used
+ to validate a transition. Wraps calls to `beforeModel`, `model`,
+ and `afterModel` in promises, and checks for redirects/aborts
+ between each.
+ */
+ function validateEntry(transition, handlerInfos, index, matchPoint, handlerParams) {
+
+ if (index === handlerInfos.length) {
+ // No more contexts to resolve.
+ return RSVP.resolve(transition.resolvedModels);
+ }
+
+ var router = transition.router,
+ handlerInfo = handlerInfos[index],
+ handler = handlerInfo.handler,
+ handlerName = handlerInfo.name,
+ seq = transition.sequence,
+ errorAlreadyHandled = false;
+
+ if (index < matchPoint) {
+ log(router, seq, handlerName + ": using context from already-active handler");
+
+ // We're before the match point, so don't run any hooks,
+ // just use the already resolved context from the handler.
+ transition.resolvedModels[handlerInfo.name] = handlerInfo.handler.context;
+ return proceed();
+ }
+
+ return RSVP.resolve().then(handleAbort)
+ .then(beforeModel)
+ .then(null, handleError)
+ .then(handleAbort)
+ .then(model)
+ .then(null, handleError)
+ .then(handleAbort)
+ .then(afterModel)
+ .then(null, handleError)
+ .then(handleAbort)
+ .then(proceed);
+
+ function handleAbort(result) {
+
+ if (transition.isAborted) {
+ log(transition.router, transition.sequence, "detected abort.");
+ errorAlreadyHandled = true;
+ return RSVP.reject(new Router.TransitionAborted());
+ }
+
+ return result;
+ }
+
+ function handleError(reason) {
+
+ if (errorAlreadyHandled) { return RSVP.reject(reason); }
+ errorAlreadyHandled = true;
+ transition.abort();
+
+ log(router, seq, handlerName + ": handling error: " + reason);
+
+ // An error was thrown / promise rejected, so fire an
+ // `error` event from this handler info up to root.
+ trigger(handlerInfos.slice(0, index + 1), true, ['error', reason, transition]);
+
+ if (handler.error) {
+ handler.error(reason, transition);
+ }
+
+ // Propagate the original error.
+ return RSVP.reject(reason);
+ }
+
+ function beforeModel() {
+
+ log(router, seq, handlerName + ": calling beforeModel hook");
+
+ return handler.beforeModel && handler.beforeModel(transition);
+ }
+
+ function model() {
+ log(router, seq, handlerName + ": resolving model");
+
+ return getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint);
+ }
+
+ function afterModel(context) {
+
+ log(router, seq, handlerName + ": calling afterModel hook");
+
+ // Pass the context and resolved parent contexts to afterModel, but we don't
+ // want to use the value returned from `afterModel` in any way, but rather
+ // always resolve with the original `context` object.
+
+ transition.resolvedModels[handlerInfo.name] = context;
+ return handler.afterModel && handler.afterModel(context, transition);
+ }
+
+ function proceed() {
+ log(router, seq, handlerName + ": validation succeeded, proceeding");
+
+ handlerInfo.context = transition.resolvedModels[handlerInfo.name];
+ return validateEntry(transition, handlerInfos, index + 1, matchPoint, handlerParams);
+ }
+ }
+
+ /**
+ @private
+
+ Throws a TransitionAborted if the provided transition has been aborted.
+ */
+ function checkAbort(transition) {
+ if (transition.isAborted) {
+ log(transition.router, transition.sequence, "detected abort.");
+ throw new Router.TransitionAborted();
+ }
+ }
+
+ /**
+ @private
+
+ Encapsulates the logic for whether to call `model` on a route,
+ or use one of the models provided to `transitionTo`.
+ */
+ function getModel(handlerInfo, transition, handlerParams, needsUpdate) {
+
+ var handler = handlerInfo.handler,
+ handlerName = handlerInfo.name;
+
+ if (!needsUpdate && handler.hasOwnProperty('context')) {
+ return handler.context;
+ }
+
+ if (transition.providedModels.hasOwnProperty(handlerName)) {
+ var providedModel = transition.providedModels[handlerName];
+ return typeof providedModel === 'function' ? providedModel() : providedModel;
+ }
+
+ return handler.model && handler.model(handlerParams || {}, transition);
+ }
+
+ /**
+ @private
+ */
+ function log(router, sequence, msg) {
+
+ if (!router.log) { return; }
+
+ if (arguments.length === 3) {
+ router.log("Transition #" + sequence + ": " + msg);
+ } else {
+ msg = sequence;
+ router.log(msg);
+ }
+ }
+
+ /**
+ @private
+
+ Begins and returns a Transition based on the provided
+ arguments. Accepts arguments in the form of both URL
+ transitions and named transitions.
+
+ @param {Router} router
+ @param {Array[Object]} args arguments passed to transitionTo,
+ replaceWith, or handleURL
+ */
+ function doTransition(router, args) {
+ // Normalize blank transitions to root URL transitions.
+ var name = args[0] || '/';
+
+ if (name.charAt(0) === '/') {
+ return createURLTransition(router, name);
+ } else {
+ return createNamedTransition(router, args);
+ }
+ }
+
+ /**
+ @private
+
+ Serializes a handler using its custom `serialize` method or
+ by a default that looks up the expected property name from
+ the dynamic segment.
+
+ @param {Object} handler a router handler
+ @param {Object} model the model to be serialized for this handler
+ @param {Array[Object]} names the names array attached to an
+ handler object returned from router.recognizer.handlersFor()
+ */
+ function serialize(handler, model, names) {
+
+ var object = {};
+ if (isParam(model)) {
+ object[names[0]] = model;
+ return object;
+ }
+
+ // Use custom serialize if it exists.
+ if (handler.serialize) {
+ return handler.serialize(model, names);
+ }
+
+ if (names.length !== 1) { return; }
+
+ var name = names[0];
+
+ if (/_id$/.test(name)) {
+ object[name] = model.id;
+ } else {
+ object[name] = model;
+ }
+ return object;
+ }
+
return Router;
});
-
})();
@@ -24511,34 +25868,17 @@ Ember.Router = Ember.Object.extend({
},
handleURL: function(url) {
- this.router.handleURL(url);
- this.notifyPropertyChange('url');
+ scheduleLoadingStateEntry(this);
+
+ return this.router.handleURL(url).then(transitionCompleted);
},
- /**
- Transition to another route via the `routeTo` event which
- will by default be handled by ApplicationRoute.
-
- @method routeTo
- @param {TransitionEvent} transitionEvent
- */
- routeTo: function(transitionEvent) {
- var handlerInfos = this.router.currentHandlerInfos;
- if (handlerInfos) {
- transitionEvent.sourceRoute = handlerInfos[handlerInfos.length - 1].handler;
- }
-
- this.send('routeTo', transitionEvent);
- },
-
- transitionTo: function(name) {
- var args = [].slice.call(arguments);
- doTransition(this, 'transitionTo', args);
+ transitionTo: function() {
+ return doTransition(this, 'transitionTo', arguments);
},
replaceWith: function() {
- var args = [].slice.call(arguments);
- doTransition(this, 'replaceWith', args);
+ return doTransition(this, 'replaceWith', arguments);
},
generate: function() {
@@ -24592,17 +25932,6 @@ Ember.Router = Ember.Object.extend({
}
});
-Ember.Router.reopenClass({
- defaultFailureHandler: {
- setup: function(error) {
- Ember.Logger.error('Error while loading route:', error);
-
- // Using setTimeout allows us to escape from the Promise's try/catch block
- setTimeout(function() { throw error; });
- }
- }
-});
-
function getHandlerFunction(router) {
var seen = {}, container = router.container,
DefaultRoute = container.resolve('route:basic');
@@ -24617,7 +25946,6 @@ function getHandlerFunction(router) {
if (!handler) {
if (name === 'loading') { return {}; }
- if (name === 'failure') { return router.constructor.defaultFailureHandler; }
container.register(routeName, DefaultRoute.extend());
handler = container.lookup(routeName);
@@ -24628,9 +25956,9 @@ function getHandlerFunction(router) {
}
if (name === 'application') {
- // Inject default `routeTo` handler.
+ // Inject default `error` handler.
handler.events = handler.events || {};
- handler.events.routeTo = handler.events.routeTo || Ember.TransitionEvent.defaultHandler;
+ handler.events.error = handler.events.error || defaultErrorHandler;
}
handler.routeName = name;
@@ -24638,6 +25966,14 @@ function getHandlerFunction(router) {
};
}
+function defaultErrorHandler(error, transition) {
+ Ember.Logger.error('Error while loading route:', error);
+
+ // Using setTimeout allows us to escape from the Promise's try/catch block
+ setTimeout(function() { throw error; });
+}
+
+
function routePath(handlerInfos) {
var path = [];
@@ -24682,34 +26018,94 @@ function setupRouter(emberRouter, router, location) {
}
function doTransition(router, method, args) {
+ // Normalize blank route to root URL.
+ args = [].slice.call(args);
+ args[0] = args[0] || '/';
+
var passedName = args[0], name;
- if (!router.router.hasRoute(args[0])) {
- name = args[0] = passedName + '.index';
- } else {
+ if (passedName.charAt(0) === '/') {
name = passedName;
+ } else {
+ if (!router.router.hasRoute(passedName)) {
+ name = args[0] = passedName + '.index';
+ } else {
+ name = passedName;
+ }
+
}
+ scheduleLoadingStateEntry(router);
- router.router[method].apply(router.router, args);
+ var transitionPromise = router.router[method].apply(router.router, args);
+ transitionPromise.then(transitionCompleted);
+
+ // We want to return the configurable promise object
+ // so that callers of this function can use `.method()` on it,
+ // which obviously doesn't exist for normal RSVP promises.
+ return transitionPromise;
+}
+
+function scheduleLoadingStateEntry(router) {
+ if (router._loadingStateActive) { return; }
+ router._shouldEnterLoadingState = true;
+ Ember.run.scheduleOnce('routerTransitions', null, enterLoadingState, router);
+}
+
+function enterLoadingState(router) {
+ if (router._loadingStateActive || !router._shouldEnterLoadingState) { return; }
+
+ var loadingRoute = router.router.getHandler('loading');
+ if (loadingRoute) {
+ if (loadingRoute.enter) { loadingRoute.enter(); }
+ if (loadingRoute.setup) { loadingRoute.setup(); }
+ router._loadingStateActive = true;
+ }
+}
+
+function exitLoadingState(router) {
+ router._shouldEnterLoadingState = false;
+ if (!router._loadingStateActive) { return; }
+
+ var loadingRoute = router.router.getHandler('loading');
+ if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); }
+ router._loadingStateActive = false;
+}
+
+function transitionCompleted(route) {
+ var router = route.router;
router.notifyPropertyChange('url');
+ exitLoadingState(router);
}
Ember.Router.reopenClass({
map: function(callback) {
- var router = this.router = new Router();
+ var router = this.router;
+ if (!router){
+ router = this.router = new Router();
+ router.callbacks = [];
+ }
+
+ if (get(this, 'namespace.LOG_TRANSITIONS_INTERNAL')) {
+ router.log = Ember.Logger.debug;
+ }
var dsl = Ember.RouterDSL.map(function() {
this.resource('application', { path: "/" }, function() {
+ for (var i=0; i < router.callbacks.length; i++){
+ router.callbacks[i].call(this);
+ }
callback.call(this);
});
});
+ router.callbacks.push(callback);
router.map(dsl.generate());
return router;
}
});
+
})();
@@ -24722,7 +26118,9 @@ Ember.Router.reopenClass({
var get = Ember.get, set = Ember.set,
classify = Ember.String.classify,
- fmt = Ember.String.fmt;
+ fmt = Ember.String.fmt,
+ a_forEach = Ember.EnumerableUtils.forEach,
+ a_replace = Ember.EnumerableUtils.replace;
/**
The `Ember.Route` class is used to define individual routes. Refer to
@@ -24740,7 +26138,7 @@ Ember.Route = Ember.Object.extend({
*/
exit: function() {
this.deactivate();
- teardownView(this);
+ this.teardownViews();
},
/**
@@ -24764,6 +26162,104 @@ Ember.Route = Ember.Object.extend({
The context of the event will be this route.
+ ## Bubbling
+
+ By default, an event will stop bubbling once a handler defined
+ on the `events` hash handles it. To continue bubbling the event,
+ you must return `true` from the handler.
+
+ ## Built-in events
+
+ There are a few built-in events pertaining to transitions that you
+ can use to customize transition behavior: `willTransition` and
+ `error`.
+
+ ### `willTransition`
+
+ The `willTransition` event is fired at the beginning of any
+ attempted transition with a `Transition` object as the sole
+ argument. This event can be used for aborting, redirecting,
+ or decorating the transition from the currently active routes.
+
+ A good example is preventing navigation when a form is
+ half-filled out:
+
+ ```js
+ App.ContactFormRoute = Ember.Route.extend({
+ events: {
+ willTransition: function(transition) {
+ if (this.controller.get('userHasEnteredData')) {
+ this.controller.displayNavigationConfirm();
+ transition.abort();
+ }
+ }
+ }
+ });
+ ```
+
+ You can also redirect elsewhere by calling
+ `this.transitionTo('elsewhere')` from within `willTransition`.
+ Note that `willTransition` will not be fired for the
+ redirecting `transitionTo`, since `willTransition` doesn't
+ fire when there is already a transition underway. If you want
+ subsequent `willTransition` events to fire for the redirecting
+ transition, you must first explicitly call
+ `transition.abort()`.
+
+ ### `error`
+
+ When attempting to transition into a route, any of the hooks
+ may throw an error, or return a promise that rejects, at which
+ point an `error` event will be fired on the partially-entered
+ routes, allowing for per-route error handling logic, or shared
+ error handling logic defined on a parent route.
+
+ Here is an example of an error handler that will be invoked
+ for rejected promises / thrown errors from the various hooks
+ on the route, as well as any unhandled errors from child
+ routes:
+
+ ```js
+ App.AdminRoute = Ember.Route.extend({
+ beforeModel: function() {
+ throw "bad things!";
+ // ...or, equivalently:
+ return Ember.RSVP.reject("bad things!");
+ },
+
+ events: {
+ error: function(error, transition) {
+ // Assuming we got here due to the error in `beforeModel`,
+ // we can expect that error === "bad things!",
+ // but a promise model rejecting would also
+ // call this hook, as would any errors encountered
+ // in `afterModel`.
+
+ // The `error` hook is also provided the failed
+ // `transition`, which can be stored and later
+ // `.retry()`d if desired.
+
+ this.transitionTo('login');
+ }
+ }
+ });
+ ```
+
+ `error` events that bubble up all the way to `ApplicationRoute`
+ will fire a default error handler that logs the error. You can
+ specify your own global default error handler by overriding the
+ `error` handler on `ApplicationRoute`:
+
+ ```js
+ App.ApplicationRoute = Ember.Route.extend({
+ events: {
+ error: function(error, transition) {
+ this.controllerFor('banner').displayError(error.message);
+ }
+ }
+ });
+ ```
+
@see {Ember.Route#send}
@see {Handlebars.helpers.action}
@@ -24789,17 +26285,6 @@ Ember.Route = Ember.Object.extend({
*/
activate: Ember.K,
- /**
- Transition to another route via the `routeTo` event which
- will by default be handled by ApplicationRoute.
-
- @method routeTo
- @param {TransitionEvent} transitionEvent
- */
- routeTo: function(transitionEvent) {
- this.router.routeTo(transitionEvent);
- },
-
/**
Transition into another route. Optionally supply a model for the
route in question. The model will be serialized into the URL
@@ -24811,13 +26296,6 @@ Ember.Route = Ember.Object.extend({
*/
transitionTo: function(name, context) {
var router = this.router;
-
- // If the transition is a no-op, just bail.
- if (router.isActive.apply(router, arguments)) {
- return;
- }
-
- if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; }
return router.transitionTo.apply(router, arguments);
},
@@ -24834,13 +26312,6 @@ Ember.Route = Ember.Object.extend({
*/
replaceWith: function() {
var router = this.router;
-
- // If the transition is a no-op, just bail.
- if (router.isActive.apply(router, arguments)) {
- return;
- }
-
- if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; }
return this.router.replaceWith.apply(this.router, arguments);
},
@@ -24848,15 +26319,6 @@ Ember.Route = Ember.Object.extend({
return this.router.send.apply(this.router, arguments);
},
- /**
- @private
-
- Internal counter for tracking whether a route handler has
- called transitionTo or replaceWith inside its redirect hook.
-
- */
- _redirectDepth: 0,
-
/**
@private
@@ -24865,59 +26327,7 @@ Ember.Route = Ember.Object.extend({
@method setup
*/
setup: function(context) {
- // Determine if this is the top-most transition.
- // If so, we'll set up a data structure to track
- // whether `transitionTo` or replaceWith gets called
- // inside our `redirect` hook.
- //
- // This is necessary because we set a flag on the route
- // inside transitionTo/replaceWith to determine afterwards
- // if they were called, but `setup` can be called
- // recursively and we need to disambiguate where in the
- // call stack the redirect happened.
-
- // Are we the first call to setup? If so, set up the
- // redirect tracking data structure, and remember that
- // we're the top-most so we can clean it up later.
- var isTop;
- if (!this._redirected) {
- isTop = true;
- this._redirected = [];
- }
-
- // Set a flag on this route saying that we are interested in
- // tracking redirects, and increment the depth count.
- this._checkingRedirect = true;
- var depth = ++this._redirectDepth;
-
- // Check to see if context is set. This check preserves
- // the correct arguments.length inside the `redirect` hook.
- if (context === undefined) {
- this.redirect();
- } else {
- this.redirect(context);
- }
-
- // After the call to `redirect` returns, decrement the depth count.
- this._redirectDepth--;
- this._checkingRedirect = false;
-
- // Save off the data structure so we can reset it on the route but
- // still reference it later in this method.
- var redirected = this._redirected;
-
- // If this is the top `setup` call in the call stack, clear the
- // redirect tracking data structure.
- if (isTop) { this._redirected = null; }
-
- // If we were redirected, there is nothing left for us to do.
- // Returning false tells router.js not to continue calling setup
- // on any children route handlers.
- if (redirected[depth]) {
- return false;
- }
-
- var controller = this.controllerFor(this.routeName, context);
+ var controller = this.controllerFor(this.controllerName || this.routeName, context);
// Assign the route's controller so that it can more easily be
// referenced in event handlers
@@ -24939,29 +26349,133 @@ Ember.Route = Ember.Object.extend({
},
/**
+ @deprecated
+
A hook you can implement to optionally redirect to another route.
If you call `this.transitionTo` from inside of this hook, this route
will not be entered in favor of the other hook.
+ This hook is deprecated in favor of using the `afterModel` hook
+ for performing redirects after the model has resolved.
+
@method redirect
@param {Object} model the model for this route
*/
redirect: Ember.K,
/**
- @private
+ This hook is the first of the route entry validation hooks
+ called when an attempt is made to transition into a route
+ or one of its children. It is called before `model` and
+ `afterModel`, and is appropriate for cases when:
- The hook called by `router.js` to convert parameters into the context
- for this handler. The public Ember hook is `model`.
+ 1) A decision can be made to redirect elsewhere without
+ needing to resolve the model first.
+ 2) Any async operations need to occur first before the
+ model is attempted to be resolved.
- @method deserialize
+ This hook is provided the current `transition` attempt
+ as a parameter, which can be used to `.abort()` the transition,
+ save it for a later `.retry()`, or retrieve values set
+ on it from a previous hook. You can also just call
+ `this.transitionTo` to another route to implicitly
+ abort the `transition`.
+
+ You can return a promise from this hook to pause the
+ transition until the promise resolves (or rejects). This could
+ be useful, for instance, for retrieving async code from
+ the server that is required to enter a route.
+
+ ```js
+ App.PostRoute = Ember.Route.extend({
+ beforeModel: function(transition) {
+ if (!App.Post) {
+ return Ember.$.getScript('/models/post.js');
+ }
+ }
+ });
+ ```
+
+ If `App.Post` doesn't exist in the above example,
+ `beforeModel` will use jQuery's `getScript`, which
+ returns a promise that resolves after the server has
+ successfully retrieved and executed the code from the
+ server. Note that if an error were to occur, it would
+ be passed to the `error` hook on `Ember.Route`, but
+ it's also possible to handle errors specific to
+ `beforeModel` right from within the hook (to distinguish
+ from the shared error handling behavior of the `error`
+ hook):
+
+ ```js
+ App.PostRoute = Ember.Route.extend({
+ beforeModel: function(transition) {
+ if (!App.Post) {
+ var self = this;
+ return Ember.$.getScript('post.js').then(null, function(e) {
+ self.transitionTo('help');
+
+ // Note that the above transitionTo will implicitly
+ // halt the transition. If you were to return
+ // nothing from this promise reject handler,
+ // according to promise semantics, that would
+ // convert the reject into a resolve and the
+ // transition would continue. To propagate the
+ // error so that it'd be handled by the `error`
+ // hook, you would have to either
+ return Ember.RSVP.reject(e);
+ // or
+ throw e;
+ });
+ }
+ }
+ });
+ ```
+
+ @method beforeModel
+ @param {Transition} transition
+ @return {Promise} if the value returned from this hook is
+ a promise, the transition will pause until the transition
+ resolves. Otherwise, non-promise return values are not
+ utilized in any way.
*/
- deserialize: function(params) {
- var model = this.model(params);
- return this.currentModel = model;
+ beforeModel: Ember.K,
+
+ /**
+ This hook is called after this route's model has resolved.
+ It follows identical async/promise semantics to `beforeModel`
+ but is provided the route's resolved model in addition to
+ the `transition`, and is therefore suited to performing
+ logic that can only take place after the model has already
+ resolved.
+
+ ```js
+ App.PostRoute = Ember.Route.extend({
+ afterModel: function(posts, transition) {
+ if (posts.length === 1) {
+ this.transitionTo('post.show', posts[0]);
+ }
+ }
+ });
+ ```
+
+ Refer to documentation for `beforeModel` for a description
+ of transition-pausing semantics when a promise is returned
+ from this hook.
+
+ @method afterModel
+ @param {Transition} transition
+ @return {Promise} if the value returned from this hook is
+ a promise, the transition will pause until the transition
+ resolves. Otherwise, non-promise return values are not
+ utilized in any way.
+ */
+ afterModel: function(resolvedModel, transition) {
+ this.redirect(resolvedModel, transition);
},
+
/**
@private
@@ -24999,10 +26513,15 @@ Ember.Route = Ember.Object.extend({
is not called. Routes without dynamic segments will always
execute the model hook.
+ This hook follows the asynchronous/promise semantics
+ described in the documentation for `beforeModel`. In particular,
+ if a promise returned from `model` fails, the error will be
+ handled by the `error` hook on `Ember.Route`.
+
@method model
@param {Object} params the parameters extracted from the URL
*/
- model: function(params) {
+ model: function(params, resolvedParentModels) {
var match, name, sawParams, value;
for (var prop in params) {
@@ -25016,9 +26535,8 @@ Ember.Route = Ember.Object.extend({
if (!name && sawParams) { return params; }
else if (!name) { return; }
- var className = classify(name),
- namespace = this.router.namespace,
- modelClass = namespace[className];
+ var modelClass = this.container.lookupFactory('model:' + name);
+ var namespace = get(this, 'router.namespace');
return modelClass.find(value);
},
@@ -25158,7 +26676,19 @@ Ember.Route = Ember.Object.extend({
@return {Object} the model object
*/
modelFor: function(name) {
- var route = this.container.lookup('route:' + name);
+
+ var route = this.container.lookup('route:' + name),
+ transition = this.router.router.activeTransition;
+
+ // If we are mid-transition, we want to try and look up
+ // resolved parent contexts on the current transitionEvent.
+ if (transition) {
+ var modelLookupName = (route && route.routeName) || name;
+ if (transition.resolvedModels.hasOwnProperty(modelLookupName)) {
+ return transition.resolvedModels[modelLookupName];
+ }
+ }
+
return route && route.currentModel;
},
@@ -25260,8 +26790,65 @@ Ember.Route = Ember.Object.extend({
appendView(this, view, options);
},
+ /**
+ Disconnects a view that has been rendered into an outlet.
+
+ You may pass any or all of the following options to `disconnectOutlet`:
+
+ * `outlet`: the name of the outlet to clear (default: 'main')
+ * `parentView`: the name of the view containing the outlet to clear
+ (default: the view rendered by the parent route)
+
+ Example:
+
+ ```js
+ App.ApplicationRoute = App.Route.extend({
+ events: {
+ showModal: function(evt) {
+ this.render(evt.modalName, {
+ outlet: 'modal',
+ into: 'application'
+ });
+ },
+ hideModal: function(evt) {
+ this.disconnectOutlet({
+ outlet: 'modal',
+ parentView: 'application'
+ });
+ }
+ }
+ });
+ ```
+
+ @method disconnectOutlet
+ @param {Object} options the options
+ */
+ disconnectOutlet: function(options) {
+ options = options || {};
+ options.parentView = options.parentView ? options.parentView.replace(/\//g, '.') : parentTemplate(this);
+ options.outlet = options.outlet || 'main';
+
+ var parentView = this.router._lookupActiveView(options.parentView);
+ parentView.disconnectOutlet(options.outlet);
+ },
+
willDestroy: function() {
- teardownView(this);
+ this.teardownViews();
+ },
+
+ teardownViews: function() {
+ // Tear down the top level view
+ if (this.teardownTopLevelView) { this.teardownTopLevelView(); }
+
+ // Tear down any outlets rendered with 'into'
+ var teardownOutletViews = this.teardownOutletViews || [];
+ a_forEach(teardownOutletViews, function(teardownOutletView) {
+ teardownOutletView();
+ });
+
+ delete this.teardownTopLevelView;
+ delete this.teardownOutletViews;
+ delete this.lastRenderedTemplate;
}
});
@@ -25348,94 +26935,30 @@ function setupView(view, container, options) {
function appendView(route, view, options) {
if (options.into) {
var parentView = route.router._lookupActiveView(options.into);
- route.teardownView = teardownOutlet(parentView, options.outlet);
+ var teardownOutletView = generateOutletTeardown(parentView, options.outlet);
+ if (!route.teardownOutletViews) { route.teardownOutletViews = []; }
+ a_replace(route.teardownOutletViews, 0, 0, [teardownOutletView]);
parentView.connectOutlet(options.outlet, view);
} else {
var rootElement = get(route, 'router.namespace.rootElement');
// tear down view if one is already rendered
- if (route.teardownView) {
- route.teardownView();
+ if (route.teardownTopLevelView) {
+ route.teardownTopLevelView();
}
route.router._connectActiveView(options.name, view);
- route.teardownView = teardownTopLevel(view);
+ route.teardownTopLevelView = generateTopLevelTeardown(view);
view.appendTo(rootElement);
}
}
-function teardownTopLevel(view) {
+function generateTopLevelTeardown(view) {
return function() { view.destroy(); };
}
-function teardownOutlet(parentView, outlet) {
+function generateOutletTeardown(parentView, outlet) {
return function() { parentView.disconnectOutlet(outlet); };
}
-function teardownView(route) {
- if (route.teardownView) { route.teardownView(); }
-
- delete route.teardownView;
- delete route.lastRenderedTemplate;
-}
-
-})();
-
-
-
-(function() {
-/**
-@module ember
-@submodule ember-routing
-*/
-
-
-/*
- A TransitionEvent is passed as the argument for `transitionTo`
- events and contains information about an attempted transition
- that can be modified or decorated by leafier `transitionTo` event
- handlers before the actual transition is committed by ApplicationRoute.
-
- @class TransitionEvent
- @namespace Ember
- @extends Ember.Deferred
- */
-Ember.TransitionEvent = Ember.Object.extend({
-
- /*
- The Ember.Route method used to perform the transition. Presently,
- the only valid values are 'transitionTo' and 'replaceWith'.
- */
- transitionMethod: 'transitionTo',
- destinationRouteName: null,
- sourceRoute: null,
- contexts: null,
-
- init: function() {
- this._super();
- this.contexts = this.contexts || [];
- },
-
- /*
- Convenience method that returns an array that can be used for
- legacy `transitionTo` and `replaceWith`.
- */
- transitionToArgs: function() {
- return [this.destinationRouteName].concat(this.contexts);
- }
-});
-
-
-Ember.TransitionEvent.reopenClass({
- /*
- This is the default transition event handler that will be injected
- into ApplicationRoute. The context, like all route event handlers in
- the events hash, will be an `Ember.Route`.
- */
- defaultHandler: function(transitionEvent) {
- var router = this.router;
- router[transitionEvent.transitionMethod].apply(router, transitionEvent.transitionToArgs());
- }
-});
-
})();
@@ -25501,60 +27024,220 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
return resolveParams(options.context, options.params, { types: types, data: data });
}
- function args(linkView, router, route) {
- var passedRouteName = route || linkView.namedRoute, routeName;
-
- routeName = fullRouteName(router, passedRouteName);
-
-
- var ret = [ routeName ];
- return ret.concat(resolvedPaths(linkView.parameters));
+ function createPath(path) {
+ var fullPath = 'paramsContext';
+ if(path !== '') {
+ fullPath += '.' + path;
+ }
+ return fullPath;
}
/**
+ `Ember.LinkView` renders an element whose `click` event triggers a
+ transition of the application's instance of `Ember.Router` to
+ a supplied route by name.
+
+ Instances of `LinkView` will most likely be created through
+ the `linkTo` Handlebars helper, but properties of this class
+ can be overridden to customize application-wide behavior.
+
@class LinkView
@namespace Ember
@extends Ember.View
+ @see {Handlebars.helpers.linkTo}
**/
var LinkView = Ember.LinkView = Ember.View.extend({
tagName: 'a',
namedRoute: null,
currentWhen: null,
+
+ /**
+ Sets the `title` attribute of the `LinkView`'s HTML element.
+
+ @property title
+ @default null
+ **/
title: null,
+
+ /**
+ The CSS class to apply to `LinkView`'s element when its `active`
+ property is `true`.
+
+ @property activeClass
+ @type String
+ @default active
+ **/
activeClass: 'active',
+
+ /**
+ The CSS class to apply to `LinkView`'s element when its `loading`
+ property is `true`.
+
+ @property loadingClass
+ @type String
+ @default loading
+ **/
+ loadingClass: 'loading',
+
+ /**
+ The CSS class to apply to a `LinkView`'s element when its `disabled`
+ property is `true`.
+
+ @property disabledClass
+ @type String
+ @default disabled
+ **/
disabledClass: 'disabled',
_isDisabled: false,
+
+ /**
+ Determines whether the `LinkView` will trigger routing via
+ the `replaceWith` routing strategy.
+
+ @type Boolean
+ @default false
+ **/
replace: false,
attributeBindings: ['href', 'title'],
- classNameBindings: ['active', 'disabled'],
+ classNameBindings: ['active', 'loading', 'disabled'],
- // Even though this isn't a virtual view, we want to treat it as if it is
- // so that you can access the parent with {{view.prop}}
+ /**
+ By default the `{{linkTo}}` helper responds to the `click` event. You
+ can override this globally by setting this property to your custom
+ event name.
+
+ This is particularly useful on mobile when one wants to avoid the 300ms
+ click delay using some sort of custom `tap` event.
+
+ @property eventName
+ @type String
+ @default click
+ */
+ eventName: 'click',
+
+ // this is doc'ed here so it shows up in the events
+ // section of the API documentation, which is where
+ // people will likely go looking for it.
+ /**
+ Triggers the `LinkView`'s routing behavior. If
+ `eventName` is changed to a value other than `click`
+ the routing behavior will trigger on that custom event
+ instead.
+
+ @event click
+ **/
+
+ init: function() {
+ this._super.apply(this, arguments);
+
+ // Map desired event name to invoke function
+ var eventName = get(this, 'eventName');
+ this.on(eventName, this, this._invoke);
+
+ var params = this.parameters.params,
+ length = params.length,
+ context = this.parameters.context,
+ self = this,
+ path, paths = Ember.A([]), i;
+
+ set(this, 'paramsContext', context);
+
+ for(i=0; i < length; i++) {
+ paths.pushObject(createPath(params[i]));
+ }
+
+ var observer = function(object, path) {
+ var notify = true, i;
+ for(i=0; i < paths.length; i++) {
+ if(!get(this, paths[i])) {
+ notify = false;
+ }
+ }
+ if(notify) {
+ this.notifyPropertyChange('routeArgs');
+ }
+ };
+
+ for(i=0; i < length; i++) {
+ this.registerObserver(this, paths[i], this, observer);
+ }
+ },
+
+ /**
+ @private
+
+ Even though this isn't a virtual view, we want to treat it as if it is
+ so that you can access the parent with {{view.prop}}
+
+ @method concreteView
+ **/
concreteView: Ember.computed(function() {
return get(this, 'parentView');
}).property('parentView'),
+ /**
+
+ Accessed as a classname binding to apply the `LinkView`'s `disabledClass`
+ CSS `class` to the element when the link is disabled.
+
+ When `true` interactions with the element will not trigger route changes.
+ @property disabled
+ */
disabled: Ember.computed(function(key, value) {
if (value !== undefined) { this.set('_isDisabled', value); }
return value ? this.get('disabledClass') : false;
}),
+ /**
+ Accessed as a classname binding to apply the `LinkView`'s `activeClass`
+ CSS `class` to the element when the link is active.
+
+ A `LinkView` is considered active when its `currentWhen` property is `true`
+ or the application's current route is the route the `LinkView` would trigger
+ transitions into.
+
+ @property active
+ **/
active: Ember.computed(function() {
- var router = this.get('router'),
+ var router = get(this, 'router'),
params = resolvedPaths(this.parameters),
- currentWithIndex = this.currentWhen + '.index',
- isActive = router.isActive.apply(router, [this.currentWhen].concat(params)) ||
+ currentWhen = this.currentWhen || get(this, 'namedRoute'),
+ currentWithIndex = currentWhen + '.index',
+ isActive = router.isActive.apply(router, [currentWhen].concat(params)) ||
router.isActive.apply(router, [currentWithIndex].concat(params));
if (isActive) { return get(this, 'activeClass'); }
}).property('namedRoute', 'router.url'),
+ loading: Ember.computed(function() {
+ if (!get(this, 'routeArgs')) { return get(this, 'loadingClass'); }
+ }).property('routeArgs'),
+
+ /**
+ Accessed as a classname binding to apply the `LinkView`'s `activeClass`
+ CSS `class` to the element when the link is active.
+
+ A `LinkView` is considered active when its `currentWhen` property is `true`
+ or the application's current route is the route the `LinkView` would trigger
+ transitions into.
+
+ @property active
+ **/
+
router: Ember.computed(function() {
return this.get('controller').container.lookup('router:main');
}),
- click: function(event) {
+ /**
+ @private
+
+ Event handler that invokes the link, activating the associated route.
+
+ @method _invoke
+ @param {Event} event
+ */
+ _invoke: function(event) {
if (!isSimpleClick(event)) { return true; }
event.preventDefault();
@@ -25562,32 +27245,72 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
if (get(this, '_isDisabled')) { return false; }
- var router = this.get('router');
+ if (get(this, 'loading')) {
+ Ember.Logger.warn("This linkTo's parameters are either not yet loaded or point to an invalid route.");
+ return false;
+ }
- if (Ember.ENV.ENABLE_ROUTE_TO) {
+ var router = get(this, 'router'),
+ routeArgs = get(this, 'routeArgs');
- var routeArgs = args(this, router);
-
- router.routeTo(Ember.TransitionEvent.create({
- transitionMethod: this.get('replace') ? 'replaceWith' : 'transitionTo',
- destinationRouteName: routeArgs[0],
- contexts: routeArgs.slice(1)
- }));
+ if (this.get('replace')) {
+ router.replaceWith.apply(router, routeArgs);
} else {
- if (this.get('replace')) {
- router.replaceWith.apply(router, args(this, router));
- } else {
- router.transitionTo.apply(router, args(this, router));
- }
+ router.transitionTo.apply(router, routeArgs);
}
},
+ routeArgs: Ember.computed(function() {
+
+ var router = get(this, 'router'),
+ namedRoute = get(this, 'namedRoute'), routeName;
+
+ if (!namedRoute && this.namedRouteBinding) {
+ // The present value of namedRoute is falsy, but since it's a binding
+ // and could be valid later, don't treat as error.
+ return;
+ }
+ namedRoute = fullRouteName(router, namedRoute);
+
+
+ var resolvedContexts = resolvedPaths(this.parameters), paramsPresent = true;
+ for (var i = 0, l = resolvedContexts.length; i < l; ++i) {
+ var context = resolvedContexts[i];
+
+ // If contexts aren't present, consider the linkView unloaded.
+ if (context === null || typeof context === 'undefined') { return; }
+ }
+
+ return [ namedRoute ].concat(resolvedContexts);
+ }).property('namedRoute'),
+
+ /**
+ Sets the element's `href` attribute to the url for
+ the `LinkView`'s targeted route.
+
+ If the `LinkView`'s `tagName` is changed to a value other
+ than `a`, this property will be ignored.
+
+ @property href
+ **/
href: Ember.computed(function() {
if (this.get('tagName') !== 'a') { return false; }
- var router = this.get('router');
- return router.generate.apply(router, args(this, router));
- })
+ var router = get(this, 'router'),
+ routeArgs = get(this, 'routeArgs');
+
+ return routeArgs ? router.generate.apply(router, routeArgs) : get(this, 'loadingHref');
+ }).property('routeArgs'),
+
+ /**
+ The default href value to use while a linkTo is loading.
+ Only applies when tagName is 'a'
+
+ @property loadingHref
+ @type String
+ @default #
+ */
+ loadingHref: '#'
});
LinkView.toString = function() { return "LinkView"; };
@@ -25600,7 +27323,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
element:
```handlebars
- {{#linkTo photoGallery}}
+ {{#linkTo 'photoGallery'}}
Great Hamster Photos
{{/linkTo}}
```
@@ -25617,7 +27340,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
a `tagName` option:
```handlebars
- {{#linkTo photoGallery tagName="li"}}
+ {{#linkTo 'photoGallery' tagName="li"}}
Great Hamster Photos
{{/linkTo}}
```
@@ -25646,7 +27369,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
use of `{{linkTo}}`:
```handlebars
- {{#linkTo photoGallery.recent}}
+ {{#linkTo 'photoGallery.recent'}}
Great Hamster Photos from the last week
{{/linkTo}}
```
@@ -25664,7 +27387,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
option:
```handlebars
- {{#linkTo photoGallery.recent activeClass="current-url"}}
+ {{#linkTo 'photoGallery.recent' activeClass="current-url"}}
Great Hamster Photos from the last week
{{/linkTo}}
```
@@ -25690,7 +27413,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
```
```handlebars
- {{#linkTo photoGallery aPhoto}}
+ {{#linkTo 'photoGallery' aPhoto}}
{{aPhoto.title}}
{{/linkTo}}
```
@@ -25718,7 +27441,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
This argument will become the model context of the linked route:
```handlebars
- {{#linkTo photoGallery.comment aPhoto comment}}
+ {{#linkTo 'photoGallery.comment' aPhoto comment}}
{{comment.body}}
{{/linkTo}}
```
@@ -25742,6 +27465,15 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
})
```
+ It is also possible to override the default event in
+ this manner:
+
+ ``` javascript
+ Ember.LinkView.reopen({
+ eventName: 'customEventName'
+ });
+ ```
+
@method linkTo
@for Ember.Handlebars.helpers
@param {String} routeName
@@ -25749,13 +27481,22 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
@return {String} HTML string
*/
Ember.Handlebars.registerHelper('linkTo', function(name) {
- var options = [].slice.call(arguments, -1)[0];
- var params = [].slice.call(arguments, 1, -1);
+ var options = [].slice.call(arguments, -1)[0],
+ params = [].slice.call(arguments, 1, -1);
var hash = options.hash;
+
+ if (options.types[0] === "ID") {
+ if (Ember.ENV.HELPER_PARAM_LOOKUPS) {
+ hash.namedRouteBinding = name;
+ } else {
+
+ hash.namedRoute = name;
+ }
+ } else {
+ hash.namedRoute = name;
+ }
- hash.namedRoute = name;
- hash.currentWhen = hash.currentWhen || name;
hash.disabledBinding = hash.disabledWhen;
hash.parameters = {
@@ -25766,10 +27507,10 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
return Ember.Handlebars.helpers.view.call(this, LinkView, options);
});
-
});
+
})();
@@ -26357,10 +28098,12 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) {
childView.rerender();
}
- Ember.addObserver(this, modelPath, observer);
- childView.one('willDestroyElement', this, function() {
- Ember.removeObserver(this, modelPath, observer);
- });
+ if (modelPath) {
+ Ember.addObserver(this, modelPath, observer);
+ childView.one('willDestroyElement', this, function() {
+ Ember.removeObserver(this, modelPath, observer);
+ });
+ }
Ember.Handlebars.helpers.view.call(this, childView, options);
});
@@ -26514,6 +28257,19 @@ Ember.View.reopen({
(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+// Add a new named queue after the 'actions' queue (where RSVP promises
+// resolve), which is used in router transitions to prevent unnecessary
+// loading state entry if all context promises resolve on the
+// 'actions' queue first.
+
+var queues = Ember.run.queues,
+ indexOf = Ember.ArrayPolyfills.indexOf;
+queues.splice(indexOf.call(queues, 'actions') + 1, 0, 'routerTransitions');
})();
@@ -26722,7 +28478,7 @@ Ember.HashLocation = Ember.Object.extend({
willDestroy: function() {
var guid = Ember.guidFor(this);
- Ember.$(window).unbind('hashchange.ember-location-'+guid);
+ Ember.$(window).off('hashchange.ember-location-'+guid);
}
});
@@ -26740,6 +28496,7 @@ Ember.Location.registerImplementation('hash', Ember.HashLocation);
var get = Ember.get, set = Ember.set;
var popstateFired = false;
+var supportsHistoryState = window.history && 'state' in window.history;
/**
Ember.HistoryLocation implements the location API using the browser's
@@ -26802,9 +28559,10 @@ Ember.HistoryLocation = Ember.Object.extend({
@param path {String}
*/
setURL: function(path) {
+ var state = this.getState();
path = this.formatURL(path);
- if (this.getState() && this.getState().path !== path) {
+ if (state && state.path !== path) {
this.pushState(path);
}
},
@@ -26819,9 +28577,10 @@ Ember.HistoryLocation = Ember.Object.extend({
@param path {String}
*/
replaceURL: function(path) {
+ var state = this.getState();
path = this.formatURL(path);
- if (this.getState() && this.getState().path !== path) {
+ if (state && state.path !== path) {
this.replaceState(path);
}
},
@@ -26830,11 +28589,13 @@ Ember.HistoryLocation = Ember.Object.extend({
@private
Get the current `history.state`
+ Polyfill checks for native browser support and falls back to retrieving
+ from a private _historyState variable
@method getState
*/
getState: function() {
- return get(this, 'history').state;
+ return supportsHistoryState ? get(this, 'history').state : this._historyState;
},
/**
@@ -26846,7 +28607,15 @@ Ember.HistoryLocation = Ember.Object.extend({
@param path {String}
*/
pushState: function(path) {
- get(this, 'history').pushState({ path: path }, null, path);
+ var state = { path: path };
+
+ get(this, 'history').pushState(state, null, path);
+
+ // store state if browser doesn't support `history.state`
+ if(!supportsHistoryState) {
+ this._historyState = state;
+ }
+
// used for webkit workaround
this._previousURL = this.getURL();
},
@@ -26860,7 +28629,15 @@ Ember.HistoryLocation = Ember.Object.extend({
@param path {String}
*/
replaceState: function(path) {
- get(this, 'history').replaceState({ path: path }, null, path);
+ var state = { path: path };
+
+ get(this, 'history').replaceState(state, null, path);
+
+ // store state if browser doesn't support `history.state`
+ if(!supportsHistoryState) {
+ this._historyState = state;
+ }
+
// used for webkit workaround
this._previousURL = this.getURL();
},
@@ -26909,7 +28686,7 @@ Ember.HistoryLocation = Ember.Object.extend({
willDestroy: function() {
var guid = Ember.guidFor(this);
- Ember.$(window).unbind('popstate.ember-location-'+guid);
+ Ember.$(window).off('popstate.ember-location-'+guid);
}
});
@@ -27120,6 +28897,7 @@ var get = Ember.get,
'view:blog/post' //=> Blog.PostView
'view:basic' //=> Ember.View
'foo:post' //=> App.PostFoo
+ 'model:post' //=> App.Post
```
@class DefaultResolver
@@ -27146,6 +28924,11 @@ Ember.DefaultResolver = Ember.Object.extend({
resolve: function(fullName) {
var parsedName = this.parseName(fullName),
typeSpecificResolveMethod = this[parsedName.resolveMethodName];
+
+ if (!parsedName.name || !parsedName.type) {
+ throw new TypeError("Invalid fullName: `" + fullName + "`, must of of the form `type:name` ");
+ }
+
if (typeSpecificResolveMethod) {
var resolved = typeSpecificResolveMethod.call(this, parsedName);
if (resolved) { return resolved; }
@@ -27239,6 +29022,17 @@ Ember.DefaultResolver = Ember.Object.extend({
this.useRouterNaming(parsedName);
return this.resolveOther(parsedName);
},
+
+ /**
+ @protected
+ @method resolveModel
+ */
+ resolveModel: function(parsedName){
+ var className = classify(parsedName.name),
+ factory = get(parsedName.root, className);
+
+ if (factory) { return factory; }
+ },
/**
Look up the specified object (from parsedName) on the appropriate
namespace (usually on the Application)
@@ -27395,11 +29189,14 @@ DeprecatedContainer.prototype = {
In addition to creating your application's router, `Ember.Application` is
also responsible for telling the router when to start routing. Transitions
- between routes can be logged with the LOG_TRANSITIONS flag:
+ between routes can be logged with the LOG_TRANSITIONS flag, and more
+ detailed intra-transition logging can be logged with
+ the LOG_TRANSITIONS_INTERNAL flag:
```javascript
window.App = Ember.Application.create({
- LOG_TRANSITIONS: true
+ LOG_TRANSITIONS: true, // basic logging of successful transitions
+ LOG_TRANSITIONS_INTERNAL: true // detailed logging of all routing steps
});
```
@@ -27792,7 +29589,6 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin
Ember.run.schedule('actions', this, function(){
this._initialize();
- this.startRouting();
});
}
@@ -28168,8 +29964,6 @@ var get = Ember.get, set = Ember.set;
*/
Ember.State = Ember.Object.extend(Ember.Evented,
/** @scope Ember.State.prototype */{
- isState: true,
-
/**
A reference to the parent state.
@@ -28279,20 +30073,24 @@ Ember.State = Ember.Object.extend(Ember.Evented,
setupChild: function(states, name, value) {
if (!value) { return false; }
+ var instance;
- if (value.isState) {
+ if (value instanceof Ember.State) {
set(value, 'name', name);
+ instance = value;
+ instance.container = this.container;
} else if (Ember.State.detect(value)) {
- value = value.create({
- name: name
+ instance = value.create({
+ name: name,
+ container: this.container
});
}
- if (value.isState) {
- set(value, 'parentState', this);
- get(this, 'childStates').pushObject(value);
- states[name] = value;
- return value;
+ if (instance instanceof Ember.State) {
+ set(instance, 'parentState', this);
+ get(this, 'childStates').pushObject(instance);
+ states[name] = instance;
+ return instance;
}
},
@@ -28981,10 +30779,10 @@ var sendEvent = function(eventName, sendRecursiveArguments, isUnhandledPass) {
})
}),
stateTwo: Ember.State.create({
- anAction: function(manager, context){
- // will not be called below because it is
- // not a parent of the current state
- }
+ anAction: function(manager, context){
+ // will not be called below because it is
+ // not a parent of the current state
+ }
})
})
diff --git a/app/assets/javascripts/preload_store.js b/app/assets/javascripts/preload_store.js
index 60e89ce1e2..801ef5b4f2 100644
--- a/app/assets/javascripts/preload_store.js
+++ b/app/assets/javascripts/preload_store.js
@@ -32,30 +32,31 @@ PreloadStore = {
**/
getAndRemove: function(key, finder) {
var preloadStore = this;
- return Ember.Deferred.promise(function(promise) {
- if (preloadStore.data[key]) {
- promise.resolve(preloadStore.data[key]);
- delete preloadStore.data[key];
- } else {
- if (finder) {
- var result = finder();
+ if (preloadStore.data[key]) {
+ var promise = Ember.RSVP.resolve(preloadStore.data[key]);
+ delete preloadStore.data[key];
+ return promise;
+ }
- // If the finder returns a promise, we support that too
- if (result.then) {
- result.then(function(result) {
- return promise.resolve(result);
- }, function(result) {
- return promise.reject(result);
- });
- } else {
- promise.resolve(result);
- }
+ if (finder) {
+ return Ember.Deferred.promise(function(promise) {
+ var result = finder();
+
+ // If the finder returns a promise, we support that too
+ if (result.then) {
+ result.then(function(result) {
+ return promise.resolve(result);
+ }, function(result) {
+ return promise.reject(result);
+ });
} else {
- promise.resolve(null);
+ promise.resolve(result);
}
- }
- });
+ });
+ }
+
+ return Ember.RSVP.resolve(null);
},
/**
diff --git a/config/application.rb b/config/application.rb
index 13e4cb5871..9fd4588208 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -112,7 +112,7 @@ module Discourse
# ember stuff only used for asset precompliation, production variant plays up
config.ember.variant = :development
config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external_production/ember.js"
- config.ember.handlebars_location = "#{Rails.root}/app/assets/javascripts/external/handlebars-1.0.rc.4.js"
+ config.ember.handlebars_location = "#{Rails.root}/app/assets/javascripts/external/handlebars.js"
# Since we are using strong_parameters, we can disable and remove
# attr_accessible.
diff --git a/config/jshint.yml b/config/jshint.yml
index ba24ccd17e..7883bc2992 100644
--- a/config/jshint.yml
+++ b/config/jshint.yml
@@ -88,9 +88,8 @@ predef:
- visit
- count
- exists
- - asyncTest
+ - asyncTestDiscourse
- find
- - resolvingPromise
- sinon
- controllerFor
diff --git a/test/javascripts/components/onebox_test.js b/test/javascripts/components/onebox_test.js
index 63159c7442..e4ec157410 100644
--- a/test/javascripts/components/onebox_test.js
+++ b/test/javascripts/components/onebox_test.js
@@ -4,17 +4,20 @@ module("Discourse.Onebox", {
}
});
-test("Stops rapid calls with cache true", function() {
- this.stub(Discourse, "ajax").returns(resolvingPromise);
+asyncTestDiscourse("Stops rapid calls with cache true", function() {
+ this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
+ Discourse.Onebox.load(this.anchor, true);
+ Discourse.Onebox.load(this.anchor, true);
- Discourse.Onebox.load(this.anchor, true);
- Discourse.Onebox.load(this.anchor, true);
+ start();
ok(Discourse.ajax.calledOnce);
});
-test("Stops rapid calls with cache false", function() {
- this.stub(Discourse, "ajax").returns(resolvingPromise);
+asyncTestDiscourse("Stops rapid calls with cache true", function() {
+ this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
Discourse.Onebox.load(this.anchor, false);
Discourse.Onebox.load(this.anchor, false);
+
+ start();
ok(Discourse.ajax.calledOnce);
-});
\ No newline at end of file
+});
diff --git a/test/javascripts/components/preload_store_test.js b/test/javascripts/components/preload_store_test.js
index c6576380e4..be8e89bb03 100644
--- a/test/javascripts/components/preload_store_test.js
+++ b/test/javascripts/components/preload_store_test.js
@@ -14,7 +14,7 @@ test("remove", function() {
blank(PreloadStore.get('bane'), "removes the value if the key exists");
});
-asyncTest("getAndRemove returns a promise that resolves to null", function() {
+asyncTestDiscourse("getAndRemove returns a promise that resolves to null", function() {
expect(1);
PreloadStore.getAndRemove('joker').then(function(result) {
@@ -23,7 +23,7 @@ asyncTest("getAndRemove returns a promise that resolves to null", function() {
});
});
-asyncTest("getAndRemove returns a promise that resolves to the result of the finder", function() {
+asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder", function() {
expect(1);
var finder = function() { return 'batdance'; };
@@ -34,7 +34,7 @@ asyncTest("getAndRemove returns a promise that resolves to the result of the fin
});
-asyncTest("getAndRemove returns a promise that resolves to the result of the finder's promise", function() {
+asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder's promise", function() {
expect(1);
var finder = function() {
@@ -47,7 +47,7 @@ asyncTest("getAndRemove returns a promise that resolves to the result of the fin
});
});
-asyncTest("returns a promise that rejects with the result of the finder's rejected promise", function() {
+asyncTestDiscourse("returns a promise that rejects with the result of the finder's rejected promise", function() {
expect(1);
var finder = function() {
@@ -61,7 +61,7 @@ asyncTest("returns a promise that rejects with the result of the finder's reject
});
-asyncTest("returns a promise that resolves to 'evil'", function() {
+asyncTestDiscourse("returns a promise that resolves to 'evil'", function() {
expect(1);
PreloadStore.getAndRemove('bane').then(function(result) {
diff --git a/test/javascripts/helpers/assertions.js b/test/javascripts/helpers/assertions.js
index 8ca0515735..a192cd8b33 100644
--- a/test/javascripts/helpers/assertions.js
+++ b/test/javascripts/helpers/assertions.js
@@ -1,11 +1,11 @@
// Test helpers
-var resolvingPromise = Ember.Deferred.promise(function (p) {
- p.resolve();
-});
+// var resolvingPromise = Ember.Deferred.promise(function (p) {
+// p.resolve();
+// });
-var resolvingPromiseWith = function(result) {
- return Ember.Deferred.promise(function (p) { p.resolve(result); });
-};
+// var resolvingPromiseWith = function(result) {
+// return Ember.Deferred.promise(function (p) { p.resolve(result); });
+// };
function exists(selector) {
return !!count(selector);
diff --git a/test/javascripts/helpers/qunit_helpers.js b/test/javascripts/helpers/qunit_helpers.js
index 5851038419..3ad3732e6e 100644
--- a/test/javascripts/helpers/qunit_helpers.js
+++ b/test/javascripts/helpers/qunit_helpers.js
@@ -18,4 +18,15 @@ function controllerFor(controller, model) {
var controller = Discourse.__container__.lookup('controller:' + controller);
if (model) { controller.set('model', model ); }
return controller;
+}
+
+function asyncTestDiscourse(text, func) {
+
+ asyncTest(text, function () {
+
+ var qunitContext = this;
+ Ember.run(function () {
+ func.call(qunitContext);
+ });
+ });
}
\ No newline at end of file
diff --git a/test/javascripts/jshint_all.js.erb b/test/javascripts/jshint_all.js.erb
index d3361934ef..1f12744a8c 100644
--- a/test/javascripts/jshint_all.js.erb
+++ b/test/javascripts/jshint_all.js.erb
@@ -8,7 +8,7 @@ var qHint = function(name, sourceFile, options, globals) {
sourceFile = name;
}
- return asyncTest(name, function() {
+ return asyncTestDiscourse(name, function() {
qHint.sendRequest(sourceFile, function(req) {
start();
@@ -113,9 +113,8 @@ var jsHintOpts = {
"visit",
"count",
"exists",
- "asyncTest",
+ "asyncTestDiscourse",
"find",
- "resolvingPromise",
"sinon",
"moment",
"start",
@@ -125,7 +124,6 @@ var jsHintOpts = {
"controllerFor",
"containsInstance",
"deepEqual",
- "resolvingPromiseWith",
"Blob",
"File"],
"node" : false,
diff --git a/test/javascripts/models/composer_test.js b/test/javascripts/models/composer_test.js
index 74f73ea108..dac2b0d7e1 100644
--- a/test/javascripts/models/composer_test.js
+++ b/test/javascripts/models/composer_test.js
@@ -130,7 +130,7 @@ test('editingFirstPost', function() {
});
-asyncTest('importQuote with a post', function() {
+asyncTestDiscourse('importQuote with a post', function() {
expect(1);
this.stub(Discourse.Post, 'load').withArgs(123).returns(Em.Deferred.promise(function (p) {
@@ -144,7 +144,7 @@ asyncTest('importQuote with a post', function() {
});
});
-asyncTest('importQuote with no post', function() {
+asyncTestDiscourse('importQuote with no post', function() {
expect(1);
this.stub(Discourse.Post, 'load').withArgs(4).returns(Em.Deferred.promise(function (p) {
diff --git a/test/javascripts/models/post_stream_test.js b/test/javascripts/models/post_stream_test.js
index 6f1acc1ef8..477b17de97 100644
--- a/test/javascripts/models/post_stream_test.js
+++ b/test/javascripts/models/post_stream_test.js
@@ -213,7 +213,7 @@ test("identity map", function() {
deepEqual(postStream.listUnloadedIds([1, 2, 3, 4]), [2, 4], "it only returns unloaded posts");
});
-asyncTest("loadIntoIdentityMap with no data", function() {
+asyncTestDiscourse("loadIntoIdentityMap with no data", function() {
var postStream = buildStream(1234);
expect(1);
@@ -224,11 +224,11 @@ asyncTest("loadIntoIdentityMap with no data", function() {
});
});
-asyncTest("loadIntoIdentityMap with post ids", function() {
+asyncTestDiscourse("loadIntoIdentityMap with post ids", function() {
var postStream = buildStream(1234);
expect(1);
- this.stub(Discourse, "ajax").returns(resolvingPromiseWith({
+ this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve({
post_stream: {
posts: [{id: 10, post_number: 10}]
}
diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js
index b661d09b31..0b2472d216 100644
--- a/test/javascripts/test_helper.js
+++ b/test/javascripts/test_helper.js
@@ -11,7 +11,7 @@
// Externals we need to load first
//= require ../../app/assets/javascripts/external/jquery-1.9.1.js
//= require ../../app/assets/javascripts/external/jquery.ui.widget.js
-//= require ../../app/assets/javascripts/external/handlebars-1.0.rc.4.js
+//= require ../../app/assets/javascripts/external/handlebars.js
//= require ../../app/assets/javascripts/external_development/ember.js
//= require ../../app/assets/javascripts/external_development/group-helper.js