diff --git a/.travis.yml b/.travis.yml index 83fefcb9..6439df11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,8 @@ env: matrix: - "TEST_SUITE=ruby ARTIFACTS_S3_BUCKET=travis-web-production" - "TEST_SUITE=phantomjs" - - "TEST_SUITE=saucelabs BROWSER='firefox:19:Windows 2012'" - - "TEST_SUITE=saucelabs BROWSER='chrome::Windows 2008'" + - "TEST_SUITE=saucelabs BROWSER='firefox::Windows XP'" + - "TEST_SUITE=saucelabs BROWSER='chrome::Windows XP'" script: "script/ci" before_script: diff --git a/Gemfile b/Gemfile index 33be7c01..a7922e0b 100644 --- a/Gemfile +++ b/Gemfile @@ -17,12 +17,12 @@ group :assets do gem 'tilt' gem 'uglifier' gem 'yui-compressor' + gem 'libv8', '~> 3.16.0' end group :development, :test do gem 'rake' gem 'localeapp' - gem 'handlebars' gem 'localeapp-handlebars_i18n' end @@ -40,61 +40,3 @@ group :test do gem 'sinatra-contrib' end -require 'bundler/installer' - -module ::Bundler - class Installer < Environment - MAX_RETRIES = 3 - - def install_gem_from_spec(spec, standalone = false) - retries = 1 - # Download the gem to get the spec, because some specs that are returned - # by rubygems.org are broken and wrong. - Bundler::Fetcher.fetch(spec) if spec.source.is_a?(Bundler::Source::Rubygems) - - # Fetch the build settings, if there are any - settings = Bundler.settings["build.#{spec.name}"] - Bundler.rubygems.with_build_args [settings] do - spec.source.install(spec) - Bundler.ui.debug "from #{spec.loaded_from} " - end - - # newline comes after installing, some gems say "with native extensions" - Bundler.ui.info "" - if Bundler.settings[:bin] && standalone - generate_standalone_bundler_executable_stubs(spec) - elsif Bundler.settings[:bin] - generate_bundler_executable_stubs(spec, :force => true) - end - - FileUtils.rm_rf(Bundler.tmp) - rescue Gem::RemoteFetcher::FetchError => e - if retries <= MAX_RETRIES - Bundler.ui.warn "#{e.class}: #{e.message}" - Bundler.ui.warn "Installing #{spec.name} (#{spec.version}) failed." - Bundler.ui.warn "Retrying (#{retries}/#{MAX_RETRIES})" - retries += 1 - sleep retries - retry - else - Bundler.ui.warn "Installing #{spec.name} (#{spec.version}) failed after #{retries} retries: #{e.message}." - Bundler.ui.warn "Giving up" - msg = "An error, most likely because of network issues, has occurred trying to install #{spec.name} (#{spec.version}), " - msg << "and Bundler cannot continue." - raise Bundler::InstallError, msg - end - rescue Exception => e - # install hook failed - raise e if e.is_a?(Bundler::InstallHookError) || e.is_a?(Bundler::SecurityError) - - # other failure, likely a native extension build failure - Bundler.ui.info "" - Bundler.ui.warn "#{e.class}: #{e.message}" - msg = "An error occurred while installing #{spec.name} (#{spec.version})," - msg << " and Bundler cannot continue.\nMake sure that `gem install" - msg << " #{spec.name} -v '#{spec.version}'` succeeds before bundling." - Bundler.ui.debug e.backtrace.join("\n") - raise Bundler::InstallError, msg - end - end -end diff --git a/Gemfile.lock b/Gemfile.lock index 5a69095f..80adb3af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,7 +29,6 @@ GEM coffee-script-source execjs coffee-script-source (1.5.0) - commonjs (0.2.6) compass (0.12.2) chunky_png (~> 1.2) fssm (>= 0.2.7) @@ -48,12 +47,9 @@ GEM pry (>= 0.9.10) terminal-table (>= 1.4.3) thor (>= 0.14.6) - handlebars (0.4.0) - commonjs (~> 0.2.3) - therubyracer (~> 0.11.1) i18n (0.6.3) json (1.7.7) - libv8 (3.11.8.13) + libv8 (3.16.14.3) listen (0.7.3) localeapp (0.6.9) gli @@ -89,7 +85,6 @@ GEM rake-pipeline-i18n-filters (0.0.5) rake-pipeline (~> 0.6) rb-fsevent (0.9.3) - ref (1.0.2) rerun (0.8.0) listen rest-client (1.6.7) @@ -116,9 +111,6 @@ GEM tilt (~> 1.3) slop (3.4.3) terminal-table (1.4.5) - therubyracer (0.11.4) - libv8 (~> 3.11.8.12) - ref thor (0.17.0) tilt (1.3.3) uglifier (1.3.0) @@ -136,7 +128,7 @@ DEPENDENCIES compass foreman guard - handlebars + libv8 (~> 3.16.0) localeapp localeapp-handlebars_i18n puma diff --git a/LICENCSE b/LICENSE similarity index 100% rename from LICENCSE rename to LICENSE diff --git a/assets/scripts/app/app.coffee b/assets/scripts/app/app.coffee index 3cc79ca6..3f5adc8e 100644 --- a/assets/scripts/app/app.coffee +++ b/assets/scripts/app/app.coffee @@ -33,7 +33,7 @@ unless window.TravisApplication @slider = new Travis.Slider() @pusher = new Travis.Pusher(Travis.config.pusher_key) if Travis.config.pusher_key - @tailing = new Travis.Tailing() + @tailing = new Travis.Tailing($(window), '#tail', '#log') @set('auth', Travis.Auth.create(app: this, endpoint: Travis.config.api_endpoint)) @@ -119,14 +119,12 @@ unless window.TravisApplication loadOrMerge: (type, hash, options) -> options ||= {} - if !type._idToReference - type._idToReference = {} - reference = type._idToReference[hash.id] + reference = type._getReferenceById(hash.id) if reference && options.skipIfExists return - reference = type._referenceForId(hash.id) + reference = type._getOrCreateReferenceForId(hash.id) if reference.record reference.record.merge(hash) else diff --git a/assets/scripts/app/controllers/build.coffee b/assets/scripts/app/controllers/build.coffee index 2446117b..ba99f348 100644 --- a/assets/scripts/app/controllers/build.coffee +++ b/assets/scripts/app/controllers/build.coffee @@ -15,11 +15,3 @@ Travis.BuildController = Ember.Controller.extend urlGithubCommit: (-> Travis.Urls.githubCommit(@get('repo.slug'), @get('commit.sha')) ).property('repo.slug', 'commit.sha') - - urlAuthor: (-> - Travis.Urls.email(@get('commit.authorEmail')) - ).property('commit.authorEmail') - - urlCommitter: (-> - Travis.Urls.email(@get('commit.committerEmail')) - ).property('commit.committerEmail') diff --git a/assets/scripts/app/controllers/flash.coffee b/assets/scripts/app/controllers/flash.coffee index c942114a..1d2c839a 100644 --- a/assets/scripts/app/controllers/flash.coffee +++ b/assets/scripts/app/controllers/flash.coffee @@ -2,14 +2,12 @@ Travis.FlashController = Ember.ArrayController.extend needs: ['currentUser'] currentUserBinding: 'controllers.currentUser' - broadcastBinding: 'currentUser.broadcasts' - init: -> @_super.apply this, arguments @set('flashes', Ember.A()) content: (-> - @get('unseenBroadcasts').concat(@get('flashes')) + @get('unseenBroadcasts').concat(@get('flashes')).filter( (o) -> o ).uniq() ).property('unseenBroadcasts.length', 'flashes.length') unseenBroadcasts: (-> diff --git a/assets/scripts/app/controllers/job.coffee b/assets/scripts/app/controllers/job.coffee index 417e3eb3..148dbad7 100644 --- a/assets/scripts/app/controllers/job.coffee +++ b/assets/scripts/app/controllers/job.coffee @@ -13,11 +13,3 @@ Travis.JobController = Em.Controller.extend urlGithubCommit: (-> Travis.Urls.githubCommit(@get('repo.slug'), @get('commit.sha')) ).property('repo.slug', 'commit.sha') - - urlAuthor: (-> - Travis.Urls.email(@get('commit.authorEmail')) - ).property('commit.authorEmail') - - urlCommitter: (-> - Travis.Urls.email(@get('commit.committerEmail')) - ).property('commit.committerEmail') diff --git a/assets/scripts/app/controllers/repo.coffee b/assets/scripts/app/controllers/repo.coffee index 5b326071..88276d71 100644 --- a/assets/scripts/app/controllers/repo.coffee +++ b/assets/scripts/app/controllers/repo.coffee @@ -50,6 +50,9 @@ Travis.RepoController = Travis.Controller.extend @connectTab('job') lastBuildDidChange: -> + Ember.run.scheduleOnce('data', this, @_lastBuildDidChange); + + _lastBuildDidChange: -> build = @get('repo.lastBuild') @set('build', build) diff --git a/assets/scripts/app/controllers/repos.coffee b/assets/scripts/app/controllers/repos.coffee index bfa09ace..f61de1a4 100644 --- a/assets/scripts/app/controllers/repos.coffee +++ b/assets/scripts/app/controllers/repos.coffee @@ -6,7 +6,7 @@ Travis.ReposController = Ember.ArrayController.extend 'owned' else 'recent' - ).property('currentUser') + ).property('currentUser.id') currentUserIdDidChange: (-> if @get('currentUser.id') @@ -16,10 +16,10 @@ Travis.ReposController = Ember.ArrayController.extend ).observes('currentUser.id') tabOrIsLoadedDidChange: (-> - if @get('tab') == 'owned' && @get('isLoaded') && @get('length') == 0 - - @container.lookup('router:main').send('renderNoOwnedRepos') - ).observes('isLoaded', 'tab') + Ember.run.scheduleOnce 'routerTransitions', this, -> + if @get('tab') == 'owned' && @get('isLoaded') && @get('length') == 0 + @container.lookup('router:main').send('renderNoOwnedRepos') + ).observes('isLoaded', 'tab', 'length') isLoadedBinding: 'content.isLoaded' needs: ['currentUser', 'repo'] diff --git a/assets/scripts/app/helpers/helpers.coffee b/assets/scripts/app/helpers/helpers.coffee index 53daa7b6..37791f1a 100644 --- a/assets/scripts/app/helpers/helpers.coffee +++ b/assets/scripts/app/helpers/helpers.coffee @@ -26,12 +26,12 @@ require 'config/emoij' (sha || '').substr(0, 7) formatConfig: (config) -> - config = $.only config, Travis.CONFIG_KEYS + config = $.only config, Object.keys(Travis.CONFIG_KEYS_MAP) values = $.map config, (value, key) -> value = (if value && value.join then value.join(', ') else value) || '' if key == 'rvm' && "#{value}".match(/^\d+$/) value = "#{value}.0" - '%@: %@'.fmt $.camelize(key), value + '%@: %@'.fmt Travis.CONFIG_KEYS_MAP[key], value if values.length == 0 then '-' else values.join(', ') formatMessage: (message, options) -> @@ -84,7 +84,7 @@ require 'config/emoij' _githubReferenceRegexp: new RegExp("([\\w-]+)?\\/?([\\w-]+)?(?:#|gh-)(\\d+)", 'g') - _githubUserRegexp: new RegExp("@([\\w-]+)", 'g') + _githubUserRegexp: new RegExp("\\B@([\\w-]+)", 'g') _githubUserLink: (reference, username) -> "#{reference}" @@ -116,4 +116,4 @@ require 'config/emoij' configKeys: (config) -> return [] unless config - $.intersect($.keys(config), Travis.CONFIG_KEYS) + $.intersect($.keys(config), Object.keys(Travis.CONFIG_KEYS_MAP)) diff --git a/assets/scripts/app/models/build.coffee b/assets/scripts/app/models/build.coffee index 26915867..667daee9 100644 --- a/assets/scripts/app/models/build.coffee +++ b/assets/scripts/app/models/build.coffee @@ -45,7 +45,7 @@ require 'travis/model' ).property('jobs.@each.allowFailure') rawConfigKeys: (-> - keys = Travis.Helpers.configKeys(@get('config')) + keys = [] @get('jobs').forEach (job) -> Travis.Helpers.configKeys(job.get('config')).forEach (key) -> @@ -57,7 +57,7 @@ require 'travis/model' configKeys: (-> keys = @get('rawConfigKeys') headers = (I18n.t(key) for key in ['build.job', 'build.duration', 'build.finished_at']) - $.map(headers.concat(keys), (key) -> return $.camelize(key)) + $.map(headers.concat(keys), (key) -> if Travis.CONFIG_KEYS_MAP.hasOwnProperty(key) then Travis.CONFIG_KEYS_MAP[key] else key) ).property('rawConfigKeys.length') canCancel: (-> diff --git a/assets/scripts/app/models/repo.coffee b/assets/scripts/app/models/repo.coffee index 790b3f67..f582fb10 100644 --- a/assets/scripts/app/models/repo.coffee +++ b/assets/scripts/app/models/repo.coffee @@ -135,5 +135,12 @@ require 'travis/model' repo = $.select(@find().toArray(), (repo) -> repo.get('slug') == slug) if repo.length > 0 then repo else @find(slug: slug) + fetchBySlug: (slug) -> + repos = $.select(@find().toArray(), (repo) -> repo.get('slug') == slug) + if repos.length > 0 + repos[0] + else + @fetch(slug: slug).then (repos) -> Ember.get(repos, 'firstObject') + # buildURL: (slug) -> # if slug then slug else 'repos' diff --git a/assets/scripts/app/models/user.coffee b/assets/scripts/app/models/user.coffee index c073abf2..553b26bc 100644 --- a/assets/scripts/app/models/user.coffee +++ b/assets/scripts/app/models/user.coffee @@ -31,9 +31,31 @@ require 'travis/model' "https://github.com/#{@get('login')}" ).property() + _rawPermissions: (-> + Travis.ajax.get('/users/permissions') + ).property() + permissions: (-> permissions = Ember.ArrayProxy.create(content: []) - Travis.ajax.get('/users/permissions', (data) => permissions.set('content', data.permissions)) + @get('_rawPermissions').then (data) => permissions.set('content', data.permissions) + permissions + ).property() + + adminPermissions: (-> + permissions = Ember.ArrayProxy.create(content: []) + @get('_rawPermissions').then (data) => permissions.set('content', data.admin) + permissions + ).property() + + pullPermissions: (-> + permissions = Ember.ArrayProxy.create(content: []) + @get('_rawPermissions').then (data) => permissions.set('content', data.pull) + permissions + ).property() + + pushPermissions: (-> + permissions = Ember.ArrayProxy.create(content: []) + @get('_rawPermissions').then (data) => permissions.set('content', data.push) permissions ).property() @@ -55,7 +77,10 @@ require 'travis/model' poll: -> Travis.ajax.get '/users', (data) => if data.user.is_syncing - Ember.run.later(this, this.poll.bind(this), 3000) + self = this + setTimeout -> + self.poll() + , 3000 else @set('isSyncing', false) @setWithSession('syncedAt', data.user.synced_at) diff --git a/assets/scripts/app/routes.coffee b/assets/scripts/app/routes.coffee index b1dac327..0f8186c1 100644 --- a/assets/scripts/app/routes.coffee +++ b/assets/scripts/app/routes.coffee @@ -17,16 +17,10 @@ Ember.Router.reopen # TODO: don't reopen Ember.Route to add events, there should be # a better way (like "parent" resource for everything inside map) Ember.Route.reopen - events: + _actions: renderDefaultTemplate: -> @renderDefaultTemplate() if @renderDefaultTemplate - renderNoOwnedRepos: -> - @render('no_owned_repos', outlet: 'main') - - renderFirstSync: -> - @renderFirstSync() - error: (error) -> if error == 'needs-auth' authController = @container.lookup('controller:auth') || @generateController('auth') @@ -35,6 +29,12 @@ Ember.Route.reopen else throw(error) + renderNoOwnedRepos: -> + @render('no_owned_repos', outlet: 'main') + + renderFirstSync: -> + @renderFirstSync() + afterSignIn: (path) -> @afterSignIn(path) @@ -123,8 +123,9 @@ Travis.SetupLastBuild = Ember.Mixin.create repoDidLoad: -> # TODO: it would be nicer to do it with promises repo = @controllerFor('repo').get('repo') - if repo && repo.get('isLoaded') && !repo.get('lastBuild') - @render('builds/not_found', outlet: 'pane', into: 'repo') + if repo && repo.get('isLoaded') && !repo.get('lastBuildId') + Ember.run.next => + @render('builds/not_found', outlet: 'pane', into: 'repo') Travis.GettingStartedRoute = Ember.Route.extend setupController: -> @@ -139,6 +140,11 @@ Travis.GettingStartedRoute = Ember.Route.extend @_super.apply(this, arguments) Travis.FirstSyncRoute = Ember.Route.extend + _actions: + renderNoOwnedRepos: (->) + # do nothing, we are showing first sync, so it's normal that there is + # no owned repos + setupController: -> $('body').attr('id', 'home') @container.lookup('controller:repos').activate() @@ -211,7 +217,7 @@ Travis.BuildRoute = Ember.Route.extend repo.set('build', model) model: (params) -> - Travis.Build.find(params.build_id) + Travis.Build.fetch(params.build_id) Travis.JobRoute = Ember.Route.extend renderTemplate: -> @@ -229,19 +235,12 @@ Travis.JobRoute = Ember.Route.extend repo.set('job', model) repo.activate('job') - # since we're no longer using promises, the setupController resolves right away, - # so we need to wait for build to be loaded - buildObserver = -> - if build = model.get('build') - @controllerFor('build').set('build', build) - repo.set('build', build) - - model.removeObserver('build', buildObserver) - model.addObserver('build', this, buildObserver) - buildObserver.apply(this) + if build = model.get('build') + @controllerFor('build').set('build', build) + repo.set('build', build) model: (params) -> - Travis.Job.find(params.job_id) + Travis.Job.fetch(params.job_id) Travis.RepoIndexRoute = Ember.Route.extend Travis.SetupLastBuild, setupController: (controller, model) -> @@ -268,29 +267,13 @@ Travis.RepoRoute = Ember.Route.extend model: (params) -> slug = "#{params.owner}/#{params.name}" - content = Ember.Object.create slug: slug, isLoaded: false, isLoading: true - proxy = Ember.ObjectProxy.create(content: content) - repos = Travis.Repo.bySlug(slug) + Travis.Repo.fetchBySlug(slug) - self = this - - observer = -> - if repos.get 'isLoaded' - repos.removeObserver 'isLoaded', observer - proxy.set 'isLoading', false - - if repos.get('length') == 0 - self.render('repos/not_found', outlet: 'main') - else - proxy.set 'content', repos.objectAt(0) - - if repos.length - proxy.set('content', repos[0]) - else - repos.addObserver 'isLoaded', observer - - proxy + actions: + error: -> + Ember.run.next this, -> + @render('repos/not_found', outlet: 'main') Travis.IndexRoute = Ember.Route.extend renderTemplate: -> @@ -304,6 +287,10 @@ Travis.IndexRoute = Ember.Route.extend @container.lookup('controller:repos').activate() @container.lookup('controller:application').connectLayout 'home' +Travis.IndexLoadingRoute = Ember.Route.extend + renderTemplate: -> + @render('index_loading') + Travis.StatsRoute = Ember.Route.extend renderTemplate: -> $('body').attr('id', 'stats') diff --git a/assets/scripts/app/tailing.coffee b/assets/scripts/app/tailing.coffee index fab3fec8..3df8c872 100644 --- a/assets/scripts/app/tailing.coffee +++ b/assets/scripts/app/tailing.coffee @@ -1,12 +1,17 @@ -@Travis.Tailing = -> - @position = $(window).scrollTop() - $(window).scroll( $.throttle( 200, @onScroll.bind(this) ) ) - this - -$.extend Travis.Tailing.prototype, +class @Travis.Tailing options: timeout: 200 + tail: -> + $(@tail_selector) + log: -> + $(@log_selector) + + constructor: (@window, @tail_selector, @log_selector) -> + @position = @window.scrollTop() + @window.scroll( $.throttle( 200, @onScroll.bind(this) ) ) + this + run: -> @autoScroll() @positionButton() @@ -16,37 +21,43 @@ $.extend Travis.Tailing.prototype, if @active() then @stop() else @start() active: -> - $('#tail').hasClass('active') + @tail().hasClass('active') start: -> - $('#tail').addClass('active') + @tail().addClass('active') @run() stop: -> - $('#tail').removeClass('active') + @tail().removeClass('active') autoScroll: -> - return unless @active() - win = $(window) - log = $('#log') - logBottom = log.offset().top + log.outerHeight() + 40 - winBottom = win.scrollTop() + win.height() - win.scrollTop(logBottom - win.height()) if logBottom - winBottom > 0 + return false unless @active() + logBottom = @log().offset().top + @log().outerHeight() + 40 + winBottom = @window.scrollTop() + @window.height() + + if logBottom - winBottom > 0 + @window.scrollTop(logBottom - @window.height()) + true + else + false onScroll: -> @positionButton() - position = $(window).scrollTop() + position = @window.scrollTop() @stop() if position < @position @position = position positionButton: -> - tail = $('#tail') - return if tail.length is 0 - offset = $(window).scrollTop() - $('#log').offset().top - max = $('#log').height() - $('#tail').height() + 5 - offset = max if offset > max - if offset > 0 - tail.css(top: offset - 2) - else - tail.css(top: 0) + return if @tail().length is 0 + offset = @window.scrollTop() - @log().offset().top + max = @log().height() - @tail().height() + 5 + if offset > 0 && offset <= max + @tail().removeClass('bottom') + @tail().addClass('scrolling') + else + if offset > max + @tail().addClass('bottom') + else + @tail().removeClass('bottom') + @tail().removeClass('scrolling') diff --git a/assets/scripts/app/templates/builds/show.hbs b/assets/scripts/app/templates/builds/show.hbs index bf8e5354..5096dd60 100644 --- a/assets/scripts/app/templates/builds/show.hbs +++ b/assets/scripts/app/templates/builds/show.hbs @@ -35,11 +35,11 @@ {{/if}} {{#if commit.authorName}}
{{t builds.author}}
-
{{commit.authorName}}
+
{{commit.authorName}}
{{/if}} {{#if commit.committerName}}
{{t builds.committer}}
-
{{commit.committerName}}
+
{{commit.committerName}}
{{/if}} {{/with}} diff --git a/assets/scripts/app/templates/index_loading.hbs b/assets/scripts/app/templates/index_loading.hbs new file mode 100644 index 00000000..0277cf2e --- /dev/null +++ b/assets/scripts/app/templates/index_loading.hbs @@ -0,0 +1 @@ +
Loading
diff --git a/assets/scripts/app/templates/jobs/pre.hbs b/assets/scripts/app/templates/jobs/pre.hbs index dd337a7e..457d9699 100644 --- a/assets/scripts/app/templates/jobs/pre.hbs +++ b/assets/scripts/app/templates/jobs/pre.hbs @@ -1,7 +1,14 @@
- + +

 
diff --git a/assets/scripts/app/templates/jobs/show.hbs b/assets/scripts/app/templates/jobs/show.hbs
index ebee6825..0d8fa914 100644
--- a/assets/scripts/app/templates/jobs/show.hbs
+++ b/assets/scripts/app/templates/jobs/show.hbs
@@ -34,11 +34,11 @@
           {{/if}}
           {{#if commit.authorName}}
             
{{t jobs.author}}
-
{{commit.authorName}}
+
{{commit.authorName}}
{{/if}} {{#if commit.committerName}}
{{t jobs.committer}}
-
{{commit.committerName}}
+
{{commit.committerName}}
{{/if}}
{{/with}} diff --git a/assets/scripts/app/templates/layouts/top.hbs b/assets/scripts/app/templates/layouts/top.hbs index 0c1d84fa..e883d6aa 100644 --- a/assets/scripts/app/templates/layouts/top.hbs +++ b/assets/scripts/app/templates/layouts/top.hbs @@ -27,7 +27,7 @@
  • - Travis CI for Private Repositories + Travis CI for Private Repositories
  • diff --git a/assets/scripts/app/templates/no_owned_repos.hbs b/assets/scripts/app/templates/no_owned_repos.hbs index 9b9bd393..ca925208 100644 --- a/assets/scripts/app/templates/no_owned_repos.hbs +++ b/assets/scripts/app/templates/no_owned_repos.hbs @@ -74,65 +74,4 @@ send us an email.

    - -

    - Hey, it looks like you're new around here and have yet to set up your first repository on Travis CI. -

    - -

    - We're here to help you get started, it's easy! -

    - -

    - -

    - -

    - Start by going to your {{#linkTo "profile.index"}}profile{{/linkTo}} and enable one of your projects. We've been - synchronizing all repositories you have administrative access to. Pick one and flip the switch next to it. -

    - -

    - -

    - -

    - Once you've enabled one of your projects, add a .travis.yml to your project, push some code, and we'll start processing your builds. Wait a - whee while and reload the page, and your newly setup and built project will show up on the right. -

    - -

    - We'll also send you an email once the build has finished. -

    - -

    - We use sensible defaults for most languages, but you can customize - both the build process and the build - environment to fit your project's needs. -

    - -

    - You can also configure how you want to be notified of build results. Email is only one channel you can use. We - support Campfire, HipChat, Flowdock, IRC, and webhooks. To avoid - exposing any private credentials, you can shield them from the public using encrypted - configuration settings. -

    - -

    - -

    - -

    - Should you have any questions or issues, have a look at our - documentation, open an issue or - send us an email. -

    ->>>>>>> f6867e8a4cddfef041367a301d22d0969c6f886d diff --git a/assets/scripts/app/templates/repos/show/tools.hbs b/assets/scripts/app/templates/repos/show/tools.hbs index b2541b3c..f6368253 100644 --- a/assets/scripts/app/templates/repos/show/tools.hbs +++ b/assets/scripts/app/templates/repos/show/tools.hbs @@ -59,7 +59,7 @@ Integrating Code Climate's test coverage reporting with your test suite on Travis CI allows to track changes in coverage over time. If you haven't tried it out already, sign - up today for to improve your code's quality. New customers get 20% off for the first three months! + up today to improve your code's quality. New customers get 20% off for the first three months!

    diff --git a/assets/scripts/app/views.coffee b/assets/scripts/app/views.coffee index ae5928c1..7ddf2bc8 100644 --- a/assets/scripts/app/views.coffee +++ b/assets/scripts/app/views.coffee @@ -43,7 +43,7 @@ Travis.FirstSyncView = Travis.View.extend ).then(null, (e) -> console.log('There was a problem while redirecting from first sync', e) ) - , 5000 + , Travis.config.syncingPageRedirectionTime require 'views/accounts' require 'views/application' diff --git a/assets/scripts/app/views/job.coffee b/assets/scripts/app/views/job.coffee index 9027ebf7..1edaf375 100644 --- a/assets/scripts/app/views/job.coffee +++ b/assets/scripts/app/views/job.coffee @@ -29,11 +29,3 @@ Travis.reopen urlGithubCommit: (-> Travis.Urls.githubCommit(@get('repo.slug'), @get('commit.sha')) ).property('repo.slug', 'commit.sha') - - urlAuthor: (-> - Travis.Urls.email(@get('commit.authorEmail')) - ).property('commit.authorEmail') - - urlCommitter: (-> - Travis.Urls.email(@get('commit.committerEmail')) - ).property('commit.committerEmail') diff --git a/assets/scripts/app/views/log.coffee b/assets/scripts/app/views/log.coffee index 5bd671a7..2bacdfe2 100644 --- a/assets/scripts/app/views/log.coffee +++ b/assets/scripts/app/views/log.coffee @@ -13,7 +13,7 @@ Travis.reopen job = @get('job') if job job.get('log').fetch() - job.subscribe() if !job.get('isFinished') + job.subscribe() willDestroyElement: -> job = @get('job') @@ -34,12 +34,12 @@ Travis.reopen parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') versionDidChange: (-> - @rerender() if @get('inDOM') + @rerender() if @get('state') == 'inDOM' ).observes('log.version') logDidChange: (-> console.log 'log view: log did change: rerender' if Log.DEBUG - @rerender() if @get('inDOM') + @rerender() if @get('state') == 'inDOM' ).observes('log') createEngine: -> @@ -97,8 +97,9 @@ Travis.reopen window.history.pushState({ path: path }, null, path); @set('controller.lineNumber', number) - toTop: () -> - $(window).scrollTop(0) + actions: + toTop: () -> + $(window).scrollTop(0) noop: -> # TODO required? diff --git a/assets/scripts/app/views/repo/show.coffee b/assets/scripts/app/views/repo/show.coffee index 4dcaafde..a27a0b29 100644 --- a/assets/scripts/app/views/repo/show.coffee +++ b/assets/scripts/app/views/repo/show.coffee @@ -107,14 +107,19 @@ Travis.reopen displayRegenerateKey: true canRegenerateKey: (-> - @get('displayRegenerateKey') && @get('hasPermission') - ).property('hasPermission') + @get('displayRegenerateKey') && @get('hasAdminPermission') + ).property('hasAdminPermission') hasPermission: (-> if permissions = @get('currentUser.permissions') permissions.contains parseInt(@get('repo.id')) ).property('currentUser.permissions.length', 'repo.id') + hasAdminPermission: (-> + if permissions = @get('currentUser.adminPermissions') + permissions.contains parseInt(@get('repo.id')) + ).property('currentUser.adminPermissions.length', 'repo.id') + statusImageUrl: (-> Travis.Urls.statusImage(@get('slug')) ).property('slug') @@ -174,6 +179,11 @@ Travis.reopen permissions.contains parseInt(@get('repo.id')) ).property('currentUser.permissions.length', 'repo.id') + hasPushPermission: (-> + if permissions = @get('currentUser.pushPermissions') + permissions.contains parseInt(@get('repo.id')) + ).property('currentUser.pushPermissions.length', 'repo.id') + displayRequeueBuild: (-> @get('isBuildTab') && @get('build.isFinished') ).property('isBuildTab', 'build.isFinished') diff --git a/assets/scripts/lib/travis/adapter.coffee b/assets/scripts/lib/travis/adapter.coffee index ca92965a..91259fdb 100644 --- a/assets/scripts/lib/travis/adapter.coffee +++ b/assets/scripts/lib/travis/adapter.coffee @@ -3,10 +3,10 @@ Travis.Adapter = Ember.RESTAdapter.extend Travis.ajax.ajax(url, method || 'get', data: params) findMany: (klass, records, ids) -> - url = @buildURL(klass) + '?' + ids.map( (id) -> "ids[]=#{id}" ).join('&') + url = @buildURL(klass) self = this - @ajax(url).then (data) -> + @ajax(url, ids: ids).then (data) -> self.didFindMany(klass, records, data) didFindMany: (klass, records, data) -> @@ -15,6 +15,7 @@ Travis.Adapter = Ember.RESTAdapter.extend @sideload(klass, data) records.load(klass, dataToLoad) + @addToRecordArrays(records.get('content')) buildURL: -> @_super.apply(this, arguments).replace(/\.json$/, '') @@ -22,26 +23,38 @@ Travis.Adapter = Ember.RESTAdapter.extend didFind: (record, id, data) -> @sideload(record.constructor, data) @_super(record, id, data) + @addToRecordArrays(record) didFindAll: (klass, records, data) -> @sideload(klass, data) @_super(klass, records, data) + @addToRecordArrays(records.get('content')) didFindQuery: (klass, records, params, data) -> @sideload(klass, data) @_super(klass, records, params, data) + @addToRecordArrays(records.get('content')) didCreateRecord: (record, data) -> @sideload(record.constructor, data) @_super(record, data) + @addToRecordArrays(record) didSaveRecord: (record, data) -> @sideload(record.constructor, data) @_super(record, data) + @addToRecordArrays(record) didDeleteRecord: (record, data) -> @sideload(record.constructor, data) @_super(record, data) + @addToRecordArrays(record) + + addToRecordArrays: (records) -> + records = [records] unless Ember.isArray(records) + for record in records + record.constructor.addToRecordArrays(record) + sideload: (klass, data) -> for name, records of data diff --git a/assets/scripts/lib/travis/ajax.coffee b/assets/scripts/lib/travis/ajax.coffee index e856d364..00c32f01 100644 --- a/assets/scripts/lib/travis/ajax.coffee +++ b/assets/scripts/lib/travis/ajax.coffee @@ -59,7 +59,7 @@ Travis.ajax = Em.Object.create options = $.extend(options, Travis.ajax.DEFAULT_OPTIONS) if testMode? - console.log('RUnning ajax with', options.url) + console.log('Running ajax with', options.url) # we use jquery.mockjax for test, I don't want to hack it or rewrite it, # so let's just pretend we still use ajax if testing mode is on diff --git a/assets/scripts/lib/travis/model.coffee b/assets/scripts/lib/travis/model.coffee index 4c37fe50..9226130b 100644 --- a/assets/scripts/lib/travis/model.coffee +++ b/assets/scripts/lib/travis/model.coffee @@ -38,18 +38,19 @@ Array.prototype.diff = (a) -> @loadedAttributes = [] @loadedRelationships = [] - attributes = this.attributes || [] - relationships = this.relationships || [] + attributes = this.constructor.getAttributes() || [] + relationships = this.constructor.getRelationships() || [] - for key in attributes - dataKey = @dataKey(key) - if hash.hasOwnProperty(dataKey) - @loadedAttributes.pushObject(key) + if hash + for key in attributes + dataKey = @dataKey(key) + if hash.hasOwnProperty(dataKey) + @loadedAttributes.pushObject(key) - for key in relationships - dataKey = @dataKey(key) - if hash.hasOwnProperty(dataKey) - @loadedRelationships.pushObject(key) + for key in relationships + dataKey = @dataKey(key) + if hash.hasOwnProperty(dataKey) + @loadedRelationships.pushObject(key) incomplete = Ember.EnumerableUtils.intersection(@loadedAttributes, attributes).length != attributes.length || Ember.EnumerableUtils.intersection(@loadedRelationships, relationships).length != relationships.length @@ -86,10 +87,10 @@ Array.prototype.diff = (a) -> @loadTheRest(key) isAttribute: (name) -> - this.attributes.contains(name) + this.constructor.getAttributes().contains(name) isRelationship: (name) -> - this.relationships.contains(name) + this.constructor.getRelationships().contains(name) loadTheRest: (key) -> # for some weird reason key comes changed to a string and for some weird reason it even is called with @@ -145,19 +146,19 @@ Array.prototype.diff = (a) -> ).property() isRecordLoaded: (id) -> - !!@_referenceForId(id).record + reference = @_getReferenceById(id) + reference && reference.record camelizeKeys: true # TODO: the functions below will be added to Ember Model, remove them when that # happens resetData: -> - @_idToReference = null - @sideloadedData = null - @recordCache = null - @recordArrays = null - @_currentBatchIds = null - @_hasManyArrays = null + @_referenceCache = {} + @sideloadedData = {} + @recordArrays = [] + @_currentBatchIds = [] + @_hasManyArrays = [] @_findAllRecordArray = null unload: (record) -> @@ -172,10 +173,8 @@ Array.prototype.diff = (a) -> delete this.recordCache[key] loadRecordForReference: (reference) -> - record = @create({ _reference: reference }) - @recordCache = {} unless @recordCache + record = @create({ _reference: reference, id: reference.id }) @sideloadedData = {} unless @sideloadedData - @recordCache[reference.id] = record reference.record = record record.load(reference.id, @sideloadedData[reference.id]) # TODO: find a nicer way to not add record to record arrays twice diff --git a/assets/scripts/spec/integration/build_spec.coffee b/assets/scripts/spec/integration/build_spec.coffee index b479ed7d..a8fd4d11 100644 --- a/assets/scripts/spec/integration/build_spec.coffee +++ b/assets/scripts/spec/integration/build_spec.coffee @@ -34,7 +34,7 @@ test "displaying information on build page", -> listsJobs table: '#jobs' - headers: ['Job', 'Duration', 'Finished', 'Rvm'] + headers: ['Job', 'Duration', 'Finished', 'Ruby'] jobs: [ { color: 'green', id: 1, number: '1.1', repo: 'travis-ci/travis-core', finishedAt: '3 minutes ago', duration: '30 sec', rvm: 'rbx' } { color: 'red', id: 2, number: '1.2', repo: 'travis-ci/travis-core', finishedAt: '2 minutes ago', duration: '40 sec', rvm: '1.9.3' } @@ -42,7 +42,7 @@ test "displaying information on build page", -> listsJobs table: '#allowed_failure_jobs' - headers: ['Job', 'Duration', 'Finished', 'Rvm'] + headers: ['Job', 'Duration', 'Finished', 'Ruby'] jobs: [ { color: '', id: 3, number: '1.3', repo: 'travis-ci/travis-core', finishedAt: '-', duration: '-', rvm: 'jruby' } ] diff --git a/assets/scripts/spec/integration/current_spec.coffee b/assets/scripts/spec/integration/current_spec.coffee index 082f6189..e602df76 100644 --- a/assets/scripts/spec/integration/current_spec.coffee +++ b/assets/scripts/spec/integration/current_spec.coffee @@ -34,7 +34,7 @@ test "displaying information on repo page", -> listsJobs table: '#jobs' - headers: ['Job', 'Duration', 'Finished', 'Rvm'] + headers: ['Job', 'Duration', 'Finished', 'Ruby'] jobs: [ { id: 1, color: 'green', number: '1.1', repo: 'travis-ci/travis-core', finishedAt: '3 minutes ago', duration: '30 sec', rvm: 'rbx' } { id: 2, color: 'red', number: '1.2', repo: 'travis-ci/travis-core', finishedAt: '2 minutes ago', duration: '40 sec', rvm: '1.9.3' } @@ -42,7 +42,7 @@ test "displaying information on repo page", -> listsJobs table: '#allowed_failure_jobs' - headers: ['Job', 'Duration', 'Finished', 'Rvm'] + headers: ['Job', 'Duration', 'Finished', 'Ruby'] jobs: [ { id: 3, color: '', number: '1.3', repo: 'travis-ci/travis-core', finishedAt: '-', duration: '-', rvm: 'jruby' } ] diff --git a/assets/scripts/spec/integration/event_spec.coffee b/assets/scripts/spec/integration/event_spec.coffee index 9c2ba882..3cd2f835 100644 --- a/assets/scripts/spec/integration/event_spec.coffee +++ b/assets/scripts/spec/integration/event_spec.coffee @@ -29,12 +29,48 @@ test "event containing a repository, adds repository to repos list", -> last_build_number: 10 last_build_started_at: '2012-07-02T00:01:00Z' last_build_finished_at: '2012-07-02T00:02:30Z' + last_build_state: 'passed' + last_build_duration: 90 wait().then -> listsRepo row: 2 item: { slug: 'travis-ci/travis-support', build: { number: 4, url: '/travis-ci/travis-support/builds/10', duration: '1 min 30 sec', finishedAt: 'less than a minute ago' } } + +test "an event containing a created job, clears the job's log", -> + payload = + job: + id: 12 + repository_id: 1 + number: '1.4' + queue: 'build.linux' + + visit('/travis-ci/travis-core/').then -> + Em.run -> + logRendered() + Travis.receive 'build:created', payload + + wait().then -> + displaysLog [] + +test "an event containing a requeued job, clears the job's log", -> + payload = + job: + id: 12 + repository_id: 1 + number: '1.4' + queue: 'build.linux' + + visit('/travis-ci/travis-core').then -> + Em.run -> + logRendered() + Travis.receive 'build:requeued', payload + + wait().then -> + displaysLog [] + + test "an event with a build adds a build to a builds list", -> visit('/travis-ci/travis-core/builds').then -> payload = @@ -62,6 +98,7 @@ test "an event with a build adds a build to a builds list", -> row: 1 item: { id: 11, slug: 'travis-ci/travis-core', number: '3', sha: '1234567', branch: 'master', message: 'commit message 3', finishedAt: 'less than a minute ago', duration: '55 sec', color: 'red' } + #test "event containing a job, adds job to jobs list", -> # visit('travis-ci/travis-core').then -> # payload = diff --git a/assets/scripts/spec/integration/index_spec.coffee b/assets/scripts/spec/integration/index_spec.coffee index ea19cdb6..8eb426f9 100644 --- a/assets/scripts/spec/integration/index_spec.coffee +++ b/assets/scripts/spec/integration/index_spec.coffee @@ -34,7 +34,7 @@ test "displaying information on index page", -> listsJobs table: '#jobs' - headers: ['Job', 'Duration', 'Finished', 'Rvm'] + headers: ['Job', 'Duration', 'Finished', 'Ruby'] jobs: [ { color: 'green', id: 1, number: '1.1', repo: 'travis-ci/travis-core', finishedAt: '3 minutes ago', duration: '30 sec', rvm: 'rbx' } { color: 'red', id: 2, number: '1.2', repo: 'travis-ci/travis-core', finishedAt: '2 minutes ago', duration: '40 sec', rvm: '1.9.3' } @@ -42,7 +42,7 @@ test "displaying information on index page", -> listsJobs table: '#allowed_failure_jobs' - headers: ['Job', 'Duration', 'Finished', 'Rvm'] + headers: ['Job', 'Duration', 'Finished', 'Ruby'] jobs: [ { color: '', id: 3, number: '1.3', repo: 'travis-ci/travis-core', finishedAt: '-', duration: '-', rvm: 'jruby' } ] diff --git a/assets/scripts/spec/integration/my_repos_spec.coffee b/assets/scripts/spec/integration/my_repos_spec.coffee index 1c086406..6011772b 100644 --- a/assets/scripts/spec/integration/my_repos_spec.coffee +++ b/assets/scripts/spec/integration/my_repos_spec.coffee @@ -5,7 +5,6 @@ module "My repos", Ember.run -> Travis.reset() test "my repos is active by default when user is signed in", -> - Ember.run -> signInUser() visit('/').then -> wait().then -> diff --git a/assets/scripts/spec/integration/sync.coffee b/assets/scripts/spec/integration/sync.coffee new file mode 100644 index 00000000..a28b95b1 --- /dev/null +++ b/assets/scripts/spec/integration/sync.coffee @@ -0,0 +1,30 @@ +#module "Sync", +# setup: -> +# Ember.run -> Travis.advanceReadiness() +# teardown: -> +# Ember.run -> Travis.reset() +# +#test "first sync page is show when user just signed up and is syncing", -> +# Ember.run -> +# signInUser +# is_syncing: true +# synced_at: null +# +# $.mockjax +# url: '/hooks' +# responseTime: 10 +# responseText: +# hooks: [] +# +# Travis.config.syncingPageRedirectionTime = 100 +# +# wait().then -> +# ok $('#first_sync').text().match(/Just a few more seconds as we talk to GitHub to find out which repositories belong to you./) +# +# stop() +# Travis.__container__.lookup('controller:currentUser').get('content').set('isSyncing', false) +# setTimeout -> +# start() +# visit('/').then -> +# ok $('#getting-started').text().match(/Welcome to Travis CI!/) +# , 120 diff --git a/assets/scripts/spec/spec_helper.coffee b/assets/scripts/spec/spec_helper.coffee index e869902c..c3edead9 100644 --- a/assets/scripts/spec/spec_helper.coffee +++ b/assets/scripts/spec/spec_helper.coffee @@ -16,15 +16,54 @@ Ember.Container.prototype.stub = (fullName, instance) -> instance.destroy = instance.destroy || (->) this.cache.dict[fullName] = instance -window.signInUser = -> +window.signInUser = (data) -> + data ||= {} + userData = { + id: 1 + email: 'tyrion@example.org' + login: 'tyrion' + token: 'abcdef' + created_at: "2011-05-10T15:43:59Z" + gravatar_id: "582034b63279abeaa8e76acf12f5ee30" + is_syncing: false + locale: "en" + name: "Tyrion" + synced_at: "2013-12-09T09:41:47Z" + } + userData = Ember.merge(userData, data) + $.mockjax + url: '/users/1' + responseTime: 10 + responseText: + user: userData + $.mockjax + url: '/users' + responseTime: 10 + responseText: + user: userData + $.mockjax + url: '/users/permissions' + responseTime: 10 + responseText: + permissions: [] + admin: [] + push: [] + pull: [] + $.mockjax + url: '/broadcasts' + responseTime: 10 + responseText: + broadcasts: [] + $.mockjax + url: '/accounts' + responseTime: 10 + responseText: + accounts: [] + # for now let's just use harcoded data to log in the user, # we may extend it in the future to pass specific user data Travis.auth.signIn - user: - id: 1 - email: 'tyrion@example.org' - login: 'tyrion' - token: 'abcdef' + user: userData token: 'abcdef' #@app = (url, options = {}) -> diff --git a/assets/scripts/spec/support/expectations.coffee b/assets/scripts/spec/support/expectations.coffee index 03050d29..f16de344 100644 --- a/assets/scripts/spec/support/expectations.coffee +++ b/assets/scripts/spec/support/expectations.coffee @@ -5,8 +5,8 @@ @displaysTabs = (tabs) -> for name, tab of tabs equal($("#tab_#{name} a").attr('href'), tab.href, "#{name} tab should link to #{tab.href}") unless tab.hidden - equal($("#tab_#{name}").hasClass('active'), !!tab.active, "#{name} tab should be active") - equal($("#tab_#{name}").hasClass('display-inline'), !tab.hidden, "#{name} tab should has class display-inline") if name in ['build', 'job'] + equal($("#tab_#{name}").hasClass('active'), !!tab.active, "#{name} tab should #{'not' unless tab.active} be active") + equal($("#tab_#{name}").hasClass('display-inline'), !tab.hidden, "#{name} tab should have class display-inline") if name in ['build', 'job'] @displaysSummaryBuildLink = (link, number) -> element = $('#summary .number a') diff --git a/assets/scripts/spec/support/mocks.coffee b/assets/scripts/spec/support/mocks.coffee index 7a5213c1..76f3b655 100644 --- a/assets/scripts/spec/support/mocks.coffee +++ b/assets/scripts/spec/support/mocks.coffee @@ -1,11 +1,11 @@ minispade.require 'ext/jquery' -responseTime = 0 +responseTime = 10 repos = [ - { id: '1', owner: 'travis-ci', name: 'travis-core', slug: 'travis-ci/travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_state: 'passed', last_build_duration: 30, last_build_started_at: '2012-07-02T00:00:00Z', last_build_finished_at: '2012-07-02T00:00:30Z', description: 'Description of travis-core' }, - { id: '2', owner: 'travis-ci', name: 'travis-assets', slug: 'travis-ci/travis-assets', build_ids: [3], last_build_id: 3, last_build_number: 3, last_build_state: 'failed', last_build_duration: 30, last_build_started_at: '2012-07-02T00:01:00Z', last_build_finished_at: '2012-07-01T00:01:30Z', description: 'Description of travis-assets'}, - { id: '3', owner: 'travis-ci', name: 'travis-hub', slug: 'travis-ci/travis-hub', build_ids: [4], last_build_id: 4, last_build_number: 4, last_build_state: null, last_build_duration: null, last_build_started_at: '2012-07-02T00:02:00Z', last_build_finished_at: null, description: 'Description of travis-hub'}, + { id: '1', owner: 'travis-ci', name: 'travis-core', slug: 'travis-ci/travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_state: 'passed', last_build_duration: 30, last_build_started_at: '2012-07-02T00:00:00Z', last_build_finished_at: '2012-07-02T00:00:30Z', description: 'Description of travis-core', github_language: 'ruby' }, + { id: '2', owner: 'travis-ci', name: 'travis-assets', slug: 'travis-ci/travis-assets', build_ids: [3], last_build_id: 3, last_build_number: 3, last_build_state: 'failed', last_build_duration: 30, last_build_started_at: '2012-07-02T00:01:00Z', last_build_finished_at: '2012-07-01T00:01:30Z', description: 'Description of travis-assets', github_language: 'ruby'}, + { id: '3', owner: 'travis-ci', name: 'travis-hub', slug: 'travis-ci/travis-hub', build_ids: [4], last_build_id: 4, last_build_number: 4, last_build_state: null, last_build_duration: null, last_build_started_at: '2012-07-02T00:02:00Z', last_build_finished_at: null, description: 'Description of travis-hub', github_language: 'ruby' }, ] reposByName = (name) -> @@ -18,9 +18,9 @@ reposByName = (name) -> builds = [ { id: '1', repository_id: '1', commit_id: 1, job_ids: [1, 2, 3], number: 1, pull_request: false, config: { rvm: ['rbx', '1.9.3', 'jruby'] }, duration: 30, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:30Z', state: 'passed' }, - { id: '2', repository_id: '1', commit_id: 2, job_ids: [4], number: 2, pull_request: false, config: { rvm: ['rbx'] }, duration: null }, + { id: '2', repository_id: '1', commit_id: 2, job_ids: [4], number: 2, pull_request: false, config: { rvm: ['rbx'] }, duration: null, state: 'created', finished_at: null }, { id: '3', repository_id: '2', commit_id: 3, job_ids: [5], number: 3, pull_request: false, config: { rvm: ['rbx'] }, duration: 30, started_at: '2012-07-02T00:01:00Z', finished_at: '2012-07-01T00:01:30Z', state: 'failed' }, - { id: '4', repository_id: '3', commit_id: 4, job_ids: [6], number: 4, pull_request: false, config: { rvm: ['rbx'] }, duration: null, started_at: '2012-07-02T00:02:00Z' }, + { id: '4', repository_id: '3', commit_id: 4, job_ids: [6], number: 4, pull_request: false, config: { rvm: ['rbx'] }, duration: null, started_at: '2012-07-02T00:02:00Z', state: 'queued', finished_at: null }, ] commits = [ @@ -174,4 +174,3 @@ $.mockjax url: '/profile/hooks' responseTime: responseTime responseText: { hooks: hooks } - diff --git a/assets/scripts/spec/unit/build_spec.coffee b/assets/scripts/spec/unit/build_spec.coffee index 5c3c34ee..1cae1aed 100644 --- a/assets/scripts/spec/unit/build_spec.coffee +++ b/assets/scripts/spec/unit/build_spec.coffee @@ -54,4 +54,4 @@ test 'it takes into account all the jobs when getting config keys', -> configKeys = build.get('configKeys') deepEqual(rawConfigKeys, ['rvm', 'env', 'gemfile', 'jdk' ]) - deepEqual(configKeys, [ 'Job', 'Duration', 'Finished', 'Rvm', 'Env', 'Gemfile', 'Jdk' ]) + deepEqual(configKeys, [ 'Job', 'Duration', 'Finished', 'Ruby', 'ENV', 'Gemfile', 'JDK' ]) diff --git a/assets/scripts/spec/unit/helpers_spec.coffee b/assets/scripts/spec/unit/helpers_spec.coffee index 829c8a13..fe049984 100644 --- a/assets/scripts/spec/unit/helpers_spec.coffee +++ b/assets/scripts/spec/unit/helpers_spec.coffee @@ -52,3 +52,9 @@ test 'replaces @user with github user link', -> expected = 'It is for you @tender_love1' equal(result, expected, "@user should be converted to a link") + +test 'does not replace @user if it is a sign-off', -> + message = 'Signed-off-by: GitHub User ' + result = Travis.Helpers.githubify(message, 'travis-ci', 'travis-web') + + equal(result, message, "@user should not be converted to a link if it matches an email") diff --git a/assets/scripts/spec/unit/tailing_spec.coffee b/assets/scripts/spec/unit/tailing_spec.coffee new file mode 100644 index 00000000..18a836f0 --- /dev/null +++ b/assets/scripts/spec/unit/tailing_spec.coffee @@ -0,0 +1,89 @@ +fakeWindow = + scroll: sinon.spy() + scrollTop: sinon.stub().returns(0) + height: sinon.stub().returns(40) +element = jQuery('

    ') +log = jQuery('
    ') +tail = new Travis.Tailing(fakeWindow, '#specTail', '#specLog') +tail.tail = -> element +tail.log = -> log + +module "Travis.Tailing", + setup: -> + jQuery('body').append(element) + jQuery('body').append(log) + + teardown: -> + element.remove() + log.remove() + tail.stop() + +test "toggle", -> + equal(element.hasClass('active'), false) + tail.toggle() + equal(element.hasClass('active'), true) + tail.toggle() + stop() + + Ember.run.later -> + start() + equal(element.hasClass('active'), false) + , 300 + +test "active", -> + equal(tail.active(), false) + element.addClass('active') + equal(tail.active(), true) + +test "autoscroll when inactive", -> + tail.scrollTo = sinon.spy() + + equal(tail.active(), false) + equal(tail.autoScroll(), false) + equal(tail.scrollTo.called, false) + +test "autoscroll", -> + element.addClass('active') + log.offset = -> {top: 1} + log.outerHeight = -> 1 + + equal(tail.active(), true) + equal(tail.autoScroll(), true) + equal(fakeWindow.scrollTop.calledWith(2), true) + +test "autoscroll when we're at the bottom", -> + element.addClass('active') + log.offset = -> {top: 0} + log.outerHeight = -> 0 + + equal(tail.active(), true) + equal(tail.autoScroll(), false) + equal(fakeWindow.scrollTop.calledWith(0), false) + +test 'should stop scrolling if the position changed', -> + element.addClass('active') + tail.position = 100 + tail.onScroll() + equal(element.hasClass('active'), false) + +test 'positionButton adds the scrolling class', -> + log.offset = -> {top: -1} + + tail.positionButton() + equal(element.hasClass('scrolling'), true) + equal(element.hasClass('bottom'), false) + +test 'positionButton removes the scrolling class', -> + log.offset = -> {top: 1} + tail.positionButton() + equal(element.hasClass('scrolling'), false) + equal(element.hasClass('bottom'), false) + +test 'positionButton sets the button as bottom', -> + log.offset = -> {top: -100} + log.height = -> 50 + tail.height = -> 1 + + tail.positionButton() + equal(element.hasClass('scrolling'), false) + equal(element.hasClass('bottom'), true) diff --git a/assets/scripts/spec/unit/user_spec.coffee b/assets/scripts/spec/unit/user_spec.coffee new file mode 100644 index 00000000..8df3fcdb --- /dev/null +++ b/assets/scripts/spec/unit/user_spec.coffee @@ -0,0 +1,47 @@ +record = null + +module "Travis.User", + setup: -> + teardown: -> + Travis.User.resetData() + +test '', -> + # TODO: we should not need to mock entire user response + # just for user creation. It happens, because whenever + # a user is created we try to get fresh data + userData = { + id: 1 + email: 'tyrion@example.org' + login: 'tyrion' + token: 'abcdef' + created_at: "2011-05-10T15:43:59Z" + gravatar_id: "582034b63279abeaa8e76acf12f5ee30" + is_syncing: false + locale: "en" + name: "Tyrion" + synced_at: "2013-12-09T09:41:47Z" + } + $.mockjax + url: '/users/1' + responseTime: 10 + responseText: + user: userData + + + Travis.User.load [{ id: '1', login: 'test@travis-ci.org' }] + user = null + pushPermissions = null + adminPermissions = null + + Ember.run -> + user = Travis.User.find(1) + user.set '_rawPermissions', + then: (func) -> + func(permissions: [1], admin: [1], pull: [2], push: [3]) + + pushPermissions = user.get('pushPermissions') + adminPermissions = user.get('adminPermissions') + + wait().then -> + deepEqual(adminPermissions.toArray(), [1]) + deepEqual(pushPermissions.toArray(), [3]) diff --git a/assets/scripts/travis.coffee b/assets/scripts/travis.coffee index 81f611e3..2491ca60 100644 --- a/assets/scripts/travis.coffee +++ b/assets/scripts/travis.coffee @@ -61,7 +61,11 @@ Ember.RecordArray.reopen ).observes('content') window.Travis = TravisApplication.create( - LOG_TRANSITIONS: true + LOG_ACTIVE_GENERATION: true, + LOG_MODULE_RESOLVER: true, + LOG_TRANSITIONS: true, + LOG_TRANSITIONS_INTERNAL: true, + LOG_VIEW_LOOKUPS: true ) Travis.deferReadiness() @@ -71,13 +75,29 @@ $.extend Travis, Travis.advanceReadiness() # bc, remove once merged to master config: + syncingPageRedirectionTime: 5000 api_endpoint: $('meta[rel="travis.api_endpoint"]').attr('href') pusher_key: $('meta[name="travis.pusher_key"]').attr('value') ga_code: $('meta[name="travis.ga_code"]').attr('value') code_climate: $('meta[name="travis.code_climate"]').attr('value') code_climate_url: $('meta[name="travis.code_climate_url"]').attr('value') - CONFIG_KEYS: ['go', 'rvm', 'gemfile', 'env', 'jdk', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala', 'compiler'] + CONFIG_KEYS_MAP: { + go: 'Go' + rvm: 'Ruby' + gemfile: 'Gemfile' + env: 'ENV' + jdk: 'JDK' + otp_release: 'OTP Release' + php: 'PHP' + node_js: 'Node.js' + perl: 'Perl' + python: 'Python' + scala: 'Scala' + compiler: 'Compiler' + ghc: 'GHC' + os: 'OS' + } QUEUES: [ { name: 'linux', display: 'Linux' } @@ -131,6 +151,9 @@ Travis.Router.reopen if Travis.config.ga_code _gaq.push ['_trackPageview', location.pathname] +Ember.LinkView.reopen + loadingClass: 'loading_link' + require 'ext/i18n' require 'travis/ajax' require 'travis/adapter' diff --git a/assets/scripts/vendor/ember-model.js b/assets/scripts/vendor/ember-model.js index 4665d5d1..b04a12ab 100644 --- a/assets/scripts/vendor/ember-model.js +++ b/assets/scripts/vendor/ember-model.js @@ -1,27 +1,41 @@ (function() { +var VERSION = '0.0.10'; + +if (Ember.libraries) { + Ember.libraries.register('Ember Model', VERSION); +} + + +})(); + +(function() { + function mustImplement(message) { var fn = function() { - throw new Error(message); + var className = this.constructor.toString(); + + throw new Error(message.replace('{{className}}', className)); }; fn.isUnimplemented = true; return fn; } Ember.Adapter = Ember.Object.extend({ - find: mustImplement('Ember.Adapter subclasses must implement find'), - findQuery: mustImplement('Ember.Adapter subclasses must implement findQuery'), - findMany: mustImplement('Ember.Adapter subclasses must implement findMany'), - findAll: mustImplement('Ember.Adapter subclasses must implement findAll'), - createRecord: mustImplement('Ember.Adapter subclasses must implement createRecord'), - saveRecord: mustImplement('Ember.Adapter subclasses must implement saveRecord'), - deleteRecord: mustImplement('Ember.Adapter subclasses must implement deleteRecord'), + find: mustImplement('{{className}} must implement find'), + findQuery: mustImplement('{{className}} must implement findQuery'), + findMany: mustImplement('{{className}} must implement findMany'), + findAll: mustImplement('{{className}} must implement findAll'), + createRecord: mustImplement('{{className}} must implement createRecord'), + saveRecord: mustImplement('{{className}} must implement saveRecord'), + deleteRecord: mustImplement('{{className}} must implement deleteRecord'), load: function(record, id, data) { record.load(id, data); } }); + })(); (function() { @@ -143,12 +157,23 @@ Ember.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { }, reload: function() { - var modelClass = this.get('modelClass'); - Ember.assert("Reload can only be called on findAll RecordArrays", - modelClass && modelClass._findAllRecordArray === this); + var modelClass = this.get('modelClass'), + self = this, + promises; set(this, 'isLoaded', false); - modelClass.adapter.findAll(modelClass, this); + if (modelClass._findAllRecordArray === this) { + return modelClass.adapter.findAll(modelClass, this); + } else if (this._query) { + return modelClass.adapter.findQuery(modelClass, this, this._query); + } else { + promises = this.map(function(record) { + return record.reload(); + }); + return Ember.RSVP.all(promises).then(function(data) { + self.notifyLoaded(); + }); + } } }); @@ -171,13 +196,13 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ throw new Error('FilteredRecordArrays must be created with filterProperties'); } - this._registeredClientIds = Ember.A([]); - var modelClass = get(this, 'modelClass'); modelClass.registerRecordArray(this); this.registerObservers(); this.updateFilter(); + + this._super(); }, updateFilter: function() { @@ -186,20 +211,17 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ get(this, 'modelClass').forEachCachedRecord(function(record) { if (self.filterFunction(record)) { results.push(record); - } else { - results.removeObject(record); } }); this.set('content', Ember.A(results)); }, updateFilterForRecord: function(record) { - var results = get(this, 'content'); - if (this.filterFunction(record)) { - if(!results.contains(record)) { - results.pushObject(record); - } - } else { + var results = get(this, 'content'), + filterMatches = this.filterFunction(record); + if (filterMatches && !results.contains(record)) { + results.pushObject(record); + } else if(!filterMatches) { results.removeObject(record); } }, @@ -213,26 +235,44 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ registerObserversOnRecord: function(record) { var self = this, - filterProperties = get(this, 'filterProperties'), - clientId = record._reference.clientId; + filterProperties = get(this, 'filterProperties'); - if(!this._registeredClientIds.contains(clientId)) { - for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { - record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); - } - this._registeredClientIds.pushObject(clientId); + for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { + record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); } } }); + })(); (function() { -var get = Ember.get; +var get = Ember.get, set = Ember.set; Ember.ManyArray = Ember.RecordArray.extend({ _records: null, + originalContent: [], + + isDirty: function() { + var originalContent = get(this, 'originalContent'), + originalContentLength = get(originalContent, 'length'), + content = get(this, 'content'), + contentLength = get(content, 'length'); + + if (originalContentLength !== contentLength) { return true; } + + var isDirty = false; + + for (var i = 0, l = contentLength; i < l; i++) { + if (!originalContent.contains(content[i])) { + isDirty = true; + break; + } + } + + return isDirty; + }.property('content.[]', 'originalContent'), objectAtContent: function(idx) { var content = get(this, 'content'); @@ -255,6 +295,48 @@ Ember.ManyArray = Ember.RecordArray.extend({ }, this); this._super(index, removed, added); + }, + + _contentWillChange: function() { + var content = get(this, 'content'); + if (content) { + content.removeArrayObserver(this); + this._setupOriginalContent(content); + } + }.observesBefore('content'), + + _contentDidChange: function() { + var content = get(this, 'content'); + if (content) { + content.addArrayObserver(this); + this.arrayDidChange(content, 0, 0, get(content, 'length')); + } + }.observes('content'), + + arrayWillChange: function(item, idx, removedCnt, addedCnt) {}, + + arrayDidChange: function(item, idx, removedCnt, addedCnt) { + var parent = get(this, 'parent'), relationshipKey = get(this, 'relationshipKey'), + isDirty = get(this, 'isDirty'); + + if (isDirty) { + parent._relationshipBecameDirty(relationshipKey); + } else { + parent._relationshipBecameClean(relationshipKey); + } + }, + + _setupOriginalContent: function(content) { + content = content || get(this, 'content'); + if (content) { + set(this, 'originalContent', content.slice()); + } + }, + + init: function() { + this._super(); + this._setupOriginalContent(); + this._contentDidChange(); } }); @@ -268,7 +350,7 @@ Ember.HasManyArray = Ember.ManyArray.extend({ if (reference.record) { record = reference.record; } else { - record = klass.findById(reference.id); + record = klass.find(reference.id); } return record; @@ -357,36 +439,6 @@ function hasCachedValue(object, key) { } } -function extractDirty(object, attrsOrRelations, dirtyAttributes) { - var key, desc, descMeta, type, dataValue, cachedValue, isDirty, dataType; - for (var i = 0, l = attrsOrRelations.length; i < l; i++) { - key = attrsOrRelations[i]; - if (!hasCachedValue(object, key)) { continue; } - cachedValue = object.cacheFor(key); - dataValue = get(object, '_data.' + object.dataKey(key)); - desc = meta(object).descs[key]; - descMeta = desc && desc.meta(); - type = descMeta.type; - dataType = Ember.Model.dataTypes[type]; - - if (type && type.isEqual) { - isDirty = !type.isEqual(dataValue, cachedValue); - } else if (dataType && dataType.isEqual) { - isDirty = !dataType.isEqual(dataValue, cachedValue); - } else if (dataValue && cachedValue instanceof Ember.Model) { // belongsTo case - isDirty = get(cachedValue, 'isDirty'); - } else if (dataValue !== cachedValue) { - isDirty = true; - } else { - isDirty = false; - } - - if (isDirty) { - dirtyAttributes.push(key); - } - } -} - Ember.run.queues.push('data'); Ember.Model = Ember.Object.extend(Ember.Evented, { @@ -407,24 +459,20 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { return value; }, - isDirty: Ember.computed(function() { - var attributes = this.attributes, - relationships = this.relationships, - dirtyAttributes = Ember.A(); // just for removeObject + isDirty: function() { + var dirtyAttributes = get(this, '_dirtyAttributes'); + return dirtyAttributes && dirtyAttributes.length !== 0 || false; + }.property('_dirtyAttributes.length'), - extractDirty(this, attributes, dirtyAttributes); - if (relationships) { - extractDirty(this, relationships, dirtyAttributes); - } + _relationshipBecameDirty: function(name) { + var dirtyAttributes = get(this, '_dirtyAttributes'); + if (!dirtyAttributes.contains(name)) { dirtyAttributes.pushObject(name); } + }, - if (dirtyAttributes.length) { - this._dirtyAttributes = dirtyAttributes; - return true; - } else { - this._dirtyAttributes = []; - return false; - } - }).property().volatile(), + _relationshipBecameClean: function(name) { + var dirtyAttributes = get(this, '_dirtyAttributes'); + dirtyAttributes.removeObject(name); + }, dataKey: function(key) { var camelizeKeys = get(this.constructor, 'camelizeKeys'); @@ -437,11 +485,10 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { init: function() { this._createReference(); + if (!this._dirtyAttributes) { + set(this, '_dirtyAttributes', []); + } this._super(); - - this.one('didLoad', function() { - this.constructor.addToRecordArrays(this); - }); }, _createReference: function() { @@ -449,9 +496,12 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { id = this.getPrimaryKey(); if (!reference) { - reference = this.constructor._referenceForId(id); + reference = this.constructor._getOrCreateReferenceForId(id); reference.record = this; this._reference = reference; + } else if (reference.id !== id) { + reference.id = id; + this.constructor._cacheReference(reference); } if (!reference.id) { @@ -469,6 +519,27 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { var data = {}; data[get(this.constructor, 'primaryKey')] = id; set(this, '_data', Ember.merge(data, hash)); + + // eagerly load embedded data + var relationships = this.constructor._relationships || [], meta = Ember.meta(this), relationshipKey, relationship, relationshipMeta, relationshipData, relationshipType; + for (var i = 0, l = relationships.length; i < l; i++) { + relationshipKey = relationships[i]; + relationship = meta.descs[relationshipKey]; + relationshipMeta = relationship.meta(); + + if (relationshipMeta.options.embedded) { + relationshipType = relationshipMeta.type; + if (typeof relationshipType === "string") { + relationshipType = Ember.get(Ember.lookup, relationshipType); + } + + relationshipData = data[relationshipKey]; + if (relationshipData) { + relationshipType.load(relationshipData); + } + } + } + set(this, 'isLoaded', true); set(this, 'isNew', false); this._createReference(); @@ -478,13 +549,15 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { didDefineProperty: function(proto, key, value) { if (value instanceof Ember.Descriptor) { var meta = value.meta(); + var klass = proto.constructor; if (meta.isAttribute) { - proto.attributes = proto.attributes ? proto.attributes.slice() : []; - proto.attributes.push(key); + if (!klass._attributes) { klass._attributes = []; } + klass._attributes.push(key); } else if (meta.isRelationship) { - proto.relationships = proto.relationships ? proto.relationships.slice() : []; - proto.relationships.push(key); + if (!klass._relationships) { klass._relationships = []; } + klass._relationships.push(key); + meta.relationshipKey = key; } } }, @@ -506,7 +579,9 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { toJSON: function() { var key, meta, json = {}, - properties = this.attributes ? this.getProperties(this.attributes) : {}, + attributes = this.constructor.getAttributes(), + relationships = this.constructor.getRelationships(), + properties = attributes ? this.getProperties(attributes) : {}, rootKey = get(this.constructor, 'rootKey'); for (key in properties) { @@ -520,11 +595,11 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { } } - if (this.relationships) { + if (relationships) { var data, relationshipKey; - for(var i = 0; i < this.relationships.length; i++) { - key = this.relationships[i]; + for(var i = 0; i < relationships.length; i++) { + key = relationships[i]; meta = this.constructor.metaForProperty(key); relationshipKey = meta.options.key || key; @@ -566,19 +641,13 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { }, reload: function() { + this.getWithDefault('_dirtyAttributes', []).clear(); return this.constructor.reload(this.get(get(this.constructor, 'primaryKey'))); }, revert: function() { - if (this.get('isDirty')) { - var data = get(this, '_data') || {}, - reverts = {}; - for (var i = 0; i < this._dirtyAttributes.length; i++) { - var attr = this._dirtyAttributes[i]; - reverts[attr] = data[attr]; - } - setProperties(this, reverts); - } + this.getWithDefault('_dirtyAttributes', []).clear(); + this.notifyPropertyChange('_data'); }, didCreateRecord: function() { @@ -587,10 +656,7 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { set(this, 'isNew', false); - if (!this.constructor.recordCache) this.constructor.recordCache = {}; - this.constructor.recordCache[id] = this; - - this._copyDirtyAttributesToData(); + set(this, '_dirtyAttributes', []); this.constructor.addToRecordArrays(this); this.trigger('didCreateRecord'); this.didSaveRecord(); @@ -627,7 +693,7 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { key = dirtyAttributes[i]; data[this.dataKey(key)] = this.cacheFor(key); } - this._dirtyAttributes = []; + set(this, '_dirtyAttributes', []); }, dataDidChange: Ember.observer(function() { @@ -642,11 +708,16 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { _reloadHasManys: function() { if (!this._hasManyArrays) { return; } - - var i; - for(i = 0; i < this._hasManyArrays.length; i++) { - var array = this._hasManyArrays[i]; - set(array, 'content', this._getHasManyContent(get(array, 'key'), get(array, 'modelClass'), get(array, 'embedded'))); + var i, j; + for (i = 0; i < this._hasManyArrays.length; i++) { + var array = this._hasManyArrays[i], + hasManyContent = this._getHasManyContent(get(array, 'key'), get(array, 'modelClass'), get(array, 'embedded')); + for (j = 0; j < array.get('length'); j++) { + if (array.objectAt(j).get('isNew')) { + hasManyContent.addObject(array.objectAt(j)._reference); + } + } + set(array, 'content', hasManyContent); } }, @@ -658,12 +729,12 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { if (embedded) { primaryKey = get(type, 'primaryKey'); mapFunction = function(attrs) { - reference = type._referenceForId(attrs[primaryKey]); + reference = type._getOrCreateReferenceForId(attrs[primaryKey]); reference.data = attrs; return reference; }; } else { - mapFunction = function(id) { return type._referenceForId(id); }; + mapFunction = function(id) { return type._getOrCreateReferenceForId(id); }; } content = Ember.EnumerableUtils.map(content, mapFunction); } @@ -679,26 +750,77 @@ Ember.Model.reopenClass({ _clientIdCounter: 1, - fetch: function() { - return Ember.loadPromise(this.find.apply(this, arguments)); + getAttributes: function() { + this.proto(); // force class "compilation" if it hasn't been done. + var attributes = this._attributes || []; + if (typeof this.superclass.getAttributes === 'function') { + attributes = this.superclass.getAttributes().concat(attributes); + } + return attributes; + }, + + getRelationships: function() { + this.proto(); // force class "compilation" if it hasn't been done. + var relationships = this._relationships || []; + if (typeof this.superclass.getRelationships === 'function') { + relationships = this.superclass.getRelationships().concat(relationships); + } + return relationships; + }, + + fetch: function(id) { + if (!arguments.length) { + return this._findFetchAll(true); + } else if (Ember.isArray(id)) { + return this._findFetchMany(id, true); + } else if (typeof id === 'object') { + return this._findFetchQuery(id, true); + } else { + return this._findFetchById(id, true); + } }, find: function(id) { if (!arguments.length) { - return this.findAll(); + return this._findFetchAll(false); } else if (Ember.isArray(id)) { - return this.findMany(id); + return this._findFetchMany(id, false); } else if (typeof id === 'object') { - return this.findQuery(id); + return this._findFetchQuery(id, false); } else { - return this.findById(id); + return this._findFetchById(id, false); } }, - findMany: function(ids) { - Ember.assert("findMany requires an array", Ember.isArray(ids)); + findQuery: function(params) { + return this._findFetchQuery(params, false); + }, - var records = Ember.RecordArray.create({_ids: ids}); + fetchQuery: function(params) { + return this._findFetchQuery(params, true); + }, + + _findFetchQuery: function(params, isFetch) { + var records = Ember.RecordArray.create({modelClass: this, _query: params}); + + var promise = this.adapter.findQuery(this, records, params); + + return isFetch ? promise : records; + }, + + findMany: function(ids) { + return this._findFetchMany(ids, false); + }, + + fetchMany: function(ids) { + return this._findFetchMany(ids, true); + }, + + _findFetchMany: function(ids, isFetch) { + Ember.assert("findFetchMany requires an array", Ember.isArray(ids)); + + var records = Ember.RecordArray.create({_ids: ids, modelClass: this}), + deferred; if (!this.recordArrays) { this.recordArrays = []; } this.recordArrays.push(records); @@ -711,43 +833,97 @@ Ember.Model.reopenClass({ this._currentBatchRecordArrays = [records]; } + if (isFetch) { + deferred = Ember.Deferred.create(); + Ember.set(deferred, 'resolveWith', records); + + if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } + this._currentBatchDeferreds.push(deferred); + } + Ember.run.scheduleOnce('data', this, this._executeBatch); - return records; + return isFetch ? deferred : records; }, findAll: function() { - if (this._findAllRecordArray) { return this._findAllRecordArray; } + return this._findFetchAll(false); + }, + + fetchAll: function() { + return this._findFetchAll(true); + }, + + _findFetchAll: function(isFetch) { + var self = this; + + if (this._findAllRecordArray) { + if (isFetch) { + return new Ember.RSVP.Promise(function(resolve) { + resolve(self._findAllRecordArray); + }); + } else { + return this._findAllRecordArray; + } + } var records = this._findAllRecordArray = Ember.RecordArray.create({modelClass: this}); - this.adapter.findAll(this, records); + var promise = this.adapter.findAll(this, records); - return records; + // Remove the cached record array if the promise is rejected + if (promise.then) { + promise.then(null, function() { + self._findAllRecordArray = null; + return Ember.RSVP.reject.apply(null, arguments); + }); + } + + return isFetch ? promise : records; + }, + + findById: function(id) { + return this._findFetchById(id, false); + }, + + fetchById: function(id) { + return this._findFetchById(id, true); + }, + + _findFetchById: function(id, isFetch) { + var record = this.cachedRecordForId(id), + isLoaded = get(record, 'isLoaded'), + adapter = get(this, 'adapter'), + deferredOrPromise; + + if (isLoaded) { + if (isFetch) { + return new Ember.RSVP.Promise(function(resolve, reject) { + resolve(record); + }); + } else { + return record; + } + } + + deferredOrPromise = this._fetchById(record, id); + + return isFetch ? deferredOrPromise : record; }, _currentBatchIds: null, _currentBatchRecordArrays: null, - - findById: function(id) { - var record = this.cachedRecordForId(id); - - if (!get(record, 'isLoaded')) { - this._fetchById(record, id); - } - return record; - }, + _currentBatchDeferreds: null, reload: function(id) { var record = this.cachedRecordForId(id); - - this._fetchById(record, id); - - return record; + record.set('isLoaded', false); + return this._fetchById(record, id); }, _fetchById: function(record, id) { - var adapter = get(this, 'adapter'); + var adapter = get(this, 'adapter'), + deferred; if (adapter.findMany && !adapter.findMany.isUnimplemented) { if (this._currentBatchIds) { @@ -757,8 +933,17 @@ Ember.Model.reopenClass({ this._currentBatchRecordArrays = []; } + deferred = Ember.Deferred.create(); + + //Attached the record to the deferred so we can resolove it later. + Ember.set(deferred, 'resolveWith', record); + + if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } + this._currentBatchDeferreds.push(deferred); + Ember.run.scheduleOnce('data', this, this._executeBatch); - // TODO: return a promise here + + return deferred; } else { return adapter.find(record, id); } @@ -766,7 +951,8 @@ Ember.Model.reopenClass({ _executeBatch: function() { var batchIds = this._currentBatchIds, - batchRecordArrays = this._currentBatchRecordArrays, + batchRecordArrays = this._currentBatchRecordArrays || [], + batchDeferreds = this._currentBatchDeferreds, self = this, requestIds = [], promise, @@ -774,41 +960,60 @@ Ember.Model.reopenClass({ this._currentBatchIds = null; this._currentBatchRecordArrays = null; + this._currentBatchDeferreds = null; + + for (i = 0; i < batchIds.length; i++) { + if (!this.cachedRecordForId(batchIds[i]).get('isLoaded')) { + requestIds.push(batchIds[i]); + } + } if (batchIds.length === 1) { promise = get(this, 'adapter').find(this.cachedRecordForId(batchIds[0]), batchIds[0]); } else { var recordArray = Ember.RecordArray.create({_ids: batchIds}); - promise = get(this, 'adapter').findMany(this, recordArray, batchIds); + if (requestIds.length === 0) { + promise = new Ember.RSVP.Promise(function(resolve, reject) { resolve(recordArray); }); + recordArray.notifyLoaded(); + } else { + promise = get(this, 'adapter').findMany(this, recordArray, requestIds); + } } promise.then(function() { for (var i = 0, l = batchRecordArrays.length; i < l; i++) { batchRecordArrays[i].loadForFindMany(self); } + + if (batchDeferreds) { + for (i = 0, l = batchDeferreds.length; i < l; i++) { + var resolveWith = Ember.get(batchDeferreds[i], 'resolveWith'); + batchDeferreds[i].resolve(resolveWith); + } + } + }).then(null, function(errorXHR) { + if (batchDeferreds) { + for (var i = 0, l = batchDeferreds.length; i < l; i++) { + batchDeferreds[i].reject(errorXHR); + } + } }); }, - findQuery: function(params) { - var records = Ember.RecordArray.create(); - - this.adapter.findQuery(this, records, params); - - return records; + getCachedReferenceRecord: function(id){ + var ref = this._getReferenceById(id); + if(ref) return ref.record; + return undefined; }, cachedRecordForId: function(id) { - if (!this.recordCache) { this.recordCache = {}; } - var record; + var record = this.getCachedReferenceRecord(id); - if (this.recordCache[id]) { - record = this.recordCache[id]; - } else { + if (!record) { var primaryKey = get(this, 'primaryKey'), - attrs = {isLoaded: false}; + attrs = {isLoaded: false}; attrs[primaryKey] = id; record = this.create(attrs); - this.recordCache[id] = record; var sideloadedData = this.sideloadedData && this.sideloadedData[id]; if (sideloadedData) { record.load(id, sideloadedData); @@ -818,6 +1023,7 @@ Ember.Model.reopenClass({ return record; }, + addToRecordArrays: function(record) { if (this._findAllRecordArray) { this._findAllRecordArray.pushObject(record); @@ -834,6 +1040,26 @@ Ember.Model.reopenClass({ } }, + unload: function (record) { + this.removeFromRecordArrays(record); + var primaryKey = record.get(get(this, 'primaryKey')); + this.removeFromCache(primaryKey); + }, + + clearCache: function () { + this.sideloadedData = undefined; + this._referenceCache = undefined; + }, + + removeFromCache: function (key) { + if (this.sideloadedData && this.sideloadedData[key]) { + delete this.sideloadedData[key]; + } + if(this._referenceCache && this._referenceCache[key]) { + delete this._referenceCache[key]; + } + }, + removeFromRecordArrays: function(record) { if (this._findAllRecordArray) { this._findAllRecordArray.removeObject(record); @@ -869,25 +1095,39 @@ Ember.Model.reopenClass({ }, forEachCachedRecord: function(callback) { - if (!this.recordCache) { return Ember.A([]); } - var ids = Object.keys(this.recordCache); + if (!this._referenceCache) { this._referenceCache = {}; } + var ids = Object.keys(this._referenceCache); ids.map(function(id) { - return this.recordCache[id]; + return this._getReferenceById(id).record; }, this).forEach(callback); }, load: function(hashes) { + if (Ember.typeOf(hashes) !== 'array') { hashes = [hashes]; } + if (!this.sideloadedData) { this.sideloadedData = {}; } + for (var i = 0, l = hashes.length; i < l; i++) { - var hash = hashes[i]; - this.sideloadedData[hash[get(this, 'primaryKey')]] = hash; + var hash = hashes[i], + primaryKey = hash[get(this, 'primaryKey')], + record = this.getCachedReferenceRecord(primaryKey); + + if (record) { + record.load(primaryKey, hash); + } else { + this.sideloadedData[primaryKey] = hash; + } } }, - _referenceForId: function(id) { - if (!this._idToReference) { this._idToReference = {}; } + _getReferenceById: function(id) { + if (!this._referenceCache) { this._referenceCache = {}; } + return this._referenceCache[id]; + }, + + _getOrCreateReferenceForId: function(id) { + var reference = this._getReferenceById(id); - var reference = this._idToReference[id]; if (!reference) { reference = this._createReference(id); } @@ -896,32 +1136,26 @@ Ember.Model.reopenClass({ }, _createReference: function(id) { - if (!this._idToReference) { this._idToReference = {}; } + if (!this._referenceCache) { this._referenceCache = {}; } - Ember.assert('The id ' + id + ' has alread been used with another record of type ' + this.toString() + '.', !id || !this._idToReference[id]); + Ember.assert('The id ' + id + ' has alread been used with another record of type ' + this.toString() + '.', !id || !this._referenceCache[id]); var reference = { id: id, clientId: this._clientIdCounter++ }; - // if we're creating an item, this process will be done - // later, once the object has been persisted. - if (id) { - this._idToReference[id] = reference; - } + this._cacheReference(reference); return reference; }, - resetData: function() { - this._idToReference = null; - this.sideloadedData = null; - this.recordCache = null; - this.recordArrays = null; - this._currentBatchIds = null; - this._hasManyArrays = null; - this._findAllRecordArray = null; + _cacheReference: function(reference) { + // if we're creating an item, this process will be done + // later, once the object has been persisted. + if (reference.id) { + this._referenceCache[reference.id] = reference; + } } }); @@ -957,7 +1191,8 @@ Ember.Model.reopen({ modelClass: type, content: this._getHasManyContent(key, type, embedded), embedded: embedded, - key: key + key: key, + relationshipKey: meta.relationshipKey }); this._registerHasManyArray(collection); @@ -971,7 +1206,8 @@ Ember.Model.reopen({ (function() { -var get = Ember.get; +var get = Ember.get, + set = Ember.set; function getType() { if (typeof this.type === "string") { @@ -986,14 +1222,32 @@ Ember.belongsTo = function(type, options) { var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo', getType: getType }, relationshipKey = options.key; - return Ember.computed(function(key, value) { + return Ember.computed(function(key, value, oldValue) { type = meta.getType(); - if (arguments.length === 2) { + var dirtyAttributes = get(this, '_dirtyAttributes'), + createdDirtyAttributes = false; + + if (!dirtyAttributes) { + dirtyAttributes = []; + createdDirtyAttributes = true; + } + + if (arguments.length > 1) { if (value) { Ember.assert(Ember.String.fmt('Attempted to set property of type: %@ with a value of type: %@', [value.constructor, type]), value instanceof type); + + if (oldValue !== value) { + dirtyAttributes.pushObject(key); + } else { + dirtyAttributes.removeObject(key); + } + + if (createdDirtyAttributes) { + set(this, '_dirtyAttributes', dirtyAttributes); + } } return value === undefined ? null : value; } else { @@ -1012,11 +1266,12 @@ Ember.Model.reopen({ } if (meta.options.embedded) { - var primaryKey = get(type, 'primaryKey'); - record = type.create({ isLoaded: false }); - record.load(idOrAttrs[primaryKey], idOrAttrs); + var primaryKey = get(type, 'primaryKey'), + id = idOrAttrs[primaryKey]; + record = type.create({ isLoaded: false, id: id }); + record.load(id, idOrAttrs); } else { - record = type.findById(idOrAttrs); + record = type.find(idOrAttrs); } return record; @@ -1032,41 +1287,15 @@ var get = Ember.get, set = Ember.set, meta = Ember.meta; -function wrapObject(value) { - if (Ember.isArray(value)) { - var clonedArray = value.slice(); - - // TODO: write test for recursive cloning - for (var i = 0, l = clonedArray.length; i < l; i++) { - clonedArray[i] = wrapObject(clonedArray[i]); - } - - return Ember.A(clonedArray); - } else if (value && value.constructor === Date) { - return new Date(value.toISOString()); - } else if (value && typeof value === "object") { - var clone = Ember.create(value), property; - - for (property in value) { - if (value.hasOwnProperty(property) && typeof value[property] === "object") { - clone[property] = wrapObject(value[property]); - } - } - return clone; - } else { - return value; - } -} - Ember.Model.dataTypes = {}; Ember.Model.dataTypes[Date] = { deserialize: function(string) { - if(!string) { return null; } + if (!string) { return null; } return new Date(string); }, serialize: function (date) { - if(!date) { return null; } + if (!date) { return null; } return date.toISOString(); }, isEqual: function(obj1, obj2) { @@ -1093,25 +1322,54 @@ function deserialize(value, type) { } else if (type && Ember.Model.dataTypes[type]) { return Ember.Model.dataTypes[type].deserialize(value); } else { - return wrapObject(value); + return value; } } +function serialize(value, type) { + if (type && type.serialize) { + return type.serialize(value); + } else if (type && Ember.Model.dataTypes[type]) { + return Ember.Model.dataTypes[type].serialize(value); + } else { + return value; + } +} Ember.attr = function(type, options) { return Ember.computed(function(key, value) { var data = get(this, '_data'), dataKey = this.dataKey(key), dataValue = data && get(data, dataKey), - beingCreated = meta(this).proto === this; + beingCreated = meta(this).proto === this, + dirtyAttributes = get(this, '_dirtyAttributes'), + createdDirtyAttributes = false; + + if (!dirtyAttributes) { + dirtyAttributes = []; + createdDirtyAttributes = true; + } if (arguments.length === 2) { - if (beingCreated && !data) { - data = {}; - set(this, '_data', data); - data[dataKey] = value; + if (beingCreated) { + if (!data) { + data = {}; + set(this, '_data', data); + } + dataValue = data[dataKey] = value; } - return wrapObject(value); + + if (dataValue !== serialize(value, type)) { + dirtyAttributes.pushObject(key); + } else { + dirtyAttributes.removeObject(key); + } + + if (createdDirtyAttributes) { + set(this, '_dirtyAttributes', dirtyAttributes); + } + + return value; } return this.getAttr(key, deserialize(dataValue, type)); @@ -1132,6 +1390,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url).then(function(data) { self.didFind(record, id, data); + return record; }); }, @@ -1148,6 +1407,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url).then(function(data) { self.didFindAll(klass, records, data); + return records; }); }, @@ -1164,6 +1424,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url, params).then(function(data) { self.didFindQuery(klass, records, params, data); + return records; }); }, @@ -1180,6 +1441,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url, record.toJSON(), "POST").then(function(data) { self.didCreateRecord(record, data); + return record; }); }, @@ -1187,7 +1449,6 @@ Ember.RESTAdapter = Ember.Adapter.extend({ var rootKey = get(record.constructor, 'rootKey'), primaryKey = get(record.constructor, 'primaryKey'), dataToLoad = rootKey ? data[rootKey] : data; - record.load(dataToLoad[primaryKey], dataToLoad); record.didCreateRecord(); }, @@ -1199,6 +1460,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url, record.toJSON(), "PUT").then(function(data) { // TODO: Some APIs may or may not return data self.didSaveRecord(record, data); + return record; }); }, @@ -1220,15 +1482,15 @@ Ember.RESTAdapter = Ember.Adapter.extend({ record.didDeleteRecord(); }, - ajax: function(url, params, method) { - return this._ajax(url, params, method || "GET"); + ajax: function(url, params, method, settings) { + return this._ajax(url, params, (method || "GET"), settings); }, buildURL: function(klass, id) { var urlRoot = get(klass, 'url'); if (!urlRoot) { throw new Error('Ember.RESTAdapter requires a `url` property to be specified'); } - if (id) { + if (!Ember.isEmpty(id)) { return urlRoot + "/" + id + ".json"; } else { return urlRoot + ".json"; @@ -1243,8 +1505,10 @@ Ember.RESTAdapter = Ember.Adapter.extend({ }; }, - _ajax: function(url, params, method) { - var settings = this.ajaxSettings(url, method); + _ajax: function(url, params, method, settings) { + if (!settings) { + settings = this.ajaxSettings(url, method); + } return new Ember.RSVP.Promise(function(resolve, reject) { if (params) { @@ -1261,6 +1525,11 @@ Ember.RESTAdapter = Ember.Adapter.extend({ }; settings.error = function(jqXHR, textStatus, errorThrown) { + // https://github.com/ebryn/ember-model/issues/202 + if (jqXHR) { + jqXHR.then = null; + } + Ember.run(null, reject, jqXHR); }; @@ -1304,4 +1573,123 @@ Ember.loadPromise = function(target) { }; +})(); + +(function() { + +// This is a debug adapter for the Ember Extension, don't let the fact this is called an "adapter" confuse you. +// Most copied from: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/debug/debug_adapter.js + +if (!Ember.DataAdapter) { return; } + +var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore; + +var DebugAdapter = Ember.DataAdapter.extend({ + getFilters: function() { + return [ + { name: 'isNew', desc: 'New' }, + { name: 'isModified', desc: 'Modified' }, + { name: 'isClean', desc: 'Clean' } + ]; + }, + + detect: function(klass) { + return klass !== Ember.Model && Ember.Model.detect(klass); + }, + + columnsForType: function(type) { + var columns = [], count = 0, self = this; + Ember.A(get(type.proto(), 'attributes')).forEach(function(name, meta) { + if (count++ > self.attributeLimit) { return false; } + var desc = capitalize(underscore(name).replace('_', ' ')); + columns.push({ name: name, desc: desc }); + }); + return columns; + }, + + getRecords: function(type) { + var records = []; + type.forEachCachedRecord(function(record) { records.push(record); }); + return records; + }, + + getRecordColumnValues: function(record) { + var self = this, count = 0, + columnValues = { id: get(record, 'id') }; + + record.get('attributes').forEach(function(key) { + if (count++ > self.attributeLimit) { + return false; + } + var value = get(record, key); + columnValues[key] = value; + }); + return columnValues; + }, + + getRecordKeywords: function(record) { + var keywords = [], keys = Ember.A(['id']); + record.get('attributes').forEach(function(key) { + keys.push(key); + }); + keys.forEach(function(key) { + keywords.push(get(record, key)); + }); + return keywords; + }, + + getRecordFilterValues: function(record) { + return { + isNew: record.get('isNew'), + isModified: record.get('isDirty') && !record.get('isNew'), + isClean: !record.get('isDirty') + }; + }, + + getRecordColor: function(record) { + var color = 'black'; + if (record.get('isNew')) { + color = 'green'; + } else if (record.get('isDirty')) { + color = 'blue'; + } + return color; + }, + + observeRecord: function(record, recordUpdated) { + var releaseMethods = Ember.A(), self = this, + keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); + + record.get('attributes').forEach(function(key) { + keysToObserve.push(key); + }); + + keysToObserve.forEach(function(key) { + var handler = function() { + recordUpdated(self.wrapRecord(record)); + }; + Ember.addObserver(record, key, handler); + releaseMethods.push(function() { + Ember.removeObserver(record, key, handler); + }); + }); + + var release = function() { + releaseMethods.forEach(function(fn) { fn(); } ); + }; + + return release; + } +}); + +Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DebugAdapter); + } + }); +}); + })(); diff --git a/assets/scripts/vendor/ember.js b/assets/scripts/vendor/ember.js index 33248fc3..735ee4b1 100644 --- a/assets/scripts/vendor/ember.js +++ b/assets/scripts/vendor/ember.js @@ -1,7 +1,15 @@ -// Version: v1.0.0-rc.7-59-g4048275 -// Last commit: 4048275 (2013-08-25 00:57:38 -0400) +// ========================================================================== +// Project: Ember - JavaScript Application Framework +// Copyright: Copyright 2011-2013 Tilde Inc. and contributors +// Portions Copyright 2006-2011 Strobe Inc. +// Portions Copyright 2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license +// See https://raw.github.com/emberjs/ember.js/master/LICENSE +// ========================================================================== + // Version: 1.2.0 + (function() { /*global __fail__*/ @@ -55,7 +63,7 @@ Ember.assert = function(desc, test) { if (Ember.testing && !test) { // when testing, ensure test failures when assertions fail - throw new Error("Assertion Failed: " + desc); + throw new Ember.Error("Assertion Failed: " + desc); } }; @@ -107,7 +115,7 @@ Ember.deprecate = function(message, test) { if (arguments.length === 1) { test = false; } if (test) { return; } - if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Ember.Error(message); } var error; @@ -138,15 +146,21 @@ Ember.deprecate = function(message, test) { /** + Alias an old, deprecated method with its new counterpart. + Display a deprecation warning with the provided message and a stack trace - (Chrome and Firefox only) when the wrapped method is called. + (Chrome and Firefox only) when the assigned method is called. Ember build tools will not remove calls to `Ember.deprecateFunc()`, though no warnings will be shown in production. + ```javascript + Ember.oldMethod = Ember.deprecateFunc("Please use the new, updated method", Ember.newMethod); + ``` + @method deprecateFunc @param {String} message A description of the deprecation. - @param {Function} func The function to be deprecated. + @param {Function} func The new function called to replace its deprecated counterpart. @return {Function} a new function that wrapped the original function with a deprecation warning */ Ember.deprecateFunc = function(message, func) { @@ -156,12 +170,32 @@ Ember.deprecateFunc = function(message, func) { }; }; + +// Inform the developer about the Ember Inspector if not installed. +if (!Ember.testing) { + if (typeof window !== 'undefined' && window.chrome && window.addEventListener) { + window.addEventListener("load", function() { + if (document.body && document.body.dataset && !document.body.dataset.emberExtension) { + Ember.debug('For more advanced debugging, install the Ember Inspector from https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'); + } + }, false); + } +} + })(); -// Version: v1.0.0-rc.7-59-g4048275 -// Last commit: 4048275 (2013-08-25 00:57:38 -0400) +// ========================================================================== +// Project: Ember - JavaScript Application Framework +// Copyright: Copyright 2011-2013 Tilde Inc. and contributors +// Portions Copyright 2006-2011 Strobe Inc. +// Portions Copyright 2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license +// See https://raw.github.com/emberjs/ember.js/master/LICENSE +// ========================================================================== + // Version: 1.2.0 + (function() { var define, requireModule; @@ -225,7 +259,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.7 + @version 1.2.0 */ if ('undefined' === typeof Ember) { @@ -252,10 +286,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.7' + @default '1.2.0' @final */ -Ember.VERSION = '1.0.0-rc.7'; +Ember.VERSION = '1.2.0'; /** Standard environmental variables. You can define these in a global `ENV` @@ -280,6 +314,45 @@ Ember.ENV = Ember.ENV || ENV; Ember.config = Ember.config || {}; +/** + Hash of enabled Canary features. Add to before creating your application. + + You can also define `ENV.FEATURES` if you need to enable features flagged at runtime. + + @property FEATURES + @type Hash +*/ + +Ember.FEATURES = Ember.ENV.FEATURES || {}; + +/** + Test that a feature is enabled. Parsed by Ember's build tools to leave + experimental features out of beta/stable builds. + + You can define the following configuration options: + + * `ENV.ENABLE_ALL_FEATURES` - force all features to be enabled. + * `ENV.ENABLE_OPTIONAL_FEATURES` - enable any features that have not been explicitly + enabled/disabled. + + @method isEnabled + @param {string} feature +*/ + +Ember.FEATURES.isEnabled = function(feature) { + var featureValue = Ember.FEATURES[feature]; + + if (Ember.ENV.ENABLE_ALL_FEATURES) { + return true; + } else if (featureValue === true || featureValue === false || featureValue === undefined) { + return featureValue; + } else if (Ember.ENV.ENABLE_OPTIONAL_FEATURES) { + return true; + } else { + return false; + } +}; + // .......................................................... // BOOTSTRAP // @@ -332,7 +405,7 @@ Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPE Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; /** - Empty function. Useful for some operations. + Empty function. Useful for some operations. Always returns `this`. @method K @private @@ -361,104 +434,21 @@ if ('undefined' === typeof Ember.deprecateFunc) { */ Ember.uuid = 0; -// .......................................................... -// LOGGER -// - -function consoleMethod(name) { - var consoleObj; - if (imports.console) { - consoleObj = imports.console; - } else if (typeof console !== 'undefined') { - consoleObj = console; - } - - var method = typeof consoleObj === 'object' ? consoleObj[name] : null; - - if (method) { - // Older IE doesn't support apply, but Chrome needs it - if (method.apply) { - return function() { - method.apply(consoleObj, arguments); - }; - } else { - return function() { - var message = Array.prototype.join.call(arguments, ', '); - method(message); - }; - } - } -} - -function assertPolyfill(test, message) { - if (!test) { - try { - // attempt to preserve the stack - throw new Error("assertion failed: " + message); - } catch(error) { - setTimeout(function() { - throw error; - }, 0); - } - } -} - /** - Inside Ember-Metal, simply uses the methods from `imports.console`. - Override this to provide more robust logging functionality. + Merge the contents of two objects together into the first object. - @class Logger - @namespace Ember -*/ -Ember.Logger = { - log: consoleMethod('log') || Ember.K, - warn: consoleMethod('warn') || Ember.K, - error: consoleMethod('error') || Ember.K, - info: consoleMethod('info') || Ember.K, - debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, - assert: consoleMethod('assert') || assertPolyfill -}; + ```javascript + Ember.merge({first: 'Tom'}, {last: 'Dale'}); // {first: 'Tom', last: 'Dale'} + var a = {first: 'Yehuda'}, b = {last: 'Katz'}; + Ember.merge(a, b); // a == {first: 'Yehuda', last: 'Katz'}, b == {last: 'Katz'} + ``` - -// .......................................................... -// ERROR HANDLING -// - -/** - A function may be assigned to `Ember.onerror` to be called when Ember - internals encounter an error. This is useful for specialized error handling - and reporting code. - - @event onerror + @method merge @for Ember - @param {Exception} error the error object + @param {Object} original The object to merge into + @param {Object} updates The object to copy properties from + @return {Object} */ -Ember.onerror = null; - -/** - @private - - Wrap code block in a try/catch if `Ember.onerror` is set. - - @method handleErrors - @for Ember - @param {Function} func - @param [context] -*/ -Ember.handleErrors = function(func, context) { - // Unfortunately in some browsers we lose the backtrace if we rethrow the existing error, - // so in the event that we don't have an `onerror` handler we don't wrap in a try/catch - if ('function' === typeof Ember.onerror) { - try { - return func.call(context || this); - } catch (error) { - Ember.onerror(error); - } - } else { - return func.call(context || this); - } -}; - Ember.merge = function(original, updates) { for (var prop in updates) { if (!updates.hasOwnProperty(prop)) { continue; } @@ -547,7 +537,7 @@ var platform = Ember.platform = {}; */ Ember.create = Object.create; -// IE8 has Object.create but it couldn't treat property descripters. +// IE8 has Object.create but it couldn't treat property descriptors. if (Ember.create) { if (Ember.create({a: 1}, {a: {value: 2}}).a !== 2) { Ember.create = null; @@ -761,6 +751,12 @@ var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.index return -1; }; +/** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills +*/ Ember.ArrayPolyfills = { map: arrayMap, forEach: arrayForEach, @@ -785,11 +781,93 @@ if (Ember.SHIM_ES5) { +(function() { +var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + +/** + A subclass of the JavaScript Error object for use in Ember. + + @class Error + @namespace Ember + @extends Error + @constructor +*/ +Ember.Error = function() { + var tmp = Error.apply(this, arguments); + + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } +}; + +Ember.Error.prototype = Ember.create(Error.prototype); + +// .......................................................... +// ERROR HANDLING +// + +/** + A function may be assigned to `Ember.onerror` to be called when Ember + internals encounter an error. This is useful for specialized error handling + and reporting code. + + ```javascript + Ember.onerror = function(error) { + Em.$.ajax('/report-error', 'POST', { + stack: error.stack, + otherInformation: 'whatever app state you want to provide' + }); + }; + ``` + + @event onerror + @for Ember + @param {Exception} error the error object +*/ +Ember.onerror = null; + +/** + @private + + Wrap code block in a try/catch if `Ember.onerror` is set. + + @method handleErrors + @for Ember + @param {Function} func + @param [context] +*/ +Ember.handleErrors = function(func, context) { + // Unfortunately in some browsers we lose the backtrace if we rethrow the existing error, + // so in the event that we don't have an `onerror` handler we don't wrap in a try/catch + if ('function' === typeof Ember.onerror) { + try { + return func.call(context || this); + } catch (error) { + Ember.onerror(error); + } + } else { + return func.call(context || this); + } +}; + +})(); + + + (function() { /** @module ember-metal */ +/** + @private + + Prefix used for guids through out Ember. + +*/ +Ember.GUID_PREFIX = 'ember'; + var o_defineProperty = Ember.platform.defineProperty, o_create = Ember.create, @@ -844,13 +922,13 @@ var GUID_DESC = { @return {String} the guid */ Ember.generateGuid = function generateGuid(obj, prefix) { - if (!prefix) prefix = 'ember'; + if (!prefix) prefix = Ember.GUID_PREFIX; var ret = (prefix + (uuid++)); if (obj) { GUID_DESC.value = ret; o_defineProperty(obj, GUID_KEY, GUID_DESC); } - return ret ; + return ret; }; /** @@ -1174,10 +1252,18 @@ function canInvoke(obj, methodName) { /** Checks to see if the `methodName` exists on the `obj`. + ```javascript + var foo = {bar: Ember.K, baz: null}; + Ember.canInvoke(foo, 'bar'); // true + Ember.canInvoke(foo, 'baz'); // false + Ember.canInvoke(foo, 'bat'); // false + ``` + @method canInvoke @for Ember @param {Object} obj The object to check for the method @param {String} methodName The method name to check for + @return {Boolean} */ Ember.canInvoke = canInvoke; @@ -1185,6 +1271,13 @@ Ember.canInvoke = canInvoke; Checks to see if the `methodName` exists on the `obj`, and if it does, invokes it with the arguments passed. + ```javascript + var d = new Date('03/15/2013'); + Ember.tryInvoke(d, 'getTime'); // 1363320000000 + Ember.tryInvoke(d, 'setFullYear', [2014]); // 1394856000000 + Ember.tryInvoke(d, 'noSuchMethod', [2014]); // undefined + ``` + @method tryInvoke @for Ember @param {Object} obj The object to check for the method @@ -1216,6 +1309,17 @@ var needsFinallyFix = (function() { Provides try { } finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + someResource.lock(); + runCallback(); // May throw error. + }; + var finalizer = function() { + someResource.unlock(); + }; + Ember.tryFinally(tryable, finalizer); + ``` + @method tryFinally @for Ember @param {Function} tryable The function to run the try callback @@ -1266,6 +1370,30 @@ if (needsFinallyFix) { Provides try { } catch finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + for (i=0, l=listeners.length; i size ? size : ends; + if (count <= 0) { count = 0; } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + }, + replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = concat.apply([idx, amt], objects); - return array.splice.apply(array, args); + return utils._replace(array, idx, amt, objects); } }, @@ -1747,7 +1931,7 @@ var normalizeTuple = Ember.normalizeTuple = function(target, path) { } // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); + if (!path || path.length===0) throw new Ember.Error('Invalid Path'); return [ target, path ]; }; @@ -1788,7 +1972,6 @@ Ember.getWithDefault = function(root, key, defaultValue) { Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); })(); @@ -2016,7 +2199,7 @@ function suspendListener(obj, eventName, target, method, callback) { Suspends multiple listeners during a callback. - + @method suspendListeners @for Ember @param obj @@ -2032,6 +2215,7 @@ function suspendListeners(obj, eventNames, target, method, callback) { } var suspendedActions = [], + actionsList = [], eventName, actions, i, l; for (i=0, l=eventNames.length; i 0 || keyName === 'length', proto = m.proto, @@ -2308,7 +2493,8 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { dependentKeysWillChange(obj, keyName, m); chainsWillChange(obj, keyName, m); notifyBeforeObservers(obj, keyName); -}; +} +Ember.propertyWillChange = propertyWillChange; /** This function is called just after an object property has changed. @@ -2316,7 +2502,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { Normally you will not need to call this method directly but if for some reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyWilLChange()` which you should call just + manually along with `Ember.propertyWillChange()` which you should call just before the property value changes. @method propertyDidChange @@ -2325,7 +2511,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { @param {String} keyName The property key (or path) that will change. @return {void} */ -var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { +function propertyDidChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -2338,9 +2524,10 @@ var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { if (!watching && keyName !== 'length') { return; } dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); notifyObservers(obj, keyName); -}; +} +Ember.propertyDidChange = propertyDidChange; var WILL_SEEN, DID_SEEN; @@ -2381,35 +2568,47 @@ function iterDeps(method, obj, depKey, seen, meta) { } } -var chainsWillChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - nodes = nodes.slice(); - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); +function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; } -}; -var chainsDidChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var nodes = m.chainWatchers[keyName], + events = [], + i, l; - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - nodes = nodes.slice(); - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].didChange(arg); + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); } -}; + + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i+1]); + } +} + +function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = suppressEvents ? null : [], + i, l; + + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } + + if (suppressEvents) { + return; + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i+1]); + } +} Ember.overrideChains = function(obj, keyName, m) { chainsDidChange(obj, keyName, m, true); @@ -2418,21 +2617,27 @@ Ember.overrideChains = function(obj, keyName, m) { /** @method beginPropertyChanges @chainable + @private */ -var beginPropertyChanges = Ember.beginPropertyChanges = function() { +function beginPropertyChanges() { deferred++; -}; +} + +Ember.beginPropertyChanges = beginPropertyChanges; /** @method endPropertyChanges + @private */ -var endPropertyChanges = Ember.endPropertyChanges = function() { +function endPropertyChanges() { deferred--; if (deferred<=0) { beforeObserverSet.clear(); observerSet.flush(); } -}; +} + +Ember.endPropertyChanges = endPropertyChanges; /** Make a series of property changes together in an @@ -2454,7 +2659,7 @@ Ember.changeProperties = function(cb, binding) { tryFinally(cb, endPropertyChanges, binding); }; -var notifyBeforeObservers = function(obj, keyName) { +function notifyBeforeObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':before', listeners, diff; @@ -2465,9 +2670,9 @@ var notifyBeforeObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} -var notifyObservers = function(obj, keyName) { +function notifyObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':change', listeners; @@ -2477,7 +2682,7 @@ var notifyObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} })(); @@ -2496,7 +2701,7 @@ var META_KEY = Ember.META_KEY, /** Sets the value of a property on an object, respecting computed properties and notifying observers and other listeners of the change. If the - property is not defined but the object implements the `unknownProperty` + property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. If you plan to run on IE8 and older browsers then you should use this @@ -2506,7 +2711,7 @@ var META_KEY = Ember.META_KEY, On all newer browsers, you only need to use this method to set properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this + to respect the `setUnknownProperty` handler. Otherwise you can ignore this method. @method set @@ -2595,19 +2800,18 @@ function setPath(root, path, value, tolerant) { } if (!keyName || keyName.length === 0) { - throw new Error('You passed an empty path'); + throw new Ember.Error('You passed an empty path'); } if (!root) { if (tolerant) { return; } - else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } + else { throw new Ember.Error('Object in path '+path+' could not be found or was destroyed.'); } } return set(root, keyName, value); } Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); /** Error-tolerant form of `Ember.set`. Will not blow up if any part of the @@ -2625,7 +2829,6 @@ Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now support Ember.trySet = function(root, path, value) { return set(root, path, value, true); }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); })(); @@ -2837,14 +3040,14 @@ Map.create = function() { Map.prototype = { /** This property will change as the number of objects in the map changes. - + @property length @type number @default 0 */ length: 0, - - + + /** Retrieve the value associated with a given key. @@ -3010,6 +3213,142 @@ MapWithDefault.prototype.copy = function() { +(function() { +function consoleMethod(name) { + var consoleObj; + if (Ember.imports.console) { + consoleObj = Ember.imports.console; + } else if (typeof console !== 'undefined') { + consoleObj = console; + } + + var method = typeof consoleObj === 'object' ? consoleObj[name] : null; + + if (method) { + // Older IE doesn't support apply, but Chrome needs it + if (method.apply) { + return function() { + method.apply(consoleObj, arguments); + }; + } else { + return function() { + var message = Array.prototype.join.call(arguments, ', '); + method(message); + }; + } + } +} + +function assertPolyfill(test, message) { + if (!test) { + try { + // attempt to preserve the stack + throw new Ember.Error("assertion failed: " + message); + } catch(error) { + setTimeout(function() { + throw error; + }, 0); + } + } +} + +/** + Inside Ember-Metal, simply uses the methods from `imports.console`. + Override this to provide more robust logging functionality. + + @class Logger + @namespace Ember +*/ +Ember.Logger = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method log + @for Ember.Logger + @param {*} arguments + */ + log: consoleMethod('log') || Ember.K, + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.warn('Something happened!'); // "Something happened!" will be printed to the console with a warning icon. + ``` + + @method warn + @for Ember.Logger + @param {*} arguments + */ + warn: consoleMethod('warn') || Ember.K, + /** + Prints the arguments to the console with an error icon, red text and a stack race. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.error('Danger! Danger!'); // "Danger! Danger!" will be printed to the console in red text. + ``` + + @method error + @for Ember.Logger + @param {*} arguments + */ + error: consoleMethod('error') || Ember.K, + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method info + @for Ember.Logger + @param {*} arguments + */ + info: consoleMethod('info') || Ember.K, + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method debug + @for Ember.Logger + @param {*} arguments + */ + debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + /** + + If the value passed into Ember.Logger.assert is not truthy it will throw an error with a stack trace. + + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ + assert: consoleMethod('assert') || assertPolyfill +}; + + +})(); + + + (function() { /** @module ember-metal @@ -3165,6 +3504,47 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { +(function() { +var get = Ember.get; + +/** + To get multiple properties at once, call `Ember.getProperties` + with an object followed by a list of strings or an array: + + ```javascript + Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param obj + @param {String...|Array} list of keys to get + @return {Hash} +*/ +Ember.getProperties = function(obj) { + var ret = {}, + propertyNames = arguments, + i = 1; + + if (arguments.length === 2 && Ember.typeOf(arguments[1]) === 'array') { + i = 0; + propertyNames = arguments[1]; + } + for(var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = get(obj, propertyNames[i]); + } + return ret; +}; + +})(); + + + (function() { var changeProperties = Ember.changeProperties, set = Ember.set; @@ -3174,6 +3554,14 @@ var changeProperties = Ember.changeProperties, a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered. + ```javascript + anObject.setProperties({ + firstName: "Stanley", + lastName: "Stuart", + age: "21" + }) + ``` + @method setProperties @param self @param {Object} hash @@ -3263,8 +3651,6 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange, FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { @@ -3498,42 +3884,50 @@ ChainNodePrototype.unchain = function(key, path) { }; -ChainNodePrototype.willChange = function() { +ChainNodePrototype.willChange = function(events) { var chains = this._chains; if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].willChange(); + chains[key].willChange(events); } } - if (this._parent) { this._parent.chainWillChange(this, this._key, 1); } + if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } }; -ChainNodePrototype.chainWillChange = function(chain, path, depth) { +ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainWillChange(this, path, depth+1); + this._parent.chainWillChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyWillChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.chainDidChange = function(chain, path, depth) { +ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainDidChange(this, path, depth+1); + this._parent.chainDidChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyDidChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.didChange = function(suppressEvent) { +ChainNodePrototype.didChange = function(events) { // invalidate my own value first. if (this._watching) { var obj = this._parent.value(); @@ -3555,14 +3949,15 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].didChange(suppressEvent); + chains[key].didChange(events); } } - if (suppressEvent) { return; } + // if no events are passed in then we only care about the above wiring update + if (events === null) { return; } // and finally tell parent about my path changing... - if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } + if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } }; Ember.finishChains = function(obj) { @@ -3571,9 +3966,20 @@ Ember.finishChains = function(obj) { if (chains.value() !== obj) { m.chains = chains = chains.copy(obj); } - chains.didChange(true); + chains.didChange(null); } }; + +})(); + + + +(function() { +/** + @module ember-metal +*/ + + })(); @@ -3641,6 +4047,7 @@ var metaFor = Ember.meta, // utils.js generateGuid = Ember.generateGuid, IS_PATH = /[\.\*]/; + // returns true if the passed path is just a keyName function isKeyName(path) { return path==='*' || !IS_PATH.test(path); @@ -3660,15 +4067,17 @@ function isKeyName(path) { @param obj @param {String} keyName */ -Ember.watch = function(obj, keyPath) { +Ember.watch = function(obj, _keyPath) { // can't watch length on Array - it is special... - if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (isKeyName(keyPath)) { - watchKey(obj, keyPath); - } else { - watchPath(obj, keyPath); - } + + if (isKeyName(_keyPath)) { + watchKey(obj, _keyPath); + } else { + watchPath(obj, _keyPath); + } + }; Ember.isWatching = function isWatching(obj, key) { @@ -3678,15 +4087,17 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, keyPath) { +Ember.unwatch = function(obj, _keyPath) { // can't watch length on Array - it is special... - if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (isKeyName(keyPath)) { - unwatchKey(obj, keyPath); - } else { - unwatchPath(obj, keyPath); - } + + if (isKeyName(_keyPath)) { + unwatchKey(obj, _keyPath); + } else { + unwatchPath(obj, _keyPath); + } + }; /** @@ -3705,7 +4116,7 @@ Ember.rewatch = function(obj) { // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - generateGuid(obj, 'ember'); + generateGuid(obj); } // make sure any chained watchers update. @@ -3778,6 +4189,7 @@ var get = Ember.get, watch = Ember.watch, unwatch = Ember.unwatch; + // .......................................................... // DEPENDENT KEYS // @@ -3854,6 +4266,81 @@ function removeDependentKeys(desc, obj, keyName, meta) { // /** + A computed property transforms an objects function into a property. + + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property is dependent on. This will force the cached + result to be recomputed if the dependencies are modified. + + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the properties + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as it's dependencies have not been changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. + + ```javascript + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); + + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); + + tom.get('fullName') // "Tom Dale" + ``` + + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. + + ```javascript + + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(" "); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + person.set('fullName', "Peter Wagenet"); + person.get('firstName') // Peter + person.get('lastName') // Wagenet + ``` + @class ComputedProperty @namespace Ember @extends Ember.Descriptor @@ -3872,7 +4359,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/* +/** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -3956,9 +4443,14 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { @chainable */ ComputedPropertyPrototype.property = function() { + var addArg; + + var args = []; for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); + + args.push(arguments[i]); + } this._dependentKeys = args; return this; @@ -4014,11 +4506,37 @@ ComputedPropertyPrototype.didChange = function(obj, keyName) { function finishChains(chainNodes) { for (var i=0, l=chainNodes.length; i 2) { - args = slice.call(arguments, 2); - - fn = function() { - method.apply(target, args); - }; - } else { - fn = function() { - method.call(target); - }; + function fn() { + method.apply(target, args); } // find position to insert - TODO: binary search @@ -4994,7 +5840,7 @@ define("backburner", throttle: function(target, method /* , args, wait */) { var self = this, args = arguments, - wait = pop.call(args), + wait = parseInt(pop.call(args), 10), throttler; for (var i = 0, l = throttlers.length; i < l; i++) { @@ -5029,13 +5875,14 @@ define("backburner", index, debouncee; - if (typeof immediate === "number") { + if (typeof immediate === "number" || typeof immediate === "string") { wait = immediate; immediate = false; } else { wait = pop.call(args); } + wait = parseInt(wait, 10); // Remove debouncee index = findDebouncee(target, method); @@ -5045,7 +5892,7 @@ define("backburner", clearTimeout(debouncee[2]); } - var timer = window.setTimeout(function() { + var timer = global.setTimeout(function() { if (!immediate) { self.run.apply(self, args); } @@ -5114,8 +5961,8 @@ define("backburner", function createAutorun(backburner) { backburner.begin(); autorun = global.setTimeout(function() { - backburner.end(); autorun = null; + backburner.end(); }); } @@ -5165,6 +6012,7 @@ define("backburner", __exports__.Backburner = Backburner; }); + })(); @@ -5762,7 +6610,7 @@ Binding.prototype = { This copies the Binding so it can be connected to another object. @method copy - @return {Ember.Binding} + @return {Ember.Binding} `this` */ copy: function () { var copy = new Binding(this._to, this._from); @@ -5970,7 +6818,7 @@ function mixinProperties(to, from) { mixinProperties(Binding, { - /** + /* See `Ember.Binding.from`. @method from @@ -5981,7 +6829,7 @@ mixinProperties(Binding, { return binding.from.apply(binding, arguments); }, - /** + /* See `Ember.Binding.to`. @method to @@ -6006,6 +6854,7 @@ mixinProperties(Binding, { @param {Boolean} [flag] (Optional) passing nothing here will make the binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the binding two way again. + @return {Ember.Binding} `this` */ oneWay: function(from, flag) { var C = this, binding = new C(null, from); @@ -6337,15 +7186,14 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats, m descs[key] = value; values[key] = undefined; } else { - // impl super if needed... - if (isMethod(value)) { - value = giveMethodSuper(base, key, value, values, descs); - } else if ((concats && a_indexOf.call(concats, key) >= 0) || + if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties' || key === 'mergedProperties') { value = applyConcatenatedProperties(base, key, value, values); } else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { value = applyMergedProperties(base, key, value, values); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); } descs[key] = undefined; @@ -6370,6 +7218,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) { if (props) { meta = Ember.meta(base); + if (base.willMergeMixin) { base.willMergeMixin(props); } concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); mergings = concatenatedMixinProperties('mergedProperties', props, values, base); @@ -6543,7 +7392,7 @@ Ember.mixin = function(obj) { // Mix mixins into classes by passing them as the first arguments to // .extend. App.CommentView = Ember.View.extend(App.Editable, { - template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') + template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') }); commentView = App.CommentView.create(); @@ -6553,6 +7402,31 @@ Ember.mixin = function(obj) { Note that Mixins are created with `Ember.Mixin.create`, not `Ember.Mixin.extend`. + Note that mixins extend a constructor's prototype so arrays and object literals + defined as properties will be shared amongst objects that implement the mixin. + If you want to define an property in a mixin that is not shared, you can define + it either as a computed property or have it be created on initialization of the object. + + ```javascript + //filters array will be shared amongst any object implementing mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.A() + }); + + //filters will be a separate array for every object implementing the mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.computed(function(){return Ember.A();}) + }); + + //filters will be created as a separate array during the object's initialization + App.Filterable = Ember.Mixin.create({ + init: function() { + this._super(); + this.set("filters", Ember.A()); + } + }); + ``` + @class Mixin @namespace Ember */ @@ -6755,11 +7629,10 @@ Alias.prototype = new Ember.Descriptor(); @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead */ Ember.alias = function(methodName) { + Ember.deprecate("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead."); return new Alias(methodName); }; -Ember.alias = Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); - /** Makes a method available via an additional name. @@ -6788,25 +7661,68 @@ Ember.aliasMethod = function(methodName) { // /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer('value', function() { + // Executes whenever the "value" property changes + }) + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. + + Also available as `Function.prototype.observes` if prototype extensions are + enabled. + @method observer @for Ember - @param {Function} func @param {String} propertyNames* + @param {Function} func @return func */ -Ember.observer = function(func) { - var paths = a_slice.call(arguments, 1); +Ember.observer = function() { + var func = a_slice.call(arguments, -1)[0]; + var paths = a_slice.call(arguments, 0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + paths = a_slice.call(arguments, 1); + } + + if (typeof func !== "function") { + throw new Ember.Error("Ember.observer called without a function"); + } + func.__ember_observes__ = paths; return func; }; -// If observers ever become asynchronous, Ember.immediateObserver -// must remain synchronous. /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver('value', function() { + // Executes whenever the "value" property changes + }) + }); + ``` + + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. + + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. + @method immediateObserver @for Ember - @param {Function} func @param {String} propertyNames* + @param {Function} func @return func */ Ember.immediateObserver = function() { @@ -6830,32 +7746,52 @@ Ember.immediateObserver = function() { ```javascript App.PersonView = Ember.View.extend({ + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], - valueWillChange: function (obj, keyName) { + + valueWillChange: Ember.beforeObserver('content.value', function(obj, keyName) { this.changingFrom = obj.get(keyName); - }.observesBefore('content.value'), - valueDidChange: function(obj, keyName) { + }), + + valueDidChange: Ember.observer('content.value', function(obj, keyName) { // only run if updating a value already in the DOM if (this.get('state') === 'inDOM') { - var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; - // logic + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; + // logic } - }.observes('content.value'), - friendsDidChange: function(obj, keyName) { + }), + + friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { // some logic // obj.get(keyName) returns friends array - }.observes('friends.@each.name') + }) }); ``` + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. + @method beforeObserver @for Ember - @param {Function} func @param {String} propertyNames* + @param {Function} func @return func */ -Ember.beforeObserver = function(func) { - var paths = a_slice.call(arguments, 1); +Ember.beforeObserver = function() { + var func = a_slice.call(arguments, -1)[0]; + var paths = a_slice.call(arguments, 0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + paths = a_slice.call(arguments, 1); + } + + if (typeof func !== "function") { + throw new Ember.Error("Ember.beforeObserver called without a function"); + } + func.__ember_observesBefore__ = paths; return func; }; @@ -6864,6 +7800,55 @@ Ember.beforeObserver = function(func) { +(function() { +// Provides a way to register library versions with ember. +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.EnumerableUtils.indexOf; + +Ember.libraries = function() { + var libraries = []; + var coreLibIndex = 0; + + var getLibrary = function(name) { + for (var i = 0; i < libraries.length; i++) { + if (libraries[i].name === name) { + return libraries[i]; + } + } + }; + + libraries.register = function(name, version) { + if (!getLibrary(name)) { + libraries.push({name: name, version: version}); + } + }; + + libraries.registerCoreLibrary = function(name, version) { + if (!getLibrary(name)) { + libraries.splice(coreLibIndex++, 0, {name: name, version: version}); + } + }; + + libraries.deRegister = function(name) { + var lib = getLibrary(name); + if (lib) libraries.splice(indexOf(libraries, lib), 1); + }; + + libraries.each = function (callback) { + forEach(libraries, function(lib) { + callback(lib.name, lib.version); + }); + }; + + return libraries; +}(); + +Ember.libraries.registerCoreLibrary('Ember', Ember.VERSION); + +})(); + + + (function() { /** Ember Metal @@ -6931,6 +7916,7 @@ define("rsvp/async", var browserGlobal = (typeof window !== 'undefined') ? window : {}; var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var async; + var local = (typeof global !== 'undefined') ? global : this; // old node function useNextTick() { @@ -6981,7 +7967,7 @@ define("rsvp/async", function useSetTimeout() { return function(callback, arg) { - setTimeout(function() { + local.setTimeout(function() { callback(arg); }, 1); }; @@ -7364,6 +8350,10 @@ define("rsvp/promise", }); return thenPromise; + }, + + fail: function(fail) { + return this.then(null, fail); } }; @@ -7466,19 +8456,36 @@ define("rsvp/resolve", __exports__.resolve = resolve; }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + var local = (typeof global === "undefined") ? this : global; + + function rethrow(reason) { + local.setTimeout(function() { + throw reason; + }); + throw reason; + } + + + __exports__.rethrow = rethrow; + }); define("rsvp", - ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { "use strict"; var EventTarget = __dependency1__.EventTarget; var Promise = __dependency2__.Promise; var denodeify = __dependency3__.denodeify; var all = __dependency4__.all; var hash = __dependency5__.hash; - var defer = __dependency6__.defer; - var config = __dependency7__.config; - var resolve = __dependency8__.resolve; - var reject = __dependency9__.reject; + var rethrow = __dependency6__.rethrow; + var defer = __dependency7__.defer; + var config = __dependency8__.config; + var resolve = __dependency9__.resolve; + var reject = __dependency10__.reject; function configure(name, value) { config[name] = value; @@ -7489,25 +8496,35 @@ define("rsvp", __exports__.EventTarget = EventTarget; __exports__.all = all; __exports__.hash = hash; + __exports__.rethrow = rethrow; __exports__.defer = defer; __exports__.denodeify = denodeify; __exports__.configure = configure; __exports__.resolve = resolve; __exports__.reject = reject; }); - })(); (function() { +/** +@private +Public api for the container is still in flux. +The public api, specified on the application namespace should be considered the stable api. +// @module container +*/ + +/* + Flag to enable/disable model factory injections (disabled by default) + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); +*/ +Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; + define("container", [], function() { - /** - A safe and simple inheriting object. - - @class InheritingDict - */ + // A safe and simple inheriting object. function InheritingDict(parent) { this.parent = parent; this.dict = {}; @@ -7580,7 +8597,7 @@ define("container", @method has @param {String} key - @returns {Boolean} + @return {Boolean} */ has: function(key) { var dict = this.dict; @@ -7614,11 +8631,10 @@ define("container", } }; - /** - A lightweight container that helps to assemble and decouple components. - @class Container - */ + // A lightweight container that helps to assemble and decouple components. + // Public api for the container is still in flux. + // The public api, specified on the application namespace should be considered the stable api. function Container(parent) { this.parent = parent; this.children = []; @@ -7707,7 +8723,7 @@ define("container", to correctly inherit from the current container. @method child - @returns {Container} + @return {Container} */ child: function() { var container = new Container(this); @@ -7743,25 +8759,25 @@ define("container", ``` @method register - @param {String} type - @param {String} name + @param {String} fullName @param {Function} factory @param {Object} options */ - register: function(type, name, factory, options) { - var fullName; + register: function(fullName, factory, options) { + if (fullName.indexOf(':') === -1) { + throw new TypeError("malformed fullName, expected: `type:name` got: " + fullName + ""); + } - if (type.indexOf(':') !== -1) { - options = factory; - factory = name; - fullName = type; - } else { - Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false); - fullName = type + ":" + name; + if (factory === undefined) { + throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); } var normalizedName = this.normalize(fullName); + if (this.cache.has(normalizedName)) { + throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); + } + this.registry.set(normalizedName, factory); this._options.set(normalizedName, options || {}); }, @@ -7777,6 +8793,7 @@ define("container", container.unregister('model:user') container.lookup('model:user') === undefined //=> true + ``` @method unregister @param {String} fullName @@ -7786,6 +8803,7 @@ define("container", this.registry.remove(normalizedName); this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); this._options.remove(normalizedName); }, @@ -7819,7 +8837,7 @@ define("container", @method resolve @param {String} fullName - @returns {Function} fullName's factory + @return {Function} fullName's factory */ resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); @@ -7850,6 +8868,17 @@ define("container", return fullName; }, + /** + @method makeToString + + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, + /** Given a fullName return a corresponding instance. @@ -7900,7 +8929,7 @@ define("container", var value = instantiate(this, fullName); - if (!value) { return; } + if (value === undefined) { return; } if (isSingleton(this, fullName) && options.singleton !== false) { this.cache.set(fullName, value); @@ -7978,7 +9007,7 @@ define("container", this.optionsForType(type, options); }, - /* + /** @private Used only via `injection`. @@ -8020,7 +9049,7 @@ define("container", addTypeInjection(this.typeInjections, type, property, fullName); }, - /* + /** Defines injection rules. These rules are used to inject dependencies onto objects when they @@ -8028,8 +9057,8 @@ define("container", Two forms of injections are possible: - * Injecting one fullName on another fullName - * Injecting one fullName on a type + * Injecting one fullName on another fullName + * Injecting one fullName on a type Example: @@ -8075,7 +9104,7 @@ define("container", }, - /* + /** @private Used only via `factoryInjection`. @@ -8112,7 +9141,7 @@ define("container", addTypeInjection(this.factoryTypeInjections, type, property, fullName); }, - /* + /** Defines factory injection rules. Similar to regular injection rules, but are run against factories, via @@ -8123,8 +9152,8 @@ define("container", Two forms of injections are possible: - * Injecting one fullName on another fullName - * Injecting one fullName on a type + * Injecting one fullName on another fullName + * Injecting one fullName on a type Example: @@ -8226,7 +9255,7 @@ define("container", injection = injections[i]; lookup = container.lookup(injection.fullName); - if (lookup) { + if (lookup !== undefined) { hash[injection.property] = lookup; } else { throw new Error('Attempting to inject an unknown injection: `' + injection.fullName + '`'); @@ -8256,20 +9285,27 @@ define("container", var factory = container.resolve(name); var injectedFactory; var cache = container.factoryCache; + var type = fullName.split(":")[0]; - if (!factory) { return; } + if (factory === undefined) { return; } if (cache.has(fullName)) { return cache.get(fullName); } - if (typeof factory.extend !== 'function') { + if (!factory || typeof factory.extend !== 'function' || (!Ember.MODEL_FACTORY_INJECTIONS && type === 'model')) { // TODO: think about a 'safe' merge style extension // for now just fallback to create time injection return factory; } else { - injectedFactory = factory.extend(injectionsFor(container, fullName)); - injectedFactory.reopenClass(factoryInjectionsFor(container, fullName)); + + var injections = injectionsFor(container, fullName); + var factoryInjections = factoryInjectionsFor(container, fullName); + + factoryInjections._toString = container.makeToString(factory, fullName); + + injectedFactory = factory.extend(injections); + injectedFactory.reopenClass(factoryInjections); cache.set(fullName, injectedFactory); @@ -8633,61 +9669,43 @@ Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ Ember.keys = Object.keys; if (!Ember.keys || Ember.create.isSimulated) { - Ember.keys = function(obj) { - var ret = []; - for(var key in obj) { - // Prevents browsers that don't respect non-enumerability from - // copying internal Ember properties - if (key.substring(0,2) === '__') continue; - if (key === '_super') continue; + var prototypeProperties = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'valueOf', + 'toLocaleString', + 'toString' + ], + pushPropertyName = function(obj, array, key) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') return; + if (key === '_super') return; + if (indexOf(array, key) >= 0) return; + if (!obj.hasOwnProperty(key)) return; - if (obj.hasOwnProperty(key)) { ret.push(key); } + array.push(key); + }; + + Ember.keys = function(obj) { + var ret = [], key; + for (key in obj) { + pushPropertyName(obj, ret, key); } + + // IE8 doesn't enumerate property that named the same as prototype properties. + for (var i = 0, l = prototypeProperties.length; i < l; i++) { + key = prototypeProperties[i]; + + pushPropertyName(obj, ret, key); + } + return ret; }; } -// .......................................................... -// ERROR -// - -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - -/** - A subclass of the JavaScript Error object for use in Ember. - - @class Error - @namespace Ember - @extends Error - @constructor -*/ -Ember.Error = function() { - var tmp = Error.apply(this, arguments); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } -}; - -Ember.Error.prototype = Ember.create(Error.prototype); - -})(); - - - -(function() { -/** - Expose RSVP implementation - - Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md - - @class RSVP - @namespace Ember - @constructor -*/ -Ember.RSVP = requireModule('rsvp'); - })(); @@ -8700,7 +9718,7 @@ Ember.RSVP = requireModule('rsvp'); var STRING_DASHERIZE_REGEXP = (/[ _]/g); var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); +var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); @@ -8826,7 +9844,7 @@ Ember.String = { }, /** - Replaces underscores or spaces with dashes. + Replaces underscores, spaces, or camelCase with dashes. ```javascript 'innerHTML'.dasherize(); // 'inner-html' @@ -8940,9 +9958,10 @@ Ember.String = { capitalize: function(str) { return str.charAt(0).toUpperCase() + str.substr(1); } - }; + + })(); @@ -8965,10 +9984,11 @@ var fmt = Ember.String.fmt, capitalize = Ember.String.capitalize, classify = Ember.String.classify; + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See `Ember.String.fmt`. + See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). @method fmt @for String @@ -8978,7 +9998,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.w`. + See [Ember.String.w](/api/classes/Ember.String.html#method_w). @method w @for String @@ -8988,7 +10008,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.loc`. + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc). @method loc @for String @@ -8998,7 +10018,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.camelize`. + See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). @method camelize @for String @@ -9008,7 +10028,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.decamelize`. + See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). @method decamelize @for String @@ -9018,7 +10038,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.dasherize`. + See [Ember.String.dasherize](/api/classes/Ember.String.html#method_dasherize). @method dasherize @for String @@ -9028,7 +10048,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.underscore`. + See [Ember.String.underscore](/api/classes/Ember.String.html#method_underscore). @method underscore @for String @@ -9038,7 +10058,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.classify`. + See [Ember.String.classify](/api/classes/Ember.String.html#method_classify). @method classify @for String @@ -9048,7 +10068,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See `Ember.String.capitalize`. + See [Ember.String.capitalize](/api/classes/Ember.String.html#method_capitalize). @method capitalize @for String @@ -9057,6 +10077,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { return capitalize(this); }; + } @@ -9070,158 +10091,1619 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { @submodule ember-runtime */ -var a_slice = Array.prototype.slice; +var get = Ember.get, + set = Ember.set, + slice = Array.prototype.slice, + getProperties = Ember.getProperties; -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { +/** + ## Overview + + This mixin provides properties and property observing functionality, core + features of the Ember object model. + + Properties and observers allow one object to observe changes to a + property on another object. This is one of the fundamental ways that + models, controllers and views communicate with each other in an Ember + application. + + Any object that has this mixin applied can be used in observer + operations. That includes `Ember.Object` and most objects you will + interact with as you write your Ember application. + + Note that you will not generally apply this mixin to classes yourself, + but you will use the features provided by this module frequently, so it + is important to understand how to use it. + + ## Using `get()` and `set()` + + Because of Ember's support for bindings and observers, you will always + access properties using the get method, and set properties using the + set method. This allows the observing objects to be notified and + computed properties to be handled properly. + + More documentation about `get` and `set` are below. + + ## Observing Property Changes + + You typically observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` + + Although this is the most common way to add an observer, this capability + is actually built into the `Ember.Object` class on top of two methods + defined in this mixin: `addObserver` and `removeObserver`. You can use + these two methods to add and remove observers yourself if you need to + do so at runtime. + + To add an observer for a property, call: + + ```javascript + object.addObserver('propertyKey', targetObject, targetAction) + ``` + + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. + + Note that if `propertyKey` is a computed property, the observer will be + called when any of the property dependencies are changed, even if the + resulting value of the computed property is unchanged. This is necessary + because computed properties are not computed until `get` is called. + + @class Observable + @namespace Ember +*/ +Ember.Observable = Ember.Mixin.create({ /** - The `property` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - `true`, which is the default. + Retrieves the value of a property from the object. - Computed properties allow you to treat a function like a property: + This method is usually similar to using `object[keyName]` or `object.keyName`, + however it supports both computed properties and the unknownProperty + handler. + + Because `get` unifies the syntax for accessing all these kinds + of properties, it can make many refactorings easier, such as replacing a + simple property with a computed property, or vice versa. + + ### Computed Properties + + Computed properties are methods defined with the `property` modifier + declared at the end, such as: ```javascript - MyApp.president = Ember.Object.create({ - firstName: "Barack", - lastName: "Obama", - - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Call this flag to mark the function as a property - }.property() - }); - - MyApp.president.get('fullName'); // "Barack Obama" + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') ``` - Treating a function like a property is useful because they can work with - bindings, just like any other property. + When you call `get` on a computed property, the function will be + called and the return value will be returned instead of the function + itself. - Many computed properties have dependencies on other properties. For - example, in the above example, the `fullName` property depends on - `firstName` and `lastName` to determine its value. You can tell Ember - about these dependencies like this: + ### Unknown Properties - ```javascript - MyApp.president = Ember.Object.create({ - firstName: "Barack", - lastName: "Obama", + Likewise, if you try to call `get` on a property whose value is + `undefined`, the `unknownProperty()` method will be called on the object. + If this method returns any value other than `undefined`, it will be returned + instead. This allows you to implement "virtual" properties that are + not defined upfront. - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Tell Ember.js that this computed property depends on firstName - // and lastName - }.property('firstName', 'lastName') - }); - ``` - - Make sure you list these dependencies so Ember knows when to update - bindings that connect to a computed property. Changing a dependency - will not immediately trigger an update of the computed property, but - will instead clear the cache so that it is updated when the next `get` - is called on the property. - - See `Ember.ComputedProperty`, `Ember.computed`. - - @method property - @for Function + @method get + @param {String} keyName The property to retrieve + @return {Object} The property value or undefined. */ - Function.prototype.property = function() { - var ret = Ember.computed(this); - return ret.property.apply(ret, arguments); - }; + get: function(keyName) { + return get(this, keyName); + }, /** - The `observes` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - true, which is the default. - - You can observe property changes simply by adding the `observes` - call to the end of your method declarations in classes that you write. - For example: + To get multiple properties at once, call `getProperties` + with a list of strings or an array: ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); + record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } ``` - See `Ember.observes`. + is equivalent to: - @method observes - @for Function + ```javascript + record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param {String...|Array} list of keys to get + @return {Hash} */ - Function.prototype.observes = function() { - this.__ember_observes__ = a_slice.call(arguments); + getProperties: function() { + return getProperties.apply(null, [this].concat(slice.call(arguments))); + }, + + /** + Sets the provided key or path to the value. + + This method is generally very similar to calling `object[key] = value` or + `object.key = value`, except that it provides support for computed + properties, the `setUnknownProperty()` method and property observers. + + ### Computed Properties + + If you try to set a value on a key that has a computed property handler + defined (see the `get()` method for an example), then `set()` will call + that method, passing both the value and key instead of simply changing + the value itself. This is useful for those times when you need to + implement a property that is composed of one or more member + properties. + + ### Unknown Properties + + If you try to set a value on a key that is undefined in the target + object, then the `setUnknownProperty()` handler will be called instead. This + gives you an opportunity to implement complex "virtual" properties that + are not predefined on the object. If `setUnknownProperty()` returns + undefined, then `set()` will simply set the value on the object. + + ### Property Observers + + In addition to changing the property, `set()` will also register a property + change with the object. Unless you have placed this call inside of a + `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers + (i.e. observer methods declared on the same object), will be called + immediately. Any "remote" observers (i.e. observer methods declared on + another object) will be placed in a queue and called at a later time in a + coalesced manner. + + ### Chaining + + In addition to property changes, `set()` returns the value of the object + itself so you can do chaining like this: + + ```javascript + record.set('firstName', 'Charles').set('lastName', 'Jolley'); + ``` + + @method set + @param {String} keyName The property to set + @param {Object} value The value to set or `null`. + @return {Ember.Observable} + */ + set: function(keyName, value) { + set(this, keyName, value); return this; - }; + }, /** - The `observesBefore` extension of Javascript's Function prototype is - available when `Ember.EXTEND_PROTOTYPES` or - `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. - - You can get notified when a property changes is about to happen by - by adding the `observesBefore` call to the end of your method - declarations in classes that you write. For example: + To set multiple properties at once, call `setProperties` + with a Hash: ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property is about to change - }.observesBefore('value') - }); + record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); ``` - See `Ember.observesBefore`. - - @method observesBefore - @for Function + @method setProperties + @param {Hash} hash the hash of keys and values to set + @return {Ember.Observable} */ - Function.prototype.observesBefore = function() { - this.__ember_observesBefore__ = a_slice.call(arguments); - return this; - }; + setProperties: function(hash) { + return Ember.setProperties(this, hash); + }, /** - The `on` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - true, which is the default. + Begins a grouping of property changes. - You can listen for events simply by adding the `on` call to the end of - your method declarations in classes or mixins that you write. For example: + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call + `endPropertyChanges()` to deliver the deferred change notifications and end + deferring. + + @method beginPropertyChanges + @return {Ember.Observable} + */ + beginPropertyChanges: function() { + Ember.beginPropertyChanges(); + return this; + }, + + /** + Ends a grouping of property changes. + + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + `beginPropertyChanges()` at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. + + @method endPropertyChanges + @return {Ember.Observable} + */ + endPropertyChanges: function() { + Ember.endPropertyChanges(); + return this; + }, + + /** + Notify the observer system that a property is about to change. + + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyDidChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. + + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. + + @method propertyWillChange + @param {String} keyName The property key that is about to change. + @return {Ember.Observable} + */ + propertyWillChange: function(keyName) { + Ember.propertyWillChange(this, keyName); + return this; + }, + + /** + Notify the observer system that a property has just changed. + + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyWillChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. + + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. + + @method propertyDidChange + @param {String} keyName The property key that has just changed. + @return {Ember.Observable} + */ + propertyDidChange: function(keyName) { + Ember.propertyDidChange(this, keyName); + return this; + }, + + /** + Convenience method to call `propertyWillChange` and `propertyDidChange` in + succession. + + @method notifyPropertyChange + @param {String} keyName The property key to be notified about. + @return {Ember.Observable} + */ + notifyPropertyChange: function(keyName) { + this.propertyWillChange(keyName); + this.propertyDidChange(keyName); + return this; + }, + + addBeforeObserver: function(key, target, method) { + Ember.addBeforeObserver(this, key, target, method); + }, + + /** + Adds an observer on a property. + + This is the core method used to register an observer for a property. + + Once you call this method, any time the key's value is set, your observer + will be notified. Note that the observers are triggered any time the + value is set, regardless of whether it has actually changed. Your + observer should be prepared to handle that. + + You can also pass an optional context parameter to this method. The + context will be passed to your observer method whenever it is triggered. + Note that if you add the same target/method pair on a key multiple times + with different context parameters, your observer will only be called once + with the last context you passed. + + ### Observer Methods + + Observer methods you pass should generally have the following signature if + you do not pass a `context` parameter: ```javascript - Ember.Mixin.create({ - doSomethingWithElement: function() { - // Executes whenever the "didInsertElement" event fires - }.on('didInsertElement') - }); + fooDidChange: function(sender, key, value, rev) { }; ``` - See `Ember.on`. + The sender is the object that changed. The key is the property that + changes. The value property is currently reserved and unused. The rev + is the last property revision of the object when it changed, which you can + use to detect if the key value has really changed or not. - @method on - @for Function + If you pass a `context` parameter, the context will be passed before the + revision like so: + + ```javascript + fooDidChange: function(sender, key, value, context, rev) { }; + ``` + + Usually you will not need the value, context or revision parameters at + the end. In this case, it is common to write observer methods that take + only a sender and key value as parameters or, if you aren't interested in + any of these values, to write an observer that has no parameters at all. + + @method addObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @return {Ember.Object} self */ - Function.prototype.on = function() { - var events = a_slice.call(arguments); - this.__ember_listens__ = events; - return this; - }; -} + addObserver: function(key, target, method) { + Ember.addObserver(this, key, target, method); + }, + /** + Remove an observer you have previously registered on this object. Pass + the same key, target, and method you passed to `addObserver()` and your + target will no longer receive notifications. + + @method removeObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @return {Ember.Observable} receiver + */ + removeObserver: function(key, target, method) { + Ember.removeObserver(this, key, target, method); + }, + + /** + Returns `true` if the object currently has observers registered for a + particular key. You can use this method to potentially defer performing + an expensive action until someone begins observing a particular property + on the object. + + @method hasObserverFor + @param {String} key Key to check + @return {Boolean} + */ + hasObserverFor: function(key) { + return Ember.hasListeners(this, key+':change'); + }, + + /** + Retrieves the value of a property, or a default value in the case that the + property returns `undefined`. + + ```javascript + person.getWithDefault('lastName', 'Doe'); + ``` + + @method getWithDefault + @param {String} keyName The name of the property to retrieve + @param {Object} defaultValue The value to return if the property value is undefined + @return {Object} The property value or the defaultValue. + */ + getWithDefault: function(keyName, defaultValue) { + return Ember.getWithDefault(this, keyName, defaultValue); + }, + + /** + Set the value of a property to the current value plus some amount. + + ```javascript + person.incrementProperty('age'); + team.incrementProperty('score', 2); + ``` + + @method incrementProperty + @param {String} keyName The name of the property to increment + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value + */ + incrementProperty: function(keyName, increment) { + if (Ember.isNone(increment)) { increment = 1; } + Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); + set(this, keyName, (get(this, keyName) || 0) + increment); + return get(this, keyName); + }, + + /** + Set the value of a property to the current value minus some amount. + + ```javascript + player.decrementProperty('lives'); + orc.decrementProperty('health', 5); + ``` + + @method decrementProperty + @param {String} keyName The name of the property to decrement + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value + */ + decrementProperty: function(keyName, decrement) { + if (Ember.isNone(decrement)) { decrement = 1; } + Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); + set(this, keyName, (get(this, keyName) || 0) - decrement); + return get(this, keyName); + }, + + /** + Set the value of a boolean property to the opposite of it's + current value. + + ```javascript + starship.toggleProperty('warpDriveEngaged'); + ``` + + @method toggleProperty + @param {String} keyName The name of the property to toggle + @return {Object} The new property value + */ + toggleProperty: function(keyName) { + set(this, keyName, !get(this, keyName)); + return get(this, keyName); + }, + + /** + Returns the cached value of a computed property, if it exists. + This allows you to inspect the value of a computed property + without accidentally invoking it if it is intended to be + generated lazily. + + @method cacheFor + @param {String} keyName + @return {Object} The cached value of the computed property, if any + */ + cacheFor: function(keyName) { + return Ember.cacheFor(this, keyName); + }, + + // intended for debugging purposes + observersForKey: function(keyName) { + return Ember.observersFor(this, keyName); + } +}); })(); (function() { +/** +@module ember +@submodule ember-runtime +*/ + + +// 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, + o_create = Ember.create, + o_defineProperty = Ember.platform.defineProperty, + GUID_KEY = Ember.GUID_KEY, + guidFor = Ember.guidFor, + generateGuid = Ember.generateGuid, + meta = Ember.meta, + rewatch = Ember.rewatch, + finishChains = Ember.finishChains, + sendEvent = Ember.sendEvent, + destroy = Ember.destroy, + schedule = Ember.run.schedule, + Mixin = Ember.Mixin, + applyMixin = Mixin._apply, + finishPartial = Mixin.finishPartial, + reopen = Mixin.prototype.reopen, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + indexOf = Ember.EnumerableUtils.indexOf; + +var undefinedDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: undefined +}; + +function makeCtor() { + + // Note: avoid accessing any properties on the object since it makes the + // method a lot faster. This is glue code so we want it to be as fast as + // possible. + + var wasApplied = false, initMixins, initProperties; + + var Class = function() { + if (!wasApplied) { + Class.proto(); // prepare prototype... + } + o_defineProperty(this, GUID_KEY, undefinedDescriptor); + o_defineProperty(this, '_super', undefinedDescriptor); + var m = meta(this), proto = m.proto; + m.proto = this; + if (initMixins) { + // capture locally so we can clear the closed over variable + var mixins = initMixins; + initMixins = null; + this.reopen.apply(this, mixins); + } + if (initProperties) { + // capture locally so we can clear the closed over variable + var props = initProperties; + initProperties = null; + + var concatenatedProperties = this.concatenatedProperties; + + for (var i = 0, l = props.length; i < l; i++) { + var properties = props[i]; + + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); + + if (properties === null || typeof properties !== 'object') { + Ember.assert("Ember.Object.create only accepts objects."); + continue; + } + + var keyNames = Ember.keys(properties); + for (var j = 0, ll = keyNames.length; j < ll; j++) { + var keyName = keyNames[j]; + if (!properties.hasOwnProperty(keyName)) { continue; } + + var value = properties[keyName], + IS_BINDING = Ember.IS_BINDING; + + if (IS_BINDING.test(keyName)) { + var bindings = m.bindings; + if (!bindings) { + bindings = m.bindings = {}; + } else if (!m.hasOwnProperty('bindings')) { + bindings = m.bindings = o_create(m.bindings); + } + bindings[keyName] = value; + } + + var desc = m.descs[keyName]; + + Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); + Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); + Ember.assert("`actions` must be provided at extend time, not at create time, when Ember.ActionHandler is used (i.e. views, controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); + + if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { + var baseValue = this[keyName]; + + if (baseValue) { + if ('function' === typeof baseValue.concat) { + value = baseValue.concat(value); + } else { + value = Ember.makeArray(baseValue).concat(value); + } + } else { + value = Ember.makeArray(value); + } + } + + if (desc) { + desc.set(this, keyName, value); + } else { + if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { + this.setUnknownProperty(keyName, value); + } else if (MANDATORY_SETTER) { + Ember.defineProperty(this, keyName, null, value); // setup mandatory setter + } else { + this[keyName] = value; + } + } + } + } + } + finishPartial(this, m); + this.init.apply(this, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); + }; + + Class.toString = Mixin.prototype.toString; + Class.willReopen = function() { + if (wasApplied) { + Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); + } + + wasApplied = false; + }; + Class._initMixins = function(args) { initMixins = args; }; + Class._initProperties = function(args) { initProperties = args; }; + + Class.proto = function() { + var superclass = Class.superclass; + if (superclass) { superclass.proto(); } + + if (!wasApplied) { + wasApplied = true; + Class.PrototypeMixin.applyPartial(Class.prototype); + rewatch(Class.prototype); + } + + return this.prototype; + }; + + return Class; + +} + +/** + @class CoreObject + @namespace Ember +*/ +var CoreObject = makeCtor(); +CoreObject.toString = function() { return "Ember.CoreObject"; }; + +CoreObject.PrototypeMixin = Mixin.create({ + reopen: function() { + applyMixin(this, arguments, true); + return this; + }, + + /** + An overridable method called when objects are instantiated. By default, + does nothing unless it is overridden during class definition. + + Example: + + ```javascript + App.Person = Ember.Object.extend({ + init: function() { + alert('Name is ' + this.get('name')); + } + }); + + var steve = App.Person.create({ + name: "Steve" + }); + + // alerts 'Name is Steve'. + ``` + + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. + + @method init + */ + init: function() {}, + + /** + Defines the properties that will be concatenated from the superclass + (instead of overridden). + + By default, when you extend an Ember class a property defined in + the subclass overrides a property with the same name that is defined + in the superclass. However, there are some cases where it is preferable + to build up a property's value by combining the superclass' property + value with the subclass' value. An example of this in use within Ember + is the `classNames` property of `Ember.View`. + + Here is some sample code showing the difference between a concatenated + property and a normal one: + + ```javascript + App.BarView = Ember.View.extend({ + someNonConcatenatedProperty: ['bar'], + classNames: ['bar'] + }); + + App.FooBarView = App.BarView.extend({ + someNonConcatenatedProperty: ['foo'], + classNames: ['foo'], + }); + + var fooBarView = App.FooBarView.create(); + fooBarView.get('someNonConcatenatedProperty'); // ['foo'] + fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] + ``` + + This behavior extends to object creation as well. Continuing the + above example: + + ```javascript + var view = App.FooBarView.create({ + someNonConcatenatedProperty: ['baz'], + classNames: ['baz'] + }) + view.get('someNonConcatenatedProperty'); // ['baz'] + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` + Adding a single property that is not an array will just add it in the array: + + ```javascript + var view = App.FooBarView.create({ + classNames: 'baz' + }) + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` + + Using the `concatenatedProperties` property, we can tell to Ember that mix + the content of the properties. + + In `Ember.View` the `classNameBindings` and `attributeBindings` properties + are also concatenated, in addition to `classNames`. + + This feature is available for you to use throughout the Ember object model, + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). + + @property concatenatedProperties + @type Array + @default null + */ + concatenatedProperties: null, + + /** + Destroyed object property flag. + + if this property is `true` the observers and bindings were already + removed by the effect of calling the `destroy()` method. + + @property isDestroyed + @default false + */ + isDestroyed: false, + + /** + Destruction scheduled flag. The `destroy()` method has been called. + + The object stays intact until the end of the run loop at which point + the `isDestroyed` flag is set. + + @property isDestroying + @default false + */ + isDestroying: false, + + /** + Destroys an object by setting the `isDestroyed` flag and removing its + metadata, which effectively destroys observers and bindings. + + If you try to set a property on a destroyed object, an exception will be + raised. + + Note that destruction is scheduled for the end of the run loop and does not + happen immediately. It will set an isDestroying flag immediately. + + @method destroy + @return {Ember.Object} receiver + */ + destroy: function() { + if (this.isDestroying) { return; } + this.isDestroying = true; + + schedule('actions', this, this.willDestroy); + schedule('destroy', this, this._scheduledDestroy); + return this; + }, + + /** + Override to implement teardown. + + @method willDestroy + */ + willDestroy: Ember.K, + + /** + @private + + Invoked by the run loop to actually destroy the object. This is + scheduled for execution by the `destroy` method. + + @method _scheduledDestroy + */ + _scheduledDestroy: function() { + if (this.isDestroyed) { return; } + destroy(this); + this.isDestroyed = true; + }, + + bind: function(to, from) { + if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } + from.to(to).connect(this); + return from; + }, + + /** + Returns a string representation which attempts to provide more information + than Javascript's `toString` typically does, in a generic way for all Ember + objects. + + App.Person = Em.Object.extend() + person = App.Person.create() + person.toString() //=> "" + + If the object's class is not defined on an Ember namespace, it will + indicate it is a subclass of the registered superclass: + + Student = App.Person.extend() + student = Student.create() + student.toString() //=> "<(subclass of App.Person):ember1025>" + + If the method `toStringExtension` is defined, its return value will be + included in the output. + + App.Teacher = App.Person.extend({ + toStringExtension: function() { + return this.get('fullName'); + } + }); + teacher = App.Teacher.create() + teacher.toString(); //=> "" + + @method toString + @return {String} string representation + */ + toString: function toString() { + var hasToStringExtension = typeof this.toStringExtension === 'function', + extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; + var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; + this.toString = makeToString(ret); + return ret; + } +}); + +CoreObject.PrototypeMixin.ownerConstructor = CoreObject; + +function makeToString(ret) { + return function() { return ret; }; +} + +if (Ember.config.overridePrototypeMixin) { + Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); +} + +CoreObject.__super__ = null; + +var ClassMixin = Mixin.create({ + + ClassMixin: Ember.required(), + + PrototypeMixin: Ember.required(), + + isClass: true, + + isMethod: false, + + /** + Creates a new subclass. + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(thing); + } + }); + ``` + + This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. + + You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: + + ```javascript + App.PersonView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['isAdministrator'] + }); + ``` + + When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + var name = this.get('name'); + alert(name + ' says: ' + thing); + } + }); + + App.Soldier = App.Person.extend({ + say: function(thing) { + this._super(thing + ", sir!"); + }, + march: function(numberOfHours) { + alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') + } + }); + + var yehuda = App.Soldier.create({ + name: "Yehuda Katz" + }); + + yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" + ``` + + The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. + + You can also pass `Ember.Mixin` classes to add additional properties to the subclass. + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(this.get('name') + ' says: ' + thing); + } + }); + + App.SingingMixin = Ember.Mixin.create({ + sing: function(thing){ + alert(this.get('name') + ' sings: la la la ' + thing); + } + }); + + App.BroadwayStar = App.Person.extend(App.SingingMixin, { + dance: function() { + alert(this.get('name') + ' dances: tap tap tap tap '); + } + }); + ``` + + The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. + + @method extend + @static + + @param {Ember.Mixin} [mixins]* One or more Ember.Mixin classes + @param {Object} [arguments]* Object containing values to use within the new class + */ + extend: function() { + var Class = makeCtor(), proto; + Class.ClassMixin = Mixin.create(this.ClassMixin); + Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); + + Class.ClassMixin.ownerConstructor = Class; + Class.PrototypeMixin.ownerConstructor = Class; + + reopen.apply(Class.PrototypeMixin, arguments); + + Class.superclass = this; + Class.__super__ = this.prototype; + + proto = Class.prototype = o_create(this.prototype); + proto.constructor = Class; + generateGuid(proto); + meta(proto).proto = proto; // this will disable observers on prototype + + Class.ClassMixin.apply(Class); + return Class; + }, + + /** + Equivalent to doing `extend(arguments).create()`. + If possible use the normal `create` method instead. + + @method createWithMixins + @static + @param [arguments]* + */ + createWithMixins: function() { + var C = this; + if (arguments.length>0) { this._initMixins(arguments); } + return new C(); + }, + + /** + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. + + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); + } + }); + + var tom = App.Person.create({ + name: 'Tom Dale' + }); + + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` + + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` + + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: + + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` + + NOTE: For performance reasons, you cannot declare methods or computed + properties during `create`. You should instead declare methods and computed + properties when using `extend` or use the `createWithMixins` shorthand. + + @method create + @static + @param [arguments]* + */ + create: function() { + var C = this; + if (arguments.length>0) { this._initProperties(arguments); } + return new C(); + }, + + /** + + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + o = MyObject.create(); + o.get('name'); // 'an object' + + MyObject.reopen({ + say: function(msg){ + console.log(msg); + } + }) + + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" + + o.say("goodbye"); // logs "goodbye" + ``` + + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ + reopen: function() { + this.willReopen(); + reopen.apply(this.PrototypeMixin, arguments); + return this; + }, + + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + In other words, this creates static properties and functions for the class. These are only available on the class + and not on any instance of that class. + + ```javascript + App.Person = Ember.Object.extend({ + name : "", + sayHello : function(){ + alert("Hello. My name is " + this.get('name')); + } + }); + + App.Person.reopenClass({ + species : "Homo sapiens", + createPerson: function(newPersonsName){ + return App.Person.create({ + name:newPersonsName + }); + } + }); + + var tom = App.Person.create({ + name : "Tom Dale" + }); + var yehuda = App.Person.createPerson("Yehuda Katz"); + + tom.sayHello(); // "Hello. My name is Tom Dale" + yehuda.sayHello(); // "Hello. My name is Yehuda Katz" + alert(App.Person.species); // "Homo sapiens" + ``` + + Note that `species` and `createPerson` are *not* valid on the `tom` and `yehuda` + variables. They are only valid on `App.Person`. + + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` + + @method reopenClass + */ + reopenClass: function() { + reopen.apply(this.ClassMixin, arguments); + applyMixin(this, arguments, false); + return this; + }, + + detect: function(obj) { + if ('function' !== typeof obj) { return false; } + while(obj) { + if (obj===this) { return true; } + obj = obj.superclass; + } + return false; + }, + + detectInstance: function(obj) { + return obj instanceof this; + }, + + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For + example, computed property functions may close over variables that are then + no longer available for introspection. + + You can pass a hash of these values to a computed property like this: + + ```javascript + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` + + Once you've done this, you can retrieve the values saved to the computed + property from your class like this: + + ```javascript + MyClass.metaForProperty('person'); + ``` + + This will return the original hash that was passed to `meta()`. + + @method metaForProperty + @param key {String} property name + */ + metaForProperty: function(key) { + var desc = meta(this.proto(), false).descs[key]; + + Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); + return desc._meta || {}; + }, + + /** + Iterate over each computed property for the class, passing its name + and any associated metadata (see `metaForProperty`) to the callback. + + @method eachComputedProperty + @param {Function} callback + @param {Object} binding + */ + eachComputedProperty: function(callback, binding) { + var proto = this.proto(), + descs = meta(proto).descs, + empty = {}, + property; + + for (var name in descs) { + property = descs[name]; + + if (property instanceof Ember.ComputedProperty) { + callback.call(binding || this, name, property._meta || empty); + } + } + } + +}); + +ClassMixin.ownerConstructor = CoreObject; + +if (Ember.config.overrideClassMixin) { + Ember.config.overrideClassMixin(ClassMixin); +} + +CoreObject.ClassMixin = ClassMixin; +ClassMixin.apply(CoreObject); + +Ember.CoreObject = CoreObject; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +/** + `Ember.Object` is the main base class for all Ember objects. It is a subclass + of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, + see the documentation for each of these. + + @class Object + @namespace Ember + @extends Ember.CoreObject + @uses Ember.Observable +*/ +Ember.Object = Ember.CoreObject.extend(Ember.Observable); +Ember.Object.toString = function() { return "Ember.Object"; }; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; + +/** + A Namespace is an object usually used to contain other objects or methods + such as an application or framework. Create a namespace anytime you want + to define one of these new containers. + + # Example Usage + + ```javascript + MyFramework = Ember.Namespace.create({ + VERSION: '1.0.0' + }); + ``` + + @class Namespace + @namespace Ember + @extends Ember.Object +*/ +var Namespace = Ember.Namespace = Ember.Object.extend({ + isNamespace: true, + + init: function() { + Ember.Namespace.NAMESPACES.push(this); + Ember.Namespace.PROCESSED = false; + }, + + toString: function() { + var name = get(this, 'name'); + if (name) { return name; } + + findNamespaces(); + return this[Ember.GUID_KEY+'_name']; + }, + + nameClasses: function() { + processNamespace([this.toString()], this, {}); + }, + + destroy: function() { + var namespaces = Ember.Namespace.NAMESPACES; + Ember.lookup[this.toString()] = undefined; + namespaces.splice(indexOf.call(namespaces, this), 1); + this._super(); + } +}); + +Namespace.reopenClass({ + NAMESPACES: [Ember], + NAMESPACES_BY_ID: {}, + PROCESSED: false, + processAll: processAllNamespaces, + byName: function(name) { + if (!Ember.BOOTED) { + processAllNamespaces(); + } + + return NAMESPACES_BY_ID[name]; + } +}); + +var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; + +var hasOwnProp = ({}).hasOwnProperty, + guidFor = Ember.guidFor; + +function processNamespace(paths, root, seen) { + var idx = paths.length; + + NAMESPACES_BY_ID[paths.join('.')] = root; + + // Loop over all of the keys in the namespace, looking for classes + for(var key in root) { + if (!hasOwnProp.call(root, key)) { continue; } + var obj = root[key]; + + // If we are processing the `Ember` namespace, for example, the + // `paths` will start with `["Ember"]`. Every iteration through + // the loop will update the **second** element of this list with + // the key, so processing `Ember.View` will make the Array + // `['Ember', 'View']`. + paths[idx] = key; + + // If we have found an unprocessed class + if (obj && obj.toString === classToString) { + // Replace the class' `toString` with the dot-separated path + // and set its `NAME_KEY` + obj.toString = makeToString(paths.join('.')); + obj[NAME_KEY] = paths.join('.'); + + // Support nested namespaces + } else if (obj && obj.isNamespace) { + // Skip aliased namespaces + if (seen[guidFor(obj)]) { continue; } + seen[guidFor(obj)] = true; + + // Process the child namespace + processNamespace(paths, obj, seen); + } + } + + paths.length = idx; // cut out last item +} + +function findNamespaces() { + var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; + + if (Namespace.PROCESSED) { return; } + + for (var prop in lookup) { + // These don't raise exceptions but can cause warnings + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } + + // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. + // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage + if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } + // Unfortunately, some versions of IE don't support window.hasOwnProperty + if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } + + // At times we are not allowed to access certain properties for security reasons. + // There are also times where even if we can access them, we are not allowed to access their properties. + try { + obj = Ember.lookup[prop]; + isNamespace = obj && obj.isNamespace; + } catch (e) { + continue; + } + + if (isNamespace) { + Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); + obj[NAME_KEY] = prop; + } + } +} + +var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; + +function superClassString(mixin) { + var superclass = mixin.superclass; + if (superclass) { + if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } + else { return superClassString(superclass); } + } else { + return; + } +} + +function classToString() { + if (!Ember.BOOTED && !this[NAME_KEY]) { + processAllNamespaces(); + } + + var ret; + + if (this[NAME_KEY]) { + ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; + } else { + var str = superClassString(this); + if (str) { + ret = "(subclass of " + str + ")"; + } else { + ret = "(unknown mixin)"; + } + this.toString = makeToString(ret); + } + + return ret; +} + +function processAllNamespaces() { + var unprocessedNamespaces = !Namespace.PROCESSED, + unprocessedMixins = Ember.anyUnprocessedMixins; + + if (unprocessedNamespaces) { + findNamespaces(); + Namespace.PROCESSED = true; + } + + if (unprocessedNamespaces || unprocessedMixins) { + var namespaces = Namespace.NAMESPACES, namespace; + for (var i=0, l=namespaces.length; i Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; + + // We suspend observers to ignore replacements from `reset` when totally + // recomputing. Unfortunately we cannot properly suspend the observers + // because we only have the key; instead we make the observers no-ops + this.suspended = false; + + // This is used to coalesce item changes from property observers. + this.changedItems = {}; +} + +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); + + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; + + this.destroyed = false; +} + +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue, true); + }, + getValue: function () { + return this.instanceMeta.getValue(); + }, + + setupObservers: function (dependentArray, dependentKey) { + Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); + + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, + + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + + delete this.dependentKeysByGuid[guidFor(dependentArray)]; + + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + }, + + suspendArrayObservers: function (callback, binding) { + var oldSuspended = this.suspended; + this.suspended = true; + callback.call(binding); + this.suspended = oldSuspended; + }, + + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); + + this.resetTransformations(dependentKey, observerContexts); + + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; + + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, + + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; + + if (!trackedArray) { return; } + + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } + + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; + + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, + + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); + + this.createPropertyObserver(observerContext); + + return observerContext; + }, + + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; + + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + }, + + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + }, + + trackAdd: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, + + trackRemove: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } + + return []; + }, + + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === Ember.TrackedArray.DELETE) { return; } + if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } + + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, + + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } + + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, 0), + normalizedRemoveCount = normalizeRemoveCount(normalizedIndex, length, removedCount), + item, + itemIndex, + sliceIndex, + observerContexts; + + observerContexts = this.trackRemove(dependentKey, normalizedIndex, normalizedRemoveCount); + + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } + + for (sliceIndex = normalizedRemoveCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = normalizedIndex + sliceIndex; + if (itemIndex >= length) { break; } + + item = dependentArray.objectAt(itemIndex); + + forEach(itemPropertyKeys, removeObservers, this); + + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } + }, + + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } + + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, addedCount), + changeMeta, + observerContext; + + forEach(dependentArray.slice(normalizedIndex, normalizedIndex + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, normalizedIndex + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + } + + changeMeta = createChangeMeta(dependentArray, item, normalizedIndex + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); + + this.trackAdd(dependentKey, normalizedIndex, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; + } + + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + this.flushChanges(); + }, + + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } + + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + } + this.changedItems = {}; + } +}; + +function normalizeIndex(index, length, newItemsOffset) { + if (index < 0) { + return Math.max(0, length + index); + } else if (index < length) { + return index; + } else /* index > length */ { + return Math.min(length - newItemsOffset, index); + } +} + +function normalizeRemoveCount(index, length, removedCount) { + return Math.min(removedCount, length - index); +} + +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; + + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } + + return meta; +} + +function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); +} + +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; + + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } + + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} + +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; + + this.dependentArrays = {}; + this.sugarMeta = {}; + + this.initialValue = initialValue; +} + +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; + } + }, + + setValue: function(newValue, triggerObservers) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue !== undefined) { + var fireObservers = triggerObservers && (newValue !== this.cache[this.propertyName]); + + if (fireObservers) { + propertyWillChange(this.context, this.propertyName); + } + + this.cache[this.propertyName] = newValue; + + if (fireObservers) { + propertyDidChange(this.context, this.propertyName); + } + } else { + delete this.cache[this.propertyName]; + } + } +}; + +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. + + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; + + this.options = options; + this._instanceMetas = {}; + + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + + this.readOnly(); + this.cacheable(); + + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by . + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + Ember.run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); + + reset.call(this, cp, propertyName); + + meta.dependentArraysObserver.suspendArrayObservers(function () { + forEach(cp._dependentKeys, function (dependentKey) { + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; + + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } + + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + }, this); + + forEach(cp._dependentKeys, function(dependentKey) { + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; + + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + + recompute.call(this, propertyName); + + return cp._instanceMeta(this, propertyName).getValue(); + }; +} + +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + +function defaultCallback(computedValue) { + return computedValue; +} + +ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; +}; + +ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName; + + return !!this._instanceMetas[key]; +}; + +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName, + meta = this._instanceMetas[key]; + + if (!meta) { + meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; +}; + +ReduceComputedProperty.prototype.initialValue = function () { + if (typeof this.options.initialValue === 'function') { + return this.options.initialValue(); + } + else { + return this.options.initialValue; + } +}; + +ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); +}; + +ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); +}; + +ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } +}; + +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = new Ember.Set(), + match, + dependentArrayKey, + itemPropertyKey; + + forEach(a_slice.call(arguments), function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Ember.Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + itemPropertyKey = match[2]; + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + propertyArgs.add(dependentArrayKey); + } else { + propertyArgs.add(dependentKey); + } + }); + + return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties: + + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is removed + from the array. + + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: + + ```javascript + function (initialValue, changeMeta, instanceMeta) + ``` + + `initialValue` - The value of the `initialValue` property from the + options object. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Note that observers will be fired if either of these functions return a value + that differs from the accumulated value. When returning an object that + mutates in response to array changes, for example an array that maps + everything from some other array (see `Ember.computed.map`), it is usually + important that the *same* array be returned to avoid accidentally triggering observers. + + Example + + ```javascript + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` + + Dependent keys may refer to `@this` to observe changes to the object itself, + which must be array-like, rather than a property of the object. This is + mostly useful for array proxies, to ensure objects are retrieved via + `objectAtContent`. This is how you could sort items by properties defined on an item controller. + + Example + + ```javascript + App.PeopleController = Ember.ArrayController.extend({ + itemController: 'person', + + sortedPeople: Ember.computed.sort('@this.@each.reversedName', function(personA, personB) { + // `reversedName` isn't defined on Person, but we have access to it via + // the item controller App.PersonController. If we'd used + // `content.@each.reversedName` above, we would be getting the objects + // directly and not have access to `reversedName`. + // + var reversedNameA = get(personA, 'reversedName'), + reversedNameB = get(personB, 'reversedName'); + + return Ember.compare(reversedNameA, reversedNameB); + }) + }); + + App.PersonController = Ember.ObjectController.extend({ + reversedName: function () { + return reverse(get(this, 'name')); + }.property('name') + }) + ``` + + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.reduceComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Ember.Error("Reduce Computed Property declared without an options hash"); + } + + if (!('initialValue' in options)) { + throw new Ember.Error("Reduce Computed Property declared without an initial value"); + } + + var cp = new ReduceComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; + +function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + Ember.addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } + + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; +} +Ember.ArrayComputedProperty = ArrayComputedProperty; +ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); +ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); +}; +ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is + removed from the array. + + `addedItem` - A function that is called each time an element is + added to the array. + + + The `initialize` function has the following signature: + + ```javascript + function (array, changeMeta, instanceMeta) + ``` + + `array` - The initial value of the arrayComputed, an empty array. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); + }; + ``` + + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Ember.Error("Array Computed Property declared without an options hash"); + } + + var cp = new ArrayComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map, + SearchProxy; + +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('maxChildAge'); // 8 + ``` + + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array +*/ +Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('minChildAge'); // 5 + ``` + + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array +*/ +Ember.computed.min = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + Returns an array mapped via the callback + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hamster = App.Hamster.create({chores: ['cook', 'clean', 'write more unit tests']}); + hamster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` + + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback +*/ +Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Returns an array mapped to the specified key. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('childAges'); // [7, 5, 8] + ``` + + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key +*/ +Ember.computed.mapBy = function(dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey +*/ +Ember.computed.mapProperty = Ember.computed.mapBy; + +/** + Filters the array by the callback. + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); + + var hamster = App.Hamster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filter = function(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new Ember.SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); + } + + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); + } + + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Filters the array by the property and value + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); + + var hamster = App.Hamster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {String} value + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filterBy = function(dependentKey, propertyKey, value) { + var callback; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead +*/ +Ember.computed.filterProperty = Ember.computed.filterBy; + +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); + + var hamster = App.Hamster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hamster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` + + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.uniq = function() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); + + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; + + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). + + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.union = Ember.computed.uniq; + +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependeny arrays. + + Example + + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); + + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` + + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays +*/ +Ember.computed.intersect = function () { + var getDependentKeyGuids = function (changeMeta) { + return map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; + + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; + + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { + + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; + } + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); + + var hamster = App.Hamster.create({fruits: [ + 'grape', + 'kale', + ]}); + hamster.get('wants'); // ['banana'] + ``` + + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array +*/ +Ember.computed.setDiff = function (setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new Ember.Error("setDiff requires exactly two dependent arrays."); + } + return Ember.arrayComputed.call(null, setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); +}; + +function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; + + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } + + if (low === high) { + return low; + } + + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); + + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); + + if (guidMid === guidItem) { + return mid; + } + + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } + + + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } + + return mid; + + function _guidFor(item) { + if (SearchProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} + + +SearchProxy = Ember.ObjectProxy.extend(); + +/** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. + + The callback method you provide should have the following signature: + + ```javascript + function(itemA, itemB); + ``` + + - `itemA` the first item to compare. + - `itemB` the second item to compare. + + This function should return `-1` when `itemA` should come before + `itemB`. It should return `1` when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + return 0; + }), + }); + var todoList = ToDoList.create({todos: [ + {name: 'Unit Test', priority: 2}, + {name: 'Documentation', priority: 3}, + {name: 'Release', priority: 1} + ]}); + + todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] + todoList.get('priorityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] + ``` + + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function +*/ +Ember.computed.sort = function (itemsKey, sortDefinition) { + Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); + + var initFn, sortPropertiesKey; + + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; + + Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); + + changeMeta.property.clearItemPropertyKeys(itemsKey); + + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } + + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); + + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } + + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } + + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } + + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + + setupSortProperties.call(this); + + + instanceMeta.order = function (itemA, itemB) { + var sortProperty, result, asc; + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = Ember.compare(get(itemA, sortProperty), get(itemB, sortProperty)); + + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } + + return 0; + }; + + instanceMeta.binarySearch = binarySearch; + }; + } + + return Ember.arrayComputed.call(null, itemsKey, { + initialize: initFn, + + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; + + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); + + searchItem = SearchProxy.create(proxyProperties); + } else { + searchItem = item; + } + + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; + +})(); + + + +(function() { +/** + Expose RSVP implementation + + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md + + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var a_slice = Array.prototype.slice; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { + + /** + The `property` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + `true`, which is the default. + + Computed properties allow you to treat a function like a property: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Call this flag to mark the function as a property + }.property() + }); + + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" + }); + + president.get('fullName'); // "Barack Obama" + ``` + + Treating a function like a property is useful because they can work with + bindings, just like any other property. + + Many computed properties have dependencies on other properties. For + example, in the above example, the `fullName` property depends on + `firstName` and `lastName` to determine its value. You can tell Ember + about these dependencies like this: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') + }); + ``` + + Make sure you list these dependencies so Ember knows when to update + bindings that connect to a computed property. Changing a dependency + will not immediately trigger an update of the computed property, but + will instead clear the cache so that it is updated when the next `get` + is called on the property. + + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). + + @method property + @for Function + */ + Function.prototype.property = function() { + var ret = Ember.computed(this); + return ret.property.apply(ret, arguments); + }; + + /** + The `observes` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. + + You can observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. + + See `Ember.observer`. + + @method observes + @for Function + */ + Function.prototype.observes = function() { + this.__ember_observes__ = a_slice.call(arguments); + return this; + }; + + /** + The `observesImmediately` extension of Javascript's Function prototype is + available when `Ember.EXTEND_PROTOTYPES` or + `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. + + You can observe property changes simply by adding the `observesImmediately` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes immediately after the "value" property changes + }.observesImmediately('value') + }); + ``` + + In the future, `observes` may become asynchronous. In this event, + `observesImmediately` will maintain the synchronous behavior. + + See `Ember.immediateObserver`. + + @method observesImmediately + @for Function + */ + Function.prototype.observesImmediately = function() { + for (var i=0, l=arguments.length; i get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; + if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; this.replace(idx, 0, [object]) ; return this ; }, @@ -11060,7 +15465,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** if ('number' === typeof start) { if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); } // fast case @@ -11254,525 +15659,6 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, set = Ember.set; - -/** - ## Overview - - This mixin provides properties and property observing functionality, core - features of the Ember object model. - - Properties and observers allow one object to observe changes to a - property on another object. This is one of the fundamental ways that - models, controllers and views communicate with each other in an Ember - application. - - Any object that has this mixin applied can be used in observer - operations. That includes `Ember.Object` and most objects you will - interact with as you write your Ember application. - - Note that you will not generally apply this mixin to classes yourself, - but you will use the features provided by this module frequently, so it - is important to understand how to use it. - - ## Using `get()` and `set()` - - Because of Ember's support for bindings and observers, you will always - access properties using the get method, and set properties using the - set method. This allows the observing objects to be notified and - computed properties to be handled properly. - - More documentation about `get` and `set` are below. - - ## Observing Property Changes - - You typically observe property changes simply by adding the `observes` - call to the end of your method declarations in classes that you write. - For example: - - ```javascript - Ember.Object.extend({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` - - Although this is the most common way to add an observer, this capability - is actually built into the `Ember.Object` class on top of two methods - defined in this mixin: `addObserver` and `removeObserver`. You can use - these two methods to add and remove observers yourself if you need to - do so at runtime. - - To add an observer for a property, call: - - ```javascript - object.addObserver('propertyKey', targetObject, targetAction) - ``` - - This will call the `targetAction` method on the `targetObject` whenever - the value of the `propertyKey` changes. - - Note that if `propertyKey` is a computed property, the observer will be - called when any of the property dependencies are changed, even if the - resulting value of the computed property is unchanged. This is necessary - because computed properties are not computed until `get` is called. - - @class Observable - @namespace Ember -*/ -Ember.Observable = Ember.Mixin.create({ - - /** - Retrieves the value of a property from the object. - - This method is usually similar to using `object[keyName]` or `object.keyName`, - however it supports both computed properties and the unknownProperty - handler. - - Because `get` unifies the syntax for accessing all these kinds - of properties, it can make many refactorings easier, such as replacing a - simple property with a computed property, or vice versa. - - ### Computed Properties - - Computed properties are methods defined with the `property` modifier - declared at the end, such as: - - ```javascript - fullName: function() { - return this.getEach('firstName', 'lastName').compact().join(' '); - }.property('firstName', 'lastName') - ``` - - When you call `get` on a computed property, the function will be - called and the return value will be returned instead of the function - itself. - - ### Unknown Properties - - Likewise, if you try to call `get` on a property whose value is - `undefined`, the `unknownProperty()` method will be called on the object. - If this method returns any value other than `undefined`, it will be returned - instead. This allows you to implement "virtual" properties that are - not defined upfront. - - @method get - @param {String} keyName The property to retrieve - @return {Object} The property value or undefined. - */ - get: function(keyName) { - return get(this, keyName); - }, - - /** - To get multiple properties at once, call `getProperties` - with a list of strings or an array: - - ```javascript - record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } - ``` - - is equivalent to: - - ```javascript - record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } - ``` - - @method getProperties - @param {String...|Array} list of keys to get - @return {Hash} - */ - getProperties: function() { - var ret = {}; - var propertyNames = arguments; - if (arguments.length === 1 && Ember.typeOf(arguments[0]) === 'array') { - propertyNames = arguments[0]; - } - for(var i = 0; i < propertyNames.length; i++) { - ret[propertyNames[i]] = get(this, propertyNames[i]); - } - return ret; - }, - - /** - Sets the provided key or path to the value. - - This method is generally very similar to calling `object[key] = value` or - `object.key = value`, except that it provides support for computed - properties, the `unknownProperty()` method and property observers. - - ### Computed Properties - - If you try to set a value on a key that has a computed property handler - defined (see the `get()` method for an example), then `set()` will call - that method, passing both the value and key instead of simply changing - the value itself. This is useful for those times when you need to - implement a property that is composed of one or more member - properties. - - ### Unknown Properties - - If you try to set a value on a key that is undefined in the target - object, then the `unknownProperty()` handler will be called instead. This - gives you an opportunity to implement complex "virtual" properties that - are not predefined on the object. If `unknownProperty()` returns - undefined, then `set()` will simply set the value on the object. - - ### Property Observers - - In addition to changing the property, `set()` will also register a property - change with the object. Unless you have placed this call inside of a - `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers - (i.e. observer methods declared on the same object), will be called - immediately. Any "remote" observers (i.e. observer methods declared on - another object) will be placed in a queue and called at a later time in a - coalesced manner. - - ### Chaining - - In addition to property changes, `set()` returns the value of the object - itself so you can do chaining like this: - - ```javascript - record.set('firstName', 'Charles').set('lastName', 'Jolley'); - ``` - - @method set - @param {String} keyName The property to set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - set: function(keyName, value) { - set(this, keyName, value); - return this; - }, - - /** - To set multiple properties at once, call `setProperties` - with a Hash: - - ```javascript - record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); - ``` - - @method setProperties - @param {Hash} hash the hash of keys and values to set - @return {Ember.Observable} - */ - setProperties: function(hash) { - return Ember.setProperties(this, hash); - }, - - /** - Begins a grouping of property changes. - - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call this - method at the beginning of the changes to begin deferring change - notifications. When you are done making changes, call - `endPropertyChanges()` to deliver the deferred change notifications and end - deferring. - - @method beginPropertyChanges - @return {Ember.Observable} - */ - beginPropertyChanges: function() { - Ember.beginPropertyChanges(); - return this; - }, - - /** - Ends a grouping of property changes. - - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call - `beginPropertyChanges()` at the beginning of the changes to defer change - notifications. When you are done making changes, call this method to - deliver the deferred change notifications and end deferring. - - @method endPropertyChanges - @return {Ember.Observable} - */ - endPropertyChanges: function() { - Ember.endPropertyChanges(); - return this; - }, - - /** - Notify the observer system that a property is about to change. - - Sometimes you need to change a value directly or indirectly without - actually calling `get()` or `set()` on it. In this case, you can use this - method and `propertyDidChange()` instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. - - Note that you must always call `propertyWillChange` and `propertyDidChange` - as a pair. If you do not, it may get the property change groups out of - order and cause notifications to be delivered more often than you would - like. - - @method propertyWillChange - @param {String} keyName The property key that is about to change. - @return {Ember.Observable} - */ - propertyWillChange: function(keyName) { - Ember.propertyWillChange(this, keyName); - return this; - }, - - /** - Notify the observer system that a property has just changed. - - Sometimes you need to change a value directly or indirectly without - actually calling `get()` or `set()` on it. In this case, you can use this - method and `propertyWillChange()` instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. - - Note that you must always call `propertyWillChange` and `propertyDidChange` - as a pair. If you do not, it may get the property change groups out of - order and cause notifications to be delivered more often than you would - like. - - @method propertyDidChange - @param {String} keyName The property key that has just changed. - @return {Ember.Observable} - */ - propertyDidChange: function(keyName) { - Ember.propertyDidChange(this, keyName); - return this; - }, - - /** - Convenience method to call `propertyWillChange` and `propertyDidChange` in - succession. - - @method notifyPropertyChange - @param {String} keyName The property key to be notified about. - @return {Ember.Observable} - */ - notifyPropertyChange: function(keyName) { - this.propertyWillChange(keyName); - this.propertyDidChange(keyName); - return this; - }, - - addBeforeObserver: function(key, target, method) { - Ember.addBeforeObserver(this, key, target, method); - }, - - /** - Adds an observer on a property. - - This is the core method used to register an observer for a property. - - Once you call this method, any time the key's value is set, your observer - will be notified. Note that the observers are triggered any time the - value is set, regardless of whether it has actually changed. Your - observer should be prepared to handle that. - - You can also pass an optional context parameter to this method. The - context will be passed to your observer method whenever it is triggered. - Note that if you add the same target/method pair on a key multiple times - with different context parameters, your observer will only be called once - with the last context you passed. - - ### Observer Methods - - Observer methods you pass should generally have the following signature if - you do not pass a `context` parameter: - - ```javascript - fooDidChange: function(sender, key, value, rev) { }; - ``` - - The sender is the object that changed. The key is the property that - changes. The value property is currently reserved and unused. The rev - is the last property revision of the object when it changed, which you can - use to detect if the key value has really changed or not. - - If you pass a `context` parameter, the context will be passed before the - revision like so: - - ```javascript - fooDidChange: function(sender, key, value, context, rev) { }; - ``` - - Usually you will not need the value, context or revision parameters at - the end. In this case, it is common to write observer methods that take - only a sender and key value as parameters or, if you aren't interested in - any of these values, to write an observer that has no parameters at all. - - @method addObserver - @param {String} key The key to observer - @param {Object} target The target object to invoke - @param {String|Function} method The method to invoke. - @return {Ember.Object} self - */ - addObserver: function(key, target, method) { - Ember.addObserver(this, key, target, method); - }, - - /** - Remove an observer you have previously registered on this object. Pass - the same key, target, and method you passed to `addObserver()` and your - target will no longer receive notifications. - - @method removeObserver - @param {String} key The key to observer - @param {Object} target The target object to invoke - @param {String|Function} method The method to invoke. - @return {Ember.Observable} receiver - */ - removeObserver: function(key, target, method) { - Ember.removeObserver(this, key, target, method); - }, - - /** - Returns `true` if the object currently has observers registered for a - particular key. You can use this method to potentially defer performing - an expensive action until someone begins observing a particular property - on the object. - - @method hasObserverFor - @param {String} key Key to check - @return {Boolean} - */ - hasObserverFor: function(key) { - return Ember.hasListeners(this, key+':change'); - }, - - /** - @deprecated - @method getPath - @param {String} path The property path to retrieve - @return {Object} The property value or undefined. - */ - getPath: function(path) { - Ember.deprecate("getPath is deprecated since get now supports paths"); - return this.get(path); - }, - - /** - @deprecated - @method setPath - @param {String} path The path to the property that will be set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - setPath: function(path, value) { - Ember.deprecate("setPath is deprecated since set now supports paths"); - return this.set(path, value); - }, - - /** - Retrieves the value of a property, or a default value in the case that the - property returns `undefined`. - - ```javascript - person.getWithDefault('lastName', 'Doe'); - ``` - - @method getWithDefault - @param {String} keyName The name of the property to retrieve - @param {Object} defaultValue The value to return if the property value is undefined - @return {Object} The property value or the defaultValue. - */ - getWithDefault: function(keyName, defaultValue) { - return Ember.getWithDefault(this, keyName, defaultValue); - }, - - /** - Set the value of a property to the current value plus some amount. - - ```javascript - person.incrementProperty('age'); - team.incrementProperty('score', 2); - ``` - - @method incrementProperty - @param {String} keyName The name of the property to increment - @param {Number} increment The amount to increment by. Defaults to 1 - @return {Number} The new property value - */ - incrementProperty: function(keyName, increment) { - if (Ember.isNone(increment)) { increment = 1; } - Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); - set(this, keyName, (get(this, keyName) || 0) + increment); - return get(this, keyName); - }, - - /** - Set the value of a property to the current value minus some amount. - - ```javascript - player.decrementProperty('lives'); - orc.decrementProperty('health', 5); - ``` - - @method decrementProperty - @param {String} keyName The name of the property to decrement - @param {Number} decrement The amount to decrement by. Defaults to 1 - @return {Number} The new property value - */ - decrementProperty: function(keyName, decrement) { - if (Ember.isNone(decrement)) { decrement = 1; } - Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); - set(this, keyName, (get(this, keyName) || 0) - decrement); - return get(this, keyName); - }, - - /** - Set the value of a boolean property to the opposite of it's - current value. - - ```javascript - starship.toggleProperty('warpDriveEngaged'); - ``` - - @method toggleProperty - @param {String} keyName The name of the property to toggle - @return {Object} The new property value - */ - toggleProperty: function(keyName) { - set(this, keyName, !get(this, keyName)); - return get(this, keyName); - }, - - /** - Returns the cached value of a computed property, if it exists. - This allows you to inspect the value of a computed property - without accidentally invoking it if it is intended to be - generated lazily. - - @method cacheFor - @param {String} keyName - @return {Object} The cached value of the computed property, if any - */ - cacheFor: function(keyName) { - return Ember.cacheFor(this, keyName); - }, - - // intended for debugging purposes - observersForKey: function(keyName) { - return Ember.observersFor(this, keyName); - } -}); - -})(); - - - (function() { /** @module ember @@ -11878,18 +15764,29 @@ Ember.TargetActionSupport = Ember.Mixin.create({ */ triggerAction: function(opts) { opts = opts || {}; - var action = opts['action'] || get(this, 'action'), - target = opts['target'] || get(this, 'targetObject'), - actionContext = opts['actionContext'] || get(this, 'actionContextObject') || this; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; + + function args(options, actionName) { + var ret = []; + if (actionName) { ret.push(actionName); } + + return ret.concat(options); + } + + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } if (target && action) { var ret; if (target.send) { - ret = target.send.apply(target, [action, actionContext]); + ret = target.send.apply(target, args(actionContext, action)); } else { Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); - ret = target[action].apply(target, [actionContext]); + ret = target[action].apply(target, args(actionContext)); } if (ret !== false) ret = true; @@ -12024,11 +15921,6 @@ Ember.Evented = Ember.Mixin.create({ Ember.sendEvent(this, name, args); }, - fire: function(name) { - Ember.deprecate("Ember.Evented#fire() has been deprecated in favor of trigger() for compatibility with jQuery. It will be removed in 1.0. Please update your code to call trigger() instead."); - this.trigger.apply(this, arguments); - }, - /** Cancels subscription for given name, target, and method. @@ -12082,8 +15974,8 @@ Ember.DeferredMixin = Ember.Mixin.create({ Add handlers to be called when the Deferred object is resolved or rejected. @method then - @param {Function} doneCallback a callback function to be called when done - @param {Function} failCallback a callback function to be called when failed + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed */ then: function(resolve, reject) { var deferred, promise, entity; @@ -12141,6 +16033,719 @@ Ember.DeferredMixin = Ember.Mixin.create({ (function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, typeOf = Ember.typeOf; + +/** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. + + `Ember.ActionHandler` is used internally by Ember in `Ember.View`, + `Ember.Controller`, and `Ember.Route`. + + @class ActionHandler + @namespace Ember +*/ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], + + /** + @private + + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + var hashName; + + if (!props._actions) { + if (typeOf(props.actions) === 'object') { + hashName = 'actions'; + } else if (typeOf(props.events) === 'object') { + Ember.deprecate('Action handlers contained in an `events` object are deprecated in favor of putting them in an `actions` object', false); + hashName = 'events'; + } + + if (hashName) { + props._actions = Ember.merge(props._actions || {}, props[hashName]); + } + + delete props[hashName]; + } + }, + + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; + + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } + + if (target = get(this, 'target')) { + Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); + target.send.apply(target, arguments); + } + } + +}); + +})(); + + + +(function() { +var set = Ember.set, get = Ember.get, + resolve = Ember.RSVP.resolve, + rethrow = Ember.RSVP.rethrow, + not = Ember.computed.not, + or = Ember.computed.or; + +/** + @module ember + @submodule ember-runtime + */ + +function observePromise(proxy, promise) { + promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + // don't re-throw, as we are merely observing + }); +} + +/** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` + + the controller has bindable attributes which + track the promises life cycle + + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` + + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. + + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` + + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. + + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } + + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` + + If the controller is backing a template, the attributes are + bindable from within that template + + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + reason: null, + isPending: not('isSettled').readOnly(), + isSettled: or('isRejected', 'isFulfilled').readOnly(), + isRejected: false, + isFulfilled: false, + + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + promise = resolve(promise); + observePromise(this, promise); + return promise.then(); // fork the promise. + } else { + throw new Ember.Error("PromiseProxy's promise must be set"); + } + }), + + then: function(fulfill, reject) { + return get(this, 'promise').then(fulfill, reject); + } +}); + + + +})(); + + + +(function() { + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; + +/** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. + + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. +*/ +Ember.TrackedArray = function (items) { + if (arguments.length < 1) { items = []; } + + var length = get(items, 'length'); + + if (length) { + this._operations = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._operations = []; + } +}; + +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; + +Ember.TrackedArray.prototype = { + + /** + Track that `newItems` were added to the tracked array at `index`. + + @method addItems + @param index + @param newItems + */ + addItems: function (index, newItems) { + var count = get(newItems, 'length'); + if (count < 1) { return; } + + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; + + newArrayOperation = new ArrayOperation(INSERT, count, newItems); + + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._operations.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } + + this._composeInsert(composeIndex); + }, + + /** + Track that `count` items were removed at `index`. + + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + if (count < 1) { return; } + + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; + + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + + return this._composeDelete(composeIndex); + }, + + /** + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. + + `callback` will be called for each operation and will be passed the following arguments: + * {array} items The items for the given operation + * {number} offset The computed offset of the items, ie the index in the + array of the first item for this operation. + * {string} operation The type of the operation. One of + `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` + + @method apply + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; + + forEach(this._operations, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.type); + + if (arrayOperation.type !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); + + this._operations = [new ArrayOperation(RETAIN, items.length, items)]; + }, + + /** + Return an ArrayOperationMatch for the operation that contains the item at `index`. + + @method _findArrayOperation + + @param {number} index the index of the item whose operation information + should be returned. + @private + */ + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; + + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._operations[arrayOperationIndex]; + + if (arrayOperation.type === DELETE) { continue; } + + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } + + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, + + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._operations[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.type, splitItems.length, splitItems); + + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + + this._operations.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, + + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._operations[index], + leftArrayOperation = this._operations[index-1], // may be undefined + rightArrayOperation = this._operations[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + rightOp = rightArrayOperation && rightArrayOperation.type; + + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + + if (rightOp === INSERT) { + // also merge right (we have split an insert with an insert) + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index, 2); + } else { + // only merge left + this._operations.splice(index, 1); + } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index + 1, 1); + } + }, + + _composeDelete: function (index) { + var arrayOperation = this._operations[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._operations[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + nextArrayOperation, + nextOp, + nextCount, + removeNewAndNextOp = false, + removedItems = []; + + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; + } + + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._operations[i]; + nextOp = nextArrayOperation.type; + nextCount = nextArrayOperation.count; + + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } + + if (nextCount > deletesToGo) { + // d:2 {r,i}:5 we reduce the retain or insert, but it stays + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; + + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; + + deletesToGo = 0; + } else { + if (nextCount === deletesToGo) { + // Handle edge case of d:2 i:2 in which case both operations go away + // during composition. + removeNewAndNextOp = true; + } + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + + if (nextOp === INSERT) { + // d:2 i:3 will result in delete going away + arrayOperation.count -= nextCount; + } + } + + if (arrayOperation.count > 0) { + // compose our new delete with possibly several operations to the right of + // disparate types + this._operations.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in d:3 i:4; it may also have eliminated that operation, + // as in d:3 i:3. + this._operations.splice(index, removeNewAndNextOp ? 2 : 1); + } + + return removedItems; + }, + + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } +}; + +/** + Internal data structure to represent an array operation. + + @method ArrayOperation + @private + @property {string} type The type of the operation. One of + `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` + @property {number} count The number of items in this operation. + @property {array} items The items of the operation, if included. RETAIN and + INSERT include their items, DELETE does not. +*/ +function ArrayOperation (operation, count, items) { + this.type = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} + +/** + Internal data structure used to include information when looking up operations + by item index. + + @method ArrayOperationMatch + @private + @property {ArrayOperation} operation + @property {number} index The index of `operation` in the array of operations. + @property {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @property {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. +*/ +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; + +function Operation (type, count) { + this.type = type; + this.count = count; +} + +/** + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. + + @class SubArray + @namespace Ember +*/ +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } + + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; + +Ember.SubArray.prototype = { + /** + Track that an item was added to the tracked array. + + @method addItem + + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. + + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; + + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; + + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; + + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } + + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } + + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); + + if (match) { + returnValue = seenInSubArray; + } + + self._composeAt(self._operations.length-1); + }); + + return returnValue; + }, + + /** + Track that an item was removed from the tracked array. + + @method removeItem + + @param {number} index The index of the item in the tracked array. + + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; + + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } + + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }, function() { + throw new Ember.Error("Can't remove an item that has never been added."); + }); + + return returnValue; + }, + + + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; + + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; + + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } + + notFoundCallback(seenInSubArray); + }, + + _composeAt: function(index) { + var op = this._operations[index], + otherOp; + + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } + + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + --index; + } + } + + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } + }, + + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } +}; })(); @@ -12154,808 +16759,6 @@ Ember.Container.set = Ember.set; -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - -// 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, - o_create = Ember.create, - o_defineProperty = Ember.platform.defineProperty, - GUID_KEY = Ember.GUID_KEY, - guidFor = Ember.guidFor, - generateGuid = Ember.generateGuid, - meta = Ember.meta, - rewatch = Ember.rewatch, - finishChains = Ember.finishChains, - sendEvent = Ember.sendEvent, - destroy = Ember.destroy, - schedule = Ember.run.schedule, - Mixin = Ember.Mixin, - applyMixin = Mixin._apply, - finishPartial = Mixin.finishPartial, - reopen = Mixin.prototype.reopen, - MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, - indexOf = Ember.EnumerableUtils.indexOf; - -var undefinedDescriptor = { - configurable: true, - writable: true, - enumerable: false, - value: undefined -}; - -function makeCtor() { - - // Note: avoid accessing any properties on the object since it makes the - // method a lot faster. This is glue code so we want it to be as fast as - // possible. - - var wasApplied = false, initMixins, initProperties; - - var Class = function() { - if (!wasApplied) { - Class.proto(); // prepare prototype... - } - o_defineProperty(this, GUID_KEY, undefinedDescriptor); - o_defineProperty(this, '_super', undefinedDescriptor); - var m = meta(this), proto = m.proto; - m.proto = this; - if (initMixins) { - // capture locally so we can clear the closed over variable - var mixins = initMixins; - initMixins = null; - this.reopen.apply(this, mixins); - } - if (initProperties) { - // capture locally so we can clear the closed over variable - var props = initProperties; - initProperties = null; - - var concatenatedProperties = this.concatenatedProperties; - - for (var i = 0, l = props.length; i < l; i++) { - var properties = props[i]; - - Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); - - for (var keyName in properties) { - if (!properties.hasOwnProperty(keyName)) { continue; } - - var value = properties[keyName], - IS_BINDING = Ember.IS_BINDING; - - if (IS_BINDING.test(keyName)) { - var bindings = m.bindings; - if (!bindings) { - bindings = m.bindings = {}; - } else if (!m.hasOwnProperty('bindings')) { - bindings = m.bindings = o_create(m.bindings); - } - bindings[keyName] = value; - } - - var desc = m.descs[keyName]; - - Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); - Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); - - if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { - var baseValue = this[keyName]; - - if (baseValue) { - if ('function' === typeof baseValue.concat) { - value = baseValue.concat(value); - } else { - value = Ember.makeArray(baseValue).concat(value); - } - } else { - value = Ember.makeArray(value); - } - } - - if (desc) { - desc.set(this, keyName, value); - } else { - if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { - this.setUnknownProperty(keyName, value); - } else if (MANDATORY_SETTER) { - Ember.defineProperty(this, keyName, null, value); // setup mandatory setter - } else { - this[keyName] = value; - } - } - } - } - } - finishPartial(this, m); - m.proto = proto; - finishChains(this); - this.init.apply(this, arguments); - sendEvent(this, "init"); - }; - - Class.toString = Mixin.prototype.toString; - Class.willReopen = function() { - if (wasApplied) { - Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); - } - - wasApplied = false; - }; - Class._initMixins = function(args) { initMixins = args; }; - Class._initProperties = function(args) { initProperties = args; }; - - Class.proto = function() { - var superclass = Class.superclass; - if (superclass) { superclass.proto(); } - - if (!wasApplied) { - wasApplied = true; - Class.PrototypeMixin.applyPartial(Class.prototype); - rewatch(Class.prototype); - } - - return this.prototype; - }; - - return Class; - -} - -/** - @class CoreObject - @namespace Ember -*/ -var CoreObject = makeCtor(); -CoreObject.toString = function() { return "Ember.CoreObject"; }; - -CoreObject.PrototypeMixin = Mixin.create({ - reopen: function() { - applyMixin(this, arguments, true); - return this; - }, - - /** - An overridable method called when objects are instantiated. By default, - does nothing unless it is overridden during class definition. - - Example: - - ```javascript - App.Person = Ember.Object.extend({ - init: function() { - this._super(); - alert('Name is ' + this.get('name')); - } - }); - - var steve = App.Person.create({ - name: "Steve" - }); - - // alerts 'Name is Steve'. - ``` - - NOTE: If you do override `init` for a framework class like `Ember.View` or - `Ember.ArrayController`, be sure to call `this._super()` in your - `init` declaration! If you don't, Ember may not have an opportunity to - do important setup work, and you'll see strange behavior in your - application. - - @method init - */ - init: function() {}, - - /** - Defines the properties that will be concatenated from the superclass - (instead of overridden). - - By default, when you extend an Ember class a property defined in - the subclass overrides a property with the same name that is defined - in the superclass. However, there are some cases where it is preferable - to build up a property's value by combining the superclass' property - value with the subclass' value. An example of this in use within Ember - is the `classNames` property of `Ember.View`. - - Here is some sample code showing the difference between a concatenated - property and a normal one: - - ```javascript - App.BarView = Ember.View.extend({ - someNonConcatenatedProperty: ['bar'], - classNames: ['bar'] - }); - - App.FooBarView = App.BarView.extend({ - someNonConcatenatedProperty: ['foo'], - classNames: ['foo'], - }); - - var fooBarView = App.FooBarView.create(); - fooBarView.get('someNonConcatenatedProperty'); // ['foo'] - fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] - ``` - - This behavior extends to object creation as well. Continuing the - above example: - - ```javascript - var view = App.FooBarView.create({ - someNonConcatenatedProperty: ['baz'], - classNames: ['baz'] - }) - view.get('someNonConcatenatedProperty'); // ['baz'] - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` - Adding a single property that is not an array will just add it in the array: - - ```javascript - var view = App.FooBarView.create({ - classNames: 'baz' - }) - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` - - Using the `concatenatedProperties` property, we can tell to Ember that mix - the content of the properties. - - In `Ember.View` the `classNameBindings` and `attributeBindings` properties - are also concatenated, in addition to `classNames`. - - This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. - - @property concatenatedProperties - @type Array - @default null - */ - concatenatedProperties: null, - - /** - Destroyed object property flag. - - if this property is `true` the observers and bindings were already - removed by the effect of calling the `destroy()` method. - - @property isDestroyed - @default false - */ - isDestroyed: false, - - /** - Destruction scheduled flag. The `destroy()` method has been called. - - The object stays intact until the end of the run loop at which point - the `isDestroyed` flag is set. - - @property isDestroying - @default false - */ - isDestroying: false, - - /** - Destroys an object by setting the `isDestroyed` flag and removing its - metadata, which effectively destroys observers and bindings. - - If you try to set a property on a destroyed object, an exception will be - raised. - - Note that destruction is scheduled for the end of the run loop and does not - happen immediately. It will set an isDestroying flag immediately. - - @method destroy - @return {Ember.Object} receiver - */ - destroy: function() { - if (this.isDestroying) { return; } - this.isDestroying = true; - - schedule('actions', this, this.willDestroy); - schedule('destroy', this, this._scheduledDestroy); - return this; - }, - - /** - Override to implement teardown. - - @method willDestroy - */ - willDestroy: Ember.K, - - /** - @private - - Invoked by the run loop to actually destroy the object. This is - scheduled for execution by the `destroy` method. - - @method _scheduledDestroy - */ - _scheduledDestroy: function() { - if (this.isDestroyed) { return; } - destroy(this); - this.isDestroyed = true; - }, - - bind: function(to, from) { - if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } - from.to(to).connect(this); - return from; - }, - - /** - Returns a string representation which attempts to provide more information - than Javascript's `toString` typically does, in a generic way for all Ember - objects. - - App.Person = Em.Object.extend() - person = App.Person.create() - person.toString() //=> "" - - If the object's class is not defined on an Ember namespace, it will - indicate it is a subclass of the registered superclass: - - Student = App.Person.extend() - student = Student.create() - student.toString() //=> "<(subclass of App.Person):ember1025>" - - If the method `toStringExtension` is defined, its return value will be - included in the output. - - App.Teacher = App.Person.extend({ - toStringExtension: function() { - return this.get('fullName'); - } - }); - teacher = App.Teacher.create() - teacher.toString(); //=> "" - - @method toString - @return {String} string representation - */ - toString: function toString() { - var hasToStringExtension = typeof this.toStringExtension === 'function', - extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; - var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; - this.toString = makeToString(ret); - return ret; - } -}); - -CoreObject.PrototypeMixin.ownerConstructor = CoreObject; - -function makeToString(ret) { - return function() { return ret; }; -} - -if (Ember.config.overridePrototypeMixin) { - Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); -} - -CoreObject.__super__ = null; - -var ClassMixin = Mixin.create({ - - ClassMixin: Ember.required(), - - PrototypeMixin: Ember.required(), - - isClass: true, - - isMethod: false, - - extend: function() { - var Class = makeCtor(), proto; - Class.ClassMixin = Mixin.create(this.ClassMixin); - Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); - - Class.ClassMixin.ownerConstructor = Class; - Class.PrototypeMixin.ownerConstructor = Class; - - reopen.apply(Class.PrototypeMixin, arguments); - - Class.superclass = this; - Class.__super__ = this.prototype; - - proto = Class.prototype = o_create(this.prototype); - proto.constructor = Class; - generateGuid(proto, 'ember'); - meta(proto).proto = proto; // this will disable observers on prototype - - Class.ClassMixin.apply(Class); - return Class; - }, - - /** - Equivalent to doing `extend(arguments).create()`. - If possible use the normal `create` method instead. - - @method createWithMixins - @static - @param [arguments]* - */ - createWithMixins: function() { - var C = this; - if (arguments.length>0) { this._initMixins(arguments); } - return new C(); - }, - - /** - Creates an instance of a class. Accepts either no arguments, or an object - containing values to initialize the newly instantiated object with. - - ```javascript - App.Person = Ember.Object.extend({ - helloWorld: function() { - alert("Hi, my name is " + this.get('name')); - } - }); - - var tom = App.Person.create({ - name: 'Tom Dale' - }); - - tom.helloWorld(); // alerts "Hi, my name is Tom Dale". - ``` - - `create` will call the `init` function if defined during - `Ember.AnyObject.extend` - - If no arguments are passed to `create`, it will not set values to the new - instance during initialization: - - ```javascript - var noName = App.Person.create(); - noName.helloWorld(); // alerts undefined - ``` - - NOTE: For performance reasons, you cannot declare methods or computed - properties during `create`. You should instead declare methods and computed - properties when using `extend` or use the `createWithMixins` shorthand. - - @method create - @static - @param [arguments]* - */ - create: function() { - var C = this; - if (arguments.length>0) { this._initProperties(arguments); } - return new C(); - }, - - reopen: function() { - this.willReopen(); - reopen.apply(this.PrototypeMixin, arguments); - return this; - }, - - reopenClass: function() { - reopen.apply(this.ClassMixin, arguments); - applyMixin(this, arguments, false); - return this; - }, - - detect: function(obj) { - if ('function' !== typeof obj) { return false; } - while(obj) { - if (obj===this) { return true; } - obj = obj.superclass; - } - return false; - }, - - detectInstance: function(obj) { - return obj instanceof this; - }, - - /** - In some cases, you may want to annotate computed properties with additional - metadata about how they function or what values they operate on. For - example, computed property functions may close over variables that are then - no longer available for introspection. - - You can pass a hash of these values to a computed property like this: - - ```javascript - person: function() { - var personId = this.get('personId'); - return App.Person.create({ id: personId }); - }.property().meta({ type: App.Person }) - ``` - - Once you've done this, you can retrieve the values saved to the computed - property from your class like this: - - ```javascript - MyClass.metaForProperty('person'); - ``` - - This will return the original hash that was passed to `meta()`. - - @method metaForProperty - @param key {String} property name - */ - metaForProperty: function(key) { - var desc = meta(this.proto(), false).descs[key]; - - Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); - return desc._meta || {}; - }, - - /** - Iterate over each computed property for the class, passing its name - and any associated metadata (see `metaForProperty`) to the callback. - - @method eachComputedProperty - @param {Function} callback - @param {Object} binding - */ - eachComputedProperty: function(callback, binding) { - var proto = this.proto(), - descs = meta(proto).descs, - empty = {}, - property; - - for (var name in descs) { - property = descs[name]; - - if (property instanceof Ember.ComputedProperty) { - callback.call(binding || this, name, property._meta || empty); - } - } - } - -}); - -ClassMixin.ownerConstructor = CoreObject; - -if (Ember.config.overrideClassMixin) { - Ember.config.overrideClassMixin(ClassMixin); -} - -CoreObject.ClassMixin = ClassMixin; -ClassMixin.apply(CoreObject); - -Ember.CoreObject = CoreObject; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -/** - `Ember.Object` is the main base class for all Ember objects. It is a subclass - of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, - see the documentation for each of these. - - @class Object - @namespace Ember - @extends Ember.CoreObject - @uses Ember.Observable -*/ -Ember.Object = Ember.CoreObject.extend(Ember.Observable); -Ember.Object.toString = function() { return "Ember.Object"; }; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; - -/** - A Namespace is an object usually used to contain other objects or methods - such as an application or framework. Create a namespace anytime you want - to define one of these new containers. - - # Example Usage - - ```javascript - MyFramework = Ember.Namespace.create({ - VERSION: '1.0.0' - }); - ``` - - @class Namespace - @namespace Ember - @extends Ember.Object -*/ -var Namespace = Ember.Namespace = Ember.Object.extend({ - isNamespace: true, - - init: function() { - Ember.Namespace.NAMESPACES.push(this); - Ember.Namespace.PROCESSED = false; - }, - - toString: function() { - var name = get(this, 'name'); - if (name) { return name; } - - findNamespaces(); - return this[Ember.GUID_KEY+'_name']; - }, - - nameClasses: function() { - processNamespace([this.toString()], this, {}); - }, - - destroy: function() { - var namespaces = Ember.Namespace.NAMESPACES; - Ember.lookup[this.toString()] = undefined; - namespaces.splice(indexOf.call(namespaces, this), 1); - this._super(); - } -}); - -Namespace.reopenClass({ - NAMESPACES: [Ember], - NAMESPACES_BY_ID: {}, - PROCESSED: false, - processAll: processAllNamespaces, - byName: function(name) { - if (!Ember.BOOTED) { - processAllNamespaces(); - } - - return NAMESPACES_BY_ID[name]; - } -}); - -var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; - -var hasOwnProp = ({}).hasOwnProperty, - guidFor = Ember.guidFor; - -function processNamespace(paths, root, seen) { - var idx = paths.length; - - NAMESPACES_BY_ID[paths.join('.')] = root; - - // Loop over all of the keys in the namespace, looking for classes - for(var key in root) { - if (!hasOwnProp.call(root, key)) { continue; } - var obj = root[key]; - - // If we are processing the `Ember` namespace, for example, the - // `paths` will start with `["Ember"]`. Every iteration through - // the loop will update the **second** element of this list with - // the key, so processing `Ember.View` will make the Array - // `['Ember', 'View']`. - paths[idx] = key; - - // If we have found an unprocessed class - if (obj && obj.toString === classToString) { - // Replace the class' `toString` with the dot-separated path - // and set its `NAME_KEY` - obj.toString = makeToString(paths.join('.')); - obj[NAME_KEY] = paths.join('.'); - - // Support nested namespaces - } else if (obj && obj.isNamespace) { - // Skip aliased namespaces - if (seen[guidFor(obj)]) { continue; } - seen[guidFor(obj)] = true; - - // Process the child namespace - processNamespace(paths, obj, seen); - } - } - - paths.length = idx; // cut out last item -} - -function findNamespaces() { - var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; - - if (Namespace.PROCESSED) { return; } - - for (var prop in lookup) { - // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } - - // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. - // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage - if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } - // Unfortunately, some versions of IE don't support window.hasOwnProperty - if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } - - // At times we are not allowed to access certain properties for security reasons. - // There are also times where even if we can access them, we are not allowed to access their properties. - try { - obj = Ember.lookup[prop]; - isNamespace = obj && obj.isNamespace; - } catch (e) { - continue; - } - - if (isNamespace) { - Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); - obj[NAME_KEY] = prop; - } - } -} - -var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; - -function superClassString(mixin) { - var superclass = mixin.superclass; - if (superclass) { - if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } - else { return superClassString(superclass); } - } else { - return; - } -} - -function classToString() { - if (!Ember.BOOTED && !this[NAME_KEY]) { - processAllNamespaces(); - } - - var ret; - - if (this[NAME_KEY]) { - ret = this[NAME_KEY]; - } else { - var str = superClassString(this); - if (str) { - ret = "(subclass of " + str + ")"; - } else { - ret = "(unknown mixin)"; - } - this.toString = makeToString(ret); - } - - return ret; -} - -function processAllNamespaces() { - var unprocessedNamespaces = !Namespace.PROCESSED, - unprocessedMixins = Ember.anyUnprocessedMixins; - - if (unprocessedNamespaces) { - findNamespaces(); - Namespace.PROCESSED = true; - } - - if (unprocessedNamespaces || unprocessedMixins) { - var namespaces = Namespace.NAMESPACES, namespace; - for (var i=0, l=namespaces.length; i get(this, 'content.length')) throw new Error(OUT_OF_RANGE_EXCEPTION); + if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); this._replace(idx, 0, [object]); return this; }, @@ -13211,7 +17014,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array indices = [], i; if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); } if (len === undefined) len = 1; @@ -13295,168 +17098,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, - set = Ember.set, - fmt = Ember.String.fmt, - addBeforeObserver = Ember.addBeforeObserver, - addObserver = Ember.addObserver, - removeBeforeObserver = Ember.removeBeforeObserver, - removeObserver = Ember.removeObserver, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange; - -function contentPropertyWillChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyWillChange(this, key); -} - -function contentPropertyDidChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyDidChange(this, key); -} - -/** - `Ember.ObjectProxy` forwards all properties not defined by the proxy itself - to a proxied `content` object. - - ```javascript - object = Ember.Object.create({ - name: 'Foo' - }); - - proxy = Ember.ObjectProxy.create({ - content: object - }); - - // Access and change existing properties - proxy.get('name') // 'Foo' - proxy.set('name', 'Bar'); - object.get('name') // 'Bar' - - // Create new 'description' property on `object` - proxy.set('description', 'Foo is a whizboo baz'); - object.get('description') // 'Foo is a whizboo baz' - ``` - - While `content` is unset, setting a property to be delegated will throw an - Error. - - ```javascript - proxy = Ember.ObjectProxy.create({ - content: null, - flag: null - }); - proxy.set('flag', true); - proxy.get('flag'); // true - proxy.get('foo'); // undefined - proxy.set('foo', 'data'); // throws Error - ``` - - Delegated properties can be bound to and will change when content is updated. - - Computed properties on the proxy itself can depend on delegated properties. - - ```javascript - ProxyWithComputedProperty = Ember.ObjectProxy.extend({ - fullName: function () { - var firstName = this.get('firstName'), - lastName = this.get('lastName'); - if (firstName && lastName) { - return firstName + ' ' + lastName; - } - return firstName || lastName; - }.property('firstName', 'lastName') - }); - - proxy = ProxyWithComputedProperty.create(); - - proxy.get('fullName'); // undefined - proxy.set('content', { - firstName: 'Tom', lastName: 'Dale' - }); // triggers property change for fullName on proxy - - proxy.get('fullName'); // 'Tom Dale' - ``` - - @class ObjectProxy - @namespace Ember - @extends Ember.Object -*/ -Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype */ { - /** - The object whose properties will be forwarded. - - @property content - @type Ember.Object - @default null - */ - content: null, - _contentDidChange: Ember.observer(function() { - Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); - }, 'content'), - - isTruthy: Ember.computed.bool('content'), - - _debugContainerKey: null, - - willWatchProperty: function (key) { - var contentKey = 'content.' + key; - addBeforeObserver(this, contentKey, null, contentPropertyWillChange); - addObserver(this, contentKey, null, contentPropertyDidChange); - }, - - didUnwatchProperty: function (key) { - var contentKey = 'content.' + key; - removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); - removeObserver(this, contentKey, null, contentPropertyDidChange); - }, - - unknownProperty: function (key) { - var content = get(this, 'content'); - if (content) { - return get(content, key); - } - }, - - setUnknownProperty: function (key, value) { - var content = get(this, 'content'); - Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); - return set(content, key, value); - } - -}); - -Ember.ObjectProxy.reopenClass({ - create: function () { - var mixin, prototype, i, l, properties, keyName; - if (arguments.length) { - prototype = this.proto(); - for (i = 0, l = arguments.length; i < l; i++) { - properties = arguments[i]; - for (keyName in properties) { - if (!properties.hasOwnProperty(keyName) || keyName in prototype) { continue; } - if (!mixin) mixin = {}; - mixin[keyName] = null; - } - } - if (mixin) this._initMixins([mixin]); - } - return this._super.apply(this, arguments); - } -}); - -})(); - - - (function() { /** @module ember @@ -13465,7 +17106,8 @@ Ember.ObjectProxy.reopenClass({ var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; -var forEach = Ember.EnumerableUtils.forEach; +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.ArrayPolyfills.indexOf; var EachArray = Ember.Object.extend(Ember.Array, { @@ -13523,7 +17165,7 @@ function removeObserverForContentKey(content, keyName, proxy, idx, loc) { guid = guidFor(item); indicies = objects[guid]; - indicies[indicies.indexOf(loc)] = null; + indicies[indexOf.call(indicies, loc)] = null; } } } @@ -13672,7 +17314,7 @@ Ember.EachProxy = Ember.Object.extend({ */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; // Add Ember.Array to Array.prototype. Remove methods with native // implementations and supply some more optimized versions of generic methods @@ -13694,7 +17336,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember // primitive for array support. replace: function(idx, amt, objects) { - if (this.isFrozen) throw Ember.FROZEN_ERROR ; + if (this.isFrozen) throw Ember.FROZEN_ERROR; // if we replaced exactly the same number of items, then pass only the // replaced range. Otherwise, pass the full remaining array length @@ -13702,15 +17344,14 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember var len = objects ? get(objects, 'length') : 0; this.arrayContentWillChange(idx, amt, len); - if (!objects || objects.length === 0) { - this.splice(idx, amt) ; + if (len === 0) { + this.splice(idx, amt); } else { - var args = [idx, amt].concat(objects) ; - this.splice.apply(this,args) ; + replace(this, idx, amt, objects); } this.arrayContentDidChange(idx, amt, len); - return this ; + return this; }, // If you ask for an unknown property, then try to collect the value @@ -13787,7 +17428,26 @@ Ember.NativeArray = NativeArray; /** Creates an `Ember.NativeArray` from an Array like object. - Does not modify the original object. + Does not modify the original object. Ember.A is not needed if + `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, + it is recommended that you use Ember.A when creating addons for + ember or when you can not guarantee that `Ember.EXTEND_PROTOTYPES` + will be `true`. + + Example + + ```js + var Pagination = Ember.CollectionView.extend({ + tagName: 'ul', + classNames: ['pagination'], + init: function() { + this._super(); + if (!this.get('content')) { + this.set('content', Ember.A([])); + } + } + }); + ``` @method A @for Ember @@ -13800,7 +17460,17 @@ Ember.A = function(arr) { /** Activates the mixin on the Array.prototype if not already applied. Calling - this method more than once is safe. + this method more than once is safe. This will be called when ember is loaded + unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` + set to `false`. + + Example + + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); + } + ``` @method activate @for Ember.NativeArray @@ -13895,8 +17565,8 @@ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.is When using `Ember.Set`, you can observe the `"[]"` property to be alerted whenever the content changes. You can also add an enumerable observer to the set to be notified of specific objects that are added and - removed from the set. See `Ember.Enumerable` for more information on - enumerables. + removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) + for more information on enumerables. This is often unhelpful. If you are filtering sets of objects, for instance, it is very inefficient to re-filter all of the items each time the set @@ -13958,7 +17628,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb @return {Ember.Set} An empty Set */ clear: function() { - if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } + if (this.isFrozen) { throw new Ember.Error(Ember.FROZEN_ERROR); } var len = get(this, 'length'); if (len === 0) { return this; } @@ -14068,7 +17738,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb @return {Object} The removed object from the set or null. */ pop: function() { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); var obj = this.length > 0 ? this[this.length-1] : null; this.remove(obj); return obj; @@ -14185,7 +17855,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable addObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), @@ -14213,7 +17883,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable removeObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), @@ -14311,6 +17981,20 @@ var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; var loaded = {}; /** + +Detects when a specific package of Ember (e.g. 'Ember.Handlebars') +has fully loaded and is available for extension. + +The provided `callback` will be called with the `name` passed +resolved from a string into the object: + +```javascript +Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); +}); +``` + + @method onLoad @for Ember @param name {String} name of hook @@ -14328,6 +18012,10 @@ Ember.onLoad = function(name, callback) { }; /** + +Called when an Ember.js package (e.g Ember.Handlebars) has finished +loading. Triggers any callbacks registered for this event. + @method runLoadHooks @for Ember @param name {String} name of hook @@ -14366,39 +18054,18 @@ var get = Ember.get; compose Ember's controller layer: `Ember.Controller`, `Ember.ArrayController`, and `Ember.ObjectController`. - Within an `Ember.Router`-managed application single shared instaces of every - Controller object in your application's namespace will be added to the - application's `Ember.Router` instance. See `Ember.Application#initialize` - for additional information. - - ## Views - - By default a controller instance will be the rendering context - for its associated `Ember.View.` This connection is made during calls to - `Ember.ControllerMixin#connectOutlet`. - - Within the view's template, the `Ember.View` instance can be accessed - through the controller with `{{view}}`. - - ## Target Forwarding - - By default a controller will target your application's `Ember.Router` - instance. Calls to `{{action}}` within the template of a controller's view - are forwarded to the router. See `Ember.Handlebars.helpers.action` for - additional information. - @class ControllerMixin @namespace Ember */ -Ember.ControllerMixin = Ember.Mixin.create({ +Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { /* ducktype as a controller */ isController: true, /** - The object to which events from the view should be sent. + The object to which actions from the view should be sent. For example, when a Handlebars template uses the `{{action}}` helper, - it will attempt to send the event to the view's controller's `target`. + it will attempt to send the action to the view's controller's `target`. By default, a controller's `target` is set to the router after it is instantiated by `Ember.Application#initialize`. @@ -14416,16 +18083,16 @@ Ember.ControllerMixin = Ember.Mixin.create({ model: Ember.computed.alias('content'), - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, - if (this[actionName]) { - Ember.assert("The controller " + this + " does not have the action " + actionName, typeof this[actionName] === 'function'); - this[actionName].apply(this, args); - } else if (target = get(this, 'target')) { - Ember.assert("The target for controller " + this + " (" + target + ") did not define a `send` method", typeof target.send === 'function'); - target.send.apply(target, arguments); - } + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + this[actionName].apply(this, args); + return; } }); @@ -14474,6 +18141,29 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} ``` + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. + + ```javascript + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ``` + + SortableMixin works by sorting the arrangedContent array, which is the array that + arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that + array will not display the sorted list: + + ```javascript + songsController.get('content').get('firstObject'); // Returns the unsorted original content + songsController.get('firstObject'); // Returns the sorted content. + ``` + + Although the sorted content can also be accessed through the arrangedContent property, + it is preferable to use the proxied class and not the arrangedContent array directly. + @class SortableMixin @namespace Ember @uses Ember.MutableEnumerable @@ -14483,6 +18173,9 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** Specifies which properties dictate the arrangedContent's sort order. + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. + @property {Array} sortProperties */ sortProperties: null, @@ -14496,7 +18189,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** 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 + 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 @@ -14553,6 +18246,13 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { isSorted: Ember.computed.bool('sortProperties'), + /** + Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. + Also sets up observers for each sortProperty on each item in the content Array. + + @property arrangedContent + */ + arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { var content = get(this, 'content'), isSorted = get(this, 'isSorted'), @@ -14575,7 +18275,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { return content; }), - _contentWillChange: Ember.beforeObserver(function() { + _contentWillChange: Ember.beforeObserver('content', function() { var content = get(this, 'content'), sortProperties = get(this, 'sortProperties'); @@ -14588,18 +18288,18 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { } this._super(); - }, 'content'), + }), - sortAscendingWillChange: Ember.beforeObserver(function() { + sortAscendingWillChange: Ember.beforeObserver('sortAscending', function() { this._lastSortAscending = get(this, 'sortAscending'); - }, 'sortAscending'), + }), - sortAscendingDidChange: Ember.observer(function() { + sortAscendingDidChange: Ember.observer('sortAscending', function() { if (get(this, 'sortAscending') !== this._lastSortAscending) { var arrangedContent = get(this, 'arrangedContent'); arrangedContent.reverseObjects(); } - }, 'sortAscending'), + }), contentArrayWillChange: function(array, idx, removedCount, addedCount) { var isSorted = get(this, 'isSorted'); @@ -14870,11 +18570,15 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } this._super(); + this.set('_subControllers', Ember.A()); }, + content: Ember.computed(function () { + return Ember.A(); + }), + controllerAt: function(idx, object, controllerClass) { var container = get(this, 'container'), subControllers = get(this, '_subControllers'), @@ -14886,7 +18590,7 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, fullName = "controller:" + controllerClass; if (!container.has(fullName)) { - throw new Error('Could not resolve itemController: "' + controllerClass + '"'); + throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); } subController = container.lookupFactory(fullName).create({ @@ -14925,9 +18629,11 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, */ /** - `Ember.ObjectController` is part of Ember's Controller layer. + `Ember.ObjectController` is part of Ember's Controller layer. It is intended + to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying + content object, and to forward unhandled action attempts to its `target`. - `Ember.ObjectController` derives its functionality from its superclass + `Ember.ObjectController` derives this functionality from its superclass `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. @class ObjectController @@ -14964,7 +18670,11 @@ Ember Runtime @submodule ember-views */ -var jQuery = Ember.imports.jQuery; +var jQuery = this.jQuery || (Ember.imports && Ember.imports.jQuery); +if (!jQuery && typeof require === 'function') { + jQuery = require('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)); /** @@ -15206,14 +18916,32 @@ function escapeAttribute(value) { return string.replace(BAD_CHARS_REGEXP, escapeChar); } +// IE 6/7 have bugs arond setting names on inputs during creation. +// From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: +// "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." +var canSetNameOnInputs = (function() { + var div = document.createElement('div'), + el = document.createElement('input'); + + el.setAttribute('name', 'foo'); + div.appendChild(el); + + return !!div.innerHTML.match('foo'); +})(); + /** `Ember.RenderBuffer` gathers information regarding the a view and generates the final representation. `Ember.RenderBuffer` will generate HTML which can be pushed to the DOM. + ```javascript + var buffer = Ember.RenderBuffer('div'); + ``` + @class RenderBuffer @namespace Ember @constructor + @param {String} tagName tag name (such as 'div' or 'p') used for the buffer */ Ember.RenderBuffer = function(tagName) { return new Ember._RenderBuffer(tagName); @@ -15560,14 +19288,22 @@ Ember._RenderBuffer.prototype = generateElement: function() { var tagName = this.tagNames.pop(), // pop since we don't need to close - element = document.createElement(tagName), - $element = Ember.$(element), id = this.elementId, classes = this.classes, attrs = this.elementAttributes, props = this.elementProperties, style = this.elementStyle, - styleBuffer = '', attr, prop; + styleBuffer = '', attr, prop, tagString; + + if (attrs && attrs.name && !canSetNameOnInputs) { + // IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. + tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; + } else { + tagString = tagName; + } + + var element = document.createElement(tagString), + $element = Ember.$(element); if (id) { $element.attr('id', id); @@ -15928,7 +19664,7 @@ Ember.ControllerMixin.reopen({ set(this, '_childContainers', {}); }, - _modelDidChange: Ember.observer(function() { + _modelDidChange: Ember.observer('model', function() { var containers = get(this, '_childContainers'); for (var prop in containers) { @@ -15937,7 +19673,7 @@ Ember.ControllerMixin.reopen({ } set(this, '_childContainers', {}); - }, 'model') + }) }); })(); @@ -15962,6 +19698,7 @@ var get = Ember.get, set = Ember.set; var guidFor = Ember.guidFor; var a_forEach = Ember.EnumerableUtils.forEach; var a_addObject = Ember.EnumerableUtils.addObject; +var meta = Ember.meta; var childViewsProperty = Ember.computed(function() { var childViews = this._childViews, ret = Ember.A(), view = this; @@ -15982,7 +19719,7 @@ var childViewsProperty = Ember.computed(function() { Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); return view.replace(idx, removedCount, addedViews); } - throw new Error("childViews is immutable"); + throw new Ember.Error("childViews is immutable"); }; return ret; @@ -16002,7 +19739,13 @@ Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionali Ember.TEMPLATES = {}; /** - `Ember.CoreView` is + `Ember.CoreView` is an abstract class that exists to give view-like behavior + to both Ember's main view class `Ember.View` and other classes like + `Ember._SimpleMetamorphView` that don't need the fully functionaltiy of + `Ember.View`. + + Unless you have specific needs for `CoreView`, you will use `Ember.View` + in your applications. @class CoreView @namespace Ember @@ -16010,7 +19753,7 @@ Ember.TEMPLATES = {}; @uses Ember.Evented */ -Ember.CoreView = Ember.Object.extend(Ember.Evented, { +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { isView: true, states: states, @@ -16125,6 +19868,18 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { } }, + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, + + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + this[actionName].apply(this, args); + return; + }, + has: function(name) { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, @@ -16226,7 +19981,7 @@ var EMPTY_ARRAY = []; The default HTML tag name used for a view's DOM representation is `div`. This can be customized by setting the `tagName` property. The following view -class: + class: ```javascript ParagraphView = Ember.View.extend({ @@ -16400,8 +20155,8 @@ class: will be removed. Both `classNames` and `classNameBindings` are concatenated properties. See - `Ember.Object` documentation for more information about concatenated - properties. + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. ## HTML Attributes @@ -16461,7 +20216,7 @@ class: Updates to the the property of an attribute binding will result in automatic update of the HTML attribute in the view's rendered HTML representation. - `attributeBindings` is a concatenated property. See `Ember.Object` + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) documentation for more information about concatenated properties. ## Templates @@ -16504,9 +20259,6 @@ class: Using a value for `templateName` that does not have a Handlebars template with a matching `data-template-name` attribute will throw an error. - Assigning a value to both `template` and `templateName` properties will throw - an error. - For views classes that may have a template later defined (e.g. as the block portion of a `{{view}}` Handlebars helper call in another template or in a subclass), you can provide a `defaultTemplate` property set to compiled @@ -16612,7 +20364,8 @@ class: ``` - See `Handlebars.helpers.yield` for more information. + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. ## Responding to Browser Events @@ -16709,7 +20462,7 @@ class: ### Handlebars `{{action}}` Helper - See `Handlebars.helpers.action`. + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). ### Event Names @@ -16764,8 +20517,8 @@ class: ## Handlebars `{{view}}` Helper Other `Ember.View` instances can be included as part of a view's template by - using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for - additional information. + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. @class View @namespace Ember @@ -16791,9 +20544,8 @@ Ember.View = Ember.CoreView.extend( /** The name of the template to lookup if no template is provided. - `Ember.View` will look for a template with this name in this view's - `templates` object. By default, this will be a global object - shared in `Ember.TEMPLATES`. + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). @property templateName @type String @@ -16804,9 +20556,8 @@ Ember.View = Ember.CoreView.extend( /** The name of the layout to lookup if no layout is provided. - `Ember.View` will look for a template with this name in this view's - `templates` object. By default, this will be a global object - shared in `Ember.TEMPLATES`. + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). @property layoutName @type String @@ -16814,15 +20565,6 @@ Ember.View = Ember.CoreView.extend( */ layoutName: null, - /** - The hash in which to look for `templateName`. - - @property templates - @type Ember.Object - @default Ember.TEMPLATES - */ - templates: Ember.TEMPLATES, - /** The template used to render the view. This should be a function that accepts an optional context parameter and returns a string of HTML that @@ -16957,9 +20699,9 @@ Ember.View = Ember.CoreView.extend( @method _contextDidChange */ - _contextDidChange: Ember.observer(function() { + _contextDidChange: Ember.observer('context', function() { this.rerender(); - }, 'context'), + }), /** If `false`, the view will appear hidden in DOM. @@ -16986,21 +20728,21 @@ Ember.View = Ember.CoreView.extend( // When it's a virtual view, we need to notify the parent that their // childViews will change. - _childViewsWillChange: Ember.beforeObserver(function() { + _childViewsWillChange: Ember.beforeObserver('childViews', function() { if (this.isVirtual) { var parentView = get(this, 'parentView'); if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); } } - }, 'childViews'), + }), // When it's a virtual view, we need to notify the parent that their // childViews did change. - _childViewsDidChange: Ember.observer(function() { + _childViewsDidChange: Ember.observer('childViews', function() { if (this.isVirtual) { var parentView = get(this, 'parentView'); if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); } } - }, 'childViews'), + }), /** Return the nearest ancestor that is an instance of the provided @@ -17082,7 +20824,7 @@ Ember.View = Ember.CoreView.extend( @method _parentViewDidChange */ - _parentViewDidChange: Ember.observer(function() { + _parentViewDidChange: Ember.observer('_parentView', function() { if (this.isDestroying) { return; } this.trigger('parentViewDidChange'); @@ -17090,9 +20832,9 @@ Ember.View = Ember.CoreView.extend( if (get(this, 'parentView.controller') && !get(this, 'controller')) { this.notifyPropertyChange('controller'); } - }, '_parentView'), + }), - _controllerDidChange: Ember.observer(function() { + _controllerDidChange: Ember.observer('controller', function() { if (this.isDestroying) { return; } this.rerender(); @@ -17100,7 +20842,7 @@ Ember.View = Ember.CoreView.extend( this.forEachChildView(function(view) { view.propertyDidChange('controller'); }); - }, 'controller'), + }), cloneKeywords: function() { var templateData = get(this, 'templateData'); @@ -17656,8 +21398,8 @@ Ember.View = Ember.CoreView.extend( If you write a `willDestroyElement()` handler, you can assume that your `didInsertElement()` handler was called earlier for the same element. - Normally you will not call or override this method yourself, but you may - want to implement the above callbacks when it is run. + You should not call or override this method yourself, but you may + want to implement the above callbacks. @method destroyElement @return {Ember.View} receiver @@ -17694,12 +21436,6 @@ Ember.View = Ember.CoreView.extend( return viewCollection; }, - _elementWillChange: Ember.beforeObserver(function() { - this.forEachChildView(function(view) { - Ember.propertyWillChange(view, 'element'); - }); - }, 'element'), - /** @private @@ -17709,11 +21445,11 @@ Ember.View = Ember.CoreView.extend( @method _elementDidChange */ - _elementDidChange: Ember.observer(function() { + _elementDidChange: Ember.observer('element', function() { this.forEachChildView(function(view) { - Ember.propertyDidChange(view, 'element'); + delete meta(view).cache.element; }); - }, 'element'), + }), /** Called when the parentView property has changed. @@ -17806,7 +21542,7 @@ Ember.View = Ember.CoreView.extend( visually challenged users navigate rich web applications. The full list of valid WAI-ARIA roles is available at: - http://www.w3.org/TR/wai-aria/roles#roles_categorization + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) @property ariaRole @type String @@ -18038,6 +21774,10 @@ Ember.View = Ember.CoreView.extend( @return {Ember.View} new instance */ createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + if (view.isView && view._parentView === this && view.container === this.container) { return view; } @@ -18090,7 +21830,7 @@ Ember.View = Ember.CoreView.extend( @method _isVisibleDidChange */ - _isVisibleDidChange: Ember.observer(function() { + _isVisibleDidChange: Ember.observer('isVisible', function() { var $el = this.$(); if (!$el) { return; } @@ -18105,7 +21845,7 @@ Ember.View = Ember.CoreView.extend( } else { this._notifyBecameHidden(); } - }, 'isVisible'), + }), _notifyBecameVisible: function() { this.trigger('becameVisible'); @@ -18155,6 +21895,7 @@ Ember.View = Ember.CoreView.extend( if (priorState && priorState.exit) { priorState.exit(this); } if (currentState.enter) { currentState.enter(this); } + if (state === 'inDOM') { delete Ember.meta(this).cache.element; } if (children !== false) { this.forEachChildView(function(view) { @@ -18186,6 +21927,10 @@ Ember.View = Ember.CoreView.extend( target = null; } + if (!root || typeof root !== 'object') { + return; + } + var view = this, stateCheckedObserver = function() { view.currentState.invokeObserver(this, observer); @@ -18281,7 +22026,7 @@ Ember.View.reopenClass({ Parse a path and return an object which holds the parsed properties. - For example a path like "content.isEnabled:enabled:disabled" wil return the + For example a path like "content.isEnabled:enabled:disabled" will return the following object: ```javascript @@ -18502,10 +22247,18 @@ Ember.merge(preRender, { var viewCollection = view.viewHierarchyCollection(); viewCollection.trigger('willInsertElement'); - // after createElement, the view will be in the hasElement state. + fn.call(view); - viewCollection.transitionTo('inDOM', false); - viewCollection.trigger('didInsertElement'); + + // We transition to `inDOM` if the element exists in the DOM + var element = view.get('element'); + while (element = element.parentNode) { + if (element === document) { + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); + } + } + }, renderToBufferIfNeeded: function(view, buffer) { @@ -18714,7 +22467,7 @@ Ember.merge(inDOM, { } view.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); + throw new Ember.Error("Changing a view's elementId after creation is not allowed"); }); }, @@ -18954,30 +22707,6 @@ var ViewCollection = Ember._ViewCollection; or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM representation will only be the rendered HTML of its child views. - ## Binding a View to Display - - If you would like to display a single view in your ContainerView, you can set - its `currentView` property. When the `currentView` property is set to a view - instance, it will be added to the ContainerView. If the `currentView` property - is later changed to a different view, the new view will replace the old view. - If `currentView` is set to `null`, the last `currentView` will be removed. - - This functionality is useful for cases where you want to bind the display of - a ContainerView to a controller or state manager. For example, you can bind - the `currentView` of a container to a controller like this: - - ```javascript - App.appController = Ember.Object.create({ - view: Ember.View.create({ - templateName: 'person_template' - }) - }); - ``` - - ```handlebars - {{view Ember.ContainerView currentViewBinding="App.appController.view"}} - ``` - @class ContainerView @namespace Ember @extends Ember.View @@ -19044,7 +22773,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { length: Ember.computed(function () { return this._childViews.length; - }), + }).volatile(), /** @private @@ -19134,20 +22863,20 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { currentView: null, - _currentViewWillChange: Ember.beforeObserver(function() { + _currentViewWillChange: Ember.beforeObserver('currentView', function() { var currentView = get(this, 'currentView'); if (currentView) { currentView.destroy(); } - }, 'currentView'), + }), - _currentViewDidChange: Ember.observer(function() { + _currentViewDidChange: Ember.observer('currentView', function() { var currentView = get(this, 'currentView'); if (currentView) { Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); this.pushObject(currentView); } - }, 'currentView'), + }), _ensureChildrenAreInDOM: function () { this.currentState.ensureChildrenAreInDOM(this); @@ -19162,7 +22891,7 @@ Ember.merge(states._default, { Ember.merge(states.inBuffer, { childViewsDidChange: function(parentView, views, start, added) { - throw new Error('You cannot modify child views while in the inBuffer state'); + throw new Ember.Error('You cannot modify child views while in the inBuffer state'); } }); @@ -19374,11 +23103,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; manipulated. Instead, add, remove, replace items from its `content` property. This will trigger appropriate changes to its rendered HTML. - ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper - - `Ember.Handlebars` provides a helper specifically for adding - `CollectionView`s to templates. See `Ember.Handlebars.collection` for more - details @class CollectionView @namespace Ember @@ -19423,19 +23147,32 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie */ itemViewClass: Ember.View, + /** + Setup a CollectionView + + @method init + */ init: function() { var ret = this._super(); this._contentDidChange(); return ret; }, - _contentWillChange: Ember.beforeObserver(function() { + /** + @private + + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver('content', function() { var content = this.get('content'); if (content) { content.removeArrayObserver(this); } var len = content ? get(content, 'length') : 0; this.arrayWillChange(content, 0, len); - }, 'content'), + }), /** @private @@ -19447,7 +23184,7 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie @method _contentDidChange */ - _contentDidChange: Ember.observer(function() { + _contentDidChange: Ember.observer('content', function() { var content = get(this, 'content'); if (content) { @@ -19457,12 +23194,24 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie var len = content ? get(content, 'length') : 0; this.arrayDidChange(content, 0, null, len); - }, 'content'), + }), + /** + @private + + Ensure that the content implements Ember.Array + + @method _assertArrayLike + */ _assertArrayLike: function(content) { Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); }, + /** + Removes the content and content observers. + + @method destroy + */ destroy: function() { if (!this._super()) { return; } @@ -19476,6 +23225,19 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie return this; }, + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ arrayWillChange: function(content, start, removedCount) { // If the contents were empty before and this template collection has an // empty view remove it now. @@ -19566,6 +23328,21 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie this.replace(start, 0, addedViews); }, + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ createChildView: function(view, attrs) { view = this._super(view, attrs); @@ -19606,7 +23383,9 @@ Ember.CollectionView.CONTAINER_MAP = { (function() { -var get = Ember.get, set = Ember.set, isNone = Ember.isNone; +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, + a_slice = Array.prototype.slice; + /** @module ember @@ -19634,7 +23413,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; ```html

    {{person.title}}

    - +

    {{person.signature}}

    ``` @@ -19656,15 +23435,18 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; 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. + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. For example, you could implement the action `hello` for the `app-profile` component: - ```js + ```javascript App.AppProfileComponent = Ember.Component.extend({ - hello: function(name) { - console.log("Hello", name) + actions: { + hello: function(name) { + console.log("Hello", name); + } } }); ``` @@ -19717,7 +23499,8 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { view.appendChild(Ember.View, { isVirtual: true, tagName: '', - template: get(this, 'template'), + _contextView: parentView, + template: template, context: get(parentView, 'context'), controller: get(parentView, 'controller'), templateData: { keywords: parentView.cloneKeywords() } @@ -19725,66 +23508,102 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { } }, + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ targetObject: Ember.computed(function(key) { var parentView = get(this, '_parentView'); return parentView ? get(parentView, 'controller') : null; }).property('_parentView'), /** - Sends an action to component's controller. A component inherits its - controller from the context in which it is used. + Triggers a named action on the controller context where the component is used if + this controller has registered for notifications of the action. - By default, calling `sendAction()` will send an action with the name - of the component's `action` property. + For example a component for playing or pausing music may translate click events + into action notifications of "play" or "stop" depending on some internal state + of the component: - For example, if the component had a property `action` with the value - `"addItem"`, calling `sendAction()` would send the `addItem` action - to the component's controller. - - If you provide the `action` argument to `sendAction()`, that key will - be used to look up the action name. - - For example, if the component had a property `playing` with the value - `didStartPlaying`, calling `sendAction('playing')` would send the - `didStartPlaying` action to the component's controller. - - Whether or not you are using the default action or a named action, if - the action name is not defined on the component, calling `sendAction()` - does not have any effect. - - For example, if you call `sendAction()` on a component that does not have - an `action` property defined, no action will be sent to the controller, - nor will an exception be raised. - - You can send a context object with the action by supplying the `context` - argument. The context will be supplied as the first argument in the - target's action method. Example: ```javascript - App.MyTree = Ember.Component.extend({ - click: function() { - this.sendAction('didClickTreeNode', this.get('node')); + App.PlayButtonComponent = Ember.Component.extend({ + click: function(){ + if (this.get('isPlaying')) { + this.triggerAction('play'); + } else { + this.triggerAction('stop'); + } } }); + ``` - App.CategoriesController = Ember.Controller.extend({ - didClickCategory: function(category) { - //Do something with the node/category that was clicked + When used inside a template these component actions are configured to + trigger actions in the outer application context: + + ```handlebars + {{! application.hbs }} + {{play-button play="musicStarted" stop="musicStopped"}} + ``` + + When the component receives a browser `click` event it translate this + interaction into application-specific semantics ("play" or "stop") and + triggers the specified action name on the controller for the template + where the component is used: + + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + musicStarted: function(){ + // called when the play button is clicked + // and the music started playing + }, + musicStopped: function(){ + // called when the play button is clicked + // and the music stopped playing + } + } + }); + ``` + + If no action name is passed to `sendAction` a default name of "action" + is assumed. + + ```javascript + App.NextButtonComponent = Ember.Component.extend({ + click: function(){ + this.sendAction(); } }); ``` ```handlebars - {{! categories.hbs}} - {{my-tree didClickTreeNode='didClickCategory'}} + {{! application.hbs }} + {{next-button action="playNextSongInAlbum"}} + ``` + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + playNextSongInAlbum: function(){ + ... + } + } + }); ``` @method sendAction @param [action] {String} the action to trigger @param [context] {*} a context to send with the action */ - sendAction: function(action, context) { - var actionName; + sendAction: function(action) { + var actionName, + contexts = a_slice.call(arguments, 1); // Send the default action if (action === undefined) { @@ -19800,7 +23619,7 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { this.triggerAction({ action: actionName, - actionContext: context + actionContext: contexts }); } }); @@ -20013,6 +23832,14 @@ define("metamorph", range.insertNode(fragment); }; + /** + * @public + * + * Remove this object (including starting and ending + * placeholders). + * + * @method remove + */ removeFunc = function() { // get a range for the current metamorph object including // the starting and ending placeholders. @@ -20053,7 +23880,7 @@ define("metamorph", }; } else { - /** + /* * This code is mostly taken from jQuery, with one exception. In jQuery's case, we * have some HTML and we need to figure out how to convert it into some nodes. * @@ -20107,12 +23934,12 @@ define("metamorph", } }; - /** + /* * Given a parent node and some HTML, generate a set of nodes. Return the first * node, which will allow us to traverse the rest using nextSibling. * * We need to do this because innerHTML in IE does not really parse the nodes. - **/ + */ var firstNodeFor = function(parentNode, html) { var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; var depth = arr[0], start = arr[1], end = arr[2]; @@ -20145,7 +23972,7 @@ define("metamorph", return element; }; - /** + /* * In some cases, Internet Explorer can create an anonymous node in * the hierarchy with no tagName. You can create this scenario via: * @@ -20155,7 +23982,7 @@ define("metamorph", * * If our script markers are inside such a node, we need to find that * node and use *it* as the marker. - **/ + */ var realNode = function(start) { while (start.parentNode.tagName === "") { start = start.parentNode; @@ -20164,7 +23991,7 @@ define("metamorph", return start; }; - /** + /* * When automatically adding a tbody, Internet Explorer inserts the * tbody immediately before the first . Other browsers create it * before the first node, no matter what. @@ -20191,7 +24018,8 @@ define("metamorph", * * This code reparents the first script tag by making it the tbody's * first child. - **/ + * + */ var fixParentage = function(start, end) { if (start.parentNode !== end.parentNode) { end.parentNode.insertBefore(start, end.parentNode.firstChild); @@ -20371,8 +24199,8 @@ if (!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -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); +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1. 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 or 1.1, 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 @@ -20390,20 +24218,6 @@ Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVIS */ Ember.Handlebars = objectCreate(Handlebars); -function makeBindings(options) { - var hash = options.hash, - hashType = options.hashTypes; - - for (var prop in hash) { - if (hashType[prop] === 'ID') { - hash[prop + 'Binding'] = hash[prop]; - hashType[prop + 'Binding'] = 'STRING'; - delete hash[prop]; - delete hashType[prop]; - } - } -} - /** Register a bound helper or custom view helper. @@ -20458,28 +24272,34 @@ 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 - }); - } - } + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); if (Ember.View.detect(value)) { - Ember.Handlebars.registerHelper(name, function(options) { - Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View", arguments.length < 2); - makeBindings(options); - return Ember.Handlebars.helpers.view.call(this, value, options); - }); + Ember.Handlebars.registerHelper(name, Ember.Handlebars.makeViewHelper(value)); } else { Ember.Handlebars.registerBoundHelper.apply(null, arguments); } }; +/** + @private + + Returns a helper function that renders the provided ViewClass. + + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. + + @method helper + @for Ember.Handlebars + @param {Function} ViewClass view class constructor +*/ +Ember.Handlebars.makeViewHelper = function(ViewClass) { + return function(options) { + Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View", arguments.length < 2); + return Ember.Handlebars.helpers.view.call(this, ViewClass, options); + }; +}; + /** @class helpers @namespace Ember.Handlebars @@ -20520,7 +24340,6 @@ if (Handlebars.JavaScriptCompiler) { Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { return "''"; }; @@ -20539,6 +24358,43 @@ Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) return "data.buffer.push("+string+");"; }; +// Hacks ahead: +// Handlebars presently has a bug where the `blockHelperMissing` hook +// doesn't get passed the name of the missing helper name, but rather +// gets passed the value of that missing helper evaluated on the current +// context, which is most likely `undefined` and totally useless. +// +// So we alter the compiled template function to pass the name of the helper +// instead, as expected. +// +// This can go away once the following is closed: +// https://github.com/wycats/handlebars.js/issues/617 + +var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, + BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, + INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; + +Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + var helperInvocation = source[source.length - 1], + helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], + matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); + + source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; +} +var stringifyBlockHelperMissing = Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; + +var originalBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.blockValue; +Ember.Handlebars.JavaScriptCompiler.prototype.blockValue = function() { + originalBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; + +var originalAmbiguousBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; +Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { + originalAmbiguousBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; + var prefix = "ember" + (+new Date()), incr = 1; /** @@ -20633,7 +24489,8 @@ if (Handlebars.compile) { })(); (function() { -var slice = Array.prototype.slice; +var slice = Array.prototype.slice, + originalTemplate = Ember.Handlebars.template; /** @private @@ -20709,7 +24566,6 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { } return value; }; -Ember.Handlebars.getPath = Ember.deprecateFunc('`Ember.Handlebars.getPath` has been changed to `Ember.Handlebars.get` for consistency.', Ember.Handlebars.get); Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; @@ -20761,9 +24617,20 @@ Ember.Handlebars.resolveHash = function(context, hash, options) { @param {String} path @param {Hash} options */ -Ember.Handlebars.registerHelper('helperMissing', function(path, options) { +Ember.Handlebars.registerHelper('helperMissing', function(path) { var error, view = ""; + var options = arguments[arguments.length - 1]; + + + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, path); + + if (helper) { + return helper.apply(this, slice.call(arguments, 1)); + } + + error = "%@ Handlebars error: Could not find property '%@' on object %@."; if (options.data) { view = options.data.view; @@ -20771,6 +24638,39 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); }); +/** + @private + + Registers a helper in Handlebars that will be called if no property with the + given name can be found on the current context object, and no helper with + that name is registered. + + This throws an exception with a more helpful error message so the user can + track down where the problem is happening. + + @method helperMissing + @for Ember.Handlebars.helpers + @param {String} path + @param {Hash} options +*/ +Ember.Handlebars.registerHelper('blockHelperMissing', function(path) { + + var options = arguments[arguments.length - 1]; + + + + Ember.assert("`blockHelperMissing` was invoked without a helper name, which is most likely due to a mismatch between the version of Ember.js you're running now and the one used to precompile your templates. Please make sure the version of `ember-handlebars-compiler` you're using is up to date.", path); + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, path); + + if (helper) { + return helper.apply(this, slice.call(arguments, 1)); + } + + + return Handlebars.helpers.blockHelperMissing.apply(this, arguments); +}); + /** Register a bound handlebars helper. Bound helpers behave similarly to regular handlebars helpers, with the added ability to re-render when the underlying data @@ -20881,68 +24781,141 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { @param {String} dependentKeys* */ Ember.Handlebars.registerBoundHelper = function(name, fn) { - var dependentKeys = slice.call(arguments, 2); + var boundHelperArgs = slice.call(arguments, 1), + boundFn = Ember.Handlebars.makeBoundHelper.apply(this, boundHelperArgs); + Ember.Handlebars.registerHelper(name, boundFn); +}; + +/** + @private + + A (mostly) private helper function to `registerBoundHelper`. Takes the + provided Handlebars helper function fn and returns it in wrapped + bound helper form. + + The main use case for using this outside of `registerBoundHelper` + is for registering helpers on the container: + + ```js + var boundHelperFn = Ember.Handlebars.makeBoundHelper(function(word) { + return word.toUpperCase(); + }); + + container.register('helper:my-bound-helper', boundHelperFn); + ``` + + In the above example, if the helper function hadn't been wrapped in + `makeBoundHelper`, the registered helper would be unbound. + + @method makeBoundHelper + @for Ember.Handlebars + @param {Function} function + @param {String} dependentKeys* +*/ +Ember.Handlebars.makeBoundHelper = function(fn) { + var dependentKeys = slice.call(arguments, 1); function helper() { var properties = slice.call(arguments, 0, -1), numProperties = properties.length, options = arguments[arguments.length - 1], normalizedProperties = [], + types = options.types, data = options.data, hash = options.hash, view = data.view, - currentContext = (options.contexts && options.contexts[0]) || this, - normalized, - pathRoot, path, prefixPathForDependentKeys = '', - loc, hashOption; + contexts = options.contexts, + currentContext = (contexts && contexts.length) ? contexts[0] : this, + prefixPathForDependentKeys = '', + loc, len, hashOption, + boundOption, property, + normalizedValue = Ember._SimpleHandlebarsView.prototype.normalizedValue; Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); // Detect bound options (e.g. countBinding="otherCount") - hash.boundOptions = {}; + var boundOptions = hash.boundOptions = {}; for (hashOption in hash) { - if (!hash.hasOwnProperty(hashOption)) { continue; } - - if (Ember.IS_BINDING.test(hashOption) && typeof hash[hashOption] === 'string') { + if (Ember.IS_BINDING.test(hashOption)) { // Lop off 'Binding' suffix. - hash.boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; + boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; } } // Expose property names on data.properties object. + var watchedProperties = []; data.properties = []; for (loc = 0; loc < numProperties; ++loc) { data.properties.push(properties[loc]); - normalizedProperties.push(normalizePath(currentContext, properties[loc], data)); + if (types[loc] === 'ID') { + var normalizedProp = normalizePath(currentContext, properties[loc], data); + normalizedProperties.push(normalizedProp); + watchedProperties.push(normalizedProp); + } else { + normalizedProperties.push(null); + } } + // Handle case when helper invocation is preceded by `unbound`, e.g. + // {{unbound myHelper foo}} if (data.isUnbound) { return evaluateUnboundHelper(this, fn, normalizedProperties, options); } - if (dependentKeys.length === 0) { - return evaluateMultiPropertyBoundHelper(currentContext, fn, normalizedProperties, options); - } - - Ember.assert("Dependent keys can only be used with single-property helpers.", properties.length === 1); - - normalized = normalizedProperties[0]; - - pathRoot = normalized.root; - path = normalized.path; - - var bindView = new Ember._SimpleHandlebarsView( - path, pathRoot, !options.hash.unescaped, options.data - ); + var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); + // Override SimpleHandlebarsView's method for generating the view's content. bindView.normalizedValue = function() { - var value = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - return fn.call(view, value, options); + var args = [], boundOption; + + // Copy over bound hash options. + for (boundOption in boundOptions) { + if (!boundOptions.hasOwnProperty(boundOption)) { continue; } + property = normalizePath(currentContext, boundOptions[boundOption], data); + bindView.path = property.path; + bindView.pathRoot = property.root; + hash[boundOption] = normalizedValue.call(bindView); + } + + for (loc = 0; loc < numProperties; ++loc) { + property = normalizedProperties[loc]; + if (property) { + bindView.path = property.path; + bindView.pathRoot = property.root; + args.push(normalizedValue.call(bindView)); + } else { + args.push(properties[loc]); + } + } + args.push(options); + + // Run the supplied helper function. + return fn.apply(currentContext, args); }; view.appendChild(bindView); - view.registerObserver(pathRoot, path, bindView, bindView.rerender); + // Assemble list of watched properties that'll re-render this helper. + for (boundOption in boundOptions) { + if (boundOptions.hasOwnProperty(boundOption)) { + watchedProperties.push(normalizePath(currentContext, boundOptions[boundOption], data)); + } + } + + // Observe each property. + for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { + property = watchedProperties[loc]; + view.registerObserver(property.root, property.path, bindView, bindView.rerender); + } + + if (types[0] !== 'ID' || normalizedProperties.length === 0) { + return; + } + + // Add dependent key observers to the first param + var normalized = normalizedProperties[0], + pathRoot = normalized.root, + path = normalized.path; if(!Ember.isEmpty(path)) { prefixPathForDependentKeys = path + '.'; @@ -20953,71 +24926,9 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { } helper._rawFunction = fn; - Ember.Handlebars.registerHelper(name, helper); + return helper; }; -/** - @private - - Renders the unbound form of an otherwise bound helper function. - - @method evaluateMultiPropertyBoundHelper - @param {Function} fn - @param {Object} context - @param {Array} normalizedProperties - @param {String} options -*/ -function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, options) { - var numProperties = normalizedProperties.length, - data = options.data, - view = data.view, - hash = options.hash, - boundOptions = hash.boundOptions, - watchedProperties, - boundOption, bindView, loc, property, len; - - bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); - bindView.normalizedValue = function() { - var args = [], boundOption; - - // Copy over bound options. - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - property = normalizePath(context, boundOptions[boundOption], data); - bindView.path = property.path; - bindView.pathRoot = property.root; - hash[boundOption] = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - } - - for (loc = 0; loc < numProperties; ++loc) { - property = normalizedProperties[loc]; - bindView.path = property.path; - bindView.pathRoot = property.root; - args.push(Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView)); - } - args.push(options); - return fn.apply(context, args); - }; - - view.appendChild(bindView); - - // Assemble list of watched properties that'll re-render this helper. - watchedProperties = []; - for (boundOption in boundOptions) { - if (boundOptions.hasOwnProperty(boundOption)) { - watchedProperties.push(normalizePath(context, boundOptions[boundOption], data)); - } - } - watchedProperties = watchedProperties.concat(normalizedProperties); - - // Observe each property. - for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { - property = watchedProperties[loc]; - view.registerObserver(property.root, property.path, bindView, bindView.rerender); - } - -} - /** @private @@ -21039,7 +24950,7 @@ function evaluateUnboundHelper(context, fn, normalizedProperties, options) { for(loc = 0, len = normalizedProperties.length; loc < len; ++loc) { property = normalizedProperties[loc]; - args.push(Ember.Handlebars.get(context, property.path, options)); + args.push(Ember.Handlebars.get(property.root, property.path, options)); } args.push(options); return fn.apply(context, args); @@ -21053,10 +24964,10 @@ function evaluateUnboundHelper(context, fn, normalizedProperties, options) { @method template @for Ember.Handlebars - @param {String} template spec + @param {String} spec */ Ember.Handlebars.template = function(spec) { - var t = Handlebars.template(spec); + var t = originalTemplate(spec); t.isTop = true; return t; }; @@ -21067,19 +24978,19 @@ Ember.Handlebars.template = function(spec) { (function() { /** - * Mark a string as safe for unescaped output with Handlebars. If you - * return HTML from a Handlebars helper, use this function to - * ensure Handlebars does not escape the HTML. - * - * ```javascript - * Ember.String.htmlSafe('
    someString
    ') - * ``` - * - * @method htmlSafe - * @for Ember.String - * @static - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as safe for unescaped output with Handlebars. If you + return HTML from a Handlebars helper, use this function to + ensure Handlebars does not escape the HTML. + + ```javascript + Ember.String.htmlSafe('
    someString
    ') + ``` + + @method htmlSafe + @for Ember.String + @static + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars +*/ Ember.String.htmlSafe = function(str) { return new Handlebars.SafeString(str); }; @@ -21089,18 +25000,18 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - * Mark a string as being safe for unescaped output with Handlebars. - * - * ```javascript - * '
    someString
    '.htmlSafe() - * ``` - * - * See `Ember.String.htmlSafe`. - * - * @method htmlSafe - * @for String - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as being safe for unescaped output with Handlebars. + + ```javascript + '
    someString
    '.htmlSafe() + ``` + + See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_htmlSafe). + + @method htmlSafe + @for String + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars + */ String.prototype.htmlSafe = function() { return htmlSafe(this); }; @@ -21280,6 +25191,8 @@ function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) { this.morph = Metamorph(); this.state = 'preRender'; this.updateId = null; + this._parentView = null; + this.buffer = null; } Ember._SimpleHandlebarsView = SimpleHandlebarsView; @@ -21293,7 +25206,11 @@ SimpleHandlebarsView.prototype = { Ember.run.cancel(this.updateId); this.updateId = null; } + if (this._parentView) { + this._parentView.removeChild(this); + } this.morph = null; + this.state = 'destroyed'; }, propertyWillChange: Ember.K, @@ -21348,7 +25265,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroying': + case 'destroyed': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -21677,16 +25594,18 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer } } -function simpleBind(property, options) { +EmberHandlebars.bind = bind; + +function simpleBind(currentContext, property, options) { var data = options.data, view = data.view, - currentContext = this, - normalized, observer; + normalized, observer, pathRoot, output; normalized = normalizePath(currentContext, property, data); + pathRoot = normalized.root; // Set up observers for observable objects - if ('object' === typeof this) { + if (pathRoot && ('object' === typeof pathRoot)) { if (data.insideGroup) { observer = function() { Ember.run.once(view, 'rerender'); @@ -21718,14 +25637,15 @@ function simpleBind(property, options) { } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGet(currentContext, property, options)); + output = handlebarsGet(currentContext, property, options); + data.buffer.push((output === null || typeof output === 'undefined') ? '' : output); } } /** @private - '_triageMustache' is used internally select between a binding and helper for + '_triageMustache' is used internally select between a binding, helper, or component for the given context. Until this point, it would be hard to determine if the mustache is a property reference or a regular helper reference. This triage helper resolves that. @@ -21735,19 +25655,47 @@ function simpleBind(property, options) { @method _triageMustache @for Ember.Handlebars.helpers @param {String} property Property/helperID to triage - @param {Function} fn Context to provide for rendering + @param {Object} options hash of template/rendering options @return {String} HTML string */ -EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { +EmberHandlebars.registerHelper('_triageMustache', function(property, options) { Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); + if (helpers[property]) { - return helpers[property].call(this, fn); - } - else { - return helpers.bind.apply(this, arguments); + return helpers[property].call(this, options); } + + + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, property); + if (helper) { + return helper.call(this, options); + } + + + return helpers.bind.call(this, property, options); }); +Ember.Handlebars.resolveHelper = function(container, name) { + + if (!container || name.indexOf('-') === -1) { + return; + } + + var helper = container.lookup('helper:' + name); + if (!helper) { + var componentLookup = container.lookup('component-lookup:main'); + Ember.assert("Could not find 'component-lookup:main' on the provided container, which is necessary for performing component lookups", componentLookup); + + var Component = componentLookup.lookupFactory(name, container); + if (Component) { + helper = EmberHandlebars.makeViewHelper(Component); + container.register('helper:' + name, helper); + } + } + return helper; +}; + /** @private @@ -21775,10 +25723,10 @@ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { EmberHandlebars.registerHelper('bind', function(property, options) { Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); - var context = (options.contexts && options.contexts[0]) || this; + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; if (!options.fn) { - return simpleBind.call(context, property, options); + return simpleBind(context, property, options); } return bind.call(context, property, options, false, exists); @@ -21803,7 +25751,7 @@ EmberHandlebars.registerHelper('bind', function(property, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('boundIf', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; var func = function(result) { var truthy = result && get(result, 'isTruthy'); if (typeof truthy === 'boolean') { return truthy; } @@ -21863,7 +25811,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { /** - See `boundIf` + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) @method if @for Ember.Handlebars.helpers @@ -21898,11 +25846,11 @@ EmberHandlebars.registerHelper('unless', function(context, options) { }); /** - `bindAttr` allows you to create a binding between DOM element attributes and + `bind-attr` allows you to create a binding between DOM element attributes and Ember objects. For example: ```handlebars - imageTitle + imageTitle ``` The above handlebars template will fill the ``'s `src` attribute will @@ -21924,26 +25872,26 @@ EmberHandlebars.registerHelper('unless', function(context, options) { A humorous image of a cat ``` - `bindAttr` cannot redeclare existing DOM element attributes. The use of `src` - in the following `bindAttr` example will be ignored and the hard coded value + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value of `src="/failwhale.gif"` will take precedence: ```handlebars - imageTitle + imageTitle ``` - ### `bindAttr` and the `class` attribute + ### `bind-attr` and the `class` attribute - `bindAttr` supports a special syntax for handling a number of cases unique + `bind-attr` supports a special syntax for handling a number of cases unique to the `class` DOM element attribute. The `class` attribute combines - multiple discreet values into a single attribute as a space-delimited + multiple discrete values into a single attribute as a space-delimited list of strings. Each string can be: * a string return value of an object's property. * a boolean return value of an object's property * a hard-coded value - A string return value works identically to other uses of `bindAttr`. The + A string return value works identically to other uses of `bind-attr`. The return value of the property will become the value of the attribute. For example, the following view and template: @@ -21956,7 +25904,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - ``` Result in the following rendered output: @@ -21978,7 +25926,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - + ``` Result in the following rendered output: @@ -21992,14 +25940,14 @@ EmberHandlebars.registerHelper('unless', function(context, options) { value changes: ```handlebars - + ``` A hard-coded value can be used by prepending `:` to the desired class name: `:class-name-to-always-apply`. ```handlebars - + ``` Results in the following rendered output: @@ -22012,19 +25960,19 @@ EmberHandlebars.registerHelper('unless', function(context, options) { hard-coded value – can be combined in a single declaration: ```handlebars - + ``` - @method bindAttr + @method bind-attr @for Ember.Handlebars.helpers @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('bindAttr', function(options) { +EmberHandlebars.registerHelper('bind-attr', function(options) { var attrs = options.hash; - Ember.assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length); + Ember.assert("You must specify at least one hash argument to bind-attr", !!Ember.keys(attrs).length); var view = options.data.view; var ret = []; @@ -22086,7 +26034,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // When the observer fires, find the element using the // unique data id and update the attribute to the new value. // Note: don't add observer when path is 'this' or path - // is whole keyword e.g. {{#each x in list}} ... {{bindAttr attr="x"}} + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { view.registerObserver(normalized.root, normalized.path, observer); } @@ -22106,6 +26054,18 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { return new EmberHandlebars.SafeString(ret.join(' ')); }); +/** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); + /** @private @@ -22241,9 +26201,38 @@ var EmberHandlebars = Ember.Handlebars; var LOWERCASE_A_Z = /^[a-z]/; var VIEW_PREFIX = /^view\./; +function makeBindings(thisContext, options) { + var hash = options.hash, + hashType = options.hashTypes; + + for (var prop in hash) { + if (hashType[prop] === 'ID') { + + var value = hash[prop]; + + if (Ember.IS_BINDING.test(prop)) { + Ember.warn("You're attempting to render a view by passing " + prop + "=" + value + " to a view helper, but this syntax is ambiguous. You should either surround " + value + " in quotes or remove `Binding` from " + prop + "."); + } else { + hash[prop + 'Binding'] = value; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; + } + } + } + + if (hash.hasOwnProperty('idBinding')) { + // id can't be bound, so just perform one-time lookup. + hash.id = EmberHandlebars.get(thisContext, hash.idBinding, options); + hashType.id = 'STRING'; + delete hash.idBinding; + delete hashType.idBinding; + } +} + EmberHandlebars.ViewHelper = Ember.Object.create({ - propertiesFromHTMLOptions: function(options, thisContext) { + propertiesFromHTMLOptions: function(options) { var hash = options.hash, data = options.data; var extensions = {}, classes = hash['class'], @@ -22350,6 +26339,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ fn = options.fn, newView; + makeBindings(thisContext, options); + if ('string' === typeof path) { // TODO: this is a lame conditional, this should likely change @@ -22585,8 +26576,8 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm /** `{{collection}}` is a `Ember.Handlebars` helper for adding instances of - `Ember.CollectionView` to a template. See `Ember.CollectionView` for - additional information on how a `CollectionView` functions. + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. `{{collection}}`'s primary use is as a block helper with a `contentBinding` option pointing towards an `Ember.Array`-compatible object. An `Ember.View` @@ -22738,7 +26729,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { var controller = data.keywords.controller; Ember.assert('You specified an itemView, but the current context has no container to look the itemView up in. This probably means that you created a view manually, instead of through the container. Instead, use container.lookup("view:viewName"), which will properly instantiate your view.', controller && controller.container); var container = controller.container; - itemViewClass = container.resolve('view:' + Ember.String.camelize(hash.itemView)); + itemViewClass = container.resolve('view:' + hash.itemView); Ember.assert('You specified the itemView ' + hash.itemView + ", but it was not found at " + container.describe("view:" + hash.itemView) + " (and it was not registered in the container)", !!itemViewClass); } else if (hash.itemViewClass) { itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); @@ -22840,7 +26831,7 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { return out; } - context = (fn.contexts && fn.contexts[0]) || this; + context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; return handlebarsGet(context, property, fn); }); @@ -22870,7 +26861,7 @@ var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.norma @param {String} property */ Ember.Handlebars.registerHelper('log', function(property, options) { - var context = (options.contexts && options.contexts[0]) || this, + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this, normalized = normalizePath(context, property, options.data), pathRoot = normalized.root, path = normalized.path, @@ -23039,6 +27030,8 @@ GroupedEach.prototype = { }, addArrayObservers: function() { + if (!this.content) { return; } + this.content.addArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -23046,6 +27039,8 @@ GroupedEach.prototype = { }, removeArrayObservers: function() { + if (!this.content) { return; } + this.content.removeArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -23063,6 +27058,8 @@ GroupedEach.prototype = { }, render: function() { + if (!this.content) { return; } + var content = this.content, contentLength = get(content, 'length'), data = this.options.data, @@ -23075,12 +27072,21 @@ GroupedEach.prototype = { }, rerenderContainingView: function() { - Ember.run.scheduleOnce('render', this.containingView, 'rerender'); + var self = this; + Ember.run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); }, destroy: function() { this.removeContentObservers(); - this.removeArrayObservers(); + if (this.content) { + this.removeArrayObservers(); + } + this.destroyed = true; } }; @@ -23204,6 +27210,49 @@ GroupedEach.prototype = { Each itemController will receive a reference to the current controller as a `parentController` property. + ### (Experimental) Grouped Each + + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). + + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` + + This can be faster than the normal way that Handlebars re-renders items + in some cases. + + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). + + For example, + + ```handlebars + {{dealershipName}} + + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} + + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} + ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). + + Note that `group` behavior is also disabled by specifying an `itemViewClass`. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -23211,6 +27260,7 @@ GroupedEach.prototype = { @param [options] {Object} Handlebars key/value pairs of options @param [options.itemViewClass] {String} a path to a view class used for each item @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper */ Ember.Handlebars.registerHelper('each', function(path, options) { if (arguments.length === 4) { @@ -23311,24 +27361,48 @@ Ember.Handlebars.registerHelper('template', function(name, options) { */ /** - `partial` renders a template directly using the current context. - If needed the context can be set using the `{{#with foo}}` helper. + The `partial` helper renders another template without + changing the template context: - ```html - + ```handlebars + {{foo}} + {{partial "nav"}} ``` - The `data-template-name` attribute of a partial template - is prefixed with an underscore. + The above example template will render a template named + "_nav", which has the same context as the parent template + it's rendered into, so if the "_nav" template also referenced + `{{foo}}`, it would print the same thing as the `{{foo}}` + in the above example. - ```html - + If a "_nav" template isn't found, the `partial` helper will + fall back to a template named "nav". + + ## Bound template names + + The parameter supplied to `partial` can also be a path + to a property containing a template name, e.g.: + + ```handlebars + {{partial someTemplateName}} + ``` + + The above example will look up the value of `someTemplateName` + on the template context (e.g. a controller) and use that + value as the name of the template to render. If the resolved + value is falsy, nothing will be rendered. If `someTemplateName` + changes, the partial will be re-rendered using the new template + name. + + ## Setting the partial's context with `with` + + The `partial` helper can be used in conjunction with the `with` + helper to set a context that will be used by the partial: + + ```handlebars + {{#with currentUser}} + {{partial "user_info"}} + {{/with}} ``` @method partial @@ -23337,6 +27411,30 @@ Ember.Handlebars.registerHelper('template', function(name, options) { */ Ember.Handlebars.registerHelper('partial', function(name, options) { + + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; + + if (options.types[0] === "ID") { + // Helper was passed a property path; we need to + // create a binding that will re-render whenever + // this property changes. + options.fn = function(context, fnOptions) { + var partialName = Ember.Handlebars.get(context, name, fnOptions); + renderPartial(context, partialName, fnOptions); + }; + + return Ember.Handlebars.bind.call(context, name, options, true, exists); + } else { + // Render the partial right into parent template. + renderPartial(context, name, options); + } +}); + +function exists(value) { + return !Ember.isNone(value); +} + +function renderPartial(context, name, options) { var nameParts = name.split("/"), lastPart = nameParts[nameParts.length - 1]; @@ -23351,8 +27449,8 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { template = template || deprecatedTemplate; - template(this, { data: options.data }); -}); + template(context, { data: options.data }); +} })(); @@ -23367,6 +27465,10 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var get = Ember.get, set = Ember.set; /** + `{{yield}}` denotes an area of a template that will be rendered inside + of another template. It has two main uses: + + ### Use with `layout` When used in a Handlebars template that is assigned to an `Ember.View` instance's `layout` property Ember will render the layout template first, inserting the view's own rendered output at the `{{yield}}` location. @@ -23409,7 +27511,34 @@ var get = Ember.get, set = Ember.set; bView.appendTo('body'); // throws - // Uncaught Error: assertion failed: You called yield in a template that was not a layout + // Uncaught Error: assertion failed: + // You called yield in a template that was not a layout + ``` + + ### Use with Ember.Component + When designing components `{{yield}}` is used to denote where, inside the component's + template, an optional block passed to the component should render: + + ```handlebars + + {{#labeled-textfield value=someProperty}} + First name: + {{/labeled-textfield}} + ``` + + ```handlebars + + + ``` + + Result: + + ```html +