From d466d11e7192e6c241826670a83c02832a74682c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 15 Jul 2013 19:47:13 -0400 Subject: [PATCH] Ember RC6 update --- Gemfile | 4 +- Gemfile.lock | 6 +- .../admin/templates/email.js.handlebars | 6 +- .../admin/templates/flags.js.handlebars | 4 +- .../admin/templates/users_list.js.handlebars | 14 +- app/assets/javascripts/application.js.erb | 2 +- .../discourse/templates/header.js.handlebars | 2 +- .../templates/list/topics.js.handlebars | 2 +- .../templates/user/activity.js.handlebars | 2 +- .../{handlebars-1.0.rc.4.js => handlebars.js} | 155 +- .../javascripts/external_development/ember.js | 3923 +++++++++++++---- .../javascripts/external_production/ember.js | 3618 +++++++++++---- app/assets/javascripts/preload_store.js | 41 +- config/application.rb | 2 +- config/jshint.yml | 3 +- test/javascripts/components/onebox_test.js | 17 +- .../components/preload_store_test.js | 10 +- test/javascripts/helpers/assertions.js | 12 +- test/javascripts/helpers/qunit_helpers.js | 11 + test/javascripts/jshint_all.js.erb | 6 +- test/javascripts/models/composer_test.js | 4 +- test/javascripts/models/post_stream_test.js | 6 +- test/javascripts/test_helper.js | 2 +- 23 files changed, 5850 insertions(+), 2002 deletions(-) rename app/assets/javascripts/external/{handlebars-1.0.rc.4.js => handlebars.js} (88%) mode change 100755 => 100644 diff --git a/Gemfile b/Gemfile index bc8abdb3cd8..e8b9a7f3b32 100644 --- a/Gemfile +++ b/Gemfile @@ -5,8 +5,8 @@ gem 'active_model_serializers', git: 'https://github.com/rails-api/active_model_ # we had issues with latest, stick to the rev till we figure this out # PR that makes it all hang together welcome gem 'ember-rails' -gem 'ember-source', '1.0.0.rc5' # or the version you need -gem 'handlebars-source', '1.0.0.rc4' # or the version you need +gem 'ember-source', '1.0.0.rc6' +gem 'handlebars-source' gem 'barber' gem 'vestal_versions', git: 'https://github.com/zhangyuan/vestal_versions' diff --git a/Gemfile.lock b/Gemfile.lock index 7d3adaffa06..457f22c345e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -169,7 +169,7 @@ GEM barber execjs (>= 1.2) railties (>= 3.1) - ember-source (1.0.0.rc5) + ember-source (1.0.0.rc6) handlebars-source (= 1.0.0.rc4) erubis (2.7.0) eventmachine (1.0.3) @@ -482,7 +482,7 @@ DEPENDENCIES em-redis email_reply_parser! ember-rails - ember-source (= 1.0.0.rc5) + ember-source (= 1.0.0.rc6) eventmachine fabrication fakeweb (~> 1.3.0) @@ -493,7 +493,7 @@ DEPENDENCIES fog guard-rspec guard-spork - handlebars-source (= 1.0.0.rc4) + handlebars-source highline hiredis image_optim diff --git a/app/assets/javascripts/admin/templates/email.js.handlebars b/app/assets/javascripts/admin/templates/email.js.handlebars index 6d8ebd5d9b6..4239864bd71 100644 --- a/app/assets/javascripts/admin/templates/email.js.handlebars +++ b/app/assets/javascripts/admin/templates/email.js.handlebars @@ -1,9 +1,9 @@
diff --git a/app/assets/javascripts/admin/templates/flags.js.handlebars b/app/assets/javascripts/admin/templates/flags.js.handlebars index f9e2858d23a..20f3b91f27d 100644 --- a/app/assets/javascripts/admin/templates/flags.js.handlebars +++ b/app/assets/javascripts/admin/templates/flags.js.handlebars @@ -1,8 +1,8 @@
diff --git a/app/assets/javascripts/admin/templates/users_list.js.handlebars b/app/assets/javascripts/admin/templates/users_list.js.handlebars index 0aa21e61b35..22fa72799f8 100644 --- a/app/assets/javascripts/admin/templates/users_list.js.handlebars +++ b/app/assets/javascripts/admin/templates/users_list.js.handlebars @@ -1,15 +1,15 @@
diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index a078ac15f6e..4d4112b7c9a 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -12,7 +12,7 @@ // Externals we need to load first //= require ./external/jquery-1.9.1.js //= require ./external/jquery.ui.widget.js -//= require ./external/handlebars-1.0.rc.4.js +//= require ./external/handlebars.js <% if Rails.env.development? require_asset ("./external_development/ember.js") diff --git a/app/assets/javascripts/discourse/templates/header.js.handlebars b/app/assets/javascripts/discourse/templates/header.js.handlebars index 76862921f0b..d4ec6beced5 100644 --- a/app/assets/javascripts/discourse/templates/header.js.handlebars +++ b/app/assets/javascripts/discourse/templates/header.js.handlebars @@ -85,7 +85,7 @@
  • {{#if currentUser}} - {{#titledLinkTo user.activity currentUser titleKey="current_user" class="icon"}}{{avatar currentUser imageSize="medium" }}{{/titledLinkTo}} + {{#titledLinkTo 'user.activity' currentUser titleKey="current_user" class="icon"}}{{avatar currentUser imageSize="medium" }}{{/titledLinkTo}} {{else}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/list/topics.js.handlebars b/app/assets/javascripts/discourse/templates/list/topics.js.handlebars index 4a3f12fa7f0..23c6b850b6a 100644 --- a/app/assets/javascripts/discourse/templates/list/topics.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/topics.js.handlebars @@ -62,7 +62,7 @@ {{i18n topic.suggest_create_topic}} {{/if}} {{else}} - {{#linkTo list.categories}}{{i18n topic.browse_all_categories}}{{/linkTo}} {{i18n or}} {{#linkTo list.latest}}{{i18n topic.view_latest_topics}}{{/linkTo}} + {{#linkTo 'list.categories'}}{{i18n topic.browse_all_categories}}{{/linkTo}} {{i18n or}} {{#linkTo 'list.latest'}}{{i18n topic.view_latest_topics}}{{/linkTo}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars index 9d4f125b50e..aad5571ecf0 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars @@ -35,7 +35,7 @@
    {{i18n user.last_seen}}:
    {{date last_seen_at}}
    {{/if}} {{#if invited_by}} -
    {{i18n user.invited_by}}:
    {{#linkTo user.activity invited_by}}{{invited_by.username}}{{/linkTo}}
    +
    {{i18n user.invited_by}}:
    {{#linkTo 'user.activity' invited_by}}{{invited_by.username}}{{/linkTo}}
    {{/if}} {{#if email}}
    {{i18n user.email.title}}:
    {{email}}
    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 96d86ea8147..c70f09d1de6 --- 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 60e89ce1e2f..801ef5b4f2d 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 13e4cb5871e..9fd45882085 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 ba24ccd17ec..7883bc29923 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 63159c7442b..e4ec1574106 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 c6576380e4e..be8e89bb036 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 8ca05157359..a192cd8b33c 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 5851038419d..3ad3732e6e4 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 d3361934ef8..1f12744a8c3 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 74f73ea1081..dac2b0d7e1d 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 6f1acc1ef89..477b17de97a 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 b661d09b312..0b2472d2169 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