diff --git a/.gitignore b/.gitignore index 975e6e2a..844d3487 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ .localeapp/key /assets/scripts/config/locales.js .DS_Store +/public/images/emoji 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/Assetfile b/Assetfile index da341d89..8c867331 100644 --- a/Assetfile +++ b/Assetfile @@ -31,10 +31,15 @@ input assets.scripts do end match 'vendor/**/*.js' do + if assets.production? + reject 'ember.js' + else + reject 'ember.prod.js' + end safe_concat assets.vendor_order, 'vendor.js' end - match '{spec,spec/unit,spec/unit/views}/*.js' do + match '{spec,spec/integration,spec/unit,spec/unit/views}/*.js' do concat 'spec/specs.js' end 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 fcdcc460..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 @@ -72,8 +68,8 @@ GEM coderay (~> 1.0.5) method_source (~> 0.8) slop (~> 3.4) - puma (1.6.3) - rack (~> 1.2) + puma (2.6.0) + rack (>= 1.1, < 2.0) rack (1.5.2) rack-cache (1.2) rack (>= 0.4) @@ -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/NOTES.txt b/NOTES.txt index 577dc5a8..1b2fc3b7 100644 --- a/NOTES.txt +++ b/NOTES.txt @@ -10,7 +10,7 @@ # Handlebars -* Can't {{bindAttr}} be just {{attr}}? Who cares it's "bound" in that context? +* Can't {{bind-attr}} be just {{attr}}? Who cares it's "bound" in that context? {{#each}} isn't {{#bindEach}} either. * Why is {{#collection contentBinding="foo"}} not just {{#collection foo}}? diff --git a/README.md b/README.md index 51aff3cc..0c7f90c7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## Travis CI ember web client -[![Build Status](https://travis-ci.org/travis-ci/travis-web.png)](https://travis-ci.org/travis-ci/travis-web) +[![Build Status](https://travis-ci.org/travis-ci/travis-web.png?branch=master)](https://travis-ci.org/travis-ci/travis-web) ### Running the app In order to run the app you need to install dependencies with: diff --git a/Rakefile b/Rakefile index ae388e82..e7afa057 100644 --- a/Rakefile +++ b/Rakefile @@ -28,3 +28,9 @@ namespace :ember do system 'cp tmp/ember.js/dist/ember.js assets/javascripts/vendor/ember.js' end end + +task :update_emojis do + s = Dir.glob('assets/images/emoji/*.png').map {|png| png.split('/', 4)[3].gsub('.png', '')}.map{|png| "'#{png}'"}.join(", ") + e = "@EmojiDictionary = [#{s}]" + File.open("assets/scripts/config/emoij.coffee", "w") {|f| f.write(e) } +end diff --git a/assets/images/emoji/+1.png b/assets/images/emoji/+1.png new file mode 100644 index 00000000..81786c1d Binary files /dev/null and b/assets/images/emoji/+1.png differ diff --git a/assets/images/emoji/-1.png b/assets/images/emoji/-1.png index 6f757ba8..41c6b825 100644 Binary files a/assets/images/emoji/-1.png and b/assets/images/emoji/-1.png differ diff --git a/assets/images/emoji/0.png b/assets/images/emoji/0.png deleted file mode 100644 index 65146943..00000000 Binary files a/assets/images/emoji/0.png and /dev/null differ diff --git a/assets/images/emoji/1.png b/assets/images/emoji/1.png deleted file mode 100644 index d9ee9e36..00000000 Binary files a/assets/images/emoji/1.png and /dev/null differ diff --git a/assets/images/emoji/100.png b/assets/images/emoji/100.png new file mode 100644 index 00000000..ca3bb9bc Binary files /dev/null and b/assets/images/emoji/100.png differ diff --git a/assets/images/emoji/109.png b/assets/images/emoji/109.png deleted file mode 100644 index 9c3512f2..00000000 Binary files a/assets/images/emoji/109.png and /dev/null differ diff --git a/assets/images/emoji/1234.png b/assets/images/emoji/1234.png new file mode 100644 index 00000000..c47c2e1f Binary files /dev/null and b/assets/images/emoji/1234.png differ diff --git a/assets/images/emoji/2.png b/assets/images/emoji/2.png deleted file mode 100644 index 670b9904..00000000 Binary files a/assets/images/emoji/2.png and /dev/null differ diff --git a/assets/images/emoji/3.png b/assets/images/emoji/3.png deleted file mode 100644 index 4884ee22..00000000 Binary files a/assets/images/emoji/3.png and /dev/null differ diff --git a/assets/images/emoji/4.png b/assets/images/emoji/4.png deleted file mode 100644 index 9023a2cf..00000000 Binary files a/assets/images/emoji/4.png and /dev/null differ diff --git a/assets/images/emoji/5.png b/assets/images/emoji/5.png deleted file mode 100644 index 0bd3dad8..00000000 Binary files a/assets/images/emoji/5.png and /dev/null differ diff --git a/assets/images/emoji/6.png b/assets/images/emoji/6.png deleted file mode 100644 index 62012079..00000000 Binary files a/assets/images/emoji/6.png and /dev/null differ diff --git a/assets/images/emoji/7.png b/assets/images/emoji/7.png deleted file mode 100644 index f510edd6..00000000 Binary files a/assets/images/emoji/7.png and /dev/null differ diff --git a/assets/images/emoji/8.png b/assets/images/emoji/8.png deleted file mode 100644 index c9996021..00000000 Binary files a/assets/images/emoji/8.png and /dev/null differ diff --git a/assets/images/emoji/8ball.png b/assets/images/emoji/8ball.png index 74db01b7..c2c710d4 100644 Binary files a/assets/images/emoji/8ball.png and b/assets/images/emoji/8ball.png differ diff --git a/assets/images/emoji/9.png b/assets/images/emoji/9.png deleted file mode 100644 index a340a91a..00000000 Binary files a/assets/images/emoji/9.png and /dev/null differ diff --git a/assets/images/emoji/a.png b/assets/images/emoji/a.png index 989e3e4b..09ff6d6f 100644 Binary files a/assets/images/emoji/a.png and b/assets/images/emoji/a.png differ diff --git a/assets/images/emoji/ab.png b/assets/images/emoji/ab.png index 8ae786c1..2a522204 100644 Binary files a/assets/images/emoji/ab.png and b/assets/images/emoji/ab.png differ diff --git a/assets/images/emoji/abc.png b/assets/images/emoji/abc.png new file mode 100644 index 00000000..505d40a1 Binary files /dev/null and b/assets/images/emoji/abc.png differ diff --git a/assets/images/emoji/abcd.png b/assets/images/emoji/abcd.png new file mode 100644 index 00000000..5218470b Binary files /dev/null and b/assets/images/emoji/abcd.png differ diff --git a/assets/images/emoji/accept.png b/assets/images/emoji/accept.png new file mode 100644 index 00000000..2d200903 Binary files /dev/null and b/assets/images/emoji/accept.png differ diff --git a/assets/images/emoji/aerial_tramway.png b/assets/images/emoji/aerial_tramway.png new file mode 100644 index 00000000..38f6dfe2 Binary files /dev/null and b/assets/images/emoji/aerial_tramway.png differ diff --git a/assets/images/emoji/airplane.png b/assets/images/emoji/airplane.png index a6adc588..8407cb67 100644 Binary files a/assets/images/emoji/airplane.png and b/assets/images/emoji/airplane.png differ diff --git a/assets/images/emoji/alarm_clock.png b/assets/images/emoji/alarm_clock.png new file mode 100644 index 00000000..86ca8c8e Binary files /dev/null and b/assets/images/emoji/alarm_clock.png differ diff --git a/assets/images/emoji/alien.png b/assets/images/emoji/alien.png index 141e2fbf..416de47b 100644 Binary files a/assets/images/emoji/alien.png and b/assets/images/emoji/alien.png differ diff --git a/assets/images/emoji/ambulance.png b/assets/images/emoji/ambulance.png index 9fe869fc..b740f45d 100644 Binary files a/assets/images/emoji/ambulance.png and b/assets/images/emoji/ambulance.png differ diff --git a/assets/images/emoji/anchor.png b/assets/images/emoji/anchor.png new file mode 100644 index 00000000..0c5192e6 Binary files /dev/null and b/assets/images/emoji/anchor.png differ diff --git a/assets/images/emoji/angel.png b/assets/images/emoji/angel.png index d57c86ab..da52c310 100644 Binary files a/assets/images/emoji/angel.png and b/assets/images/emoji/angel.png differ diff --git a/assets/images/emoji/anger.png b/assets/images/emoji/anger.png index c9245ca7..6fb4dca1 100644 Binary files a/assets/images/emoji/anger.png and b/assets/images/emoji/anger.png differ diff --git a/assets/images/emoji/angry.png b/assets/images/emoji/angry.png index 3c8e8920..f95bfa89 100644 Binary files a/assets/images/emoji/angry.png and b/assets/images/emoji/angry.png differ diff --git a/assets/images/emoji/anguished.png b/assets/images/emoji/anguished.png new file mode 100644 index 00000000..c6259472 Binary files /dev/null and b/assets/images/emoji/anguished.png differ diff --git a/assets/images/emoji/ant.png b/assets/images/emoji/ant.png new file mode 100644 index 00000000..b92d1cc1 Binary files /dev/null and b/assets/images/emoji/ant.png differ diff --git a/assets/images/emoji/apple.png b/assets/images/emoji/apple.png index 4137b45a..08aa17b9 100644 Binary files a/assets/images/emoji/apple.png and b/assets/images/emoji/apple.png differ diff --git a/assets/images/emoji/aquarius.png b/assets/images/emoji/aquarius.png index 49ce4316..cbff66ed 100644 Binary files a/assets/images/emoji/aquarius.png and b/assets/images/emoji/aquarius.png differ diff --git a/assets/images/emoji/aries.png b/assets/images/emoji/aries.png index 05835de5..89990223 100644 Binary files a/assets/images/emoji/aries.png and b/assets/images/emoji/aries.png differ diff --git a/assets/images/emoji/arrow_backward.png b/assets/images/emoji/arrow_backward.png index 9ad1296e..08862183 100644 Binary files a/assets/images/emoji/arrow_backward.png and b/assets/images/emoji/arrow_backward.png differ diff --git a/assets/images/emoji/arrow_double_down.png b/assets/images/emoji/arrow_double_down.png new file mode 100644 index 00000000..2ecbebcd Binary files /dev/null and b/assets/images/emoji/arrow_double_down.png differ diff --git a/assets/images/emoji/arrow_double_up.png b/assets/images/emoji/arrow_double_up.png new file mode 100644 index 00000000..d42979d4 Binary files /dev/null and b/assets/images/emoji/arrow_double_up.png differ diff --git a/assets/images/emoji/arrow_down.png b/assets/images/emoji/arrow_down.png index 3f9258d8..e6702f02 100644 Binary files a/assets/images/emoji/arrow_down.png and b/assets/images/emoji/arrow_down.png differ diff --git a/assets/images/emoji/arrow_down_small.png b/assets/images/emoji/arrow_down_small.png new file mode 100644 index 00000000..d168a555 Binary files /dev/null and b/assets/images/emoji/arrow_down_small.png differ diff --git a/assets/images/emoji/arrow_forward.png b/assets/images/emoji/arrow_forward.png index 200a6447..fbfe711b 100644 Binary files a/assets/images/emoji/arrow_forward.png and b/assets/images/emoji/arrow_forward.png differ diff --git a/assets/images/emoji/arrow_heading_down.png b/assets/images/emoji/arrow_heading_down.png new file mode 100644 index 00000000..56dd3b9d Binary files /dev/null and b/assets/images/emoji/arrow_heading_down.png differ diff --git a/assets/images/emoji/arrow_heading_up.png b/assets/images/emoji/arrow_heading_up.png new file mode 100644 index 00000000..c8f670a1 Binary files /dev/null and b/assets/images/emoji/arrow_heading_up.png differ diff --git a/assets/images/emoji/arrow_left.png b/assets/images/emoji/arrow_left.png index 59be8e30..d64ac619 100644 Binary files a/assets/images/emoji/arrow_left.png and b/assets/images/emoji/arrow_left.png differ diff --git a/assets/images/emoji/arrow_lower_left.png b/assets/images/emoji/arrow_lower_left.png index 7cc33a31..55fb03c4 100644 Binary files a/assets/images/emoji/arrow_lower_left.png and b/assets/images/emoji/arrow_lower_left.png differ diff --git a/assets/images/emoji/arrow_lower_right.png b/assets/images/emoji/arrow_lower_right.png index 67d1c281..da8fb829 100644 Binary files a/assets/images/emoji/arrow_lower_right.png and b/assets/images/emoji/arrow_lower_right.png differ diff --git a/assets/images/emoji/arrow_right.png b/assets/images/emoji/arrow_right.png index 99ae84e6..6d483b51 100644 Binary files a/assets/images/emoji/arrow_right.png and b/assets/images/emoji/arrow_right.png differ diff --git a/assets/images/emoji/arrow_right_hook.png b/assets/images/emoji/arrow_right_hook.png new file mode 100644 index 00000000..8b4ea6e1 Binary files /dev/null and b/assets/images/emoji/arrow_right_hook.png differ diff --git a/assets/images/emoji/arrow_up.png b/assets/images/emoji/arrow_up.png index fd80ace4..b5b0688d 100644 Binary files a/assets/images/emoji/arrow_up.png and b/assets/images/emoji/arrow_up.png differ diff --git a/assets/images/emoji/arrow_up_down.png b/assets/images/emoji/arrow_up_down.png new file mode 100644 index 00000000..b718c214 Binary files /dev/null and b/assets/images/emoji/arrow_up_down.png differ diff --git a/assets/images/emoji/arrow_up_small.png b/assets/images/emoji/arrow_up_small.png new file mode 100644 index 00000000..3f40bfb8 Binary files /dev/null and b/assets/images/emoji/arrow_up_small.png differ diff --git a/assets/images/emoji/arrow_upper_left.png b/assets/images/emoji/arrow_upper_left.png index 52b9b39f..e895fd7b 100644 Binary files a/assets/images/emoji/arrow_upper_left.png and b/assets/images/emoji/arrow_upper_left.png differ diff --git a/assets/images/emoji/arrow_upper_right.png b/assets/images/emoji/arrow_upper_right.png index e8370c8a..e23790ba 100644 Binary files a/assets/images/emoji/arrow_upper_right.png and b/assets/images/emoji/arrow_upper_right.png differ diff --git a/assets/images/emoji/arrows_clockwise.png b/assets/images/emoji/arrows_clockwise.png new file mode 100644 index 00000000..5f84d7e7 Binary files /dev/null and b/assets/images/emoji/arrows_clockwise.png differ diff --git a/assets/images/emoji/arrows_counterclockwise.png b/assets/images/emoji/arrows_counterclockwise.png new file mode 100644 index 00000000..1933ae18 Binary files /dev/null and b/assets/images/emoji/arrows_counterclockwise.png differ diff --git a/assets/images/emoji/art.png b/assets/images/emoji/art.png index 109f664f..d45212b0 100644 Binary files a/assets/images/emoji/art.png and b/assets/images/emoji/art.png differ diff --git a/assets/images/emoji/articulated_lorry.png b/assets/images/emoji/articulated_lorry.png new file mode 100644 index 00000000..81ec1f91 Binary files /dev/null and b/assets/images/emoji/articulated_lorry.png differ diff --git a/assets/images/emoji/astonished.png b/assets/images/emoji/astonished.png index 052dfdbf..858a8348 100644 Binary files a/assets/images/emoji/astonished.png and b/assets/images/emoji/astonished.png differ diff --git a/assets/images/emoji/atm.png b/assets/images/emoji/atm.png index 357caf4d..c2846e79 100644 Binary files a/assets/images/emoji/atm.png and b/assets/images/emoji/atm.png differ diff --git a/assets/images/emoji/b.png b/assets/images/emoji/b.png index dab96329..8742b3d2 100644 Binary files a/assets/images/emoji/b.png and b/assets/images/emoji/b.png differ diff --git a/assets/images/emoji/baby.png b/assets/images/emoji/baby.png index 25b4d645..3b29da40 100644 Binary files a/assets/images/emoji/baby.png and b/assets/images/emoji/baby.png differ diff --git a/assets/images/emoji/baby_bottle.png b/assets/images/emoji/baby_bottle.png new file mode 100644 index 00000000..1b2cfe5e Binary files /dev/null and b/assets/images/emoji/baby_bottle.png differ diff --git a/assets/images/emoji/baby_chick.png b/assets/images/emoji/baby_chick.png index 9551846a..9be8d293 100644 Binary files a/assets/images/emoji/baby_chick.png and b/assets/images/emoji/baby_chick.png differ diff --git a/assets/images/emoji/baby_symbol.png b/assets/images/emoji/baby_symbol.png index 149dabf2..2e58725c 100644 Binary files a/assets/images/emoji/baby_symbol.png and b/assets/images/emoji/baby_symbol.png differ diff --git a/assets/images/emoji/baggage_claim.png b/assets/images/emoji/baggage_claim.png new file mode 100644 index 00000000..59ae044a Binary files /dev/null and b/assets/images/emoji/baggage_claim.png differ diff --git a/assets/images/emoji/balloon.png b/assets/images/emoji/balloon.png index 9f0cbd76..03448970 100644 Binary files a/assets/images/emoji/balloon.png and b/assets/images/emoji/balloon.png differ diff --git a/assets/images/emoji/ballot_box_with_check.png b/assets/images/emoji/ballot_box_with_check.png new file mode 100644 index 00000000..f07a466c Binary files /dev/null and b/assets/images/emoji/ballot_box_with_check.png differ diff --git a/assets/images/emoji/bamboo.png b/assets/images/emoji/bamboo.png index fe642bfc..fc858d0f 100644 Binary files a/assets/images/emoji/bamboo.png and b/assets/images/emoji/bamboo.png differ diff --git a/assets/images/emoji/banana.png b/assets/images/emoji/banana.png new file mode 100644 index 00000000..a0563afb Binary files /dev/null and b/assets/images/emoji/banana.png differ diff --git a/assets/images/emoji/bangbang.png b/assets/images/emoji/bangbang.png new file mode 100644 index 00000000..7270f0af Binary files /dev/null and b/assets/images/emoji/bangbang.png differ diff --git a/assets/images/emoji/bank.png b/assets/images/emoji/bank.png index 74402657..1faa8777 100644 Binary files a/assets/images/emoji/bank.png and b/assets/images/emoji/bank.png differ diff --git a/assets/images/emoji/bar_chart.png b/assets/images/emoji/bar_chart.png new file mode 100644 index 00000000..09d7301c Binary files /dev/null and b/assets/images/emoji/bar_chart.png differ diff --git a/assets/images/emoji/barber.png b/assets/images/emoji/barber.png index 6081b417..a10cb232 100644 Binary files a/assets/images/emoji/barber.png and b/assets/images/emoji/barber.png differ diff --git a/assets/images/emoji/baseball.png b/assets/images/emoji/baseball.png index 39b29b37..da004e2e 100644 Binary files a/assets/images/emoji/baseball.png and b/assets/images/emoji/baseball.png differ diff --git a/assets/images/emoji/basketball.png b/assets/images/emoji/basketball.png index 0c4f880b..ef694bec 100644 Binary files a/assets/images/emoji/basketball.png and b/assets/images/emoji/basketball.png differ diff --git a/assets/images/emoji/bath.png b/assets/images/emoji/bath.png index 9672442c..8f75d1d2 100644 Binary files a/assets/images/emoji/bath.png and b/assets/images/emoji/bath.png differ diff --git a/assets/images/emoji/bathtub.png b/assets/images/emoji/bathtub.png new file mode 100644 index 00000000..1c3f844a Binary files /dev/null and b/assets/images/emoji/bathtub.png differ diff --git a/assets/images/emoji/battery.png b/assets/images/emoji/battery.png new file mode 100644 index 00000000..aa7eedce Binary files /dev/null and b/assets/images/emoji/battery.png differ diff --git a/assets/images/emoji/bear.png b/assets/images/emoji/bear.png index d01c4ce3..f5afe920 100644 Binary files a/assets/images/emoji/bear.png and b/assets/images/emoji/bear.png differ diff --git a/assets/images/emoji/bee.png b/assets/images/emoji/bee.png new file mode 100644 index 00000000..f5373395 Binary files /dev/null and b/assets/images/emoji/bee.png differ diff --git a/assets/images/emoji/beer.png b/assets/images/emoji/beer.png index da1e34ab..cd78bed7 100644 Binary files a/assets/images/emoji/beer.png and b/assets/images/emoji/beer.png differ diff --git a/assets/images/emoji/beers.png b/assets/images/emoji/beers.png index c6e2d638..cc5e4ab5 100644 Binary files a/assets/images/emoji/beers.png and b/assets/images/emoji/beers.png differ diff --git a/assets/images/emoji/beetle.png b/assets/images/emoji/beetle.png new file mode 100644 index 00000000..222577ca Binary files /dev/null and b/assets/images/emoji/beetle.png differ diff --git a/assets/images/emoji/beginner.png b/assets/images/emoji/beginner.png index 784a81e1..1f022d17 100644 Binary files a/assets/images/emoji/beginner.png and b/assets/images/emoji/beginner.png differ diff --git a/assets/images/emoji/bell.png b/assets/images/emoji/bell.png index df5d66ab..69acceb2 100644 Binary files a/assets/images/emoji/bell.png and b/assets/images/emoji/bell.png differ diff --git a/assets/images/emoji/bento.png b/assets/images/emoji/bento.png index a66a2f5f..c6d99e89 100644 Binary files a/assets/images/emoji/bento.png and b/assets/images/emoji/bento.png differ diff --git a/assets/images/emoji/bicyclist.png b/assets/images/emoji/bicyclist.png new file mode 100644 index 00000000..4e3e0549 Binary files /dev/null and b/assets/images/emoji/bicyclist.png differ diff --git a/assets/images/emoji/bike.png b/assets/images/emoji/bike.png index 36bc09ba..65738602 100644 Binary files a/assets/images/emoji/bike.png and b/assets/images/emoji/bike.png differ diff --git a/assets/images/emoji/bikini.png b/assets/images/emoji/bikini.png index e62cf066..4ff63b40 100644 Binary files a/assets/images/emoji/bikini.png and b/assets/images/emoji/bikini.png differ diff --git a/assets/images/emoji/bird.png b/assets/images/emoji/bird.png index 88e5786a..e6be8c02 100644 Binary files a/assets/images/emoji/bird.png and b/assets/images/emoji/bird.png differ diff --git a/assets/images/emoji/birthday.png b/assets/images/emoji/birthday.png index 5b2c28c0..36e8edcb 100644 Binary files a/assets/images/emoji/birthday.png and b/assets/images/emoji/birthday.png differ diff --git a/assets/images/emoji/black_circle.png b/assets/images/emoji/black_circle.png new file mode 100644 index 00000000..e46f9df6 Binary files /dev/null and b/assets/images/emoji/black_circle.png differ diff --git a/assets/images/emoji/black_joker.png b/assets/images/emoji/black_joker.png new file mode 100644 index 00000000..4c78f361 Binary files /dev/null and b/assets/images/emoji/black_joker.png differ diff --git a/assets/images/emoji/black_nib.png b/assets/images/emoji/black_nib.png new file mode 100644 index 00000000..29f6994c Binary files /dev/null and b/assets/images/emoji/black_nib.png differ diff --git a/assets/images/emoji/black_square.png b/assets/images/emoji/black_square.png index dcd4ca95..71da10de 100644 Binary files a/assets/images/emoji/black_square.png and b/assets/images/emoji/black_square.png differ diff --git a/assets/images/emoji/black_square_button.png b/assets/images/emoji/black_square_button.png new file mode 100644 index 00000000..f2597e95 Binary files /dev/null and b/assets/images/emoji/black_square_button.png differ diff --git a/assets/images/emoji/blossom.png b/assets/images/emoji/blossom.png new file mode 100644 index 00000000..55a97353 Binary files /dev/null and b/assets/images/emoji/blossom.png differ diff --git a/assets/images/emoji/blowfish.png b/assets/images/emoji/blowfish.png new file mode 100644 index 00000000..d3ad4658 Binary files /dev/null and b/assets/images/emoji/blowfish.png differ diff --git a/assets/images/emoji/blue_book.png b/assets/images/emoji/blue_book.png new file mode 100644 index 00000000..e2b9e8c7 Binary files /dev/null and b/assets/images/emoji/blue_book.png differ diff --git a/assets/images/emoji/blue_car.png b/assets/images/emoji/blue_car.png index badd22ea..978291e0 100644 Binary files a/assets/images/emoji/blue_car.png and b/assets/images/emoji/blue_car.png differ diff --git a/assets/images/emoji/blue_heart.png b/assets/images/emoji/blue_heart.png index a8105782..baa29b31 100644 Binary files a/assets/images/emoji/blue_heart.png and b/assets/images/emoji/blue_heart.png differ diff --git a/assets/images/emoji/blush.png b/assets/images/emoji/blush.png index 0b2628e9..3a95eb61 100644 Binary files a/assets/images/emoji/blush.png and b/assets/images/emoji/blush.png differ diff --git a/assets/images/emoji/boar.png b/assets/images/emoji/boar.png index 2bd9362f..8196ad4a 100644 Binary files a/assets/images/emoji/boar.png and b/assets/images/emoji/boar.png differ diff --git a/assets/images/emoji/boat.png b/assets/images/emoji/boat.png index 8084f0e5..ff656dc6 100644 Binary files a/assets/images/emoji/boat.png and b/assets/images/emoji/boat.png differ diff --git a/assets/images/emoji/bomb.png b/assets/images/emoji/bomb.png index 322a3f1b..3289787d 100644 Binary files a/assets/images/emoji/bomb.png and b/assets/images/emoji/bomb.png differ diff --git a/assets/images/emoji/book.png b/assets/images/emoji/book.png index 3fa6b7c2..8b698415 100644 Binary files a/assets/images/emoji/book.png and b/assets/images/emoji/book.png differ diff --git a/assets/images/emoji/bookmark.png b/assets/images/emoji/bookmark.png new file mode 100644 index 00000000..dbee45c6 Binary files /dev/null and b/assets/images/emoji/bookmark.png differ diff --git a/assets/images/emoji/bookmark_tabs.png b/assets/images/emoji/bookmark_tabs.png new file mode 100644 index 00000000..0c4e3bf1 Binary files /dev/null and b/assets/images/emoji/bookmark_tabs.png differ diff --git a/assets/images/emoji/books.png b/assets/images/emoji/books.png new file mode 100644 index 00000000..dca06a1a Binary files /dev/null and b/assets/images/emoji/books.png differ diff --git a/assets/images/emoji/boom.png b/assets/images/emoji/boom.png new file mode 100644 index 00000000..bddeb8f4 Binary files /dev/null and b/assets/images/emoji/boom.png differ diff --git a/assets/images/emoji/boot.png b/assets/images/emoji/boot.png index fd52cafa..58d0fdbc 100644 Binary files a/assets/images/emoji/boot.png and b/assets/images/emoji/boot.png differ diff --git a/assets/images/emoji/bouquet.png b/assets/images/emoji/bouquet.png index 791bfe86..ce637832 100644 Binary files a/assets/images/emoji/bouquet.png and b/assets/images/emoji/bouquet.png differ diff --git a/assets/images/emoji/bow.png b/assets/images/emoji/bow.png index 87c85122..024cb610 100644 Binary files a/assets/images/emoji/bow.png and b/assets/images/emoji/bow.png differ diff --git a/assets/images/emoji/bowling.png b/assets/images/emoji/bowling.png new file mode 100644 index 00000000..13d8ece2 Binary files /dev/null and b/assets/images/emoji/bowling.png differ diff --git a/assets/images/emoji/bowtie.png b/assets/images/emoji/bowtie.png index 86550b32..28ff0c78 100644 Binary files a/assets/images/emoji/bowtie.png and b/assets/images/emoji/bowtie.png differ diff --git a/assets/images/emoji/boy.png b/assets/images/emoji/boy.png index 584bb395..f79f1f29 100644 Binary files a/assets/images/emoji/boy.png and b/assets/images/emoji/boy.png differ diff --git a/assets/images/emoji/bread.png b/assets/images/emoji/bread.png index 934bb3c5..7e7c6375 100644 Binary files a/assets/images/emoji/bread.png and b/assets/images/emoji/bread.png differ diff --git a/assets/images/emoji/bride_with_veil.png b/assets/images/emoji/bride_with_veil.png new file mode 100644 index 00000000..dd0b0cfd Binary files /dev/null and b/assets/images/emoji/bride_with_veil.png differ diff --git a/assets/images/emoji/bridge_at_night.png b/assets/images/emoji/bridge_at_night.png new file mode 100644 index 00000000..495b06c3 Binary files /dev/null and b/assets/images/emoji/bridge_at_night.png differ diff --git a/assets/images/emoji/briefcase.png b/assets/images/emoji/briefcase.png index 3491b84d..46e82b00 100644 Binary files a/assets/images/emoji/briefcase.png and b/assets/images/emoji/briefcase.png differ diff --git a/assets/images/emoji/broken_heart.png b/assets/images/emoji/broken_heart.png index cc70df0c..a1bc850e 100644 Binary files a/assets/images/emoji/broken_heart.png and b/assets/images/emoji/broken_heart.png differ diff --git a/assets/images/emoji/bug.png b/assets/images/emoji/bug.png index 2230749e..c2eaf7a7 100644 Binary files a/assets/images/emoji/bug.png and b/assets/images/emoji/bug.png differ diff --git a/assets/images/emoji/bulb.png b/assets/images/emoji/bulb.png index 02638b30..23afca1c 100644 Binary files a/assets/images/emoji/bulb.png and b/assets/images/emoji/bulb.png differ diff --git a/assets/images/emoji/bullettrain_front.png b/assets/images/emoji/bullettrain_front.png index c64c517c..16651acf 100644 Binary files a/assets/images/emoji/bullettrain_front.png and b/assets/images/emoji/bullettrain_front.png differ diff --git a/assets/images/emoji/bullettrain_side.png b/assets/images/emoji/bullettrain_side.png index 39499c1e..8eca3684 100644 Binary files a/assets/images/emoji/bullettrain_side.png and b/assets/images/emoji/bullettrain_side.png differ diff --git a/assets/images/emoji/bus.png b/assets/images/emoji/bus.png index 7014c9ae..823aa39e 100644 Binary files a/assets/images/emoji/bus.png and b/assets/images/emoji/bus.png differ diff --git a/assets/images/emoji/busstop.png b/assets/images/emoji/busstop.png index c806a037..94894847 100644 Binary files a/assets/images/emoji/busstop.png and b/assets/images/emoji/busstop.png differ diff --git a/assets/images/emoji/bust_in_silhouette.png b/assets/images/emoji/bust_in_silhouette.png new file mode 100644 index 00000000..d1313986 Binary files /dev/null and b/assets/images/emoji/bust_in_silhouette.png differ diff --git a/assets/images/emoji/busts_in_silhouette.png b/assets/images/emoji/busts_in_silhouette.png new file mode 100644 index 00000000..1f3aabcf Binary files /dev/null and b/assets/images/emoji/busts_in_silhouette.png differ diff --git a/assets/images/emoji/cactus.png b/assets/images/emoji/cactus.png index fb8eb2de..5a2c3cc7 100644 Binary files a/assets/images/emoji/cactus.png and b/assets/images/emoji/cactus.png differ diff --git a/assets/images/emoji/cake.png b/assets/images/emoji/cake.png index 80b9e654..efeb9b4b 100644 Binary files a/assets/images/emoji/cake.png and b/assets/images/emoji/cake.png differ diff --git a/assets/images/emoji/calendar.png b/assets/images/emoji/calendar.png new file mode 100644 index 00000000..900b868b Binary files /dev/null and b/assets/images/emoji/calendar.png differ diff --git a/assets/images/emoji/calling.png b/assets/images/emoji/calling.png index 4309feb5..837897f2 100644 Binary files a/assets/images/emoji/calling.png and b/assets/images/emoji/calling.png differ diff --git a/assets/images/emoji/camel.png b/assets/images/emoji/camel.png index 5d022a41..496c186a 100644 Binary files a/assets/images/emoji/camel.png and b/assets/images/emoji/camel.png differ diff --git a/assets/images/emoji/camera.png b/assets/images/emoji/camera.png index 9909c287..397d03b3 100644 Binary files a/assets/images/emoji/camera.png and b/assets/images/emoji/camera.png differ diff --git a/assets/images/emoji/cancer.png b/assets/images/emoji/cancer.png index e0c95e8c..ea43a4a2 100644 Binary files a/assets/images/emoji/cancer.png and b/assets/images/emoji/cancer.png differ diff --git a/assets/images/emoji/candy.png b/assets/images/emoji/candy.png new file mode 100644 index 00000000..33722f23 Binary files /dev/null and b/assets/images/emoji/candy.png differ diff --git a/assets/images/emoji/capital_abcd.png b/assets/images/emoji/capital_abcd.png new file mode 100644 index 00000000..ffc0cba4 Binary files /dev/null and b/assets/images/emoji/capital_abcd.png differ diff --git a/assets/images/emoji/capricorn.png b/assets/images/emoji/capricorn.png index 6c73c8e3..f2044e78 100644 Binary files a/assets/images/emoji/capricorn.png and b/assets/images/emoji/capricorn.png differ diff --git a/assets/images/emoji/car.png b/assets/images/emoji/car.png index c8bc8020..d70a2f06 100644 Binary files a/assets/images/emoji/car.png and b/assets/images/emoji/car.png differ diff --git a/assets/images/emoji/card_index.png b/assets/images/emoji/card_index.png new file mode 100644 index 00000000..374e94e9 Binary files /dev/null and b/assets/images/emoji/card_index.png differ diff --git a/assets/images/emoji/carousel_horse.png b/assets/images/emoji/carousel_horse.png new file mode 100644 index 00000000..765d2c0a Binary files /dev/null and b/assets/images/emoji/carousel_horse.png differ diff --git a/assets/images/emoji/cat.png b/assets/images/emoji/cat.png index 31631e59..09b9ef79 100644 Binary files a/assets/images/emoji/cat.png and b/assets/images/emoji/cat.png differ diff --git a/assets/images/emoji/cat2.png b/assets/images/emoji/cat2.png new file mode 100644 index 00000000..6dbc4c71 Binary files /dev/null and b/assets/images/emoji/cat2.png differ diff --git a/assets/images/emoji/cd.png b/assets/images/emoji/cd.png index 9dec2a4b..baff835c 100644 Binary files a/assets/images/emoji/cd.png and b/assets/images/emoji/cd.png differ diff --git a/assets/images/emoji/chart.png b/assets/images/emoji/chart.png index 14853baf..ac2c4bb0 100644 Binary files a/assets/images/emoji/chart.png and b/assets/images/emoji/chart.png differ diff --git a/assets/images/emoji/chart_with_downwards_trend.png b/assets/images/emoji/chart_with_downwards_trend.png new file mode 100644 index 00000000..cb0d2a11 Binary files /dev/null and b/assets/images/emoji/chart_with_downwards_trend.png differ diff --git a/assets/images/emoji/chart_with_upwards_trend.png b/assets/images/emoji/chart_with_upwards_trend.png new file mode 100644 index 00000000..7c66745c Binary files /dev/null and b/assets/images/emoji/chart_with_upwards_trend.png differ diff --git a/assets/images/emoji/checkered_flag.png b/assets/images/emoji/checkered_flag.png index f65aa36c..ead4a68d 100644 Binary files a/assets/images/emoji/checkered_flag.png and b/assets/images/emoji/checkered_flag.png differ diff --git a/assets/images/emoji/cherries.png b/assets/images/emoji/cherries.png new file mode 100644 index 00000000..8d3e044f Binary files /dev/null and b/assets/images/emoji/cherries.png differ diff --git a/assets/images/emoji/cherry_blossom.png b/assets/images/emoji/cherry_blossom.png index 2d7612c2..e0315549 100644 Binary files a/assets/images/emoji/cherry_blossom.png and b/assets/images/emoji/cherry_blossom.png differ diff --git a/assets/images/emoji/chestnut.png b/assets/images/emoji/chestnut.png new file mode 100644 index 00000000..066fb6bf Binary files /dev/null and b/assets/images/emoji/chestnut.png differ diff --git a/assets/images/emoji/chicken.png b/assets/images/emoji/chicken.png index 2b57ed25..6d25c0ef 100644 Binary files a/assets/images/emoji/chicken.png and b/assets/images/emoji/chicken.png differ diff --git a/assets/images/emoji/children_crossing.png b/assets/images/emoji/children_crossing.png new file mode 100644 index 00000000..b0302ae6 Binary files /dev/null and b/assets/images/emoji/children_crossing.png differ diff --git a/assets/images/emoji/chocolate_bar.png b/assets/images/emoji/chocolate_bar.png new file mode 100644 index 00000000..c7ec19d0 Binary files /dev/null and b/assets/images/emoji/chocolate_bar.png differ diff --git a/assets/images/emoji/christmas_tree.png b/assets/images/emoji/christmas_tree.png index ec99e0be..d813b959 100644 Binary files a/assets/images/emoji/christmas_tree.png and b/assets/images/emoji/christmas_tree.png differ diff --git a/assets/images/emoji/church.png b/assets/images/emoji/church.png index 3600c589..4c07c6b9 100644 Binary files a/assets/images/emoji/church.png and b/assets/images/emoji/church.png differ diff --git a/assets/images/emoji/cinema.png b/assets/images/emoji/cinema.png index 448ff809..a990ccf9 100644 Binary files a/assets/images/emoji/cinema.png and b/assets/images/emoji/cinema.png differ diff --git a/assets/images/emoji/circus_tent.png b/assets/images/emoji/circus_tent.png new file mode 100644 index 00000000..4af8719a Binary files /dev/null and b/assets/images/emoji/circus_tent.png differ diff --git a/assets/images/emoji/city_sunrise.png b/assets/images/emoji/city_sunrise.png index a076d9df..91ca2a40 100644 Binary files a/assets/images/emoji/city_sunrise.png and b/assets/images/emoji/city_sunrise.png differ diff --git a/assets/images/emoji/city_sunset.png b/assets/images/emoji/city_sunset.png index 78fc5c4a..7cb178a2 100644 Binary files a/assets/images/emoji/city_sunset.png and b/assets/images/emoji/city_sunset.png differ diff --git a/assets/images/emoji/cl.png b/assets/images/emoji/cl.png new file mode 100644 index 00000000..15ac6752 Binary files /dev/null and b/assets/images/emoji/cl.png differ diff --git a/assets/images/emoji/clap.png b/assets/images/emoji/clap.png index a3845d70..d01c982a 100644 Binary files a/assets/images/emoji/clap.png and b/assets/images/emoji/clap.png differ diff --git a/assets/images/emoji/clapper.png b/assets/images/emoji/clapper.png index 1af03bd7..4e1dc111 100644 Binary files a/assets/images/emoji/clapper.png and b/assets/images/emoji/clapper.png differ diff --git a/assets/images/emoji/clipboard.png b/assets/images/emoji/clipboard.png new file mode 100644 index 00000000..e2c74e6d Binary files /dev/null and b/assets/images/emoji/clipboard.png differ diff --git a/assets/images/emoji/clock1.png b/assets/images/emoji/clock1.png index 4c55f8f7..9174d4e0 100644 Binary files a/assets/images/emoji/clock1.png and b/assets/images/emoji/clock1.png differ diff --git a/assets/images/emoji/clock10.png b/assets/images/emoji/clock10.png index d097b7f0..39f590d6 100644 Binary files a/assets/images/emoji/clock10.png and b/assets/images/emoji/clock10.png differ diff --git a/assets/images/emoji/clock1030.png b/assets/images/emoji/clock1030.png new file mode 100644 index 00000000..0483b305 Binary files /dev/null and b/assets/images/emoji/clock1030.png differ diff --git a/assets/images/emoji/clock11.png b/assets/images/emoji/clock11.png index 0a101daa..ddb53fad 100644 Binary files a/assets/images/emoji/clock11.png and b/assets/images/emoji/clock11.png differ diff --git a/assets/images/emoji/clock1130.png b/assets/images/emoji/clock1130.png new file mode 100644 index 00000000..415999ec Binary files /dev/null and b/assets/images/emoji/clock1130.png differ diff --git a/assets/images/emoji/clock12.png b/assets/images/emoji/clock12.png index e028d673..87b13287 100644 Binary files a/assets/images/emoji/clock12.png and b/assets/images/emoji/clock12.png differ diff --git a/assets/images/emoji/clock1230.png b/assets/images/emoji/clock1230.png new file mode 100644 index 00000000..a6527154 Binary files /dev/null and b/assets/images/emoji/clock1230.png differ diff --git a/assets/images/emoji/clock130.png b/assets/images/emoji/clock130.png new file mode 100644 index 00000000..90ea5b91 Binary files /dev/null and b/assets/images/emoji/clock130.png differ diff --git a/assets/images/emoji/clock2.png b/assets/images/emoji/clock2.png index 83a23ddb..65b3b3af 100644 Binary files a/assets/images/emoji/clock2.png and b/assets/images/emoji/clock2.png differ diff --git a/assets/images/emoji/clock230.png b/assets/images/emoji/clock230.png new file mode 100644 index 00000000..f12c6912 Binary files /dev/null and b/assets/images/emoji/clock230.png differ diff --git a/assets/images/emoji/clock3.png b/assets/images/emoji/clock3.png index 880176e4..3e44d64e 100644 Binary files a/assets/images/emoji/clock3.png and b/assets/images/emoji/clock3.png differ diff --git a/assets/images/emoji/clock330.png b/assets/images/emoji/clock330.png new file mode 100644 index 00000000..1dc9628e Binary files /dev/null and b/assets/images/emoji/clock330.png differ diff --git a/assets/images/emoji/clock4.png b/assets/images/emoji/clock4.png index cc9f3648..948ed1a3 100644 Binary files a/assets/images/emoji/clock4.png and b/assets/images/emoji/clock4.png differ diff --git a/assets/images/emoji/clock430.png b/assets/images/emoji/clock430.png new file mode 100644 index 00000000..5d6b16a2 Binary files /dev/null and b/assets/images/emoji/clock430.png differ diff --git a/assets/images/emoji/clock5.png b/assets/images/emoji/clock5.png index 49626ea7..b010b4f8 100644 Binary files a/assets/images/emoji/clock5.png and b/assets/images/emoji/clock5.png differ diff --git a/assets/images/emoji/clock530.png b/assets/images/emoji/clock530.png new file mode 100644 index 00000000..e08d4ad2 Binary files /dev/null and b/assets/images/emoji/clock530.png differ diff --git a/assets/images/emoji/clock6.png b/assets/images/emoji/clock6.png index 08cc70d2..76bf8cf1 100644 Binary files a/assets/images/emoji/clock6.png and b/assets/images/emoji/clock6.png differ diff --git a/assets/images/emoji/clock630.png b/assets/images/emoji/clock630.png new file mode 100644 index 00000000..46f0681f Binary files /dev/null and b/assets/images/emoji/clock630.png differ diff --git a/assets/images/emoji/clock7.png b/assets/images/emoji/clock7.png index 26fcf6e0..d48f645d 100644 Binary files a/assets/images/emoji/clock7.png and b/assets/images/emoji/clock7.png differ diff --git a/assets/images/emoji/clock730.png b/assets/images/emoji/clock730.png new file mode 100644 index 00000000..f2807de2 Binary files /dev/null and b/assets/images/emoji/clock730.png differ diff --git a/assets/images/emoji/clock8.png b/assets/images/emoji/clock8.png index df1b0ad2..74c770d8 100644 Binary files a/assets/images/emoji/clock8.png and b/assets/images/emoji/clock8.png differ diff --git a/assets/images/emoji/clock830.png b/assets/images/emoji/clock830.png new file mode 100644 index 00000000..f58f3dad Binary files /dev/null and b/assets/images/emoji/clock830.png differ diff --git a/assets/images/emoji/clock9.png b/assets/images/emoji/clock9.png index 649a79cb..f009d14a 100644 Binary files a/assets/images/emoji/clock9.png and b/assets/images/emoji/clock9.png differ diff --git a/assets/images/emoji/clock930.png b/assets/images/emoji/clock930.png new file mode 100644 index 00000000..fd352214 Binary files /dev/null and b/assets/images/emoji/clock930.png differ diff --git a/assets/images/emoji/closed_book.png b/assets/images/emoji/closed_book.png new file mode 100644 index 00000000..484029c5 Binary files /dev/null and b/assets/images/emoji/closed_book.png differ diff --git a/assets/images/emoji/closed_lock_with_key.png b/assets/images/emoji/closed_lock_with_key.png new file mode 100644 index 00000000..e6fdf6cb Binary files /dev/null and b/assets/images/emoji/closed_lock_with_key.png differ diff --git a/assets/images/emoji/closed_umbrella.png b/assets/images/emoji/closed_umbrella.png index 0fdfb872..0b719f08 100644 Binary files a/assets/images/emoji/closed_umbrella.png and b/assets/images/emoji/closed_umbrella.png differ diff --git a/assets/images/emoji/cloud.png b/assets/images/emoji/cloud.png index 564d056d..b31c08c0 100644 Binary files a/assets/images/emoji/cloud.png and b/assets/images/emoji/cloud.png differ diff --git a/assets/images/emoji/clubs.png b/assets/images/emoji/clubs.png index cc1b874b..bfab5365 100644 Binary files a/assets/images/emoji/clubs.png and b/assets/images/emoji/clubs.png differ diff --git a/assets/images/emoji/cn.png b/assets/images/emoji/cn.png index 75cf144d..b30dcc53 100644 Binary files a/assets/images/emoji/cn.png and b/assets/images/emoji/cn.png differ diff --git a/assets/images/emoji/cocktail.png b/assets/images/emoji/cocktail.png index c003656e..28b45ea5 100644 Binary files a/assets/images/emoji/cocktail.png and b/assets/images/emoji/cocktail.png differ diff --git a/assets/images/emoji/coffee.png b/assets/images/emoji/coffee.png index 076281d5..57e1adcb 100644 Binary files a/assets/images/emoji/coffee.png and b/assets/images/emoji/coffee.png differ diff --git a/assets/images/emoji/cold_sweat.png b/assets/images/emoji/cold_sweat.png index dc53962a..b9e39bc6 100644 Binary files a/assets/images/emoji/cold_sweat.png and b/assets/images/emoji/cold_sweat.png differ diff --git a/assets/images/emoji/collision.png b/assets/images/emoji/collision.png new file mode 100644 index 00000000..bddeb8f4 Binary files /dev/null and b/assets/images/emoji/collision.png differ diff --git a/assets/images/emoji/computer.png b/assets/images/emoji/computer.png index f7075d4b..d4d26876 100644 Binary files a/assets/images/emoji/computer.png and b/assets/images/emoji/computer.png differ diff --git a/assets/images/emoji/confetti_ball.png b/assets/images/emoji/confetti_ball.png new file mode 100644 index 00000000..bd293e3d Binary files /dev/null and b/assets/images/emoji/confetti_ball.png differ diff --git a/assets/images/emoji/confounded.png b/assets/images/emoji/confounded.png index 219dafef..762c3766 100644 Binary files a/assets/images/emoji/confounded.png and b/assets/images/emoji/confounded.png differ diff --git a/assets/images/emoji/confused.png b/assets/images/emoji/confused.png new file mode 100644 index 00000000..8dc494db Binary files /dev/null and b/assets/images/emoji/confused.png differ diff --git a/assets/images/emoji/congratulations.png b/assets/images/emoji/congratulations.png index 7d783977..dcbb1d22 100644 Binary files a/assets/images/emoji/congratulations.png and b/assets/images/emoji/congratulations.png differ diff --git a/assets/images/emoji/construction.png b/assets/images/emoji/construction.png index b0c95584..523e9f10 100644 Binary files a/assets/images/emoji/construction.png and b/assets/images/emoji/construction.png differ diff --git a/assets/images/emoji/construction_worker.png b/assets/images/emoji/construction_worker.png index 7eed109b..4d648604 100644 Binary files a/assets/images/emoji/construction_worker.png and b/assets/images/emoji/construction_worker.png differ diff --git a/assets/images/emoji/convenience_store.png b/assets/images/emoji/convenience_store.png index 5c1a906a..671696c2 100644 Binary files a/assets/images/emoji/convenience_store.png and b/assets/images/emoji/convenience_store.png differ diff --git a/assets/images/emoji/cookie.png b/assets/images/emoji/cookie.png new file mode 100644 index 00000000..653edb25 Binary files /dev/null and b/assets/images/emoji/cookie.png differ diff --git a/assets/images/emoji/cool.png b/assets/images/emoji/cool.png index 8dbf6aa6..937dcd79 100644 Binary files a/assets/images/emoji/cool.png and b/assets/images/emoji/cool.png differ diff --git a/assets/images/emoji/cop.png b/assets/images/emoji/cop.png index b3a62914..43a5a84f 100644 Binary files a/assets/images/emoji/cop.png and b/assets/images/emoji/cop.png differ diff --git a/assets/images/emoji/copyright.png b/assets/images/emoji/copyright.png index 00a6b07a..38493c33 100644 Binary files a/assets/images/emoji/copyright.png and b/assets/images/emoji/copyright.png differ diff --git a/assets/images/emoji/corn.png b/assets/images/emoji/corn.png new file mode 100644 index 00000000..fe5d8b12 Binary files /dev/null and b/assets/images/emoji/corn.png differ diff --git a/assets/images/emoji/couple.png b/assets/images/emoji/couple.png index 698ed8ce..9e51f40e 100644 Binary files a/assets/images/emoji/couple.png and b/assets/images/emoji/couple.png differ diff --git a/assets/images/emoji/couple_with_heart.png b/assets/images/emoji/couple_with_heart.png index 8652482f..c503f40a 100644 Binary files a/assets/images/emoji/couple_with_heart.png and b/assets/images/emoji/couple_with_heart.png differ diff --git a/assets/images/emoji/couplekiss.png b/assets/images/emoji/couplekiss.png index 0468b41c..d0279082 100644 Binary files a/assets/images/emoji/couplekiss.png and b/assets/images/emoji/couplekiss.png differ diff --git a/assets/images/emoji/cow.png b/assets/images/emoji/cow.png index efe8a44e..12e1ab6c 100644 Binary files a/assets/images/emoji/cow.png and b/assets/images/emoji/cow.png differ diff --git a/assets/images/emoji/cow2.png b/assets/images/emoji/cow2.png new file mode 100644 index 00000000..594c9215 Binary files /dev/null and b/assets/images/emoji/cow2.png differ diff --git a/assets/images/emoji/credit_card.png b/assets/images/emoji/credit_card.png new file mode 100644 index 00000000..be1c1dd3 Binary files /dev/null and b/assets/images/emoji/credit_card.png differ diff --git a/assets/images/emoji/crocodile.png b/assets/images/emoji/crocodile.png new file mode 100644 index 00000000..7435d5ab Binary files /dev/null and b/assets/images/emoji/crocodile.png differ diff --git a/assets/images/emoji/crossed_flags.png b/assets/images/emoji/crossed_flags.png index 9f95128c..2397bcd0 100644 Binary files a/assets/images/emoji/crossed_flags.png and b/assets/images/emoji/crossed_flags.png differ diff --git a/assets/images/emoji/crown.png b/assets/images/emoji/crown.png index 5b7e2a9e..39da1d52 100644 Binary files a/assets/images/emoji/crown.png and b/assets/images/emoji/crown.png differ diff --git a/assets/images/emoji/cry.png b/assets/images/emoji/cry.png index 11175cee..6d0d9afd 100644 Binary files a/assets/images/emoji/cry.png and b/assets/images/emoji/cry.png differ diff --git a/assets/images/emoji/crying_cat_face.png b/assets/images/emoji/crying_cat_face.png new file mode 100644 index 00000000..42d4c27c Binary files /dev/null and b/assets/images/emoji/crying_cat_face.png differ diff --git a/assets/images/emoji/crystal_ball.png b/assets/images/emoji/crystal_ball.png new file mode 100644 index 00000000..6d2c6c42 Binary files /dev/null and b/assets/images/emoji/crystal_ball.png differ diff --git a/assets/images/emoji/cupid.png b/assets/images/emoji/cupid.png index 9791bfb0..49872847 100644 Binary files a/assets/images/emoji/cupid.png and b/assets/images/emoji/cupid.png differ diff --git a/assets/images/emoji/curly_loop.png b/assets/images/emoji/curly_loop.png new file mode 100644 index 00000000..7dd841d0 Binary files /dev/null and b/assets/images/emoji/curly_loop.png differ diff --git a/assets/images/emoji/currency_exchange.png b/assets/images/emoji/currency_exchange.png index 8d846b85..6ebebe70 100644 Binary files a/assets/images/emoji/currency_exchange.png and b/assets/images/emoji/currency_exchange.png differ diff --git a/assets/images/emoji/curry.png b/assets/images/emoji/curry.png index cd470810..7983c706 100644 Binary files a/assets/images/emoji/curry.png and b/assets/images/emoji/curry.png differ diff --git a/assets/images/emoji/custard.png b/assets/images/emoji/custard.png new file mode 100644 index 00000000..9f843b4c Binary files /dev/null and b/assets/images/emoji/custard.png differ diff --git a/assets/images/emoji/customs.png b/assets/images/emoji/customs.png new file mode 100644 index 00000000..92691e31 Binary files /dev/null and b/assets/images/emoji/customs.png differ diff --git a/assets/images/emoji/cyclone.png b/assets/images/emoji/cyclone.png index 94216ffb..5fd2e451 100644 Binary files a/assets/images/emoji/cyclone.png and b/assets/images/emoji/cyclone.png differ diff --git a/assets/images/emoji/dancer.png b/assets/images/emoji/dancer.png index 2a9895b4..6885a0bc 100644 Binary files a/assets/images/emoji/dancer.png and b/assets/images/emoji/dancer.png differ diff --git a/assets/images/emoji/dancers.png b/assets/images/emoji/dancers.png index 4e48231e..2dfb451a 100644 Binary files a/assets/images/emoji/dancers.png and b/assets/images/emoji/dancers.png differ diff --git a/assets/images/emoji/dango.png b/assets/images/emoji/dango.png index 27d2c1f8..2d042aeb 100644 Binary files a/assets/images/emoji/dango.png and b/assets/images/emoji/dango.png differ diff --git a/assets/images/emoji/dart.png b/assets/images/emoji/dart.png index d35614a2..0438fe54 100644 Binary files a/assets/images/emoji/dart.png and b/assets/images/emoji/dart.png differ diff --git a/assets/images/emoji/dash.png b/assets/images/emoji/dash.png index 534ef442..dc2c0a8f 100644 Binary files a/assets/images/emoji/dash.png and b/assets/images/emoji/dash.png differ diff --git a/assets/images/emoji/date.png b/assets/images/emoji/date.png new file mode 100644 index 00000000..6ad2efa5 Binary files /dev/null and b/assets/images/emoji/date.png differ diff --git a/assets/images/emoji/de.png b/assets/images/emoji/de.png index f0dde235..16a28548 100644 Binary files a/assets/images/emoji/de.png and b/assets/images/emoji/de.png differ diff --git a/assets/images/emoji/deciduous_tree.png b/assets/images/emoji/deciduous_tree.png new file mode 100644 index 00000000..3fdf8c00 Binary files /dev/null and b/assets/images/emoji/deciduous_tree.png differ diff --git a/assets/images/emoji/department_store.png b/assets/images/emoji/department_store.png index 4330c8ac..68d959c5 100644 Binary files a/assets/images/emoji/department_store.png and b/assets/images/emoji/department_store.png differ diff --git a/assets/images/emoji/diamond_shape_with_a_dot_inside.png b/assets/images/emoji/diamond_shape_with_a_dot_inside.png new file mode 100644 index 00000000..dfd1098b Binary files /dev/null and b/assets/images/emoji/diamond_shape_with_a_dot_inside.png differ diff --git a/assets/images/emoji/diamonds.png b/assets/images/emoji/diamonds.png index be9cdffc..fe082775 100644 Binary files a/assets/images/emoji/diamonds.png and b/assets/images/emoji/diamonds.png differ diff --git a/assets/images/emoji/disappointed.png b/assets/images/emoji/disappointed.png index cc18531c..82552008 100644 Binary files a/assets/images/emoji/disappointed.png and b/assets/images/emoji/disappointed.png differ diff --git a/assets/images/emoji/disappointed_relieved.png b/assets/images/emoji/disappointed_relieved.png new file mode 100644 index 00000000..fa5f9e7f Binary files /dev/null and b/assets/images/emoji/disappointed_relieved.png differ diff --git a/assets/images/emoji/dizzy.png b/assets/images/emoji/dizzy.png new file mode 100644 index 00000000..467f73e8 Binary files /dev/null and b/assets/images/emoji/dizzy.png differ diff --git a/assets/images/emoji/dizzy_face.png b/assets/images/emoji/dizzy_face.png new file mode 100644 index 00000000..8001d6ff Binary files /dev/null and b/assets/images/emoji/dizzy_face.png differ diff --git a/assets/images/emoji/do_not_litter.png b/assets/images/emoji/do_not_litter.png new file mode 100644 index 00000000..38c7ae7a Binary files /dev/null and b/assets/images/emoji/do_not_litter.png differ diff --git a/assets/images/emoji/dog.png b/assets/images/emoji/dog.png index 43dbf654..389a02bf 100644 Binary files a/assets/images/emoji/dog.png and b/assets/images/emoji/dog.png differ diff --git a/assets/images/emoji/dog2.png b/assets/images/emoji/dog2.png new file mode 100644 index 00000000..c7f6a24a Binary files /dev/null and b/assets/images/emoji/dog2.png differ diff --git a/assets/images/emoji/dollar.png b/assets/images/emoji/dollar.png new file mode 100644 index 00000000..63de8849 Binary files /dev/null and b/assets/images/emoji/dollar.png differ diff --git a/assets/images/emoji/dolls.png b/assets/images/emoji/dolls.png index 0edb37b0..47ce3390 100644 Binary files a/assets/images/emoji/dolls.png and b/assets/images/emoji/dolls.png differ diff --git a/assets/images/emoji/dolphin.png b/assets/images/emoji/dolphin.png index bc15516f..9326077a 100644 Binary files a/assets/images/emoji/dolphin.png and b/assets/images/emoji/dolphin.png differ diff --git a/assets/images/emoji/donut.png b/assets/images/emoji/donut.png new file mode 100644 index 00000000..ccf86912 Binary files /dev/null and b/assets/images/emoji/donut.png differ diff --git a/assets/images/emoji/door.png b/assets/images/emoji/door.png new file mode 100644 index 00000000..83c819ae Binary files /dev/null and b/assets/images/emoji/door.png differ diff --git a/assets/images/emoji/doughnut.png b/assets/images/emoji/doughnut.png new file mode 100644 index 00000000..ccf86912 Binary files /dev/null and b/assets/images/emoji/doughnut.png differ diff --git a/assets/images/emoji/dragon.png b/assets/images/emoji/dragon.png new file mode 100644 index 00000000..88d4784b Binary files /dev/null and b/assets/images/emoji/dragon.png differ diff --git a/assets/images/emoji/dragon_face.png b/assets/images/emoji/dragon_face.png new file mode 100644 index 00000000..e5e556bd Binary files /dev/null and b/assets/images/emoji/dragon_face.png differ diff --git a/assets/images/emoji/dress.png b/assets/images/emoji/dress.png index 3884e546..6434e2e2 100644 Binary files a/assets/images/emoji/dress.png and b/assets/images/emoji/dress.png differ diff --git a/assets/images/emoji/dromedary_camel.png b/assets/images/emoji/dromedary_camel.png new file mode 100644 index 00000000..c8c7b9ff Binary files /dev/null and b/assets/images/emoji/dromedary_camel.png differ diff --git a/assets/images/emoji/droplet.png b/assets/images/emoji/droplet.png new file mode 100644 index 00000000..cae7f495 Binary files /dev/null and b/assets/images/emoji/droplet.png differ diff --git a/assets/images/emoji/dvd.png b/assets/images/emoji/dvd.png index f38631ae..363c83d0 100644 Binary files a/assets/images/emoji/dvd.png and b/assets/images/emoji/dvd.png differ diff --git a/assets/images/emoji/e-mail.png b/assets/images/emoji/e-mail.png new file mode 100644 index 00000000..176a8e1e Binary files /dev/null and b/assets/images/emoji/e-mail.png differ diff --git a/assets/images/emoji/ear.png b/assets/images/emoji/ear.png index 26fd9757..2bbbf10c 100644 Binary files a/assets/images/emoji/ear.png and b/assets/images/emoji/ear.png differ diff --git a/assets/images/emoji/ear_of_rice.png b/assets/images/emoji/ear_of_rice.png index 9a64b786..a9bba5c2 100644 Binary files a/assets/images/emoji/ear_of_rice.png and b/assets/images/emoji/ear_of_rice.png differ diff --git a/assets/images/emoji/earth_africa.png b/assets/images/emoji/earth_africa.png new file mode 100644 index 00000000..44ce5ecb Binary files /dev/null and b/assets/images/emoji/earth_africa.png differ diff --git a/assets/images/emoji/earth_americas.png b/assets/images/emoji/earth_americas.png new file mode 100644 index 00000000..97d71767 Binary files /dev/null and b/assets/images/emoji/earth_americas.png differ diff --git a/assets/images/emoji/earth_asia.png b/assets/images/emoji/earth_asia.png new file mode 100644 index 00000000..95ec357c Binary files /dev/null and b/assets/images/emoji/earth_asia.png differ diff --git a/assets/images/emoji/egg.png b/assets/images/emoji/egg.png index 7ca9357f..c3de6ae4 100644 Binary files a/assets/images/emoji/egg.png and b/assets/images/emoji/egg.png differ diff --git a/assets/images/emoji/eggplant.png b/assets/images/emoji/eggplant.png index 73582101..66f25fce 100644 Binary files a/assets/images/emoji/eggplant.png and b/assets/images/emoji/eggplant.png differ diff --git a/assets/images/emoji/egplant.png b/assets/images/emoji/egplant.png deleted file mode 100644 index b12cad33..00000000 Binary files a/assets/images/emoji/egplant.png and /dev/null differ diff --git a/assets/images/emoji/eight.png b/assets/images/emoji/eight.png new file mode 100644 index 00000000..7bdb4223 Binary files /dev/null and b/assets/images/emoji/eight.png differ diff --git a/assets/images/emoji/eight_pointed_black_star.png b/assets/images/emoji/eight_pointed_black_star.png index 9693e50c..6ddaa21f 100644 Binary files a/assets/images/emoji/eight_pointed_black_star.png and b/assets/images/emoji/eight_pointed_black_star.png differ diff --git a/assets/images/emoji/eight_spoked_asterisk.png b/assets/images/emoji/eight_spoked_asterisk.png index 459c1d2d..946a2033 100644 Binary files a/assets/images/emoji/eight_spoked_asterisk.png and b/assets/images/emoji/eight_spoked_asterisk.png differ diff --git a/assets/images/emoji/electric_plug.png b/assets/images/emoji/electric_plug.png new file mode 100644 index 00000000..fbef4068 Binary files /dev/null and b/assets/images/emoji/electric_plug.png differ diff --git a/assets/images/emoji/elephant.png b/assets/images/emoji/elephant.png index c8010a66..5ca04570 100644 Binary files a/assets/images/emoji/elephant.png and b/assets/images/emoji/elephant.png differ diff --git a/assets/images/emoji/email.png b/assets/images/emoji/email.png index 22584b9b..0e01fd5f 100644 Binary files a/assets/images/emoji/email.png and b/assets/images/emoji/email.png differ diff --git a/assets/images/emoji/end.png b/assets/images/emoji/end.png new file mode 100644 index 00000000..edb0bda2 Binary files /dev/null and b/assets/images/emoji/end.png differ diff --git a/assets/images/emoji/envelope.png b/assets/images/emoji/envelope.png new file mode 100644 index 00000000..3631861b Binary files /dev/null and b/assets/images/emoji/envelope.png differ diff --git a/assets/images/emoji/es.png b/assets/images/emoji/es.png index e6fa0f2f..71b30bff 100644 Binary files a/assets/images/emoji/es.png and b/assets/images/emoji/es.png differ diff --git a/assets/images/emoji/euro.png b/assets/images/emoji/euro.png new file mode 100644 index 00000000..1c5904b7 Binary files /dev/null and b/assets/images/emoji/euro.png differ diff --git a/assets/images/emoji/european_castle.png b/assets/images/emoji/european_castle.png index 8a8a4c99..8229b8a8 100644 Binary files a/assets/images/emoji/european_castle.png and b/assets/images/emoji/european_castle.png differ diff --git a/assets/images/emoji/european_post_office.png b/assets/images/emoji/european_post_office.png new file mode 100644 index 00000000..0f65b145 Binary files /dev/null and b/assets/images/emoji/european_post_office.png differ diff --git a/assets/images/emoji/evergreen_tree.png b/assets/images/emoji/evergreen_tree.png new file mode 100644 index 00000000..ae8ad103 Binary files /dev/null and b/assets/images/emoji/evergreen_tree.png differ diff --git a/assets/images/emoji/exclamation.png b/assets/images/emoji/exclamation.png index 2171aa8d..77bbdeab 100644 Binary files a/assets/images/emoji/exclamation.png and b/assets/images/emoji/exclamation.png differ diff --git a/assets/images/emoji/expressionless.png b/assets/images/emoji/expressionless.png new file mode 100644 index 00000000..913ff4e2 Binary files /dev/null and b/assets/images/emoji/expressionless.png differ diff --git a/assets/images/emoji/eyeglasses.png b/assets/images/emoji/eyeglasses.png new file mode 100644 index 00000000..a3cf75a2 Binary files /dev/null and b/assets/images/emoji/eyeglasses.png differ diff --git a/assets/images/emoji/eyes.png b/assets/images/emoji/eyes.png index b02c44e3..dc2216f6 100644 Binary files a/assets/images/emoji/eyes.png and b/assets/images/emoji/eyes.png differ diff --git a/assets/images/emoji/facepunch.png b/assets/images/emoji/facepunch.png new file mode 100644 index 00000000..277047b7 Binary files /dev/null and b/assets/images/emoji/facepunch.png differ diff --git a/assets/images/emoji/factory.png b/assets/images/emoji/factory.png index f80c4ec6..64046347 100644 Binary files a/assets/images/emoji/factory.png and b/assets/images/emoji/factory.png differ diff --git a/assets/images/emoji/fallen_leaf.png b/assets/images/emoji/fallen_leaf.png index dc6c3137..d49f9c17 100644 Binary files a/assets/images/emoji/fallen_leaf.png and b/assets/images/emoji/fallen_leaf.png differ diff --git a/assets/images/emoji/family.png b/assets/images/emoji/family.png new file mode 100644 index 00000000..b4b365f3 Binary files /dev/null and b/assets/images/emoji/family.png differ diff --git a/assets/images/emoji/fast_forward.png b/assets/images/emoji/fast_forward.png index c478e558..8830e146 100644 Binary files a/assets/images/emoji/fast_forward.png and b/assets/images/emoji/fast_forward.png differ diff --git a/assets/images/emoji/fax.png b/assets/images/emoji/fax.png index e20110fe..62be2c95 100644 Binary files a/assets/images/emoji/fax.png and b/assets/images/emoji/fax.png differ diff --git a/assets/images/emoji/fearful.png b/assets/images/emoji/fearful.png index 46765149..513fce47 100644 Binary files a/assets/images/emoji/fearful.png and b/assets/images/emoji/fearful.png differ diff --git a/assets/images/emoji/feelsgood.png b/assets/images/emoji/feelsgood.png index 54567b97..361f969b 100644 Binary files a/assets/images/emoji/feelsgood.png and b/assets/images/emoji/feelsgood.png differ diff --git a/assets/images/emoji/feet.png b/assets/images/emoji/feet.png index af5261aa..1b0147b1 100644 Binary files a/assets/images/emoji/feet.png and b/assets/images/emoji/feet.png differ diff --git a/assets/images/emoji/ferris_wheel.png b/assets/images/emoji/ferris_wheel.png index 9b791ca5..54a1dcfa 100644 Binary files a/assets/images/emoji/ferris_wheel.png and b/assets/images/emoji/ferris_wheel.png differ diff --git a/assets/images/emoji/file_folder.png b/assets/images/emoji/file_folder.png new file mode 100644 index 00000000..4d8bebf8 Binary files /dev/null and b/assets/images/emoji/file_folder.png differ diff --git a/assets/images/emoji/finnadie.png b/assets/images/emoji/finnadie.png index 12394fc8..bfc5a0d9 100644 Binary files a/assets/images/emoji/finnadie.png and b/assets/images/emoji/finnadie.png differ diff --git a/assets/images/emoji/fire.png b/assets/images/emoji/fire.png index fd6bc84e..f2a3149b 100644 Binary files a/assets/images/emoji/fire.png and b/assets/images/emoji/fire.png differ diff --git a/assets/images/emoji/fire_engine.png b/assets/images/emoji/fire_engine.png index 1c115bba..9e6c59c9 100644 Binary files a/assets/images/emoji/fire_engine.png and b/assets/images/emoji/fire_engine.png differ diff --git a/assets/images/emoji/fireworks.png b/assets/images/emoji/fireworks.png index 435247db..b4eccd57 100644 Binary files a/assets/images/emoji/fireworks.png and b/assets/images/emoji/fireworks.png differ diff --git a/assets/images/emoji/first_quarter_moon.png b/assets/images/emoji/first_quarter_moon.png new file mode 100644 index 00000000..f38c2369 Binary files /dev/null and b/assets/images/emoji/first_quarter_moon.png differ diff --git a/assets/images/emoji/first_quarter_moon_with_face.png b/assets/images/emoji/first_quarter_moon_with_face.png new file mode 100644 index 00000000..85ae2ce7 Binary files /dev/null and b/assets/images/emoji/first_quarter_moon_with_face.png differ diff --git a/assets/images/emoji/fish.png b/assets/images/emoji/fish.png index e0195747..90bdda2c 100644 Binary files a/assets/images/emoji/fish.png and b/assets/images/emoji/fish.png differ diff --git a/assets/images/emoji/fish_cake.png b/assets/images/emoji/fish_cake.png new file mode 100644 index 00000000..a8f22614 Binary files /dev/null and b/assets/images/emoji/fish_cake.png differ diff --git a/assets/images/emoji/fishing_pole_and_fish.png b/assets/images/emoji/fishing_pole_and_fish.png new file mode 100644 index 00000000..d84609c3 Binary files /dev/null and b/assets/images/emoji/fishing_pole_and_fish.png differ diff --git a/assets/images/emoji/fist.png b/assets/images/emoji/fist.png index 2d4a5147..ecc8874c 100644 Binary files a/assets/images/emoji/fist.png and b/assets/images/emoji/fist.png differ diff --git a/assets/images/emoji/five.png b/assets/images/emoji/five.png new file mode 100644 index 00000000..794321aa Binary files /dev/null and b/assets/images/emoji/five.png differ diff --git a/assets/images/emoji/flags.png b/assets/images/emoji/flags.png index ed6d0153..540164e8 100644 Binary files a/assets/images/emoji/flags.png and b/assets/images/emoji/flags.png differ diff --git a/assets/images/emoji/flashlight.png b/assets/images/emoji/flashlight.png new file mode 100644 index 00000000..215940aa Binary files /dev/null and b/assets/images/emoji/flashlight.png differ diff --git a/assets/images/emoji/floppy_disk.png b/assets/images/emoji/floppy_disk.png new file mode 100644 index 00000000..4ad56315 Binary files /dev/null and b/assets/images/emoji/floppy_disk.png differ diff --git a/assets/images/emoji/flower_playing_cards.png b/assets/images/emoji/flower_playing_cards.png new file mode 100644 index 00000000..cc46a6a1 Binary files /dev/null and b/assets/images/emoji/flower_playing_cards.png differ diff --git a/assets/images/emoji/flushed.png b/assets/images/emoji/flushed.png index 866466ba..74b78c9c 100644 Binary files a/assets/images/emoji/flushed.png and b/assets/images/emoji/flushed.png differ diff --git a/assets/images/emoji/foggy.png b/assets/images/emoji/foggy.png new file mode 100644 index 00000000..3c7b8b04 Binary files /dev/null and b/assets/images/emoji/foggy.png differ diff --git a/assets/images/emoji/football.png b/assets/images/emoji/football.png index 07d67ece..0e4e168f 100644 Binary files a/assets/images/emoji/football.png and b/assets/images/emoji/football.png differ diff --git a/assets/images/emoji/fork_and_knife.png b/assets/images/emoji/fork_and_knife.png index 8f031940..8ba4bc65 100644 Binary files a/assets/images/emoji/fork_and_knife.png and b/assets/images/emoji/fork_and_knife.png differ diff --git a/assets/images/emoji/fountain.png b/assets/images/emoji/fountain.png index 38b52329..da126e64 100644 Binary files a/assets/images/emoji/fountain.png and b/assets/images/emoji/fountain.png differ diff --git a/assets/images/emoji/four.png b/assets/images/emoji/four.png new file mode 100644 index 00000000..14782ba2 Binary files /dev/null and b/assets/images/emoji/four.png differ diff --git a/assets/images/emoji/four_leaf_clover.png b/assets/images/emoji/four_leaf_clover.png index adaf4ce1..f2014bea 100644 Binary files a/assets/images/emoji/four_leaf_clover.png and b/assets/images/emoji/four_leaf_clover.png differ diff --git a/assets/images/emoji/fr.png b/assets/images/emoji/fr.png index 2a2afb76..6311c911 100644 Binary files a/assets/images/emoji/fr.png and b/assets/images/emoji/fr.png differ diff --git a/assets/images/emoji/free.png b/assets/images/emoji/free.png new file mode 100644 index 00000000..c886cf24 Binary files /dev/null and b/assets/images/emoji/free.png differ diff --git a/assets/images/emoji/fried_shrimp.png b/assets/images/emoji/fried_shrimp.png new file mode 100644 index 00000000..c8c284bf Binary files /dev/null and b/assets/images/emoji/fried_shrimp.png differ diff --git a/assets/images/emoji/fries.png b/assets/images/emoji/fries.png index 727b5578..cfef6696 100644 Binary files a/assets/images/emoji/fries.png and b/assets/images/emoji/fries.png differ diff --git a/assets/images/emoji/frog.png b/assets/images/emoji/frog.png index bac9a67e..cfe11b18 100644 Binary files a/assets/images/emoji/frog.png and b/assets/images/emoji/frog.png differ diff --git a/assets/images/emoji/frowning.png b/assets/images/emoji/frowning.png new file mode 100644 index 00000000..087a6627 Binary files /dev/null and b/assets/images/emoji/frowning.png differ diff --git a/assets/images/emoji/fu.png b/assets/images/emoji/fu.png new file mode 100644 index 00000000..61a3fee8 Binary files /dev/null and b/assets/images/emoji/fu.png differ diff --git a/assets/images/emoji/fuelpump.png b/assets/images/emoji/fuelpump.png index d229b667..54c29aeb 100644 Binary files a/assets/images/emoji/fuelpump.png and b/assets/images/emoji/fuelpump.png differ diff --git a/assets/images/emoji/full_moon.png b/assets/images/emoji/full_moon.png new file mode 100644 index 00000000..8ff657a2 Binary files /dev/null and b/assets/images/emoji/full_moon.png differ diff --git a/assets/images/emoji/full_moon_with_face.png b/assets/images/emoji/full_moon_with_face.png new file mode 100644 index 00000000..d42b3f0f Binary files /dev/null and b/assets/images/emoji/full_moon_with_face.png differ diff --git a/assets/images/emoji/game_die.png b/assets/images/emoji/game_die.png new file mode 100644 index 00000000..4136e78e Binary files /dev/null and b/assets/images/emoji/game_die.png differ diff --git a/assets/images/emoji/gb.png b/assets/images/emoji/gb.png index d9eab20a..2a62c7a0 100644 Binary files a/assets/images/emoji/gb.png and b/assets/images/emoji/gb.png differ diff --git a/assets/images/emoji/gem.png b/assets/images/emoji/gem.png index 1245c0ad..8a5d8dad 100644 Binary files a/assets/images/emoji/gem.png and b/assets/images/emoji/gem.png differ diff --git a/assets/images/emoji/gemini.png b/assets/images/emoji/gemini.png index db22fba8..d926f6e8 100644 Binary files a/assets/images/emoji/gemini.png and b/assets/images/emoji/gemini.png differ diff --git a/assets/images/emoji/ghost.png b/assets/images/emoji/ghost.png index 23408c40..671dd0c9 100644 Binary files a/assets/images/emoji/ghost.png and b/assets/images/emoji/ghost.png differ diff --git a/assets/images/emoji/gift.png b/assets/images/emoji/gift.png index b6262ebb..552cfdc2 100644 Binary files a/assets/images/emoji/gift.png and b/assets/images/emoji/gift.png differ diff --git a/assets/images/emoji/gift_heart.png b/assets/images/emoji/gift_heart.png index 12305e24..f31c26a3 100644 Binary files a/assets/images/emoji/gift_heart.png and b/assets/images/emoji/gift_heart.png differ diff --git a/assets/images/emoji/girl.png b/assets/images/emoji/girl.png index 98c032da..ea412694 100644 Binary files a/assets/images/emoji/girl.png and b/assets/images/emoji/girl.png differ diff --git a/assets/images/emoji/globe_with_meridians.png b/assets/images/emoji/globe_with_meridians.png new file mode 100644 index 00000000..b1986466 Binary files /dev/null and b/assets/images/emoji/globe_with_meridians.png differ diff --git a/assets/images/emoji/goat.png b/assets/images/emoji/goat.png new file mode 100644 index 00000000..4be9cf30 Binary files /dev/null and b/assets/images/emoji/goat.png differ diff --git a/assets/images/emoji/goberserk.png b/assets/images/emoji/goberserk.png index 8d3917a5..59a742aa 100644 Binary files a/assets/images/emoji/goberserk.png and b/assets/images/emoji/goberserk.png differ diff --git a/assets/images/emoji/godmode.png b/assets/images/emoji/godmode.png index d8feb0d8..7e75ab20 100644 Binary files a/assets/images/emoji/godmode.png and b/assets/images/emoji/godmode.png differ diff --git a/assets/images/emoji/golf.png b/assets/images/emoji/golf.png index f30131f6..cba2116a 100644 Binary files a/assets/images/emoji/golf.png and b/assets/images/emoji/golf.png differ diff --git a/assets/images/emoji/grapes.png b/assets/images/emoji/grapes.png new file mode 100644 index 00000000..0f9f007a Binary files /dev/null and b/assets/images/emoji/grapes.png differ diff --git a/assets/images/emoji/green_apple.png b/assets/images/emoji/green_apple.png new file mode 100644 index 00000000..337205cd Binary files /dev/null and b/assets/images/emoji/green_apple.png differ diff --git a/assets/images/emoji/green_book.png b/assets/images/emoji/green_book.png new file mode 100644 index 00000000..e86651e5 Binary files /dev/null and b/assets/images/emoji/green_book.png differ diff --git a/assets/images/emoji/green_heart.png b/assets/images/emoji/green_heart.png index 1b0b4d16..7289cb81 100644 Binary files a/assets/images/emoji/green_heart.png and b/assets/images/emoji/green_heart.png differ diff --git a/assets/images/emoji/grey_exclamation.png b/assets/images/emoji/grey_exclamation.png index c94a2b93..a50d265e 100644 Binary files a/assets/images/emoji/grey_exclamation.png and b/assets/images/emoji/grey_exclamation.png differ diff --git a/assets/images/emoji/grey_question.png b/assets/images/emoji/grey_question.png index a50cca2a..fb97ba75 100644 Binary files a/assets/images/emoji/grey_question.png and b/assets/images/emoji/grey_question.png differ diff --git a/assets/images/emoji/grimacing.png b/assets/images/emoji/grimacing.png new file mode 100644 index 00000000..18a63e3e Binary files /dev/null and b/assets/images/emoji/grimacing.png differ diff --git a/assets/images/emoji/grin.png b/assets/images/emoji/grin.png index 694bf56f..591cfcef 100644 Binary files a/assets/images/emoji/grin.png and b/assets/images/emoji/grin.png differ diff --git a/assets/images/emoji/grinning.png b/assets/images/emoji/grinning.png new file mode 100644 index 00000000..7e812b7e Binary files /dev/null and b/assets/images/emoji/grinning.png differ diff --git a/assets/images/emoji/guardsman.png b/assets/images/emoji/guardsman.png index c228553b..b67b335d 100644 Binary files a/assets/images/emoji/guardsman.png and b/assets/images/emoji/guardsman.png differ diff --git a/assets/images/emoji/guitar.png b/assets/images/emoji/guitar.png index 81ee4722..2b7fa43c 100644 Binary files a/assets/images/emoji/guitar.png and b/assets/images/emoji/guitar.png differ diff --git a/assets/images/emoji/gun.png b/assets/images/emoji/gun.png index 24209c4a..c49dc52c 100644 Binary files a/assets/images/emoji/gun.png and b/assets/images/emoji/gun.png differ diff --git a/assets/images/emoji/haircut.png b/assets/images/emoji/haircut.png index c3bca9b4..902d273f 100644 Binary files a/assets/images/emoji/haircut.png and b/assets/images/emoji/haircut.png differ diff --git a/assets/images/emoji/hamburger.png b/assets/images/emoji/hamburger.png index f3c76f63..9f1a3fdf 100644 Binary files a/assets/images/emoji/hamburger.png and b/assets/images/emoji/hamburger.png differ diff --git a/assets/images/emoji/hammer.png b/assets/images/emoji/hammer.png index 99f5a2f5..482b1c74 100644 Binary files a/assets/images/emoji/hammer.png and b/assets/images/emoji/hammer.png differ diff --git a/assets/images/emoji/hamster.png b/assets/images/emoji/hamster.png index 5c9340b0..addfd2e6 100644 Binary files a/assets/images/emoji/hamster.png and b/assets/images/emoji/hamster.png differ diff --git a/assets/images/emoji/hand.png b/assets/images/emoji/hand.png index 1cdf992f..5e45c25a 100644 Binary files a/assets/images/emoji/hand.png and b/assets/images/emoji/hand.png differ diff --git a/assets/images/emoji/handbag.png b/assets/images/emoji/handbag.png index 34fec65c..d7adf04d 100644 Binary files a/assets/images/emoji/handbag.png and b/assets/images/emoji/handbag.png differ diff --git a/assets/images/emoji/hankey.png b/assets/images/emoji/hankey.png index 69bc33f1..73a4dc84 100644 Binary files a/assets/images/emoji/hankey.png and b/assets/images/emoji/hankey.png differ diff --git a/assets/images/emoji/hash.png b/assets/images/emoji/hash.png index 4fdc5cc0..6765d7d3 100644 Binary files a/assets/images/emoji/hash.png and b/assets/images/emoji/hash.png differ diff --git a/assets/images/emoji/hatched_chick.png b/assets/images/emoji/hatched_chick.png new file mode 100644 index 00000000..39c25bc7 Binary files /dev/null and b/assets/images/emoji/hatched_chick.png differ diff --git a/assets/images/emoji/hatching_chick.png b/assets/images/emoji/hatching_chick.png new file mode 100644 index 00000000..005a5551 Binary files /dev/null and b/assets/images/emoji/hatching_chick.png differ diff --git a/assets/images/emoji/headphones.png b/assets/images/emoji/headphones.png index 440ed43f..ad83000e 100644 Binary files a/assets/images/emoji/headphones.png and b/assets/images/emoji/headphones.png differ diff --git a/assets/images/emoji/hear_no_evil.png b/assets/images/emoji/hear_no_evil.png new file mode 100644 index 00000000..f97a1f9a Binary files /dev/null and b/assets/images/emoji/hear_no_evil.png differ diff --git a/assets/images/emoji/heart.png b/assets/images/emoji/heart.png index 5de16c9f..7d7790ce 100644 Binary files a/assets/images/emoji/heart.png and b/assets/images/emoji/heart.png differ diff --git a/assets/images/emoji/heart_decoration.png b/assets/images/emoji/heart_decoration.png index f895cf53..b8be44db 100644 Binary files a/assets/images/emoji/heart_decoration.png and b/assets/images/emoji/heart_decoration.png differ diff --git a/assets/images/emoji/heart_eyes.png b/assets/images/emoji/heart_eyes.png index ef85cb66..0e579427 100644 Binary files a/assets/images/emoji/heart_eyes.png and b/assets/images/emoji/heart_eyes.png differ diff --git a/assets/images/emoji/heart_eyes_cat.png b/assets/images/emoji/heart_eyes_cat.png new file mode 100644 index 00000000..eeba240e Binary files /dev/null and b/assets/images/emoji/heart_eyes_cat.png differ diff --git a/assets/images/emoji/heartbeat.png b/assets/images/emoji/heartbeat.png index ce670748..b6628f6f 100644 Binary files a/assets/images/emoji/heartbeat.png and b/assets/images/emoji/heartbeat.png differ diff --git a/assets/images/emoji/heartpulse.png b/assets/images/emoji/heartpulse.png index dc7e55d3..a7491cbe 100644 Binary files a/assets/images/emoji/heartpulse.png and b/assets/images/emoji/heartpulse.png differ diff --git a/assets/images/emoji/hearts.png b/assets/images/emoji/hearts.png index be35ebdd..e8947153 100644 Binary files a/assets/images/emoji/hearts.png and b/assets/images/emoji/hearts.png differ diff --git a/assets/images/emoji/heavy_check_mark.png b/assets/images/emoji/heavy_check_mark.png new file mode 100644 index 00000000..d0f010b4 Binary files /dev/null and b/assets/images/emoji/heavy_check_mark.png differ diff --git a/assets/images/emoji/heavy_division_sign.png b/assets/images/emoji/heavy_division_sign.png new file mode 100644 index 00000000..ac757a23 Binary files /dev/null and b/assets/images/emoji/heavy_division_sign.png differ diff --git a/assets/images/emoji/heavy_dollar_sign.png b/assets/images/emoji/heavy_dollar_sign.png new file mode 100644 index 00000000..361e26ae Binary files /dev/null and b/assets/images/emoji/heavy_dollar_sign.png differ diff --git a/assets/images/emoji/heavy_exclamation_mark.png b/assets/images/emoji/heavy_exclamation_mark.png new file mode 100644 index 00000000..4c560f5e Binary files /dev/null and b/assets/images/emoji/heavy_exclamation_mark.png differ diff --git a/assets/images/emoji/heavy_minus_sign.png b/assets/images/emoji/heavy_minus_sign.png new file mode 100644 index 00000000..4a33f905 Binary files /dev/null and b/assets/images/emoji/heavy_minus_sign.png differ diff --git a/assets/images/emoji/heavy_multiplication_x.png b/assets/images/emoji/heavy_multiplication_x.png new file mode 100644 index 00000000..13d66607 Binary files /dev/null and b/assets/images/emoji/heavy_multiplication_x.png differ diff --git a/assets/images/emoji/heavy_plus_sign.png b/assets/images/emoji/heavy_plus_sign.png new file mode 100644 index 00000000..61595387 Binary files /dev/null and b/assets/images/emoji/heavy_plus_sign.png differ diff --git a/assets/images/emoji/helicopter.png b/assets/images/emoji/helicopter.png new file mode 100644 index 00000000..8e82a0d5 Binary files /dev/null and b/assets/images/emoji/helicopter.png differ diff --git a/assets/images/emoji/herb.png b/assets/images/emoji/herb.png new file mode 100644 index 00000000..de1ff1b7 Binary files /dev/null and b/assets/images/emoji/herb.png differ diff --git a/assets/images/emoji/hibiscus.png b/assets/images/emoji/hibiscus.png index 0f1c4af0..9365ae21 100644 Binary files a/assets/images/emoji/hibiscus.png and b/assets/images/emoji/hibiscus.png differ diff --git a/assets/images/emoji/high_brightness.png b/assets/images/emoji/high_brightness.png new file mode 100644 index 00000000..ba9de7d4 Binary files /dev/null and b/assets/images/emoji/high_brightness.png differ diff --git a/assets/images/emoji/high_heel.png b/assets/images/emoji/high_heel.png index 275389ed..525b6a0d 100644 Binary files a/assets/images/emoji/high_heel.png and b/assets/images/emoji/high_heel.png differ diff --git a/assets/images/emoji/hocho.png b/assets/images/emoji/hocho.png new file mode 100644 index 00000000..3f05193c Binary files /dev/null and b/assets/images/emoji/hocho.png differ diff --git a/assets/images/emoji/honey_pot.png b/assets/images/emoji/honey_pot.png new file mode 100644 index 00000000..73278898 Binary files /dev/null and b/assets/images/emoji/honey_pot.png differ diff --git a/assets/images/emoji/honeybee.png b/assets/images/emoji/honeybee.png new file mode 100644 index 00000000..f5373395 Binary files /dev/null and b/assets/images/emoji/honeybee.png differ diff --git a/assets/images/emoji/horse.png b/assets/images/emoji/horse.png index 9e43abfd..78d580ad 100644 Binary files a/assets/images/emoji/horse.png and b/assets/images/emoji/horse.png differ diff --git a/assets/images/emoji/horse_racing.png b/assets/images/emoji/horse_racing.png new file mode 100644 index 00000000..e3bbaec1 Binary files /dev/null and b/assets/images/emoji/horse_racing.png differ diff --git a/assets/images/emoji/hospital.png b/assets/images/emoji/hospital.png index 2c72c9a5..c05c4937 100644 Binary files a/assets/images/emoji/hospital.png and b/assets/images/emoji/hospital.png differ diff --git a/assets/images/emoji/hotel.png b/assets/images/emoji/hotel.png index 1b1b48ee..d29f276a 100644 Binary files a/assets/images/emoji/hotel.png and b/assets/images/emoji/hotel.png differ diff --git a/assets/images/emoji/hotsprings.png b/assets/images/emoji/hotsprings.png index 64c6a1d7..a0bc9d75 100644 Binary files a/assets/images/emoji/hotsprings.png and b/assets/images/emoji/hotsprings.png differ diff --git a/assets/images/emoji/hourglass.png b/assets/images/emoji/hourglass.png new file mode 100644 index 00000000..405aab41 Binary files /dev/null and b/assets/images/emoji/hourglass.png differ diff --git a/assets/images/emoji/hourglass_flowing_sand.png b/assets/images/emoji/hourglass_flowing_sand.png new file mode 100644 index 00000000..b68eb695 Binary files /dev/null and b/assets/images/emoji/hourglass_flowing_sand.png differ diff --git a/assets/images/emoji/house.png b/assets/images/emoji/house.png index fdaeca1a..95b9ee09 100644 Binary files a/assets/images/emoji/house.png and b/assets/images/emoji/house.png differ diff --git a/assets/images/emoji/house_with_garden.png b/assets/images/emoji/house_with_garden.png new file mode 100644 index 00000000..6261cd3c Binary files /dev/null and b/assets/images/emoji/house_with_garden.png differ diff --git a/assets/images/emoji/hurtrealbad.png b/assets/images/emoji/hurtrealbad.png index 68153c60..146ef1a6 100644 Binary files a/assets/images/emoji/hurtrealbad.png and b/assets/images/emoji/hurtrealbad.png differ diff --git a/assets/images/emoji/hushed.png b/assets/images/emoji/hushed.png new file mode 100644 index 00000000..b1c0108a Binary files /dev/null and b/assets/images/emoji/hushed.png differ diff --git a/assets/images/emoji/ice_cream.png b/assets/images/emoji/ice_cream.png new file mode 100644 index 00000000..190be016 Binary files /dev/null and b/assets/images/emoji/ice_cream.png differ diff --git a/assets/images/emoji/icecream.png b/assets/images/emoji/icecream.png index 22d32c5b..871ce097 100644 Binary files a/assets/images/emoji/icecream.png and b/assets/images/emoji/icecream.png differ diff --git a/assets/images/emoji/id.png b/assets/images/emoji/id.png index 0bd32b0e..47437a76 100644 Binary files a/assets/images/emoji/id.png and b/assets/images/emoji/id.png differ diff --git a/assets/images/emoji/ideograph_advantage.png b/assets/images/emoji/ideograph_advantage.png index fad3f964..e79af784 100644 Binary files a/assets/images/emoji/ideograph_advantage.png and b/assets/images/emoji/ideograph_advantage.png differ diff --git a/assets/images/emoji/imp.png b/assets/images/emoji/imp.png index c69f6b70..5acca337 100644 Binary files a/assets/images/emoji/imp.png and b/assets/images/emoji/imp.png differ diff --git a/assets/images/emoji/inbox_tray.png b/assets/images/emoji/inbox_tray.png new file mode 100644 index 00000000..e2df0f89 Binary files /dev/null and b/assets/images/emoji/inbox_tray.png differ diff --git a/assets/images/emoji/incoming_envelope.png b/assets/images/emoji/incoming_envelope.png new file mode 100644 index 00000000..afc82712 Binary files /dev/null and b/assets/images/emoji/incoming_envelope.png differ diff --git a/assets/images/emoji/information_desk_person.png b/assets/images/emoji/information_desk_person.png index f6eb7216..52c0a50a 100644 Binary files a/assets/images/emoji/information_desk_person.png and b/assets/images/emoji/information_desk_person.png differ diff --git a/assets/images/emoji/information_source.png b/assets/images/emoji/information_source.png new file mode 100644 index 00000000..9cb8b09b Binary files /dev/null and b/assets/images/emoji/information_source.png differ diff --git a/assets/images/emoji/innocent.png b/assets/images/emoji/innocent.png new file mode 100644 index 00000000..503b614f Binary files /dev/null and b/assets/images/emoji/innocent.png differ diff --git a/assets/images/emoji/interrobang.png b/assets/images/emoji/interrobang.png new file mode 100644 index 00000000..64304b9f Binary files /dev/null and b/assets/images/emoji/interrobang.png differ diff --git a/assets/images/emoji/iphone.png b/assets/images/emoji/iphone.png index 7995a678..df007103 100644 Binary files a/assets/images/emoji/iphone.png and b/assets/images/emoji/iphone.png differ diff --git a/assets/images/emoji/it.png b/assets/images/emoji/it.png index fc3e8a2d..70bc9f32 100644 Binary files a/assets/images/emoji/it.png and b/assets/images/emoji/it.png differ diff --git a/assets/images/emoji/izakaya_lantern.png b/assets/images/emoji/izakaya_lantern.png new file mode 100644 index 00000000..18730ad5 Binary files /dev/null and b/assets/images/emoji/izakaya_lantern.png differ diff --git a/assets/images/emoji/jack_o_lantern.png b/assets/images/emoji/jack_o_lantern.png index 23ae0e98..1f7667ea 100644 Binary files a/assets/images/emoji/jack_o_lantern.png and b/assets/images/emoji/jack_o_lantern.png differ diff --git a/assets/images/emoji/japan.png b/assets/images/emoji/japan.png new file mode 100644 index 00000000..45932803 Binary files /dev/null and b/assets/images/emoji/japan.png differ diff --git a/assets/images/emoji/japanese_castle.png b/assets/images/emoji/japanese_castle.png index bb76bcc0..f225ab21 100644 Binary files a/assets/images/emoji/japanese_castle.png and b/assets/images/emoji/japanese_castle.png differ diff --git a/assets/images/emoji/japanese_goblin.png b/assets/images/emoji/japanese_goblin.png new file mode 100644 index 00000000..bd21b187 Binary files /dev/null and b/assets/images/emoji/japanese_goblin.png differ diff --git a/assets/images/emoji/japanese_ogre.png b/assets/images/emoji/japanese_ogre.png new file mode 100644 index 00000000..e9f5471c Binary files /dev/null and b/assets/images/emoji/japanese_ogre.png differ diff --git a/assets/images/emoji/jeans.png b/assets/images/emoji/jeans.png new file mode 100644 index 00000000..d721cea5 Binary files /dev/null and b/assets/images/emoji/jeans.png differ diff --git a/assets/images/emoji/joy.png b/assets/images/emoji/joy.png index f6f27e48..47df693d 100644 Binary files a/assets/images/emoji/joy.png and b/assets/images/emoji/joy.png differ diff --git a/assets/images/emoji/joy_cat.png b/assets/images/emoji/joy_cat.png new file mode 100644 index 00000000..6c60cb0e Binary files /dev/null and b/assets/images/emoji/joy_cat.png differ diff --git a/assets/images/emoji/jp.png b/assets/images/emoji/jp.png index 8c6e76d7..b786efbb 100644 Binary files a/assets/images/emoji/jp.png and b/assets/images/emoji/jp.png differ diff --git a/assets/images/emoji/key.png b/assets/images/emoji/key.png index 05159c57..34673213 100644 Binary files a/assets/images/emoji/key.png and b/assets/images/emoji/key.png differ diff --git a/assets/images/emoji/keycap_ten.png b/assets/images/emoji/keycap_ten.png new file mode 100644 index 00000000..71dac1c1 Binary files /dev/null and b/assets/images/emoji/keycap_ten.png differ diff --git a/assets/images/emoji/kimono.png b/assets/images/emoji/kimono.png index 4c9456e7..34ffe137 100644 Binary files a/assets/images/emoji/kimono.png and b/assets/images/emoji/kimono.png differ diff --git a/assets/images/emoji/kiss.png b/assets/images/emoji/kiss.png index 141bb429..14fd9918 100644 Binary files a/assets/images/emoji/kiss.png and b/assets/images/emoji/kiss.png differ diff --git a/assets/images/emoji/kissing.png b/assets/images/emoji/kissing.png new file mode 100644 index 00000000..f3c8dcd7 Binary files /dev/null and b/assets/images/emoji/kissing.png differ diff --git a/assets/images/emoji/kissing_cat.png b/assets/images/emoji/kissing_cat.png new file mode 100644 index 00000000..adc62fbe Binary files /dev/null and b/assets/images/emoji/kissing_cat.png differ diff --git a/assets/images/emoji/kissing_closed_eyes.png b/assets/images/emoji/kissing_closed_eyes.png new file mode 100644 index 00000000..449de197 Binary files /dev/null and b/assets/images/emoji/kissing_closed_eyes.png differ diff --git a/assets/images/emoji/kissing_face.png b/assets/images/emoji/kissing_face.png index 6143085a..449de197 100644 Binary files a/assets/images/emoji/kissing_face.png and b/assets/images/emoji/kissing_face.png differ diff --git a/assets/images/emoji/kissing_heart.png b/assets/images/emoji/kissing_heart.png index df01721b..af9a80b7 100644 Binary files a/assets/images/emoji/kissing_heart.png and b/assets/images/emoji/kissing_heart.png differ diff --git a/assets/images/emoji/kissing_smiling_eyes.png b/assets/images/emoji/kissing_smiling_eyes.png new file mode 100644 index 00000000..57f7b493 Binary files /dev/null and b/assets/images/emoji/kissing_smiling_eyes.png differ diff --git a/assets/images/emoji/koala.png b/assets/images/emoji/koala.png index cac71a4e..e17bd3cf 100644 Binary files a/assets/images/emoji/koala.png and b/assets/images/emoji/koala.png differ diff --git a/assets/images/emoji/koko.png b/assets/images/emoji/koko.png index dd6e90c1..3bef28c9 100644 Binary files a/assets/images/emoji/koko.png and b/assets/images/emoji/koko.png differ diff --git a/assets/images/emoji/kr.png b/assets/images/emoji/kr.png index c37b34d5..b4c0c1b6 100644 Binary files a/assets/images/emoji/kr.png and b/assets/images/emoji/kr.png differ diff --git a/assets/images/emoji/large_blue_circle.png b/assets/images/emoji/large_blue_circle.png new file mode 100644 index 00000000..a5b4ad4a Binary files /dev/null and b/assets/images/emoji/large_blue_circle.png differ diff --git a/assets/images/emoji/large_blue_diamond.png b/assets/images/emoji/large_blue_diamond.png new file mode 100644 index 00000000..f4598ec0 Binary files /dev/null and b/assets/images/emoji/large_blue_diamond.png differ diff --git a/assets/images/emoji/large_orange_diamond.png b/assets/images/emoji/large_orange_diamond.png new file mode 100644 index 00000000..803725aa Binary files /dev/null and b/assets/images/emoji/large_orange_diamond.png differ diff --git a/assets/images/emoji/last_quarter_moon.png b/assets/images/emoji/last_quarter_moon.png new file mode 100644 index 00000000..8a692c23 Binary files /dev/null and b/assets/images/emoji/last_quarter_moon.png differ diff --git a/assets/images/emoji/last_quarter_moon_with_face.png b/assets/images/emoji/last_quarter_moon_with_face.png new file mode 100644 index 00000000..9ece82df Binary files /dev/null and b/assets/images/emoji/last_quarter_moon_with_face.png differ diff --git a/assets/images/emoji/laughing.png b/assets/images/emoji/laughing.png new file mode 100644 index 00000000..11c91eb2 Binary files /dev/null and b/assets/images/emoji/laughing.png differ diff --git a/assets/images/emoji/leaves.png b/assets/images/emoji/leaves.png index 9021b3ea..5229e06b 100644 Binary files a/assets/images/emoji/leaves.png and b/assets/images/emoji/leaves.png differ diff --git a/assets/images/emoji/ledger.png b/assets/images/emoji/ledger.png new file mode 100644 index 00000000..e4f72ace Binary files /dev/null and b/assets/images/emoji/ledger.png differ diff --git a/assets/images/emoji/left_luggage.png b/assets/images/emoji/left_luggage.png new file mode 100644 index 00000000..1c08b464 Binary files /dev/null and b/assets/images/emoji/left_luggage.png differ diff --git a/assets/images/emoji/left_right_arrow.png b/assets/images/emoji/left_right_arrow.png new file mode 100644 index 00000000..b9fd11c5 Binary files /dev/null and b/assets/images/emoji/left_right_arrow.png differ diff --git a/assets/images/emoji/leftwards_arrow_with_hook.png b/assets/images/emoji/leftwards_arrow_with_hook.png new file mode 100644 index 00000000..bc45dfef Binary files /dev/null and b/assets/images/emoji/leftwards_arrow_with_hook.png differ diff --git a/assets/images/emoji/lemon.png b/assets/images/emoji/lemon.png new file mode 100644 index 00000000..9814dc95 Binary files /dev/null and b/assets/images/emoji/lemon.png differ diff --git a/assets/images/emoji/leo.png b/assets/images/emoji/leo.png index 1432e36c..e025933b 100644 Binary files a/assets/images/emoji/leo.png and b/assets/images/emoji/leo.png differ diff --git a/assets/images/emoji/leopard.png b/assets/images/emoji/leopard.png new file mode 100644 index 00000000..3e738d2d Binary files /dev/null and b/assets/images/emoji/leopard.png differ diff --git a/assets/images/emoji/libra.png b/assets/images/emoji/libra.png index bd35d66d..c9062dd2 100644 Binary files a/assets/images/emoji/libra.png and b/assets/images/emoji/libra.png differ diff --git a/assets/images/emoji/light_rail.png b/assets/images/emoji/light_rail.png new file mode 100644 index 00000000..bcfe801e Binary files /dev/null and b/assets/images/emoji/light_rail.png differ diff --git a/assets/images/emoji/link.png b/assets/images/emoji/link.png new file mode 100644 index 00000000..0239e48e Binary files /dev/null and b/assets/images/emoji/link.png differ diff --git a/assets/images/emoji/lips.png b/assets/images/emoji/lips.png index f996a8a6..826ed110 100644 Binary files a/assets/images/emoji/lips.png and b/assets/images/emoji/lips.png differ diff --git a/assets/images/emoji/lipstick.png b/assets/images/emoji/lipstick.png index 214ecded..82f990c5 100644 Binary files a/assets/images/emoji/lipstick.png and b/assets/images/emoji/lipstick.png differ diff --git a/assets/images/emoji/lock.png b/assets/images/emoji/lock.png index 5c35d91d..4892b023 100644 Binary files a/assets/images/emoji/lock.png and b/assets/images/emoji/lock.png differ diff --git a/assets/images/emoji/lock_with_ink_pen.png b/assets/images/emoji/lock_with_ink_pen.png new file mode 100644 index 00000000..375e67e8 Binary files /dev/null and b/assets/images/emoji/lock_with_ink_pen.png differ diff --git a/assets/images/emoji/lollipop.png b/assets/images/emoji/lollipop.png new file mode 100644 index 00000000..ba55e709 Binary files /dev/null and b/assets/images/emoji/lollipop.png differ diff --git a/assets/images/emoji/loop.png b/assets/images/emoji/loop.png index 68807b91..ef34df3a 100644 Binary files a/assets/images/emoji/loop.png and b/assets/images/emoji/loop.png differ diff --git a/assets/images/emoji/loudspeaker.png b/assets/images/emoji/loudspeaker.png index d5ffa73b..752385e5 100644 Binary files a/assets/images/emoji/loudspeaker.png and b/assets/images/emoji/loudspeaker.png differ diff --git a/assets/images/emoji/love_hotel.png b/assets/images/emoji/love_hotel.png index dc168a91..44d7db82 100644 Binary files a/assets/images/emoji/love_hotel.png and b/assets/images/emoji/love_hotel.png differ diff --git a/assets/images/emoji/love_letter.png b/assets/images/emoji/love_letter.png new file mode 100644 index 00000000..e29981f4 Binary files /dev/null and b/assets/images/emoji/love_letter.png differ diff --git a/assets/images/emoji/low_brightness.png b/assets/images/emoji/low_brightness.png new file mode 100644 index 00000000..ea15bde4 Binary files /dev/null and b/assets/images/emoji/low_brightness.png differ diff --git a/assets/images/emoji/m.png b/assets/images/emoji/m.png new file mode 100644 index 00000000..7424665e Binary files /dev/null and b/assets/images/emoji/m.png differ diff --git a/assets/images/emoji/mag.png b/assets/images/emoji/mag.png index 9b03915a..aa5b1d7c 100644 Binary files a/assets/images/emoji/mag.png and b/assets/images/emoji/mag.png differ diff --git a/assets/images/emoji/mag_right.png b/assets/images/emoji/mag_right.png new file mode 100644 index 00000000..6e6cf11e Binary files /dev/null and b/assets/images/emoji/mag_right.png differ diff --git a/assets/images/emoji/mahjong.png b/assets/images/emoji/mahjong.png index 9fe286e2..f51ce65f 100644 Binary files a/assets/images/emoji/mahjong.png and b/assets/images/emoji/mahjong.png differ diff --git a/assets/images/emoji/mailbox.png b/assets/images/emoji/mailbox.png index 47d2ee7e..8351e707 100644 Binary files a/assets/images/emoji/mailbox.png and b/assets/images/emoji/mailbox.png differ diff --git a/assets/images/emoji/mailbox_closed.png b/assets/images/emoji/mailbox_closed.png new file mode 100644 index 00000000..a5982b69 Binary files /dev/null and b/assets/images/emoji/mailbox_closed.png differ diff --git a/assets/images/emoji/mailbox_with_mail.png b/assets/images/emoji/mailbox_with_mail.png new file mode 100644 index 00000000..dae34594 Binary files /dev/null and b/assets/images/emoji/mailbox_with_mail.png differ diff --git a/assets/images/emoji/mailbox_with_no_mail.png b/assets/images/emoji/mailbox_with_no_mail.png new file mode 100644 index 00000000..59f15c5d Binary files /dev/null and b/assets/images/emoji/mailbox_with_no_mail.png differ diff --git a/assets/images/emoji/man.png b/assets/images/emoji/man.png index be0c82c6..d9bfa26a 100644 Binary files a/assets/images/emoji/man.png and b/assets/images/emoji/man.png differ diff --git a/assets/images/emoji/man_with_gua_pi_mao.png b/assets/images/emoji/man_with_gua_pi_mao.png index 79448395..7aad74b5 100644 Binary files a/assets/images/emoji/man_with_gua_pi_mao.png and b/assets/images/emoji/man_with_gua_pi_mao.png differ diff --git a/assets/images/emoji/man_with_turban.png b/assets/images/emoji/man_with_turban.png index 4a31f317..036604ca 100644 Binary files a/assets/images/emoji/man_with_turban.png and b/assets/images/emoji/man_with_turban.png differ diff --git a/assets/images/emoji/mans_shoe.png b/assets/images/emoji/mans_shoe.png new file mode 100644 index 00000000..ecba9ba7 Binary files /dev/null and b/assets/images/emoji/mans_shoe.png differ diff --git a/assets/images/emoji/maple_leaf.png b/assets/images/emoji/maple_leaf.png index dc7d1bbc..4e9b4720 100644 Binary files a/assets/images/emoji/maple_leaf.png and b/assets/images/emoji/maple_leaf.png differ diff --git a/assets/images/emoji/mask.png b/assets/images/emoji/mask.png index e71b212c..05887e99 100644 Binary files a/assets/images/emoji/mask.png and b/assets/images/emoji/mask.png differ diff --git a/assets/images/emoji/massage.png b/assets/images/emoji/massage.png index 89215138..dd30d159 100644 Binary files a/assets/images/emoji/massage.png and b/assets/images/emoji/massage.png differ diff --git a/assets/images/emoji/meat_on_bone.png b/assets/images/emoji/meat_on_bone.png new file mode 100644 index 00000000..5b79a660 Binary files /dev/null and b/assets/images/emoji/meat_on_bone.png differ diff --git a/assets/images/emoji/mega.png b/assets/images/emoji/mega.png index d66178b2..022df2f8 100644 Binary files a/assets/images/emoji/mega.png and b/assets/images/emoji/mega.png differ diff --git a/assets/images/emoji/melon.png b/assets/images/emoji/melon.png new file mode 100644 index 00000000..11c13cbb Binary files /dev/null and b/assets/images/emoji/melon.png differ diff --git a/assets/images/emoji/memo.png b/assets/images/emoji/memo.png index 9b890c3d..fc97ddbc 100644 Binary files a/assets/images/emoji/memo.png and b/assets/images/emoji/memo.png differ diff --git a/assets/images/emoji/mens.png b/assets/images/emoji/mens.png index 1749c4aa..abccfc9f 100644 Binary files a/assets/images/emoji/mens.png and b/assets/images/emoji/mens.png differ diff --git a/assets/images/emoji/metal.png b/assets/images/emoji/metal.png index 1bbac1d4..94f1fda2 100644 Binary files a/assets/images/emoji/metal.png and b/assets/images/emoji/metal.png differ diff --git a/assets/images/emoji/metro.png b/assets/images/emoji/metro.png index ca0204a0..4acf5ab3 100644 Binary files a/assets/images/emoji/metro.png and b/assets/images/emoji/metro.png differ diff --git a/assets/images/emoji/microphone.png b/assets/images/emoji/microphone.png index 79dffda8..68c74ada 100644 Binary files a/assets/images/emoji/microphone.png and b/assets/images/emoji/microphone.png differ diff --git a/assets/images/emoji/microscope.png b/assets/images/emoji/microscope.png new file mode 100644 index 00000000..f11d54c0 Binary files /dev/null and b/assets/images/emoji/microscope.png differ diff --git a/assets/images/emoji/milky_way.png b/assets/images/emoji/milky_way.png new file mode 100644 index 00000000..901090a1 Binary files /dev/null and b/assets/images/emoji/milky_way.png differ diff --git a/assets/images/emoji/minibus.png b/assets/images/emoji/minibus.png new file mode 100644 index 00000000..c52cef23 Binary files /dev/null and b/assets/images/emoji/minibus.png differ diff --git a/assets/images/emoji/minidisc.png b/assets/images/emoji/minidisc.png index 8e8245bd..e19cc5d0 100644 Binary files a/assets/images/emoji/minidisc.png and b/assets/images/emoji/minidisc.png differ diff --git a/assets/images/emoji/mobile_phone_off.png b/assets/images/emoji/mobile_phone_off.png index 621cf190..fa16c763 100644 Binary files a/assets/images/emoji/mobile_phone_off.png and b/assets/images/emoji/mobile_phone_off.png differ diff --git a/assets/images/emoji/money_with_wings.png b/assets/images/emoji/money_with_wings.png new file mode 100644 index 00000000..135e3981 Binary files /dev/null and b/assets/images/emoji/money_with_wings.png differ diff --git a/assets/images/emoji/moneybag.png b/assets/images/emoji/moneybag.png index efafdf14..5546c04b 100644 Binary files a/assets/images/emoji/moneybag.png and b/assets/images/emoji/moneybag.png differ diff --git a/assets/images/emoji/monkey.png b/assets/images/emoji/monkey.png index 33b1381c..64070359 100644 Binary files a/assets/images/emoji/monkey.png and b/assets/images/emoji/monkey.png differ diff --git a/assets/images/emoji/monkey_face.png b/assets/images/emoji/monkey_face.png index c137de49..6964cf4d 100644 Binary files a/assets/images/emoji/monkey_face.png and b/assets/images/emoji/monkey_face.png differ diff --git a/assets/images/emoji/monorail.png b/assets/images/emoji/monorail.png new file mode 100644 index 00000000..913d3002 Binary files /dev/null and b/assets/images/emoji/monorail.png differ diff --git a/assets/images/emoji/moon.png b/assets/images/emoji/moon.png index a2ace7ab..afdb450d 100644 Binary files a/assets/images/emoji/moon.png and b/assets/images/emoji/moon.png differ diff --git a/assets/images/emoji/mortar_board.png b/assets/images/emoji/mortar_board.png index a7df3e48..2e811b09 100644 Binary files a/assets/images/emoji/mortar_board.png and b/assets/images/emoji/mortar_board.png differ diff --git a/assets/images/emoji/mount_fuji.png b/assets/images/emoji/mount_fuji.png index 2fdebf6e..4c313e58 100644 Binary files a/assets/images/emoji/mount_fuji.png and b/assets/images/emoji/mount_fuji.png differ diff --git a/assets/images/emoji/mountain_bicyclist.png b/assets/images/emoji/mountain_bicyclist.png new file mode 100644 index 00000000..b6988975 Binary files /dev/null and b/assets/images/emoji/mountain_bicyclist.png differ diff --git a/assets/images/emoji/mountain_cableway.png b/assets/images/emoji/mountain_cableway.png new file mode 100644 index 00000000..5688bb23 Binary files /dev/null and b/assets/images/emoji/mountain_cableway.png differ diff --git a/assets/images/emoji/mountain_railway.png b/assets/images/emoji/mountain_railway.png new file mode 100644 index 00000000..1f3d1aab Binary files /dev/null and b/assets/images/emoji/mountain_railway.png differ diff --git a/assets/images/emoji/mouse.png b/assets/images/emoji/mouse.png index 35da0e5c..8ff162e2 100644 Binary files a/assets/images/emoji/mouse.png and b/assets/images/emoji/mouse.png differ diff --git a/assets/images/emoji/mouse2.png b/assets/images/emoji/mouse2.png new file mode 100644 index 00000000..2d777e5e Binary files /dev/null and b/assets/images/emoji/mouse2.png differ diff --git a/assets/images/emoji/movie_camera.png b/assets/images/emoji/movie_camera.png index ec2047ce..9c143840 100644 Binary files a/assets/images/emoji/movie_camera.png and b/assets/images/emoji/movie_camera.png differ diff --git a/assets/images/emoji/moyai.png b/assets/images/emoji/moyai.png new file mode 100644 index 00000000..61a1a9c2 Binary files /dev/null and b/assets/images/emoji/moyai.png differ diff --git a/assets/images/emoji/muscle.png b/assets/images/emoji/muscle.png index 5381079f..19f92efb 100644 Binary files a/assets/images/emoji/muscle.png and b/assets/images/emoji/muscle.png differ diff --git a/assets/images/emoji/mushroom.png b/assets/images/emoji/mushroom.png new file mode 100644 index 00000000..5eeed8e7 Binary files /dev/null and b/assets/images/emoji/mushroom.png differ diff --git a/assets/images/emoji/musical_keyboard.png b/assets/images/emoji/musical_keyboard.png new file mode 100644 index 00000000..93647a4a Binary files /dev/null and b/assets/images/emoji/musical_keyboard.png differ diff --git a/assets/images/emoji/musical_note.png b/assets/images/emoji/musical_note.png index 476d49e4..68b261bc 100644 Binary files a/assets/images/emoji/musical_note.png and b/assets/images/emoji/musical_note.png differ diff --git a/assets/images/emoji/musical_score.png b/assets/images/emoji/musical_score.png new file mode 100644 index 00000000..c99e3381 Binary files /dev/null and b/assets/images/emoji/musical_score.png differ diff --git a/assets/images/emoji/mute.png b/assets/images/emoji/mute.png new file mode 100644 index 00000000..4cf67c36 Binary files /dev/null and b/assets/images/emoji/mute.png differ diff --git a/assets/images/emoji/nail_care.png b/assets/images/emoji/nail_care.png index acb0d774..6a66e63d 100644 Binary files a/assets/images/emoji/nail_care.png and b/assets/images/emoji/nail_care.png differ diff --git a/assets/images/emoji/name_badge.png b/assets/images/emoji/name_badge.png new file mode 100644 index 00000000..2b712dcd Binary files /dev/null and b/assets/images/emoji/name_badge.png differ diff --git a/assets/images/emoji/neckbeard.png b/assets/images/emoji/neckbeard.png new file mode 100644 index 00000000..35ebdf06 Binary files /dev/null and b/assets/images/emoji/neckbeard.png differ diff --git a/assets/images/emoji/necktie.png b/assets/images/emoji/necktie.png index e5909380..80461c66 100644 Binary files a/assets/images/emoji/necktie.png and b/assets/images/emoji/necktie.png differ diff --git a/assets/images/emoji/negative_squared_cross_mark.png b/assets/images/emoji/negative_squared_cross_mark.png new file mode 100644 index 00000000..b47a0cec Binary files /dev/null and b/assets/images/emoji/negative_squared_cross_mark.png differ diff --git a/assets/images/emoji/neutral_face.png b/assets/images/emoji/neutral_face.png new file mode 100644 index 00000000..682a1ba0 Binary files /dev/null and b/assets/images/emoji/neutral_face.png differ diff --git a/assets/images/emoji/new.png b/assets/images/emoji/new.png index 8c586250..28d1570e 100644 Binary files a/assets/images/emoji/new.png and b/assets/images/emoji/new.png differ diff --git a/assets/images/emoji/new_moon.png b/assets/images/emoji/new_moon.png new file mode 100644 index 00000000..72492cb9 Binary files /dev/null and b/assets/images/emoji/new_moon.png differ diff --git a/assets/images/emoji/new_moon_with_face.png b/assets/images/emoji/new_moon_with_face.png new file mode 100644 index 00000000..b9aff7a0 Binary files /dev/null and b/assets/images/emoji/new_moon_with_face.png differ diff --git a/assets/images/emoji/newspaper.png b/assets/images/emoji/newspaper.png new file mode 100644 index 00000000..d171394e Binary files /dev/null and b/assets/images/emoji/newspaper.png differ diff --git a/assets/images/emoji/ng.png b/assets/images/emoji/ng.png new file mode 100644 index 00000000..2ca180ae Binary files /dev/null and b/assets/images/emoji/ng.png differ diff --git a/assets/images/emoji/nine.png b/assets/images/emoji/nine.png new file mode 100644 index 00000000..8006cc90 Binary files /dev/null and b/assets/images/emoji/nine.png differ diff --git a/assets/images/emoji/no_bell.png b/assets/images/emoji/no_bell.png new file mode 100644 index 00000000..613b81cd Binary files /dev/null and b/assets/images/emoji/no_bell.png differ diff --git a/assets/images/emoji/no_bicycles.png b/assets/images/emoji/no_bicycles.png new file mode 100644 index 00000000..4b262166 Binary files /dev/null and b/assets/images/emoji/no_bicycles.png differ diff --git a/assets/images/emoji/no_entry.png b/assets/images/emoji/no_entry.png new file mode 100644 index 00000000..cf2086a8 Binary files /dev/null and b/assets/images/emoji/no_entry.png differ diff --git a/assets/images/emoji/no_entry_sign.png b/assets/images/emoji/no_entry_sign.png new file mode 100644 index 00000000..a8444d18 Binary files /dev/null and b/assets/images/emoji/no_entry_sign.png differ diff --git a/assets/images/emoji/no_good.png b/assets/images/emoji/no_good.png index 06bc3a87..d459a35b 100644 Binary files a/assets/images/emoji/no_good.png and b/assets/images/emoji/no_good.png differ diff --git a/assets/images/emoji/no_mobile_phones.png b/assets/images/emoji/no_mobile_phones.png new file mode 100644 index 00000000..41df57cf Binary files /dev/null and b/assets/images/emoji/no_mobile_phones.png differ diff --git a/assets/images/emoji/no_mouth.png b/assets/images/emoji/no_mouth.png new file mode 100644 index 00000000..e6780204 Binary files /dev/null and b/assets/images/emoji/no_mouth.png differ diff --git a/assets/images/emoji/no_pedestrians.png b/assets/images/emoji/no_pedestrians.png new file mode 100644 index 00000000..c35f530b Binary files /dev/null and b/assets/images/emoji/no_pedestrians.png differ diff --git a/assets/images/emoji/no_smoking.png b/assets/images/emoji/no_smoking.png index a304c808..5880ddfd 100644 Binary files a/assets/images/emoji/no_smoking.png and b/assets/images/emoji/no_smoking.png differ diff --git a/assets/images/emoji/non-potable_water.png b/assets/images/emoji/non-potable_water.png new file mode 100644 index 00000000..1b29d35b Binary files /dev/null and b/assets/images/emoji/non-potable_water.png differ diff --git a/assets/images/emoji/nose.png b/assets/images/emoji/nose.png index 6ecf9fbc..ad17c16c 100644 Binary files a/assets/images/emoji/nose.png and b/assets/images/emoji/nose.png differ diff --git a/assets/images/emoji/notebook.png b/assets/images/emoji/notebook.png new file mode 100644 index 00000000..07ea6087 Binary files /dev/null and b/assets/images/emoji/notebook.png differ diff --git a/assets/images/emoji/notebook_with_decorative_cover.png b/assets/images/emoji/notebook_with_decorative_cover.png new file mode 100644 index 00000000..4f3b14c8 Binary files /dev/null and b/assets/images/emoji/notebook_with_decorative_cover.png differ diff --git a/assets/images/emoji/notes.png b/assets/images/emoji/notes.png index 4f445684..0956d6ab 100644 Binary files a/assets/images/emoji/notes.png and b/assets/images/emoji/notes.png differ diff --git a/assets/images/emoji/nut_and_bolt.png b/assets/images/emoji/nut_and_bolt.png new file mode 100644 index 00000000..bddfa72a Binary files /dev/null and b/assets/images/emoji/nut_and_bolt.png differ diff --git a/assets/images/emoji/o.png b/assets/images/emoji/o.png index a283db8f..0ededebe 100644 Binary files a/assets/images/emoji/o.png and b/assets/images/emoji/o.png differ diff --git a/assets/images/emoji/o2.png b/assets/images/emoji/o2.png index 9858eed4..d85f9fb9 100644 Binary files a/assets/images/emoji/o2.png and b/assets/images/emoji/o2.png differ diff --git a/assets/images/emoji/ocean.png b/assets/images/emoji/ocean.png index 6f1bb9d1..f8d520cd 100644 Binary files a/assets/images/emoji/ocean.png and b/assets/images/emoji/ocean.png differ diff --git a/assets/images/emoji/octocat.png b/assets/images/emoji/octocat.png index 0b68cf0d..361f6822 100644 Binary files a/assets/images/emoji/octocat.png and b/assets/images/emoji/octocat.png differ diff --git a/assets/images/emoji/octopus.png b/assets/images/emoji/octopus.png index 7deac05f..52ce64b4 100644 Binary files a/assets/images/emoji/octopus.png and b/assets/images/emoji/octopus.png differ diff --git a/assets/images/emoji/oden.png b/assets/images/emoji/oden.png index 66fbc2c7..73add1c7 100644 Binary files a/assets/images/emoji/oden.png and b/assets/images/emoji/oden.png differ diff --git a/assets/images/emoji/office.png b/assets/images/emoji/office.png index 1b2a2862..ea9281a4 100644 Binary files a/assets/images/emoji/office.png and b/assets/images/emoji/office.png differ diff --git a/assets/images/emoji/ok.png b/assets/images/emoji/ok.png index 015870e2..6433d1a9 100644 Binary files a/assets/images/emoji/ok.png and b/assets/images/emoji/ok.png differ diff --git a/assets/images/emoji/ok_hand.png b/assets/images/emoji/ok_hand.png index 636d39bb..80c5aebb 100644 Binary files a/assets/images/emoji/ok_hand.png and b/assets/images/emoji/ok_hand.png differ diff --git a/assets/images/emoji/ok_woman.png b/assets/images/emoji/ok_woman.png index 3b3ebba6..e8b98194 100644 Binary files a/assets/images/emoji/ok_woman.png and b/assets/images/emoji/ok_woman.png differ diff --git a/assets/images/emoji/older_man.png b/assets/images/emoji/older_man.png index ecfb575a..149f0cfb 100644 Binary files a/assets/images/emoji/older_man.png and b/assets/images/emoji/older_man.png differ diff --git a/assets/images/emoji/older_woman.png b/assets/images/emoji/older_woman.png index 1d565d8d..f839565f 100644 Binary files a/assets/images/emoji/older_woman.png and b/assets/images/emoji/older_woman.png differ diff --git a/assets/images/emoji/on.png b/assets/images/emoji/on.png new file mode 100644 index 00000000..3595387f Binary files /dev/null and b/assets/images/emoji/on.png differ diff --git a/assets/images/emoji/oncoming_automobile.png b/assets/images/emoji/oncoming_automobile.png new file mode 100644 index 00000000..cb46de22 Binary files /dev/null and b/assets/images/emoji/oncoming_automobile.png differ diff --git a/assets/images/emoji/oncoming_bus.png b/assets/images/emoji/oncoming_bus.png new file mode 100644 index 00000000..3695f762 Binary files /dev/null and b/assets/images/emoji/oncoming_bus.png differ diff --git a/assets/images/emoji/oncoming_police_car.png b/assets/images/emoji/oncoming_police_car.png new file mode 100644 index 00000000..af20e7ef Binary files /dev/null and b/assets/images/emoji/oncoming_police_car.png differ diff --git a/assets/images/emoji/oncoming_taxi.png b/assets/images/emoji/oncoming_taxi.png new file mode 100644 index 00000000..f78cf310 Binary files /dev/null and b/assets/images/emoji/oncoming_taxi.png differ diff --git a/assets/images/emoji/one.png b/assets/images/emoji/one.png new file mode 100644 index 00000000..2d1f9f8c Binary files /dev/null and b/assets/images/emoji/one.png differ diff --git a/assets/images/emoji/open_file_folder.png b/assets/images/emoji/open_file_folder.png new file mode 100644 index 00000000..2bbbbf5e Binary files /dev/null and b/assets/images/emoji/open_file_folder.png differ diff --git a/assets/images/emoji/open_hands.png b/assets/images/emoji/open_hands.png index bc2fe3de..2cc25bd4 100644 Binary files a/assets/images/emoji/open_hands.png and b/assets/images/emoji/open_hands.png differ diff --git a/assets/images/emoji/open_mouth.png b/assets/images/emoji/open_mouth.png new file mode 100644 index 00000000..daf91427 Binary files /dev/null and b/assets/images/emoji/open_mouth.png differ diff --git a/assets/images/emoji/ophiuchus.png b/assets/images/emoji/ophiuchus.png index 8c4be843..4eef715b 100644 Binary files a/assets/images/emoji/ophiuchus.png and b/assets/images/emoji/ophiuchus.png differ diff --git a/assets/images/emoji/orange_book.png b/assets/images/emoji/orange_book.png new file mode 100644 index 00000000..49650d59 Binary files /dev/null and b/assets/images/emoji/orange_book.png differ diff --git a/assets/images/emoji/outbox_tray.png b/assets/images/emoji/outbox_tray.png new file mode 100644 index 00000000..7ad15e64 Binary files /dev/null and b/assets/images/emoji/outbox_tray.png differ diff --git a/assets/images/emoji/ox.png b/assets/images/emoji/ox.png new file mode 100644 index 00000000..8d981946 Binary files /dev/null and b/assets/images/emoji/ox.png differ diff --git a/assets/images/emoji/page_facing_up.png b/assets/images/emoji/page_facing_up.png new file mode 100644 index 00000000..64cd2e1b Binary files /dev/null and b/assets/images/emoji/page_facing_up.png differ diff --git a/assets/images/emoji/page_with_curl.png b/assets/images/emoji/page_with_curl.png new file mode 100644 index 00000000..bf8f979d Binary files /dev/null and b/assets/images/emoji/page_with_curl.png differ diff --git a/assets/images/emoji/pager.png b/assets/images/emoji/pager.png new file mode 100644 index 00000000..e3e1fc44 Binary files /dev/null and b/assets/images/emoji/pager.png differ diff --git a/assets/images/emoji/palm_tree.png b/assets/images/emoji/palm_tree.png index 92fc3c2c..d534785e 100644 Binary files a/assets/images/emoji/palm_tree.png and b/assets/images/emoji/palm_tree.png differ diff --git a/assets/images/emoji/panda_face.png b/assets/images/emoji/panda_face.png new file mode 100644 index 00000000..a794fb17 Binary files /dev/null and b/assets/images/emoji/panda_face.png differ diff --git a/assets/images/emoji/paperclip.png b/assets/images/emoji/paperclip.png new file mode 100644 index 00000000..677669a8 Binary files /dev/null and b/assets/images/emoji/paperclip.png differ diff --git a/assets/images/emoji/parking.png b/assets/images/emoji/parking.png index bd114958..c24af81c 100644 Binary files a/assets/images/emoji/parking.png and b/assets/images/emoji/parking.png differ diff --git a/assets/images/emoji/part_alternation_mark.png b/assets/images/emoji/part_alternation_mark.png index bff6f750..45dc9b85 100644 Binary files a/assets/images/emoji/part_alternation_mark.png and b/assets/images/emoji/part_alternation_mark.png differ diff --git a/assets/images/emoji/partly_sunny.png b/assets/images/emoji/partly_sunny.png new file mode 100644 index 00000000..020dd5ff Binary files /dev/null and b/assets/images/emoji/partly_sunny.png differ diff --git a/assets/images/emoji/passport_control.png b/assets/images/emoji/passport_control.png new file mode 100644 index 00000000..675b76d3 Binary files /dev/null and b/assets/images/emoji/passport_control.png differ diff --git a/assets/images/emoji/paw_prints.png b/assets/images/emoji/paw_prints.png new file mode 100644 index 00000000..89b9fec9 Binary files /dev/null and b/assets/images/emoji/paw_prints.png differ diff --git a/assets/images/emoji/peach.png b/assets/images/emoji/peach.png new file mode 100644 index 00000000..ee2139ec Binary files /dev/null and b/assets/images/emoji/peach.png differ diff --git a/assets/images/emoji/pear.png b/assets/images/emoji/pear.png new file mode 100644 index 00000000..f24aca8c Binary files /dev/null and b/assets/images/emoji/pear.png differ diff --git a/assets/images/emoji/pencil.png b/assets/images/emoji/pencil.png index 9b890c3d..fc97ddbc 100644 Binary files a/assets/images/emoji/pencil.png and b/assets/images/emoji/pencil.png differ diff --git a/assets/images/emoji/pencil2.png b/assets/images/emoji/pencil2.png new file mode 100644 index 00000000..61bfef97 Binary files /dev/null and b/assets/images/emoji/pencil2.png differ diff --git a/assets/images/emoji/penguin.png b/assets/images/emoji/penguin.png index 541e8531..d8edbcb8 100644 Binary files a/assets/images/emoji/penguin.png and b/assets/images/emoji/penguin.png differ diff --git a/assets/images/emoji/pensive.png b/assets/images/emoji/pensive.png index e6e1624a..4159f3c4 100644 Binary files a/assets/images/emoji/pensive.png and b/assets/images/emoji/pensive.png differ diff --git a/assets/images/emoji/performing_arts.png b/assets/images/emoji/performing_arts.png new file mode 100644 index 00000000..899fbe5a Binary files /dev/null and b/assets/images/emoji/performing_arts.png differ diff --git a/assets/images/emoji/persevere.png b/assets/images/emoji/persevere.png index a6a347ed..f99f6da4 100644 Binary files a/assets/images/emoji/persevere.png and b/assets/images/emoji/persevere.png differ diff --git a/assets/images/emoji/person_frowning.png b/assets/images/emoji/person_frowning.png new file mode 100644 index 00000000..6f34d5e1 Binary files /dev/null and b/assets/images/emoji/person_frowning.png differ diff --git a/assets/images/emoji/person_with_blond_hair.png b/assets/images/emoji/person_with_blond_hair.png index 4abdfdf0..c144301c 100644 Binary files a/assets/images/emoji/person_with_blond_hair.png and b/assets/images/emoji/person_with_blond_hair.png differ diff --git a/assets/images/emoji/person_with_pouting_face.png b/assets/images/emoji/person_with_pouting_face.png new file mode 100644 index 00000000..c4a95c3b Binary files /dev/null and b/assets/images/emoji/person_with_pouting_face.png differ diff --git a/assets/images/emoji/phone.png b/assets/images/emoji/phone.png index e1d64368..87d2559b 100644 Binary files a/assets/images/emoji/phone.png and b/assets/images/emoji/phone.png differ diff --git a/assets/images/emoji/pig.png b/assets/images/emoji/pig.png index 38b05b41..f7f273c7 100644 Binary files a/assets/images/emoji/pig.png and b/assets/images/emoji/pig.png differ diff --git a/assets/images/emoji/pig2.png b/assets/images/emoji/pig2.png new file mode 100644 index 00000000..fec3374d Binary files /dev/null and b/assets/images/emoji/pig2.png differ diff --git a/assets/images/emoji/pig_nose.png b/assets/images/emoji/pig_nose.png new file mode 100644 index 00000000..38d61244 Binary files /dev/null and b/assets/images/emoji/pig_nose.png differ diff --git a/assets/images/emoji/pill.png b/assets/images/emoji/pill.png index 405d1c57..cd84a78f 100644 Binary files a/assets/images/emoji/pill.png and b/assets/images/emoji/pill.png differ diff --git a/assets/images/emoji/pineapple.png b/assets/images/emoji/pineapple.png new file mode 100644 index 00000000..d6f8e287 Binary files /dev/null and b/assets/images/emoji/pineapple.png differ diff --git a/assets/images/emoji/pisces.png b/assets/images/emoji/pisces.png index 5f49272f..6db2c3d5 100644 Binary files a/assets/images/emoji/pisces.png and b/assets/images/emoji/pisces.png differ diff --git a/assets/images/emoji/pizza.png b/assets/images/emoji/pizza.png new file mode 100644 index 00000000..460367d0 Binary files /dev/null and b/assets/images/emoji/pizza.png differ diff --git a/assets/images/emoji/plus1.png b/assets/images/emoji/plus1.png index 4dae7a09..81786c1d 100644 Binary files a/assets/images/emoji/plus1.png and b/assets/images/emoji/plus1.png differ diff --git a/assets/images/emoji/point_down.png b/assets/images/emoji/point_down.png index 37f4eb1b..658c6d91 100644 Binary files a/assets/images/emoji/point_down.png and b/assets/images/emoji/point_down.png differ diff --git a/assets/images/emoji/point_left.png b/assets/images/emoji/point_left.png index 4faf0db8..38a99b43 100644 Binary files a/assets/images/emoji/point_left.png and b/assets/images/emoji/point_left.png differ diff --git a/assets/images/emoji/point_right.png b/assets/images/emoji/point_right.png index 6d0b8f3b..6f9f029a 100644 Binary files a/assets/images/emoji/point_right.png and b/assets/images/emoji/point_right.png differ diff --git a/assets/images/emoji/point_up.png b/assets/images/emoji/point_up.png index 533a3d1b..01896e21 100644 Binary files a/assets/images/emoji/point_up.png and b/assets/images/emoji/point_up.png differ diff --git a/assets/images/emoji/point_up_2.png b/assets/images/emoji/point_up_2.png index 87956d23..1cfe7367 100644 Binary files a/assets/images/emoji/point_up_2.png and b/assets/images/emoji/point_up_2.png differ diff --git a/assets/images/emoji/police_car.png b/assets/images/emoji/police_car.png index 00a4e173..b8f17275 100644 Binary files a/assets/images/emoji/police_car.png and b/assets/images/emoji/police_car.png differ diff --git a/assets/images/emoji/poodle.png b/assets/images/emoji/poodle.png new file mode 100644 index 00000000..adac80bd Binary files /dev/null and b/assets/images/emoji/poodle.png differ diff --git a/assets/images/emoji/poop.png b/assets/images/emoji/poop.png index 69bc33f1..73a4dc84 100644 Binary files a/assets/images/emoji/poop.png and b/assets/images/emoji/poop.png differ diff --git a/assets/images/emoji/post_office.png b/assets/images/emoji/post_office.png index 4ec24548..43b59e30 100644 Binary files a/assets/images/emoji/post_office.png and b/assets/images/emoji/post_office.png differ diff --git a/assets/images/emoji/postal_horn.png b/assets/images/emoji/postal_horn.png new file mode 100644 index 00000000..b12c3002 Binary files /dev/null and b/assets/images/emoji/postal_horn.png differ diff --git a/assets/images/emoji/postbox.png b/assets/images/emoji/postbox.png index 1cb2ea99..ce04b700 100644 Binary files a/assets/images/emoji/postbox.png and b/assets/images/emoji/postbox.png differ diff --git a/assets/images/emoji/potable_water.png b/assets/images/emoji/potable_water.png new file mode 100644 index 00000000..e9fd5607 Binary files /dev/null and b/assets/images/emoji/potable_water.png differ diff --git a/assets/images/emoji/pouch.png b/assets/images/emoji/pouch.png new file mode 100644 index 00000000..dc35ae8e Binary files /dev/null and b/assets/images/emoji/pouch.png differ diff --git a/assets/images/emoji/poultry_leg.png b/assets/images/emoji/poultry_leg.png new file mode 100644 index 00000000..43ad8596 Binary files /dev/null and b/assets/images/emoji/poultry_leg.png differ diff --git a/assets/images/emoji/pound.png b/assets/images/emoji/pound.png new file mode 100644 index 00000000..f8be91d7 Binary files /dev/null and b/assets/images/emoji/pound.png differ diff --git a/assets/images/emoji/pouting_cat.png b/assets/images/emoji/pouting_cat.png new file mode 100644 index 00000000..4325fd48 Binary files /dev/null and b/assets/images/emoji/pouting_cat.png differ diff --git a/assets/images/emoji/pray.png b/assets/images/emoji/pray.png index b91bde40..f86c992d 100644 Binary files a/assets/images/emoji/pray.png and b/assets/images/emoji/pray.png differ diff --git a/assets/images/emoji/princess.png b/assets/images/emoji/princess.png index 90a2f9e1..1ebb2ce9 100644 Binary files a/assets/images/emoji/princess.png and b/assets/images/emoji/princess.png differ diff --git a/assets/images/emoji/punch.png b/assets/images/emoji/punch.png index 52dbccae..277047b7 100644 Binary files a/assets/images/emoji/punch.png and b/assets/images/emoji/punch.png differ diff --git a/assets/images/emoji/purple_heart.png b/assets/images/emoji/purple_heart.png index 98c0bc72..d5f87504 100644 Binary files a/assets/images/emoji/purple_heart.png and b/assets/images/emoji/purple_heart.png differ diff --git a/assets/images/emoji/purse.png b/assets/images/emoji/purse.png new file mode 100644 index 00000000..8f06a2b9 Binary files /dev/null and b/assets/images/emoji/purse.png differ diff --git a/assets/images/emoji/pushpin.png b/assets/images/emoji/pushpin.png new file mode 100644 index 00000000..540c4ecb Binary files /dev/null and b/assets/images/emoji/pushpin.png differ diff --git a/assets/images/emoji/put_litter_in_its_place.png b/assets/images/emoji/put_litter_in_its_place.png new file mode 100644 index 00000000..c2e350c2 Binary files /dev/null and b/assets/images/emoji/put_litter_in_its_place.png differ diff --git a/assets/images/emoji/question.png b/assets/images/emoji/question.png index 1c3edc0a..38cedf56 100644 Binary files a/assets/images/emoji/question.png and b/assets/images/emoji/question.png differ diff --git a/assets/images/emoji/rabbit.png b/assets/images/emoji/rabbit.png index 178f91dd..5cb3ef6f 100644 Binary files a/assets/images/emoji/rabbit.png and b/assets/images/emoji/rabbit.png differ diff --git a/assets/images/emoji/rabbit2.png b/assets/images/emoji/rabbit2.png new file mode 100644 index 00000000..789de15c Binary files /dev/null and b/assets/images/emoji/rabbit2.png differ diff --git a/assets/images/emoji/racehorse.png b/assets/images/emoji/racehorse.png index e3267946..4d09c64d 100644 Binary files a/assets/images/emoji/racehorse.png and b/assets/images/emoji/racehorse.png differ diff --git a/assets/images/emoji/radio.png b/assets/images/emoji/radio.png index e377db2c..ea589efe 100644 Binary files a/assets/images/emoji/radio.png and b/assets/images/emoji/radio.png differ diff --git a/assets/images/emoji/radio_button.png b/assets/images/emoji/radio_button.png new file mode 100644 index 00000000..63755eec Binary files /dev/null and b/assets/images/emoji/radio_button.png differ diff --git a/assets/images/emoji/rage.png b/assets/images/emoji/rage.png index cdd8858c..c65ddff5 100644 Binary files a/assets/images/emoji/rage.png and b/assets/images/emoji/rage.png differ diff --git a/assets/images/emoji/rage1.png b/assets/images/emoji/rage1.png index d4d84679..1506ba40 100644 Binary files a/assets/images/emoji/rage1.png and b/assets/images/emoji/rage1.png differ diff --git a/assets/images/emoji/rage2.png b/assets/images/emoji/rage2.png index 0cd7052c..f792e063 100644 Binary files a/assets/images/emoji/rage2.png and b/assets/images/emoji/rage2.png differ diff --git a/assets/images/emoji/rage3.png b/assets/images/emoji/rage3.png index 340ad870..58764cbc 100644 Binary files a/assets/images/emoji/rage3.png and b/assets/images/emoji/rage3.png differ diff --git a/assets/images/emoji/rage4.png b/assets/images/emoji/rage4.png index 6e6c1238..c726c94a 100644 Binary files a/assets/images/emoji/rage4.png and b/assets/images/emoji/rage4.png differ diff --git a/assets/images/emoji/railway_car.png b/assets/images/emoji/railway_car.png new file mode 100644 index 00000000..22361158 Binary files /dev/null and b/assets/images/emoji/railway_car.png differ diff --git a/assets/images/emoji/rainbow.png b/assets/images/emoji/rainbow.png index 4b150bd5..6b1faa03 100644 Binary files a/assets/images/emoji/rainbow.png and b/assets/images/emoji/rainbow.png differ diff --git a/assets/images/emoji/raised_hand.png b/assets/images/emoji/raised_hand.png new file mode 100644 index 00000000..5e45c25a Binary files /dev/null and b/assets/images/emoji/raised_hand.png differ diff --git a/assets/images/emoji/raised_hands.png b/assets/images/emoji/raised_hands.png index f9f2a95f..e03142bd 100644 Binary files a/assets/images/emoji/raised_hands.png and b/assets/images/emoji/raised_hands.png differ diff --git a/assets/images/emoji/raising_hand.png b/assets/images/emoji/raising_hand.png new file mode 100644 index 00000000..e1741a40 Binary files /dev/null and b/assets/images/emoji/raising_hand.png differ diff --git a/assets/images/emoji/ram.png b/assets/images/emoji/ram.png new file mode 100644 index 00000000..5ea7bfbc Binary files /dev/null and b/assets/images/emoji/ram.png differ diff --git a/assets/images/emoji/ramen.png b/assets/images/emoji/ramen.png index c6524f3e..78dc7d53 100644 Binary files a/assets/images/emoji/ramen.png and b/assets/images/emoji/ramen.png differ diff --git a/assets/images/emoji/rat.png b/assets/images/emoji/rat.png new file mode 100644 index 00000000..1c463dfd Binary files /dev/null and b/assets/images/emoji/rat.png differ diff --git a/assets/images/emoji/recycle.png b/assets/images/emoji/recycle.png new file mode 100644 index 00000000..99104c0e Binary files /dev/null and b/assets/images/emoji/recycle.png differ diff --git a/assets/images/emoji/red_car.png b/assets/images/emoji/red_car.png index c8bc8020..d70a2f06 100644 Binary files a/assets/images/emoji/red_car.png and b/assets/images/emoji/red_car.png differ diff --git a/assets/images/emoji/red_circle.png b/assets/images/emoji/red_circle.png index ef13dc4a..b391289b 100644 Binary files a/assets/images/emoji/red_circle.png and b/assets/images/emoji/red_circle.png differ diff --git a/assets/images/emoji/registered.png b/assets/images/emoji/registered.png index dcecc555..31c68a80 100644 Binary files a/assets/images/emoji/registered.png and b/assets/images/emoji/registered.png differ diff --git a/assets/images/emoji/relaxed.png b/assets/images/emoji/relaxed.png index 4b8412cd..bbab82d3 100644 Binary files a/assets/images/emoji/relaxed.png and b/assets/images/emoji/relaxed.png differ diff --git a/assets/images/emoji/relieved.png b/assets/images/emoji/relieved.png index e9405da0..fe5629f4 100644 Binary files a/assets/images/emoji/relieved.png and b/assets/images/emoji/relieved.png differ diff --git a/assets/images/emoji/repeat.png b/assets/images/emoji/repeat.png new file mode 100644 index 00000000..80113b69 Binary files /dev/null and b/assets/images/emoji/repeat.png differ diff --git a/assets/images/emoji/repeat_one.png b/assets/images/emoji/repeat_one.png new file mode 100644 index 00000000..3c47bcc1 Binary files /dev/null and b/assets/images/emoji/repeat_one.png differ diff --git a/assets/images/emoji/restroom.png b/assets/images/emoji/restroom.png index 612eccda..d6c111b2 100644 Binary files a/assets/images/emoji/restroom.png and b/assets/images/emoji/restroom.png differ diff --git a/assets/images/emoji/revolving_hearts.png b/assets/images/emoji/revolving_hearts.png new file mode 100644 index 00000000..ea3317c4 Binary files /dev/null and b/assets/images/emoji/revolving_hearts.png differ diff --git a/assets/images/emoji/rewind.png b/assets/images/emoji/rewind.png index 3997771b..26289dc3 100644 Binary files a/assets/images/emoji/rewind.png and b/assets/images/emoji/rewind.png differ diff --git a/assets/images/emoji/ribbon.png b/assets/images/emoji/ribbon.png index b7281b0f..63ee5ba5 100644 Binary files a/assets/images/emoji/ribbon.png and b/assets/images/emoji/ribbon.png differ diff --git a/assets/images/emoji/rice.png b/assets/images/emoji/rice.png index 13ce2747..1fd22027 100644 Binary files a/assets/images/emoji/rice.png and b/assets/images/emoji/rice.png differ diff --git a/assets/images/emoji/rice_ball.png b/assets/images/emoji/rice_ball.png index 084660ef..04f8a880 100644 Binary files a/assets/images/emoji/rice_ball.png and b/assets/images/emoji/rice_ball.png differ diff --git a/assets/images/emoji/rice_cracker.png b/assets/images/emoji/rice_cracker.png index 3f7cbffc..954c901e 100644 Binary files a/assets/images/emoji/rice_cracker.png and b/assets/images/emoji/rice_cracker.png differ diff --git a/assets/images/emoji/rice_scene.png b/assets/images/emoji/rice_scene.png index ce41fe81..14361988 100644 Binary files a/assets/images/emoji/rice_scene.png and b/assets/images/emoji/rice_scene.png differ diff --git a/assets/images/emoji/ring.png b/assets/images/emoji/ring.png index 0fd547aa..8a57fd68 100644 Binary files a/assets/images/emoji/ring.png and b/assets/images/emoji/ring.png differ diff --git a/assets/images/emoji/rocket.png b/assets/images/emoji/rocket.png index 4b52a2e6..783078d3 100644 Binary files a/assets/images/emoji/rocket.png and b/assets/images/emoji/rocket.png differ diff --git a/assets/images/emoji/roller_coaster.png b/assets/images/emoji/roller_coaster.png index d33d7c54..9180b986 100644 Binary files a/assets/images/emoji/roller_coaster.png and b/assets/images/emoji/roller_coaster.png differ diff --git a/assets/images/emoji/rooster.png b/assets/images/emoji/rooster.png new file mode 100644 index 00000000..fab23ad3 Binary files /dev/null and b/assets/images/emoji/rooster.png differ diff --git a/assets/images/emoji/rose.png b/assets/images/emoji/rose.png index d28df9eb..3479fbcb 100644 Binary files a/assets/images/emoji/rose.png and b/assets/images/emoji/rose.png differ diff --git a/assets/images/emoji/rotating_light.png b/assets/images/emoji/rotating_light.png new file mode 100644 index 00000000..6cf4a775 Binary files /dev/null and b/assets/images/emoji/rotating_light.png differ diff --git a/assets/images/emoji/round_pushpin.png b/assets/images/emoji/round_pushpin.png new file mode 100644 index 00000000..e498e92c Binary files /dev/null and b/assets/images/emoji/round_pushpin.png differ diff --git a/assets/images/emoji/rowboat.png b/assets/images/emoji/rowboat.png new file mode 100644 index 00000000..e370d0fb Binary files /dev/null and b/assets/images/emoji/rowboat.png differ diff --git a/assets/images/emoji/ru.png b/assets/images/emoji/ru.png index 1fefb2ca..55fcf354 100644 Binary files a/assets/images/emoji/ru.png and b/assets/images/emoji/ru.png differ diff --git a/assets/images/emoji/rugby_football.png b/assets/images/emoji/rugby_football.png new file mode 100644 index 00000000..f8db67d7 Binary files /dev/null and b/assets/images/emoji/rugby_football.png differ diff --git a/assets/images/emoji/runner.png b/assets/images/emoji/runner.png index a2c78f55..cb004296 100644 Binary files a/assets/images/emoji/runner.png and b/assets/images/emoji/runner.png differ diff --git a/assets/images/emoji/running.png b/assets/images/emoji/running.png new file mode 100644 index 00000000..cb004296 Binary files /dev/null and b/assets/images/emoji/running.png differ diff --git a/assets/images/emoji/running_shirt_with_sash.png b/assets/images/emoji/running_shirt_with_sash.png new file mode 100644 index 00000000..0d68bba0 Binary files /dev/null and b/assets/images/emoji/running_shirt_with_sash.png differ diff --git a/assets/images/emoji/sa.png b/assets/images/emoji/sa.png index 82a27c7a..387f098b 100644 Binary files a/assets/images/emoji/sa.png and b/assets/images/emoji/sa.png differ diff --git a/assets/images/emoji/sagittarius.png b/assets/images/emoji/sagittarius.png index 8c14a431..8b5435ba 100644 Binary files a/assets/images/emoji/sagittarius.png and b/assets/images/emoji/sagittarius.png differ diff --git a/assets/images/emoji/sailboat.png b/assets/images/emoji/sailboat.png index 8084f0e5..ff656dc6 100644 Binary files a/assets/images/emoji/sailboat.png and b/assets/images/emoji/sailboat.png differ diff --git a/assets/images/emoji/sake.png b/assets/images/emoji/sake.png index ce544d72..1f69907e 100644 Binary files a/assets/images/emoji/sake.png and b/assets/images/emoji/sake.png differ diff --git a/assets/images/emoji/sandal.png b/assets/images/emoji/sandal.png index 4f740a33..0bb3f663 100644 Binary files a/assets/images/emoji/sandal.png and b/assets/images/emoji/sandal.png differ diff --git a/assets/images/emoji/santa.png b/assets/images/emoji/santa.png index c753675e..a2240c07 100644 Binary files a/assets/images/emoji/santa.png and b/assets/images/emoji/santa.png differ diff --git a/assets/images/emoji/satellite.png b/assets/images/emoji/satellite.png index d3f07107..3481cc2e 100644 Binary files a/assets/images/emoji/satellite.png and b/assets/images/emoji/satellite.png differ diff --git a/assets/images/emoji/satisfied.png b/assets/images/emoji/satisfied.png index f68006dd..11c91eb2 100644 Binary files a/assets/images/emoji/satisfied.png and b/assets/images/emoji/satisfied.png differ diff --git a/assets/images/emoji/saxophone.png b/assets/images/emoji/saxophone.png index ac27b0de..011559a7 100644 Binary files a/assets/images/emoji/saxophone.png and b/assets/images/emoji/saxophone.png differ diff --git a/assets/images/emoji/school.png b/assets/images/emoji/school.png index bdc0fa00..afd922bf 100644 Binary files a/assets/images/emoji/school.png and b/assets/images/emoji/school.png differ diff --git a/assets/images/emoji/school_satchel.png b/assets/images/emoji/school_satchel.png index ba7925e4..edfb19ae 100644 Binary files a/assets/images/emoji/school_satchel.png and b/assets/images/emoji/school_satchel.png differ diff --git a/assets/images/emoji/scissors.png b/assets/images/emoji/scissors.png index 77b38148..be916043 100644 Binary files a/assets/images/emoji/scissors.png and b/assets/images/emoji/scissors.png differ diff --git a/assets/images/emoji/scorpius.png b/assets/images/emoji/scorpius.png index 69734fa0..67fcea16 100644 Binary files a/assets/images/emoji/scorpius.png and b/assets/images/emoji/scorpius.png differ diff --git a/assets/images/emoji/scream.png b/assets/images/emoji/scream.png index 222aae8d..9e93c885 100644 Binary files a/assets/images/emoji/scream.png and b/assets/images/emoji/scream.png differ diff --git a/assets/images/emoji/scream_cat.png b/assets/images/emoji/scream_cat.png new file mode 100644 index 00000000..d94cd34f Binary files /dev/null and b/assets/images/emoji/scream_cat.png differ diff --git a/assets/images/emoji/scroll.png b/assets/images/emoji/scroll.png new file mode 100644 index 00000000..c5a10e6b Binary files /dev/null and b/assets/images/emoji/scroll.png differ diff --git a/assets/images/emoji/seat.png b/assets/images/emoji/seat.png index 9d7311a8..d1cb864b 100644 Binary files a/assets/images/emoji/seat.png and b/assets/images/emoji/seat.png differ diff --git a/assets/images/emoji/secret.png b/assets/images/emoji/secret.png index 26b63912..82e383a6 100644 Binary files a/assets/images/emoji/secret.png and b/assets/images/emoji/secret.png differ diff --git a/assets/images/emoji/see_no_evil.png b/assets/images/emoji/see_no_evil.png new file mode 100644 index 00000000..0890a622 Binary files /dev/null and b/assets/images/emoji/see_no_evil.png differ diff --git a/assets/images/emoji/seedling.png b/assets/images/emoji/seedling.png new file mode 100644 index 00000000..2ab07931 Binary files /dev/null and b/assets/images/emoji/seedling.png differ diff --git a/assets/images/emoji/seven.png b/assets/images/emoji/seven.png new file mode 100644 index 00000000..354e89ae Binary files /dev/null and b/assets/images/emoji/seven.png differ diff --git a/assets/images/emoji/shaved_ice.png b/assets/images/emoji/shaved_ice.png index a8f6833b..0d0b382c 100644 Binary files a/assets/images/emoji/shaved_ice.png and b/assets/images/emoji/shaved_ice.png differ diff --git a/assets/images/emoji/sheep.png b/assets/images/emoji/sheep.png index 7ca93eaf..c7277d28 100644 Binary files a/assets/images/emoji/sheep.png and b/assets/images/emoji/sheep.png differ diff --git a/assets/images/emoji/shell.png b/assets/images/emoji/shell.png index d2892f08..3145b564 100644 Binary files a/assets/images/emoji/shell.png and b/assets/images/emoji/shell.png differ diff --git a/assets/images/emoji/ship.png b/assets/images/emoji/ship.png index 7b709089..5d2d8b60 100644 Binary files a/assets/images/emoji/ship.png and b/assets/images/emoji/ship.png differ diff --git a/assets/images/emoji/shipit.png b/assets/images/emoji/shipit.png index 5097004b..a58a47f6 100644 Binary files a/assets/images/emoji/shipit.png and b/assets/images/emoji/shipit.png differ diff --git a/assets/images/emoji/shirt.png b/assets/images/emoji/shirt.png index 591b55bf..297a6d63 100644 Binary files a/assets/images/emoji/shirt.png and b/assets/images/emoji/shirt.png differ diff --git a/assets/images/emoji/shit.png b/assets/images/emoji/shit.png index 69bc33f1..73a4dc84 100644 Binary files a/assets/images/emoji/shit.png and b/assets/images/emoji/shit.png differ diff --git a/assets/images/emoji/shoe.png b/assets/images/emoji/shoe.png index 3771bfb6..45b82e61 100644 Binary files a/assets/images/emoji/shoe.png and b/assets/images/emoji/shoe.png differ diff --git a/assets/images/emoji/shower.png b/assets/images/emoji/shower.png new file mode 100644 index 00000000..0d72ab86 Binary files /dev/null and b/assets/images/emoji/shower.png differ diff --git a/assets/images/emoji/signal_strength.png b/assets/images/emoji/signal_strength.png index 05d422c1..a4bd23eb 100644 Binary files a/assets/images/emoji/signal_strength.png and b/assets/images/emoji/signal_strength.png differ diff --git a/assets/images/emoji/six.png b/assets/images/emoji/six.png new file mode 100644 index 00000000..56880556 Binary files /dev/null and b/assets/images/emoji/six.png differ diff --git a/assets/images/emoji/six_pointed_star.png b/assets/images/emoji/six_pointed_star.png index 28e49230..010f8f5f 100644 Binary files a/assets/images/emoji/six_pointed_star.png and b/assets/images/emoji/six_pointed_star.png differ diff --git a/assets/images/emoji/ski.png b/assets/images/emoji/ski.png index e8d54f58..98f5cb0f 100644 Binary files a/assets/images/emoji/ski.png and b/assets/images/emoji/ski.png differ diff --git a/assets/images/emoji/skull.png b/assets/images/emoji/skull.png index d29156dc..bd4ee382 100644 Binary files a/assets/images/emoji/skull.png and b/assets/images/emoji/skull.png differ diff --git a/assets/images/emoji/sleeping.png b/assets/images/emoji/sleeping.png new file mode 100644 index 00000000..093b8523 Binary files /dev/null and b/assets/images/emoji/sleeping.png differ diff --git a/assets/images/emoji/sleepy.png b/assets/images/emoji/sleepy.png index e607a0ca..df4f55ef 100644 Binary files a/assets/images/emoji/sleepy.png and b/assets/images/emoji/sleepy.png differ diff --git a/assets/images/emoji/slot_machine.png b/assets/images/emoji/slot_machine.png index 7a98d229..26f11483 100644 Binary files a/assets/images/emoji/slot_machine.png and b/assets/images/emoji/slot_machine.png differ diff --git a/assets/images/emoji/small_blue_diamond.png b/assets/images/emoji/small_blue_diamond.png new file mode 100644 index 00000000..8cd49205 Binary files /dev/null and b/assets/images/emoji/small_blue_diamond.png differ diff --git a/assets/images/emoji/small_orange_diamond.png b/assets/images/emoji/small_orange_diamond.png new file mode 100644 index 00000000..04941d37 Binary files /dev/null and b/assets/images/emoji/small_orange_diamond.png differ diff --git a/assets/images/emoji/small_red_triangle.png b/assets/images/emoji/small_red_triangle.png new file mode 100644 index 00000000..8c4428da Binary files /dev/null and b/assets/images/emoji/small_red_triangle.png differ diff --git a/assets/images/emoji/small_red_triangle_down.png b/assets/images/emoji/small_red_triangle_down.png new file mode 100644 index 00000000..94832f06 Binary files /dev/null and b/assets/images/emoji/small_red_triangle_down.png differ diff --git a/assets/images/emoji/smile.png b/assets/images/emoji/smile.png index 636390ec..81a83968 100644 Binary files a/assets/images/emoji/smile.png and b/assets/images/emoji/smile.png differ diff --git a/assets/images/emoji/smile_cat.png b/assets/images/emoji/smile_cat.png new file mode 100644 index 00000000..ad333ba3 Binary files /dev/null and b/assets/images/emoji/smile_cat.png differ diff --git a/assets/images/emoji/smiley.png b/assets/images/emoji/smiley.png index 6c4ef119..77b581d6 100644 Binary files a/assets/images/emoji/smiley.png and b/assets/images/emoji/smiley.png differ diff --git a/assets/images/emoji/smiley_cat.png b/assets/images/emoji/smiley_cat.png new file mode 100644 index 00000000..dbf1b027 Binary files /dev/null and b/assets/images/emoji/smiley_cat.png differ diff --git a/assets/images/emoji/smiling_imp.png b/assets/images/emoji/smiling_imp.png new file mode 100644 index 00000000..d9040493 Binary files /dev/null and b/assets/images/emoji/smiling_imp.png differ diff --git a/assets/images/emoji/smirk.png b/assets/images/emoji/smirk.png index 57a69306..bc6e5082 100644 Binary files a/assets/images/emoji/smirk.png and b/assets/images/emoji/smirk.png differ diff --git a/assets/images/emoji/smirk_cat.png b/assets/images/emoji/smirk_cat.png new file mode 100644 index 00000000..351565e2 Binary files /dev/null and b/assets/images/emoji/smirk_cat.png differ diff --git a/assets/images/emoji/smoking.png b/assets/images/emoji/smoking.png index c7c39a12..4aad6cbd 100644 Binary files a/assets/images/emoji/smoking.png and b/assets/images/emoji/smoking.png differ diff --git a/assets/images/emoji/snail.png b/assets/images/emoji/snail.png new file mode 100644 index 00000000..e75e69a8 Binary files /dev/null and b/assets/images/emoji/snail.png differ diff --git a/assets/images/emoji/snake.png b/assets/images/emoji/snake.png index b8e2e241..ef58933e 100644 Binary files a/assets/images/emoji/snake.png and b/assets/images/emoji/snake.png differ diff --git a/assets/images/emoji/snowboarder.png b/assets/images/emoji/snowboarder.png new file mode 100644 index 00000000..aeda5c8d Binary files /dev/null and b/assets/images/emoji/snowboarder.png differ diff --git a/assets/images/emoji/snowflake.png b/assets/images/emoji/snowflake.png new file mode 100644 index 00000000..54b68ff4 Binary files /dev/null and b/assets/images/emoji/snowflake.png differ diff --git a/assets/images/emoji/snowman.png b/assets/images/emoji/snowman.png index 0daf91c1..a97902e5 100644 Binary files a/assets/images/emoji/snowman.png and b/assets/images/emoji/snowman.png differ diff --git a/assets/images/emoji/sob.png b/assets/images/emoji/sob.png index 079a944f..1561df92 100644 Binary files a/assets/images/emoji/sob.png and b/assets/images/emoji/sob.png differ diff --git a/assets/images/emoji/soccer.png b/assets/images/emoji/soccer.png index fe4a6759..1e118b5b 100644 Binary files a/assets/images/emoji/soccer.png and b/assets/images/emoji/soccer.png differ diff --git a/assets/images/emoji/soon.png b/assets/images/emoji/soon.png new file mode 100644 index 00000000..9386615a Binary files /dev/null and b/assets/images/emoji/soon.png differ diff --git a/assets/images/emoji/sos.png b/assets/images/emoji/sos.png new file mode 100644 index 00000000..e3e16ef7 Binary files /dev/null and b/assets/images/emoji/sos.png differ diff --git a/assets/images/emoji/sound.png b/assets/images/emoji/sound.png new file mode 100644 index 00000000..6aa4dbff Binary files /dev/null and b/assets/images/emoji/sound.png differ diff --git a/assets/images/emoji/space_invader.png b/assets/images/emoji/space_invader.png index 3c7804c7..38404916 100644 Binary files a/assets/images/emoji/space_invader.png and b/assets/images/emoji/space_invader.png differ diff --git a/assets/images/emoji/spades.png b/assets/images/emoji/spades.png index 17084df6..133a1aba 100644 Binary files a/assets/images/emoji/spades.png and b/assets/images/emoji/spades.png differ diff --git a/assets/images/emoji/spaghetti.png b/assets/images/emoji/spaghetti.png index 8aec1011..08de243f 100644 Binary files a/assets/images/emoji/spaghetti.png and b/assets/images/emoji/spaghetti.png differ diff --git a/assets/images/emoji/sparkler.png b/assets/images/emoji/sparkler.png index 306e0c59..4aabd7e0 100644 Binary files a/assets/images/emoji/sparkler.png and b/assets/images/emoji/sparkler.png differ diff --git a/assets/images/emoji/sparkles.png b/assets/images/emoji/sparkles.png index db6afa96..92138828 100644 Binary files a/assets/images/emoji/sparkles.png and b/assets/images/emoji/sparkles.png differ diff --git a/assets/images/emoji/sparkling_heart.png b/assets/images/emoji/sparkling_heart.png new file mode 100644 index 00000000..64ac0666 Binary files /dev/null and b/assets/images/emoji/sparkling_heart.png differ diff --git a/assets/images/emoji/speak_no_evil.png b/assets/images/emoji/speak_no_evil.png new file mode 100644 index 00000000..87944c4d Binary files /dev/null and b/assets/images/emoji/speak_no_evil.png differ diff --git a/assets/images/emoji/speaker.png b/assets/images/emoji/speaker.png index ad245cfe..470476e1 100644 Binary files a/assets/images/emoji/speaker.png and b/assets/images/emoji/speaker.png differ diff --git a/assets/images/emoji/speech_balloon.png b/assets/images/emoji/speech_balloon.png new file mode 100644 index 00000000..2896c278 Binary files /dev/null and b/assets/images/emoji/speech_balloon.png differ diff --git a/assets/images/emoji/speedboat.png b/assets/images/emoji/speedboat.png index 9af4c3ba..da6689b3 100644 Binary files a/assets/images/emoji/speedboat.png and b/assets/images/emoji/speedboat.png differ diff --git a/assets/images/emoji/squirrel.png b/assets/images/emoji/squirrel.png index 5097004b..a58a47f6 100644 Binary files a/assets/images/emoji/squirrel.png and b/assets/images/emoji/squirrel.png differ diff --git a/assets/images/emoji/star.png b/assets/images/emoji/star.png index 7ca7067b..1bfddc86 100644 Binary files a/assets/images/emoji/star.png and b/assets/images/emoji/star.png differ diff --git a/assets/images/emoji/star2.png b/assets/images/emoji/star2.png index 94f9ceb4..8b40ff4c 100644 Binary files a/assets/images/emoji/star2.png and b/assets/images/emoji/star2.png differ diff --git a/assets/images/emoji/stars.png b/assets/images/emoji/stars.png index 3c9c684c..097a8424 100644 Binary files a/assets/images/emoji/stars.png and b/assets/images/emoji/stars.png differ diff --git a/assets/images/emoji/station.png b/assets/images/emoji/station.png index 70fdf630..e77daa8a 100644 Binary files a/assets/images/emoji/station.png and b/assets/images/emoji/station.png differ diff --git a/assets/images/emoji/statue_of_liberty.png b/assets/images/emoji/statue_of_liberty.png index a5212907..9ad90280 100644 Binary files a/assets/images/emoji/statue_of_liberty.png and b/assets/images/emoji/statue_of_liberty.png differ diff --git a/assets/images/emoji/steam_locomotive.png b/assets/images/emoji/steam_locomotive.png new file mode 100644 index 00000000..54950776 Binary files /dev/null and b/assets/images/emoji/steam_locomotive.png differ diff --git a/assets/images/emoji/stew.png b/assets/images/emoji/stew.png index 7168767d..e9687f9e 100644 Binary files a/assets/images/emoji/stew.png and b/assets/images/emoji/stew.png differ diff --git a/assets/images/emoji/straight_ruler.png b/assets/images/emoji/straight_ruler.png new file mode 100644 index 00000000..d96658ea Binary files /dev/null and b/assets/images/emoji/straight_ruler.png differ diff --git a/assets/images/emoji/strawberry.png b/assets/images/emoji/strawberry.png index ac74fb83..13eb827a 100644 Binary files a/assets/images/emoji/strawberry.png and b/assets/images/emoji/strawberry.png differ diff --git a/assets/images/emoji/stuck_out_tongue.png b/assets/images/emoji/stuck_out_tongue.png new file mode 100644 index 00000000..fa7b58e2 Binary files /dev/null and b/assets/images/emoji/stuck_out_tongue.png differ diff --git a/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/assets/images/emoji/stuck_out_tongue_closed_eyes.png new file mode 100644 index 00000000..333716ee Binary files /dev/null and b/assets/images/emoji/stuck_out_tongue_closed_eyes.png differ diff --git a/assets/images/emoji/stuck_out_tongue_winking_eye.png b/assets/images/emoji/stuck_out_tongue_winking_eye.png new file mode 100644 index 00000000..6ae9d497 Binary files /dev/null and b/assets/images/emoji/stuck_out_tongue_winking_eye.png differ diff --git a/assets/images/emoji/sun_with_face.png b/assets/images/emoji/sun_with_face.png new file mode 100644 index 00000000..ee276636 Binary files /dev/null and b/assets/images/emoji/sun_with_face.png differ diff --git a/assets/images/emoji/sunflower.png b/assets/images/emoji/sunflower.png index 9eae979e..d9bad194 100644 Binary files a/assets/images/emoji/sunflower.png and b/assets/images/emoji/sunflower.png differ diff --git a/assets/images/emoji/sunglasses.png b/assets/images/emoji/sunglasses.png new file mode 100644 index 00000000..1c468a1c Binary files /dev/null and b/assets/images/emoji/sunglasses.png differ diff --git a/assets/images/emoji/sunny.png b/assets/images/emoji/sunny.png index 81a412dd..d23c095e 100644 Binary files a/assets/images/emoji/sunny.png and b/assets/images/emoji/sunny.png differ diff --git a/assets/images/emoji/sunrise.png b/assets/images/emoji/sunrise.png index 4822140b..ec58dcc9 100644 Binary files a/assets/images/emoji/sunrise.png and b/assets/images/emoji/sunrise.png differ diff --git a/assets/images/emoji/sunrise_over_mountains.png b/assets/images/emoji/sunrise_over_mountains.png index ea3fa19a..ebc3db14 100644 Binary files a/assets/images/emoji/sunrise_over_mountains.png and b/assets/images/emoji/sunrise_over_mountains.png differ diff --git a/assets/images/emoji/surfer.png b/assets/images/emoji/surfer.png index 27248dc6..b067e8cb 100644 Binary files a/assets/images/emoji/surfer.png and b/assets/images/emoji/surfer.png differ diff --git a/assets/images/emoji/sushi.png b/assets/images/emoji/sushi.png index 135e3633..0d179bd9 100644 Binary files a/assets/images/emoji/sushi.png and b/assets/images/emoji/sushi.png differ diff --git a/assets/images/emoji/suspect.png b/assets/images/emoji/suspect.png index d96e0bc7..58e8921c 100644 Binary files a/assets/images/emoji/suspect.png and b/assets/images/emoji/suspect.png differ diff --git a/assets/images/emoji/suspension_railway.png b/assets/images/emoji/suspension_railway.png new file mode 100644 index 00000000..aaa45f61 Binary files /dev/null and b/assets/images/emoji/suspension_railway.png differ diff --git a/assets/images/emoji/sweat.png b/assets/images/emoji/sweat.png index d7179708..e894b769 100644 Binary files a/assets/images/emoji/sweat.png and b/assets/images/emoji/sweat.png differ diff --git a/assets/images/emoji/sweat_drops.png b/assets/images/emoji/sweat_drops.png index 2c4ac5c6..a83b3e96 100644 Binary files a/assets/images/emoji/sweat_drops.png and b/assets/images/emoji/sweat_drops.png differ diff --git a/assets/images/emoji/sweat_smile.png b/assets/images/emoji/sweat_smile.png new file mode 100644 index 00000000..3903f717 Binary files /dev/null and b/assets/images/emoji/sweat_smile.png differ diff --git a/assets/images/emoji/sweet_potato.png b/assets/images/emoji/sweet_potato.png new file mode 100644 index 00000000..cde7880a Binary files /dev/null and b/assets/images/emoji/sweet_potato.png differ diff --git a/assets/images/emoji/swimmer.png b/assets/images/emoji/swimmer.png index d384f95c..d3878a06 100644 Binary files a/assets/images/emoji/swimmer.png and b/assets/images/emoji/swimmer.png differ diff --git a/assets/images/emoji/symbols.png b/assets/images/emoji/symbols.png new file mode 100644 index 00000000..16bc1da9 Binary files /dev/null and b/assets/images/emoji/symbols.png differ diff --git a/assets/images/emoji/syringe.png b/assets/images/emoji/syringe.png index c066625d..7314255e 100644 Binary files a/assets/images/emoji/syringe.png and b/assets/images/emoji/syringe.png differ diff --git a/assets/images/emoji/tada.png b/assets/images/emoji/tada.png index 78c674fd..7411b526 100644 Binary files a/assets/images/emoji/tada.png and b/assets/images/emoji/tada.png differ diff --git a/assets/images/emoji/tanabata_tree.png b/assets/images/emoji/tanabata_tree.png new file mode 100644 index 00000000..6dea4b2d Binary files /dev/null and b/assets/images/emoji/tanabata_tree.png differ diff --git a/assets/images/emoji/tangerine.png b/assets/images/emoji/tangerine.png index ad2f3d48..fc9d4f82 100644 Binary files a/assets/images/emoji/tangerine.png and b/assets/images/emoji/tangerine.png differ diff --git a/assets/images/emoji/taurus.png b/assets/images/emoji/taurus.png index 86bbf864..6af582f6 100644 Binary files a/assets/images/emoji/taurus.png and b/assets/images/emoji/taurus.png differ diff --git a/assets/images/emoji/taxi.png b/assets/images/emoji/taxi.png index 0e74be12..60a50d36 100644 Binary files a/assets/images/emoji/taxi.png and b/assets/images/emoji/taxi.png differ diff --git a/assets/images/emoji/tea.png b/assets/images/emoji/tea.png index a323e485..3ece0b70 100644 Binary files a/assets/images/emoji/tea.png and b/assets/images/emoji/tea.png differ diff --git a/assets/images/emoji/telephone.png b/assets/images/emoji/telephone.png index e1d64368..87d2559b 100644 Binary files a/assets/images/emoji/telephone.png and b/assets/images/emoji/telephone.png differ diff --git a/assets/images/emoji/telephone_receiver.png b/assets/images/emoji/telephone_receiver.png new file mode 100644 index 00000000..36e21e01 Binary files /dev/null and b/assets/images/emoji/telephone_receiver.png differ diff --git a/assets/images/emoji/telescope.png b/assets/images/emoji/telescope.png new file mode 100644 index 00000000..8511fa96 Binary files /dev/null and b/assets/images/emoji/telescope.png differ diff --git a/assets/images/emoji/tennis.png b/assets/images/emoji/tennis.png index 18559da3..278d904e 100644 Binary files a/assets/images/emoji/tennis.png and b/assets/images/emoji/tennis.png differ diff --git a/assets/images/emoji/tent.png b/assets/images/emoji/tent.png index 452aebda..5c0d20e4 100644 Binary files a/assets/images/emoji/tent.png and b/assets/images/emoji/tent.png differ diff --git a/assets/images/emoji/thought_balloon.png b/assets/images/emoji/thought_balloon.png new file mode 100644 index 00000000..febe30d0 Binary files /dev/null and b/assets/images/emoji/thought_balloon.png differ diff --git a/assets/images/emoji/three.png b/assets/images/emoji/three.png new file mode 100644 index 00000000..55644c99 Binary files /dev/null and b/assets/images/emoji/three.png differ diff --git a/assets/images/emoji/thumbsdown.png b/assets/images/emoji/thumbsdown.png index 6f757ba8..41c6b825 100644 Binary files a/assets/images/emoji/thumbsdown.png and b/assets/images/emoji/thumbsdown.png differ diff --git a/assets/images/emoji/thumbsup.png b/assets/images/emoji/thumbsup.png index 4dae7a09..81786c1d 100644 Binary files a/assets/images/emoji/thumbsup.png and b/assets/images/emoji/thumbsup.png differ diff --git a/assets/images/emoji/ticket.png b/assets/images/emoji/ticket.png index c926699b..cdacf1a7 100644 Binary files a/assets/images/emoji/ticket.png and b/assets/images/emoji/ticket.png differ diff --git a/assets/images/emoji/tiger.png b/assets/images/emoji/tiger.png index 1f44a975..d6cc84a3 100644 Binary files a/assets/images/emoji/tiger.png and b/assets/images/emoji/tiger.png differ diff --git a/assets/images/emoji/tiger2.png b/assets/images/emoji/tiger2.png new file mode 100644 index 00000000..b0c7d8dc Binary files /dev/null and b/assets/images/emoji/tiger2.png differ diff --git a/assets/images/emoji/tired_face.png b/assets/images/emoji/tired_face.png new file mode 100644 index 00000000..3a8eefe5 Binary files /dev/null and b/assets/images/emoji/tired_face.png differ diff --git a/assets/images/emoji/tm.png b/assets/images/emoji/tm.png index 202ec305..c7dec75a 100644 Binary files a/assets/images/emoji/tm.png and b/assets/images/emoji/tm.png differ diff --git a/assets/images/emoji/toilet.png b/assets/images/emoji/toilet.png index 18147e6d..e5cc4119 100644 Binary files a/assets/images/emoji/toilet.png and b/assets/images/emoji/toilet.png differ diff --git a/assets/images/emoji/tokyo_tower.png b/assets/images/emoji/tokyo_tower.png index b2712ac9..e1cbd7a3 100644 Binary files a/assets/images/emoji/tokyo_tower.png and b/assets/images/emoji/tokyo_tower.png differ diff --git a/assets/images/emoji/tomato.png b/assets/images/emoji/tomato.png index b2f14ece..a129700b 100644 Binary files a/assets/images/emoji/tomato.png and b/assets/images/emoji/tomato.png differ diff --git a/assets/images/emoji/tongue.png b/assets/images/emoji/tongue.png index 1b76e224..b0bab120 100644 Binary files a/assets/images/emoji/tongue.png and b/assets/images/emoji/tongue.png differ diff --git a/assets/images/emoji/top.png b/assets/images/emoji/top.png index 3a4f2cd1..5aa4dd44 100644 Binary files a/assets/images/emoji/top.png and b/assets/images/emoji/top.png differ diff --git a/assets/images/emoji/tophat.png b/assets/images/emoji/tophat.png index f12f2321..7d27134d 100644 Binary files a/assets/images/emoji/tophat.png and b/assets/images/emoji/tophat.png differ diff --git a/assets/images/emoji/tractor.png b/assets/images/emoji/tractor.png new file mode 100644 index 00000000..058fd3ed Binary files /dev/null and b/assets/images/emoji/tractor.png differ diff --git a/assets/images/emoji/traffic_light.png b/assets/images/emoji/traffic_light.png index c15a6442..50c78101 100644 Binary files a/assets/images/emoji/traffic_light.png and b/assets/images/emoji/traffic_light.png differ diff --git a/assets/images/emoji/train.png b/assets/images/emoji/train.png index 26288046..3202d80e 100644 Binary files a/assets/images/emoji/train.png and b/assets/images/emoji/train.png differ diff --git a/assets/images/emoji/train2.png b/assets/images/emoji/train2.png new file mode 100644 index 00000000..9c0d3ab6 Binary files /dev/null and b/assets/images/emoji/train2.png differ diff --git a/assets/images/emoji/tram.png b/assets/images/emoji/tram.png new file mode 100644 index 00000000..5eb29fb7 Binary files /dev/null and b/assets/images/emoji/tram.png differ diff --git a/assets/images/emoji/triangular_flag_on_post.png b/assets/images/emoji/triangular_flag_on_post.png new file mode 100644 index 00000000..f9a3f32d Binary files /dev/null and b/assets/images/emoji/triangular_flag_on_post.png differ diff --git a/assets/images/emoji/triangular_ruler.png b/assets/images/emoji/triangular_ruler.png new file mode 100644 index 00000000..383677cb Binary files /dev/null and b/assets/images/emoji/triangular_ruler.png differ diff --git a/assets/images/emoji/trident.png b/assets/images/emoji/trident.png index 900a5a4d..d79a7b4c 100644 Binary files a/assets/images/emoji/trident.png and b/assets/images/emoji/trident.png differ diff --git a/assets/images/emoji/triumph.png b/assets/images/emoji/triumph.png new file mode 100644 index 00000000..92f93bd1 Binary files /dev/null and b/assets/images/emoji/triumph.png differ diff --git a/assets/images/emoji/trolleybus.png b/assets/images/emoji/trolleybus.png new file mode 100644 index 00000000..b9740a53 Binary files /dev/null and b/assets/images/emoji/trolleybus.png differ diff --git a/assets/images/emoji/trollface.png b/assets/images/emoji/trollface.png new file mode 100644 index 00000000..e234893c Binary files /dev/null and b/assets/images/emoji/trollface.png differ diff --git a/assets/images/emoji/trophy.png b/assets/images/emoji/trophy.png index 39177993..95d3b63f 100644 Binary files a/assets/images/emoji/trophy.png and b/assets/images/emoji/trophy.png differ diff --git a/assets/images/emoji/tropical_drink.png b/assets/images/emoji/tropical_drink.png new file mode 100644 index 00000000..55ca9eed Binary files /dev/null and b/assets/images/emoji/tropical_drink.png differ diff --git a/assets/images/emoji/tropical_fish.png b/assets/images/emoji/tropical_fish.png index fcaefe85..a6d73498 100644 Binary files a/assets/images/emoji/tropical_fish.png and b/assets/images/emoji/tropical_fish.png differ diff --git a/assets/images/emoji/truck.png b/assets/images/emoji/truck.png index d346efd0..3f25ba1f 100644 Binary files a/assets/images/emoji/truck.png and b/assets/images/emoji/truck.png differ diff --git a/assets/images/emoji/trumpet.png b/assets/images/emoji/trumpet.png index e98f5b76..c84cfb13 100644 Binary files a/assets/images/emoji/trumpet.png and b/assets/images/emoji/trumpet.png differ diff --git a/assets/images/emoji/tshirt.png b/assets/images/emoji/tshirt.png index 591b55bf..297a6d63 100644 Binary files a/assets/images/emoji/tshirt.png and b/assets/images/emoji/tshirt.png differ diff --git a/assets/images/emoji/tulip.png b/assets/images/emoji/tulip.png index 1f20c543..b3ee1102 100644 Binary files a/assets/images/emoji/tulip.png and b/assets/images/emoji/tulip.png differ diff --git a/assets/images/emoji/turtle.png b/assets/images/emoji/turtle.png new file mode 100644 index 00000000..04d1d968 Binary files /dev/null and b/assets/images/emoji/turtle.png differ diff --git a/assets/images/emoji/tv.png b/assets/images/emoji/tv.png index 90b80974..803dc3d4 100644 Binary files a/assets/images/emoji/tv.png and b/assets/images/emoji/tv.png differ diff --git a/assets/images/emoji/twisted_rightwards_arrows.png b/assets/images/emoji/twisted_rightwards_arrows.png new file mode 100644 index 00000000..25cde18b Binary files /dev/null and b/assets/images/emoji/twisted_rightwards_arrows.png differ diff --git a/assets/images/emoji/two.png b/assets/images/emoji/two.png new file mode 100644 index 00000000..c191f8a3 Binary files /dev/null and b/assets/images/emoji/two.png differ diff --git a/assets/images/emoji/two_hearts.png b/assets/images/emoji/two_hearts.png new file mode 100644 index 00000000..b189e9ae Binary files /dev/null and b/assets/images/emoji/two_hearts.png differ diff --git a/assets/images/emoji/two_men_holding_hands.png b/assets/images/emoji/two_men_holding_hands.png new file mode 100644 index 00000000..d1099f21 Binary files /dev/null and b/assets/images/emoji/two_men_holding_hands.png differ diff --git a/assets/images/emoji/two_women_holding_hands.png b/assets/images/emoji/two_women_holding_hands.png new file mode 100644 index 00000000..619646c4 Binary files /dev/null and b/assets/images/emoji/two_women_holding_hands.png differ diff --git a/assets/images/emoji/u5272.png b/assets/images/emoji/u5272.png index 6ce43a06..2148253f 100644 Binary files a/assets/images/emoji/u5272.png and b/assets/images/emoji/u5272.png differ diff --git a/assets/images/emoji/u5408.png b/assets/images/emoji/u5408.png new file mode 100644 index 00000000..03ab0d87 Binary files /dev/null and b/assets/images/emoji/u5408.png differ diff --git a/assets/images/emoji/u55b6.png b/assets/images/emoji/u55b6.png index d10e0764..ba946d3f 100644 Binary files a/assets/images/emoji/u55b6.png and b/assets/images/emoji/u55b6.png differ diff --git a/assets/images/emoji/u6307.png b/assets/images/emoji/u6307.png index d185ca7a..6557f567 100644 Binary files a/assets/images/emoji/u6307.png and b/assets/images/emoji/u6307.png differ diff --git a/assets/images/emoji/u6708.png b/assets/images/emoji/u6708.png index ab493e4f..e4dfe5aa 100644 Binary files a/assets/images/emoji/u6708.png and b/assets/images/emoji/u6708.png differ diff --git a/assets/images/emoji/u6709.png b/assets/images/emoji/u6709.png index 0287b16a..cd8fb3f6 100644 Binary files a/assets/images/emoji/u6709.png and b/assets/images/emoji/u6709.png differ diff --git a/assets/images/emoji/u6e80.png b/assets/images/emoji/u6e80.png index 90dd116d..5df1cb87 100644 Binary files a/assets/images/emoji/u6e80.png and b/assets/images/emoji/u6e80.png differ diff --git a/assets/images/emoji/u7121.png b/assets/images/emoji/u7121.png index 9ff16c51..25f694ed 100644 Binary files a/assets/images/emoji/u7121.png and b/assets/images/emoji/u7121.png differ diff --git a/assets/images/emoji/u7533.png b/assets/images/emoji/u7533.png index 7efbd9d8..fc4a9901 100644 Binary files a/assets/images/emoji/u7533.png and b/assets/images/emoji/u7533.png differ diff --git a/assets/images/emoji/u7981.png b/assets/images/emoji/u7981.png new file mode 100644 index 00000000..f550a573 Binary files /dev/null and b/assets/images/emoji/u7981.png differ diff --git a/assets/images/emoji/u7a7a.png b/assets/images/emoji/u7a7a.png index eb9774e1..c05f5cff 100644 Binary files a/assets/images/emoji/u7a7a.png and b/assets/images/emoji/u7a7a.png differ diff --git a/assets/images/emoji/uk.png b/assets/images/emoji/uk.png new file mode 100644 index 00000000..2a62c7a0 Binary files /dev/null and b/assets/images/emoji/uk.png differ diff --git a/assets/images/emoji/umbrella.png b/assets/images/emoji/umbrella.png index a6d3890e..1db722fa 100644 Binary files a/assets/images/emoji/umbrella.png and b/assets/images/emoji/umbrella.png differ diff --git a/assets/images/emoji/unamused.png b/assets/images/emoji/unamused.png index 234c9c08..3722e6f5 100644 Binary files a/assets/images/emoji/unamused.png and b/assets/images/emoji/unamused.png differ diff --git a/assets/images/emoji/underage.png b/assets/images/emoji/underage.png index ae3a771d..a789b3c6 100644 Binary files a/assets/images/emoji/underage.png and b/assets/images/emoji/underage.png differ diff --git a/assets/images/emoji/unlock.png b/assets/images/emoji/unlock.png index b664853f..22b429cd 100644 Binary files a/assets/images/emoji/unlock.png and b/assets/images/emoji/unlock.png differ diff --git a/assets/images/emoji/up.png b/assets/images/emoji/up.png index b065186e..829219a8 100644 Binary files a/assets/images/emoji/up.png and b/assets/images/emoji/up.png differ diff --git a/assets/images/emoji/us.png b/assets/images/emoji/us.png index 97323c7d..38137669 100644 Binary files a/assets/images/emoji/us.png and b/assets/images/emoji/us.png differ diff --git a/assets/images/emoji/v.png b/assets/images/emoji/v.png index e9cc8dc8..f61267c2 100644 Binary files a/assets/images/emoji/v.png and b/assets/images/emoji/v.png differ diff --git a/assets/images/emoji/vertical_traffic_light.png b/assets/images/emoji/vertical_traffic_light.png new file mode 100644 index 00000000..7a5ba35f Binary files /dev/null and b/assets/images/emoji/vertical_traffic_light.png differ diff --git a/assets/images/emoji/vhs.png b/assets/images/emoji/vhs.png index 34027fb2..881081c1 100644 Binary files a/assets/images/emoji/vhs.png and b/assets/images/emoji/vhs.png differ diff --git a/assets/images/emoji/vibration_mode.png b/assets/images/emoji/vibration_mode.png index 7d9b811b..a716e96c 100644 Binary files a/assets/images/emoji/vibration_mode.png and b/assets/images/emoji/vibration_mode.png differ diff --git a/assets/images/emoji/video_camera.png b/assets/images/emoji/video_camera.png new file mode 100644 index 00000000..274cecdd Binary files /dev/null and b/assets/images/emoji/video_camera.png differ diff --git a/assets/images/emoji/video_game.png b/assets/images/emoji/video_game.png new file mode 100644 index 00000000..59d45bae Binary files /dev/null and b/assets/images/emoji/video_game.png differ diff --git a/assets/images/emoji/violin.png b/assets/images/emoji/violin.png new file mode 100644 index 00000000..27fdc8f7 Binary files /dev/null and b/assets/images/emoji/violin.png differ diff --git a/assets/images/emoji/virgo.png b/assets/images/emoji/virgo.png index 37219d75..72e1763f 100644 Binary files a/assets/images/emoji/virgo.png and b/assets/images/emoji/virgo.png differ diff --git a/assets/images/emoji/volcano.png b/assets/images/emoji/volcano.png new file mode 100644 index 00000000..9b434539 Binary files /dev/null and b/assets/images/emoji/volcano.png differ diff --git a/assets/images/emoji/vs.png b/assets/images/emoji/vs.png index da838c3f..86363885 100644 Binary files a/assets/images/emoji/vs.png and b/assets/images/emoji/vs.png differ diff --git a/assets/images/emoji/walking.png b/assets/images/emoji/walking.png index 4c7501fa..52bc0381 100644 Binary files a/assets/images/emoji/walking.png and b/assets/images/emoji/walking.png differ diff --git a/assets/images/emoji/waning_crescent_moon.png b/assets/images/emoji/waning_crescent_moon.png new file mode 100644 index 00000000..30387780 Binary files /dev/null and b/assets/images/emoji/waning_crescent_moon.png differ diff --git a/assets/images/emoji/waning_gibbous_moon.png b/assets/images/emoji/waning_gibbous_moon.png new file mode 100644 index 00000000..51009907 Binary files /dev/null and b/assets/images/emoji/waning_gibbous_moon.png differ diff --git a/assets/images/emoji/warning.png b/assets/images/emoji/warning.png index d2de14ce..466658d9 100644 Binary files a/assets/images/emoji/warning.png and b/assets/images/emoji/warning.png differ diff --git a/assets/images/emoji/watch.png b/assets/images/emoji/watch.png new file mode 100644 index 00000000..d503bb87 Binary files /dev/null and b/assets/images/emoji/watch.png differ diff --git a/assets/images/emoji/water_buffalo.png b/assets/images/emoji/water_buffalo.png new file mode 100644 index 00000000..3bcde3ed Binary files /dev/null and b/assets/images/emoji/water_buffalo.png differ diff --git a/assets/images/emoji/watermelon.png b/assets/images/emoji/watermelon.png index af8e3aca..fc212be7 100644 Binary files a/assets/images/emoji/watermelon.png and b/assets/images/emoji/watermelon.png differ diff --git a/assets/images/emoji/wave.png b/assets/images/emoji/wave.png index 17641964..e78402eb 100644 Binary files a/assets/images/emoji/wave.png and b/assets/images/emoji/wave.png differ diff --git a/assets/images/emoji/wavy_dash.png b/assets/images/emoji/wavy_dash.png new file mode 100644 index 00000000..77f626cc Binary files /dev/null and b/assets/images/emoji/wavy_dash.png differ diff --git a/assets/images/emoji/waxing_crescent_moon.png b/assets/images/emoji/waxing_crescent_moon.png new file mode 100644 index 00000000..c8f13dd3 Binary files /dev/null and b/assets/images/emoji/waxing_crescent_moon.png differ diff --git a/assets/images/emoji/waxing_gibbous_moon.png b/assets/images/emoji/waxing_gibbous_moon.png new file mode 100644 index 00000000..8cdfdf10 Binary files /dev/null and b/assets/images/emoji/waxing_gibbous_moon.png differ diff --git a/assets/images/emoji/wc.png b/assets/images/emoji/wc.png index 27125f19..dfe84d2a 100644 Binary files a/assets/images/emoji/wc.png and b/assets/images/emoji/wc.png differ diff --git a/assets/images/emoji/weary.png b/assets/images/emoji/weary.png new file mode 100644 index 00000000..0c547541 Binary files /dev/null and b/assets/images/emoji/weary.png differ diff --git a/assets/images/emoji/wedding.png b/assets/images/emoji/wedding.png index 75bbf800..ead19d52 100644 Binary files a/assets/images/emoji/wedding.png and b/assets/images/emoji/wedding.png differ diff --git a/assets/images/emoji/whale.png b/assets/images/emoji/whale.png index 43b319a7..5bb113e4 100644 Binary files a/assets/images/emoji/whale.png and b/assets/images/emoji/whale.png differ diff --git a/assets/images/emoji/whale2.png b/assets/images/emoji/whale2.png new file mode 100644 index 00000000..f6fb07ea Binary files /dev/null and b/assets/images/emoji/whale2.png differ diff --git a/assets/images/emoji/wheelchair.png b/assets/images/emoji/wheelchair.png index 6d53c499..eddcdd79 100644 Binary files a/assets/images/emoji/wheelchair.png and b/assets/images/emoji/wheelchair.png differ diff --git a/assets/images/emoji/white_check_mark.png b/assets/images/emoji/white_check_mark.png new file mode 100644 index 00000000..61dc0583 Binary files /dev/null and b/assets/images/emoji/white_check_mark.png differ diff --git a/assets/images/emoji/white_circle.png b/assets/images/emoji/white_circle.png new file mode 100644 index 00000000..3f648d1b Binary files /dev/null and b/assets/images/emoji/white_circle.png differ diff --git a/assets/images/emoji/white_flower.png b/assets/images/emoji/white_flower.png new file mode 100644 index 00000000..c0929d0d Binary files /dev/null and b/assets/images/emoji/white_flower.png differ diff --git a/assets/images/emoji/white_square.png b/assets/images/emoji/white_square.png index 65b56745..60cb19a1 100644 Binary files a/assets/images/emoji/white_square.png and b/assets/images/emoji/white_square.png differ diff --git a/assets/images/emoji/white_square_button.png b/assets/images/emoji/white_square_button.png new file mode 100644 index 00000000..ad54d55c Binary files /dev/null and b/assets/images/emoji/white_square_button.png differ diff --git a/assets/images/emoji/wind_chime.png b/assets/images/emoji/wind_chime.png index 5de30d7f..efacf5dd 100644 Binary files a/assets/images/emoji/wind_chime.png and b/assets/images/emoji/wind_chime.png differ diff --git a/assets/images/emoji/wine_glass.png b/assets/images/emoji/wine_glass.png new file mode 100644 index 00000000..82b0f000 Binary files /dev/null and b/assets/images/emoji/wine_glass.png differ diff --git a/assets/images/emoji/wink.png b/assets/images/emoji/wink.png index 9d593b88..756766dd 100644 Binary files a/assets/images/emoji/wink.png and b/assets/images/emoji/wink.png differ diff --git a/assets/images/emoji/wink2.png b/assets/images/emoji/wink2.png deleted file mode 100644 index 3d1b442a..00000000 Binary files a/assets/images/emoji/wink2.png and /dev/null differ diff --git a/assets/images/emoji/wolf.png b/assets/images/emoji/wolf.png index f1969e6b..c60c9689 100644 Binary files a/assets/images/emoji/wolf.png and b/assets/images/emoji/wolf.png differ diff --git a/assets/images/emoji/woman.png b/assets/images/emoji/woman.png index b052f4fa..6bf0d2b1 100644 Binary files a/assets/images/emoji/woman.png and b/assets/images/emoji/woman.png differ diff --git a/assets/images/emoji/womans_clothes.png b/assets/images/emoji/womans_clothes.png new file mode 100644 index 00000000..aa297c7b Binary files /dev/null and b/assets/images/emoji/womans_clothes.png differ diff --git a/assets/images/emoji/womans_hat.png b/assets/images/emoji/womans_hat.png index 0afbe823..4cb2e6a6 100644 Binary files a/assets/images/emoji/womans_hat.png and b/assets/images/emoji/womans_hat.png differ diff --git a/assets/images/emoji/womens.png b/assets/images/emoji/womens.png index 83deabfa..110f8851 100644 Binary files a/assets/images/emoji/womens.png and b/assets/images/emoji/womens.png differ diff --git a/assets/images/emoji/worried.png b/assets/images/emoji/worried.png new file mode 100644 index 00000000..276291a9 Binary files /dev/null and b/assets/images/emoji/worried.png differ diff --git a/assets/images/emoji/wrench.png b/assets/images/emoji/wrench.png new file mode 100644 index 00000000..a87072ad Binary files /dev/null and b/assets/images/emoji/wrench.png differ diff --git a/assets/images/emoji/x.png b/assets/images/emoji/x.png index a2b46bfc..dff9efa8 100644 Binary files a/assets/images/emoji/x.png and b/assets/images/emoji/x.png differ diff --git a/assets/images/emoji/yellow_heart.png b/assets/images/emoji/yellow_heart.png index 409cc176..fa41ce78 100644 Binary files a/assets/images/emoji/yellow_heart.png and b/assets/images/emoji/yellow_heart.png differ diff --git a/assets/images/emoji/yen.png b/assets/images/emoji/yen.png new file mode 100644 index 00000000..139bc936 Binary files /dev/null and b/assets/images/emoji/yen.png differ diff --git a/assets/images/emoji/yum.png b/assets/images/emoji/yum.png new file mode 100644 index 00000000..fc39637e Binary files /dev/null and b/assets/images/emoji/yum.png differ diff --git a/assets/images/emoji/zap.png b/assets/images/emoji/zap.png index d3dd89f6..260c531b 100644 Binary files a/assets/images/emoji/zap.png and b/assets/images/emoji/zap.png differ diff --git a/assets/images/emoji/zero.png b/assets/images/emoji/zero.png new file mode 100644 index 00000000..6e57b334 Binary files /dev/null and b/assets/images/emoji/zero.png differ diff --git a/assets/images/emoji/zzz.png b/assets/images/emoji/zzz.png index 55e31ae1..30be0465 100644 Binary files a/assets/images/emoji/zzz.png and b/assets/images/emoji/zzz.png differ diff --git a/assets/images/getting-started/build-email.jpg b/assets/images/getting-started/build-email.jpg new file mode 100644 index 00000000..706a57dc Binary files /dev/null and b/assets/images/getting-started/build-email.jpg differ diff --git a/assets/images/getting-started/build-email.png b/assets/images/getting-started/build-email.png index acde95b5..d39e04b9 100644 Binary files a/assets/images/getting-started/build-email.png and b/assets/images/getting-started/build-email.png differ diff --git a/assets/images/getting-started/first-build.png b/assets/images/getting-started/first-build.png index 9a367ec5..599a75c6 100644 Binary files a/assets/images/getting-started/first-build.png and b/assets/images/getting-started/first-build.png differ diff --git a/assets/images/getting-started/first-project-no-recent.png b/assets/images/getting-started/first-project-no-recent.png index d9a72ce3..a37bb154 100644 Binary files a/assets/images/getting-started/first-project-no-recent.png and b/assets/images/getting-started/first-project-no-recent.png differ diff --git a/assets/images/getting-started/mustache-no-spin.png b/assets/images/getting-started/mustache-no-spin.png new file mode 100644 index 00000000..a8f5af92 Binary files /dev/null and b/assets/images/getting-started/mustache-no-spin.png differ diff --git a/assets/images/getting-started/mustache-spinner.gif b/assets/images/getting-started/mustache-spinner.gif new file mode 100644 index 00000000..461229f7 Binary files /dev/null and b/assets/images/getting-started/mustache-spinner.gif differ diff --git a/assets/images/getting-started/project-switch.png b/assets/images/getting-started/project-switch.png index 6e23b6ec..1696aa3c 100644 Binary files a/assets/images/getting-started/project-switch.png and b/assets/images/getting-started/project-switch.png differ diff --git a/assets/images/icons/align-justify.png b/assets/images/icons/align-justify.png new file mode 100755 index 00000000..96bfc24b Binary files /dev/null and b/assets/images/icons/align-justify.png differ diff --git a/assets/images/icons/code-climate-icon.png b/assets/images/icons/code-climate-icon.png new file mode 100644 index 00000000..4d547a94 Binary files /dev/null and b/assets/images/icons/code-climate-icon.png differ diff --git a/assets/images/icons/code-climate-logo.png b/assets/images/icons/code-climate-logo.png new file mode 100644 index 00000000..4c739e45 Binary files /dev/null and b/assets/images/icons/code-climate-logo.png differ diff --git a/assets/images/icons/github.png b/assets/images/icons/github.png index 182a1a3f..d99646c7 100644 Binary files a/assets/images/icons/github.png and b/assets/images/icons/github.png differ diff --git a/assets/images/icons/off.png b/assets/images/icons/off.png new file mode 100755 index 00000000..8e11bea2 Binary files /dev/null and b/assets/images/icons/off.png differ diff --git a/assets/images/icons/repeat.png b/assets/images/icons/repeat.png new file mode 100755 index 00000000..6e5b206f Binary files /dev/null and b/assets/images/icons/repeat.png differ diff --git a/assets/images/mailer/arrow-error.png b/assets/images/mailer/arrow-error.png new file mode 100644 index 00000000..466ee5bc Binary files /dev/null and b/assets/images/mailer/arrow-error.png differ diff --git a/assets/images/mailer/arrow-failed.png b/assets/images/mailer/arrow-failed.png new file mode 100644 index 00000000..43c4e98f Binary files /dev/null and b/assets/images/mailer/arrow-failed.png differ diff --git a/assets/images/mailer/arrow-success.png b/assets/images/mailer/arrow-success.png new file mode 100644 index 00000000..29ca38ce Binary files /dev/null and b/assets/images/mailer/arrow-success.png differ diff --git a/assets/images/mailer/canceled-header-bg.png b/assets/images/mailer/canceled-header-bg.png index bbab26f9..b82e9dbe 100644 Binary files a/assets/images/mailer/canceled-header-bg.png and b/assets/images/mailer/canceled-header-bg.png differ diff --git a/assets/images/mailer/clock.png b/assets/images/mailer/clock.png new file mode 100644 index 00000000..7c977e1e Binary files /dev/null and b/assets/images/mailer/clock.png differ diff --git a/assets/images/mailer/email-footer-travis-logo.png b/assets/images/mailer/email-footer-travis-logo.png new file mode 100644 index 00000000..3c53a96f Binary files /dev/null and b/assets/images/mailer/email-footer-travis-logo.png differ diff --git a/assets/images/mailer/error.png b/assets/images/mailer/error.png new file mode 100755 index 00000000..036a9371 Binary files /dev/null and b/assets/images/mailer/error.png differ diff --git a/assets/images/mailer/errored-header-bg.png b/assets/images/mailer/errored-header-bg.png index bbab26f9..b82e9dbe 100644 Binary files a/assets/images/mailer/errored-header-bg.png and b/assets/images/mailer/errored-header-bg.png differ diff --git a/assets/images/mailer/failed-header-bg.png b/assets/images/mailer/failed-header-bg.png index 4f933cba..6eaa7f8d 100644 Binary files a/assets/images/mailer/failed-header-bg.png and b/assets/images/mailer/failed-header-bg.png differ diff --git a/assets/images/mailer/failed.png b/assets/images/mailer/failed.png new file mode 100755 index 00000000..95515446 Binary files /dev/null and b/assets/images/mailer/failed.png differ diff --git a/assets/images/mailer/footer-logo-38x38.png b/assets/images/mailer/footer-logo-38x38.png index 817417ec..d97b233a 100644 Binary files a/assets/images/mailer/footer-logo-38x38.png and b/assets/images/mailer/footer-logo-38x38.png differ diff --git a/assets/images/mailer/mascot-avatar-15px.png b/assets/images/mailer/mascot-avatar-15px.png new file mode 100644 index 00000000..f575d73c Binary files /dev/null and b/assets/images/mailer/mascot-avatar-15px.png differ diff --git a/assets/images/mailer/mascot-avatar-40px.png b/assets/images/mailer/mascot-avatar-40px.png new file mode 100644 index 00000000..1c0b1e8d Binary files /dev/null and b/assets/images/mailer/mascot-avatar-40px.png differ diff --git a/assets/images/mailer/passed-header-bg.png b/assets/images/mailer/passed-header-bg.png index 7823c81d..fcba8ab3 100644 Binary files a/assets/images/mailer/passed-header-bg.png and b/assets/images/mailer/passed-header-bg.png differ diff --git a/assets/images/mailer/please-donate.png b/assets/images/mailer/please-donate.png index 7f087c39..439694dd 100644 Binary files a/assets/images/mailer/please-donate.png and b/assets/images/mailer/please-donate.png differ diff --git a/assets/images/mailer/stopwatch-error.png b/assets/images/mailer/stopwatch-error.png new file mode 100755 index 00000000..33f0b12f Binary files /dev/null and b/assets/images/mailer/stopwatch-error.png differ diff --git a/assets/images/mailer/stopwatch-failed.png b/assets/images/mailer/stopwatch-failed.png new file mode 100755 index 00000000..725985c3 Binary files /dev/null and b/assets/images/mailer/stopwatch-failed.png differ diff --git a/assets/images/mailer/stopwatch-success.png b/assets/images/mailer/stopwatch-success.png new file mode 100755 index 00000000..0a67ed9c Binary files /dev/null and b/assets/images/mailer/stopwatch-success.png differ diff --git a/assets/images/mailer/success.png b/assets/images/mailer/success.png new file mode 100755 index 00000000..414856cf Binary files /dev/null and b/assets/images/mailer/success.png differ diff --git a/assets/images/mailer/travis-mascot.png b/assets/images/mailer/travis-mascot.png new file mode 100644 index 00000000..915b8865 Binary files /dev/null and b/assets/images/mailer/travis-mascot.png differ diff --git a/assets/images/rgsoc.jpg b/assets/images/rgsoc.jpg index fe383a95..80e72c06 100644 Binary files a/assets/images/rgsoc.jpg and b/assets/images/rgsoc.jpg differ diff --git a/assets/images/sponsors/8thlight-205x60.jpg b/assets/images/sponsors/8thlight-205x60.jpg index 4783a5c2..e51d68ad 100644 Binary files a/assets/images/sponsors/8thlight-205x60.jpg and b/assets/images/sponsors/8thlight-205x60.jpg differ diff --git a/assets/images/sponsors/agileanimal-205x60.png b/assets/images/sponsors/agileanimal-205x60.png index 1c53f966..aa4546af 100644 Binary files a/assets/images/sponsors/agileanimal-205x60.png and b/assets/images/sponsors/agileanimal-205x60.png differ diff --git a/assets/images/sponsors/atomicobject-205x60.png b/assets/images/sponsors/atomicobject-205x60.png index 62166365..c3021bc7 100644 Binary files a/assets/images/sponsors/atomicobject-205x60.png and b/assets/images/sponsors/atomicobject-205x60.png differ diff --git a/assets/images/sponsors/avarteq-140x40.png b/assets/images/sponsors/avarteq-140x40.png index e13e0143..a11cb466 100644 Binary files a/assets/images/sponsors/avarteq-140x40.png and b/assets/images/sponsors/avarteq-140x40.png differ diff --git a/assets/images/sponsors/basho-205x60.png b/assets/images/sponsors/basho-205x60.png index 8d95c094..eb814dbb 100644 Binary files a/assets/images/sponsors/basho-205x60.png and b/assets/images/sponsors/basho-205x60.png differ diff --git a/assets/images/sponsors/bendyworks-100x60.png b/assets/images/sponsors/bendyworks-100x60.png index 9c98b7e7..f38dedb5 100644 Binary files a/assets/images/sponsors/bendyworks-100x60.png and b/assets/images/sponsors/bendyworks-100x60.png differ diff --git a/assets/images/sponsors/bendyworks-205x130.png b/assets/images/sponsors/bendyworks-205x130.png index c8654cd0..e15e9f58 100644 Binary files a/assets/images/sponsors/bendyworks-205x130.png and b/assets/images/sponsors/bendyworks-205x130.png differ diff --git a/assets/images/sponsors/bendyworks-210x210.png b/assets/images/sponsors/bendyworks-210x210.png index 4ce673df..03d3bb52 100644 Binary files a/assets/images/sponsors/bendyworks-210x210.png and b/assets/images/sponsors/bendyworks-210x210.png differ diff --git a/assets/images/sponsors/bluebox-78x15.png b/assets/images/sponsors/bluebox-78x15.png index b3ae538c..15020a08 100644 Binary files a/assets/images/sponsors/bluebox-78x15.png and b/assets/images/sponsors/bluebox-78x15.png differ diff --git a/assets/images/sponsors/cloudcontrol-100x60.png b/assets/images/sponsors/cloudcontrol-100x60.png index ffb2e163..60fb4163 100644 Binary files a/assets/images/sponsors/cloudcontrol-100x60.png and b/assets/images/sponsors/cloudcontrol-100x60.png differ diff --git a/assets/images/sponsors/cloudcontrol-205x130.png b/assets/images/sponsors/cloudcontrol-205x130.png index 9f790453..6f73d9f8 100644 Binary files a/assets/images/sponsors/cloudcontrol-205x130.png and b/assets/images/sponsors/cloudcontrol-205x130.png differ diff --git a/assets/images/sponsors/cloudcontrol-210x210.png b/assets/images/sponsors/cloudcontrol-210x210.png index 11748a85..700ea8a7 100644 Binary files a/assets/images/sponsors/cloudcontrol-210x210.png and b/assets/images/sponsors/cloudcontrol-210x210.png differ diff --git a/assets/images/sponsors/cobotme-205x60.png b/assets/images/sponsors/cobotme-205x60.png index 775485cd..2e26f43c 100644 Binary files a/assets/images/sponsors/cobotme-205x60.png and b/assets/images/sponsors/cobotme-205x60.png differ diff --git a/assets/images/sponsors/codeminer-205x60.png b/assets/images/sponsors/codeminer-205x60.png index f4b2caec..def37456 100644 Binary files a/assets/images/sponsors/codeminer-205x60.png and b/assets/images/sponsors/codeminer-205x60.png differ diff --git a/assets/images/sponsors/crowdinteractive-205x60.png b/assets/images/sponsors/crowdinteractive-205x60.png index d4399390..6987bac9 100644 Binary files a/assets/images/sponsors/crowdinteractive-205x60.png and b/assets/images/sponsors/crowdinteractive-205x60.png differ diff --git a/assets/images/sponsors/engineyard-140x40.png b/assets/images/sponsors/engineyard-140x40.png index 46fec6bb..cac0287c 100644 Binary files a/assets/images/sponsors/engineyard-140x40.png and b/assets/images/sponsors/engineyard-140x40.png differ diff --git a/assets/images/sponsors/enterprise-rails-140x40.png b/assets/images/sponsors/enterprise-rails-140x40.png index 3f992209..ed2d012e 100644 Binary files a/assets/images/sponsors/enterprise-rails-140x40.png and b/assets/images/sponsors/enterprise-rails-140x40.png differ diff --git a/assets/images/sponsors/esm-205x60.png b/assets/images/sponsors/esm-205x60.png index d6c93c94..7b1c8be6 100644 Binary files a/assets/images/sponsors/esm-205x60.png and b/assets/images/sponsors/esm-205x60.png differ diff --git a/assets/images/sponsors/evilmartians-205x60.png b/assets/images/sponsors/evilmartians-205x60.png index 0322fa27..c47e6a5d 100644 Binary files a/assets/images/sponsors/evilmartians-205x60.png and b/assets/images/sponsors/evilmartians-205x60.png differ diff --git a/assets/images/sponsors/fingertips-205x60.png b/assets/images/sponsors/fingertips-205x60.png index f30e8164..3d9fe39f 100644 Binary files a/assets/images/sponsors/fingertips-205x60.png and b/assets/images/sponsors/fingertips-205x60.png differ diff --git a/assets/images/sponsors/gidsy.png b/assets/images/sponsors/gidsy.png index a1f7ca1b..7b29864d 100644 Binary files a/assets/images/sponsors/gidsy.png and b/assets/images/sponsors/gidsy.png differ diff --git a/assets/images/sponsors/github.png b/assets/images/sponsors/github.png index 224c946f..40d39bf7 100644 Binary files a/assets/images/sponsors/github.png and b/assets/images/sponsors/github.png differ diff --git a/assets/images/sponsors/heroku-100x60.png b/assets/images/sponsors/heroku-100x60.png index a79e3a8d..4030d161 100644 Binary files a/assets/images/sponsors/heroku-100x60.png and b/assets/images/sponsors/heroku-100x60.png differ diff --git a/assets/images/sponsors/heroku-140x40.png b/assets/images/sponsors/heroku-140x40.png index e104cd98..9ad13de3 100644 Binary files a/assets/images/sponsors/heroku-140x40.png and b/assets/images/sponsors/heroku-140x40.png differ diff --git a/assets/images/sponsors/heroku-205x130.png b/assets/images/sponsors/heroku-205x130.png index f540df48..2cf3884b 100644 Binary files a/assets/images/sponsors/heroku-205x130.png and b/assets/images/sponsors/heroku-205x130.png differ diff --git a/assets/images/sponsors/heroku-205x60.png b/assets/images/sponsors/heroku-205x60.png index b3e08851..d69886bb 100644 Binary files a/assets/images/sponsors/heroku-205x60.png and b/assets/images/sponsors/heroku-205x60.png differ diff --git a/assets/images/sponsors/iriscouch-140x40.png b/assets/images/sponsors/iriscouch-140x40.png index 345807ce..a9939779 100644 Binary files a/assets/images/sponsors/iriscouch-140x40.png and b/assets/images/sponsors/iriscouch-140x40.png differ diff --git a/assets/images/sponsors/jumpstartlab-140x40.png b/assets/images/sponsors/jumpstartlab-140x40.png index cc097f2b..5beaac27 100644 Binary files a/assets/images/sponsors/jumpstartlab-140x40.png and b/assets/images/sponsors/jumpstartlab-140x40.png differ diff --git a/assets/images/sponsors/jumpstartlab-205x60.png b/assets/images/sponsors/jumpstartlab-205x60.png index 463887ec..2ef8cda7 100644 Binary files a/assets/images/sponsors/jumpstartlab-205x60.png and b/assets/images/sponsors/jumpstartlab-205x60.png differ diff --git a/assets/images/sponsors/kanbanery-100x60.png b/assets/images/sponsors/kanbanery-100x60.png index 8e986406..bb6e6d52 100644 Binary files a/assets/images/sponsors/kanbanery-100x60.png and b/assets/images/sponsors/kanbanery-100x60.png differ diff --git a/assets/images/sponsors/kanbanery-205x130.png b/assets/images/sponsors/kanbanery-205x130.png index 186d11d0..9763a4a0 100644 Binary files a/assets/images/sponsors/kanbanery-205x130.png and b/assets/images/sponsors/kanbanery-205x130.png differ diff --git a/assets/images/sponsors/kanbanery-205x60.png b/assets/images/sponsors/kanbanery-205x60.png index 5a402e39..75a575d3 100644 Binary files a/assets/images/sponsors/kanbanery-205x60.png and b/assets/images/sponsors/kanbanery-205x60.png differ diff --git a/assets/images/sponsors/librato-metrics-140x40.png b/assets/images/sponsors/librato-metrics-140x40.png index 19be7adb..9d1a9cfd 100644 Binary files a/assets/images/sponsors/librato-metrics-140x40.png and b/assets/images/sponsors/librato-metrics-140x40.png differ diff --git a/assets/images/sponsors/medidata-205x60.png b/assets/images/sponsors/medidata-205x60.png index 19e0db9f..59b5a4f3 100644 Binary files a/assets/images/sponsors/medidata-205x60.png and b/assets/images/sponsors/medidata-205x60.png differ diff --git a/assets/images/sponsors/meltmedia-205x60.png b/assets/images/sponsors/meltmedia-205x60.png index c7d65875..6aa879e9 100644 Binary files a/assets/images/sponsors/meltmedia-205x60.png and b/assets/images/sponsors/meltmedia-205x60.png differ diff --git a/assets/images/sponsors/mindmatters-205x60.png b/assets/images/sponsors/mindmatters-205x60.png index 9edd7b9e..f6a9f0a4 100644 Binary files a/assets/images/sponsors/mindmatters-205x60.png and b/assets/images/sponsors/mindmatters-205x60.png differ diff --git a/assets/images/sponsors/mongohq-100x60.png b/assets/images/sponsors/mongohq-100x60.png index 1f50505d..87bf53f9 100644 Binary files a/assets/images/sponsors/mongohq-100x60.png and b/assets/images/sponsors/mongohq-100x60.png differ diff --git a/assets/images/sponsors/mongohq-205x130.png b/assets/images/sponsors/mongohq-205x130.png index 6b431f17..ddf604e4 100644 Binary files a/assets/images/sponsors/mongohq-205x130.png and b/assets/images/sponsors/mongohq-205x130.png differ diff --git a/assets/images/sponsors/mongohq-205x60.png b/assets/images/sponsors/mongohq-205x60.png index dfe04e5c..cd4341be 100644 Binary files a/assets/images/sponsors/mongohq-205x60.png and b/assets/images/sponsors/mongohq-205x60.png differ diff --git a/assets/images/sponsors/nedap-100x60.png b/assets/images/sponsors/nedap-100x60.png index 6aa9a9ee..9dc6e5ef 100644 Binary files a/assets/images/sponsors/nedap-100x60.png and b/assets/images/sponsors/nedap-100x60.png differ diff --git a/assets/images/sponsors/nedap-205x130.png b/assets/images/sponsors/nedap-205x130.png index 69294bf4..98f08c0d 100644 Binary files a/assets/images/sponsors/nedap-205x130.png and b/assets/images/sponsors/nedap-205x130.png differ diff --git a/assets/images/sponsors/nedap-205x60.png b/assets/images/sponsors/nedap-205x60.png index a9e2895c..bd4ef160 100644 Binary files a/assets/images/sponsors/nedap-205x60.png and b/assets/images/sponsors/nedap-205x60.png differ diff --git a/assets/images/sponsors/nedap-430x130.png b/assets/images/sponsors/nedap-430x130.png index 4423d1b3..b09f97c2 100644 Binary files a/assets/images/sponsors/nedap-430x130.png and b/assets/images/sponsors/nedap-430x130.png differ diff --git a/assets/images/sponsors/papertrail-140x40.png b/assets/images/sponsors/papertrail-140x40.png index 4a55288c..db9c2167 100644 Binary files a/assets/images/sponsors/papertrail-140x40.png and b/assets/images/sponsors/papertrail-140x40.png differ diff --git a/assets/images/sponsors/planio-100x60.png b/assets/images/sponsors/planio-100x60.png index 8438d775..481bc4f2 100644 Binary files a/assets/images/sponsors/planio-100x60.png and b/assets/images/sponsors/planio-100x60.png differ diff --git a/assets/images/sponsors/planio-205x130.png b/assets/images/sponsors/planio-205x130.png index d858e26f..90520389 100644 Binary files a/assets/images/sponsors/planio-205x130.png and b/assets/images/sponsors/planio-205x130.png differ diff --git a/assets/images/sponsors/planio-205x60.png b/assets/images/sponsors/planio-205x60.png index c70b7faf..c1de4de0 100644 Binary files a/assets/images/sponsors/planio-205x60.png and b/assets/images/sponsors/planio-205x60.png differ diff --git a/assets/images/sponsors/postmark-140x40.png b/assets/images/sponsors/postmark-140x40.png index cbfd4acf..eb40ce5a 100644 Binary files a/assets/images/sponsors/postmark-140x40.png and b/assets/images/sponsors/postmark-140x40.png differ diff --git a/assets/images/sponsors/pusher-140x40.png b/assets/images/sponsors/pusher-140x40.png index a3869727..e566c9b2 100644 Binary files a/assets/images/sponsors/pusher-140x40.png and b/assets/images/sponsors/pusher-140x40.png differ diff --git a/assets/images/sponsors/railslove-140x40.png b/assets/images/sponsors/railslove-140x40.png index dc6bd9dc..cb49bfdd 100644 Binary files a/assets/images/sponsors/railslove-140x40.png and b/assets/images/sponsors/railslove-140x40.png differ diff --git a/assets/images/sponsors/railslove.png b/assets/images/sponsors/railslove.png index 4c3da1ac..e936484e 100644 Binary files a/assets/images/sponsors/railslove.png and b/assets/images/sponsors/railslove.png differ diff --git a/assets/images/sponsors/servergrove-140x40.png b/assets/images/sponsors/servergrove-140x40.png index 05360ea4..e135ef37 100644 Binary files a/assets/images/sponsors/servergrove-140x40.png and b/assets/images/sponsors/servergrove-140x40.png differ diff --git a/assets/images/sponsors/shopify-140x40.png b/assets/images/sponsors/shopify-140x40.png index 70d37581..5ea05850 100644 Binary files a/assets/images/sponsors/shopify-140x40.png and b/assets/images/sponsors/shopify-140x40.png differ diff --git a/assets/images/sponsors/shopify.png b/assets/images/sponsors/shopify.png index e513522e..7f3da5ea 100644 Binary files a/assets/images/sponsors/shopify.png and b/assets/images/sponsors/shopify.png differ diff --git a/assets/images/sponsors/site5-205x60.png b/assets/images/sponsors/site5-205x60.png index 1e03b3b0..c6d4b9fa 100644 Binary files a/assets/images/sponsors/site5-205x60.png and b/assets/images/sponsors/site5-205x60.png differ diff --git a/assets/images/sponsors/soundcloud-100x60.png b/assets/images/sponsors/soundcloud-100x60.png index 7bce4957..9e474822 100644 Binary files a/assets/images/sponsors/soundcloud-100x60.png and b/assets/images/sponsors/soundcloud-100x60.png differ diff --git a/assets/images/sponsors/soundcloud-205x130.png b/assets/images/sponsors/soundcloud-205x130.png index eda0ebee..13699592 100644 Binary files a/assets/images/sponsors/soundcloud-205x130.png and b/assets/images/sponsors/soundcloud-205x130.png differ diff --git a/assets/images/sponsors/soundcloud-205x60.png b/assets/images/sponsors/soundcloud-205x60.png index 5aa886ad..f940a56b 100644 Binary files a/assets/images/sponsors/soundcloud-205x60.png and b/assets/images/sponsors/soundcloud-205x60.png differ diff --git a/assets/images/sponsors/soundcloud.png b/assets/images/sponsors/soundcloud.png index c00faba7..34dbd9fd 100644 Binary files a/assets/images/sponsors/soundcloud.png and b/assets/images/sponsors/soundcloud.png differ diff --git a/assets/images/sponsors/stickermule-140x40.png b/assets/images/sponsors/stickermule-140x40.png index 5fb9498c..52ae2df1 100644 Binary files a/assets/images/sponsors/stickermule-140x40.png and b/assets/images/sponsors/stickermule-140x40.png differ diff --git a/assets/images/sponsors/stripe-140x40.png b/assets/images/sponsors/stripe-140x40.png index d440f9ce..4ec41a20 100644 Binary files a/assets/images/sponsors/stripe-140x40.png and b/assets/images/sponsors/stripe-140x40.png differ diff --git a/assets/images/sponsors/stripe-205x60.png b/assets/images/sponsors/stripe-205x60.png index f284cb94..2abc8448 100644 Binary files a/assets/images/sponsors/stripe-205x60.png and b/assets/images/sponsors/stripe-205x60.png differ diff --git a/assets/images/sponsors/stripe-stamp-111x103.png b/assets/images/sponsors/stripe-stamp-111x103.png index ce37710e..661049d1 100644 Binary files a/assets/images/sponsors/stripe-stamp-111x103.png and b/assets/images/sponsors/stripe-stamp-111x103.png differ diff --git a/assets/images/sponsors/thinkrelevance-205x60.png b/assets/images/sponsors/thinkrelevance-205x60.png index 45b3f912..165396c3 100644 Binary files a/assets/images/sponsors/thinkrelevance-205x60.png and b/assets/images/sponsors/thinkrelevance-205x60.png differ diff --git a/assets/images/sponsors/thoughtbot-140x40.png b/assets/images/sponsors/thoughtbot-140x40.png index b12cd310..54148f79 100644 Binary files a/assets/images/sponsors/thoughtbot-140x40.png and b/assets/images/sponsors/thoughtbot-140x40.png differ diff --git a/assets/images/sponsors/ticketevolution-100x60.png b/assets/images/sponsors/ticketevolution-100x60.png index f0b4bfbf..0371385b 100644 Binary files a/assets/images/sponsors/ticketevolution-100x60.png and b/assets/images/sponsors/ticketevolution-100x60.png differ diff --git a/assets/images/sponsors/ticketevolution-205x130.jpg b/assets/images/sponsors/ticketevolution-205x130.jpg index c1f233a0..767f96ca 100644 Binary files a/assets/images/sponsors/ticketevolution-205x130.jpg and b/assets/images/sponsors/ticketevolution-205x130.jpg differ diff --git a/assets/images/sponsors/ticketevolution-205x60.jpg b/assets/images/sponsors/ticketevolution-205x60.jpg index 058d45f4..3ee7ccb5 100644 Binary files a/assets/images/sponsors/ticketevolution-205x60.jpg and b/assets/images/sponsors/ticketevolution-205x60.jpg differ diff --git a/assets/images/sponsors/tupalo-205x60.png b/assets/images/sponsors/tupalo-205x60.png index 2bbb0c4d..4e6b04a8 100644 Binary files a/assets/images/sponsors/tupalo-205x60.png and b/assets/images/sponsors/tupalo-205x60.png differ diff --git a/assets/images/sponsors/twitter-205x60.png b/assets/images/sponsors/twitter-205x60.png index 1b886678..9249a59e 100644 Binary files a/assets/images/sponsors/twitter-205x60.png and b/assets/images/sponsors/twitter-205x60.png differ diff --git a/assets/images/sponsors/wooga-100x60.png b/assets/images/sponsors/wooga-100x60.png index e511d203..c5f48f87 100644 Binary files a/assets/images/sponsors/wooga-100x60.png and b/assets/images/sponsors/wooga-100x60.png differ diff --git a/assets/images/sponsors/wooga-205x130.png b/assets/images/sponsors/wooga-205x130.png index 3c0f9138..5b4fe43c 100644 Binary files a/assets/images/sponsors/wooga-205x130.png and b/assets/images/sponsors/wooga-205x130.png differ diff --git a/assets/images/sponsors/wooga-210x210.png b/assets/images/sponsors/wooga-210x210.png index ccf039f1..5019959e 100644 Binary files a/assets/images/sponsors/wooga-210x210.png and b/assets/images/sponsors/wooga-210x210.png differ diff --git a/assets/images/sponsors/xing-100x60.png b/assets/images/sponsors/xing-100x60.png index d85ee60f..bdc56101 100644 Binary files a/assets/images/sponsors/xing-100x60.png and b/assets/images/sponsors/xing-100x60.png differ diff --git a/assets/images/sponsors/xing-205x130.png b/assets/images/sponsors/xing-205x130.png index 605aa9d8..6ea14bc0 100644 Binary files a/assets/images/sponsors/xing-205x130.png and b/assets/images/sponsors/xing-205x130.png differ diff --git a/assets/images/sponsors/xing-210x210.png b/assets/images/sponsors/xing-210x210.png index edde528c..ed52b9fc 100644 Binary files a/assets/images/sponsors/xing-210x210.png and b/assets/images/sponsors/xing-210x210.png differ diff --git a/assets/images/sponsors/zendesk-205x60.png b/assets/images/sponsors/zendesk-205x60.png index f08c70bd..8044b7b5 100644 Binary files a/assets/images/sponsors/zendesk-205x60.png and b/assets/images/sponsors/zendesk-205x60.png differ diff --git a/assets/images/sponsors/zweitag-100x60.png b/assets/images/sponsors/zweitag-100x60.png index 1a5a9e92..d441e798 100644 Binary files a/assets/images/sponsors/zweitag-100x60.png and b/assets/images/sponsors/zweitag-100x60.png differ diff --git a/assets/images/sponsors/zweitag-205x130.png b/assets/images/sponsors/zweitag-205x130.png index efcb3f47..1ad343cf 100644 Binary files a/assets/images/sponsors/zweitag-205x130.png and b/assets/images/sponsors/zweitag-205x130.png differ diff --git a/assets/images/sponsors/zweitag-205x60.png b/assets/images/sponsors/zweitag-205x60.png index c2a01f20..62d2fec1 100644 Binary files a/assets/images/sponsors/zweitag-205x60.png and b/assets/images/sponsors/zweitag-205x60.png differ diff --git a/assets/images/travis-mascot-150.png b/assets/images/travis-mascot-150.png new file mode 100644 index 00000000..c7995568 Binary files /dev/null and b/assets/images/travis-mascot-150.png differ diff --git a/assets/images/ui/activate.png b/assets/images/ui/activate.png index 6219207d..29b4cded 100644 Binary files a/assets/images/ui/activate.png and b/assets/images/ui/activate.png differ diff --git a/assets/images/ui/activated.png b/assets/images/ui/activated.png index 53a808f0..450c6099 100644 Binary files a/assets/images/ui/activated.png and b/assets/images/ui/activated.png differ diff --git a/assets/images/ui/cal.png b/assets/images/ui/cal.png new file mode 100644 index 00000000..ed89d223 Binary files /dev/null and b/assets/images/ui/cal.png differ diff --git a/assets/images/ui/clock.png b/assets/images/ui/clock.png index 70996793..03b66950 100644 Binary files a/assets/images/ui/clock.png and b/assets/images/ui/clock.png differ diff --git a/assets/images/ui/close-white.png b/assets/images/ui/close-white.png new file mode 100644 index 00000000..1348122d Binary files /dev/null and b/assets/images/ui/close-white.png differ diff --git a/assets/images/ui/close.png b/assets/images/ui/close.png index 40d1f626..c8a66316 100644 Binary files a/assets/images/ui/close.png and b/assets/images/ui/close.png differ diff --git a/assets/images/ui/github-admin.png b/assets/images/ui/github-admin.png index 3e3ae4f3..96fe3fa5 100644 Binary files a/assets/images/ui/github-admin.png and b/assets/images/ui/github-admin.png differ diff --git a/assets/images/ui/github-forks.png b/assets/images/ui/github-forks.png index 147787bb..3e235ff0 100644 Binary files a/assets/images/ui/github-forks.png and b/assets/images/ui/github-forks.png differ diff --git a/assets/images/ui/github-watchers.png b/assets/images/ui/github-watchers.png index 0e4c5ec1..871e200d 100644 Binary files a/assets/images/ui/github-watchers.png and b/assets/images/ui/github-watchers.png differ diff --git a/assets/images/ui/help.png b/assets/images/ui/help.png index cff36cc0..b0c5be51 100644 Binary files a/assets/images/ui/help.png and b/assets/images/ui/help.png differ diff --git a/assets/images/ui/info.png b/assets/images/ui/info.png index 8ff77223..ce3c9cc5 100644 Binary files a/assets/images/ui/info.png and b/assets/images/ui/info.png differ diff --git a/assets/images/ui/log.fold.closed.2.png b/assets/images/ui/log.fold.closed.2.png index fe9ae810..51509978 100644 Binary files a/assets/images/ui/log.fold.closed.2.png and b/assets/images/ui/log.fold.closed.2.png differ diff --git a/assets/images/ui/log.fold.closed.3.png b/assets/images/ui/log.fold.closed.3.png index 304b09e7..07c0a351 100644 Binary files a/assets/images/ui/log.fold.closed.3.png and b/assets/images/ui/log.fold.closed.3.png differ diff --git a/assets/images/ui/log.fold.closed.png b/assets/images/ui/log.fold.closed.png index 66b51eec..2f8190a5 100644 Binary files a/assets/images/ui/log.fold.closed.png and b/assets/images/ui/log.fold.closed.png differ diff --git a/assets/images/ui/log.fold.open.2.png b/assets/images/ui/log.fold.open.2.png index f2bc248e..42916db5 100644 Binary files a/assets/images/ui/log.fold.open.2.png and b/assets/images/ui/log.fold.open.2.png differ diff --git a/assets/images/ui/log.fold.open.png b/assets/images/ui/log.fold.open.png index 2f0a4f71..45542ba8 100644 Binary files a/assets/images/ui/log.fold.open.png and b/assets/images/ui/log.fold.open.png differ diff --git a/assets/images/ui/logo.png b/assets/images/ui/logo.png index e2492550..1e8a48e7 100644 Binary files a/assets/images/ui/logo.png and b/assets/images/ui/logo.png differ diff --git a/assets/images/ui/org.png b/assets/images/ui/org.png index 88345c39..ea47fc65 100644 Binary files a/assets/images/ui/org.png and b/assets/images/ui/org.png differ diff --git a/assets/images/ui/search.png b/assets/images/ui/search.png index 09362097..d8d0278a 100644 Binary files a/assets/images/ui/search.png and b/assets/images/ui/search.png differ diff --git a/assets/images/ui/slider-closed.png b/assets/images/ui/slider-closed.png index e08a3214..4f2ca670 100644 Binary files a/assets/images/ui/slider-closed.png and b/assets/images/ui/slider-closed.png differ diff --git a/assets/images/ui/slider-open.png b/assets/images/ui/slider-open.png index f3d33b97..6a037b7b 100644 Binary files a/assets/images/ui/slider-open.png and b/assets/images/ui/slider-open.png differ diff --git a/assets/images/ui/to-top.png b/assets/images/ui/to-top.png index 3f03161c..25e63fff 100644 Binary files a/assets/images/ui/to-top.png and b/assets/images/ui/to-top.png differ diff --git a/assets/images/ui/tools-button.png b/assets/images/ui/tools-button.png index bceffe9f..4358df83 100644 Binary files a/assets/images/ui/tools-button.png and b/assets/images/ui/tools-button.png differ diff --git a/assets/images/ui/user.png b/assets/images/ui/user.png index f8842fd3..0250506a 100644 Binary files a/assets/images/ui/user.png and b/assets/images/ui/user.png differ diff --git a/assets/images/ui/workers-close.png b/assets/images/ui/workers-close.png index 664066c2..25e63fff 100644 Binary files a/assets/images/ui/workers-close.png and b/assets/images/ui/workers-close.png differ diff --git a/assets/images/ui/workers-open.png b/assets/images/ui/workers-open.png index d6704e1c..4723ec48 100644 Binary files a/assets/images/ui/workers-open.png and b/assets/images/ui/workers-open.png differ diff --git a/assets/scripts/app/app.coffee b/assets/scripts/app/app.coffee index 36dcd0c1..4e16be6f 100644 --- a/assets/scripts/app/app.coffee +++ b/assets/scripts/app/app.coffee @@ -1,13 +1,35 @@ unless window.TravisApplication window.TravisApplication = Em.Application.extend(Ember.Evented, - LOG_TRANSITIONS: true - authStateBinding: 'auth.state' + LOG_TRANSITIONS: true, + authState: Ember.computed.alias('auth.state') signedIn: (-> @get('authState') == 'signed-in' ).property('authState') + mappings: (-> + broadcasts: Travis.Broadcast + repositories: Travis.Repo + repository: Travis.Repo + repos: Travis.Repo + repo: Travis.Repo + builds: Travis.Build + build: Travis.Build + commits: Travis.Commit + commit: Travis.Commit + jobs: Travis.Job + job: Travis.Job + account: Travis.Account + accounts: Travis.Account + worker: Travis.Worker + workers: Travis.Worker + ).property() + + modelClasses: (-> + [Travis.User, Travis.Build, Travis.Job, Travis.Repo, Travis.Commit, Travis.Worker, Travis.Account, Travis.Broadcast, Travis.Hook] + ).property() + setup: -> - @store = Travis.Store.create( - adapter: Travis.RestAdapter.create() - ) + @get('modelClasses').forEach (klass) -> + klass.adapter = Travis.Adapter.create() + klass.url = "/#{klass.pluralName()}" @slider = new Travis.Slider() @pusher = new Travis.Pusher(Travis.config.pusher_key) if Travis.config.pusher_key @@ -16,12 +38,17 @@ unless window.TravisApplication @set('auth', Travis.Auth.create(app: this, endpoint: Travis.config.api_endpoint)) reset: -> - @_super.apply(this, arguments); + @_super.apply(this, arguments) + @get('modelClasses').forEach (klass) -> + klass.resetData() @setup() lookup: -> @__container__.lookup.apply @__container__, arguments + flash: (options) -> + Travis.lookup('controller:flash').loadFlashes([options]) + storeAfterSignInPath: (path) -> @get('auth').storeAfterSignInPath(path) @@ -34,8 +61,79 @@ unless window.TravisApplication signOut: -> @get('auth').signOut() - receive: -> - @store.receive.apply(@store, arguments) + signingIn: (-> + Travis.get('authState') == 'signing-in' + ).property('authState') + + receive: (event, data) -> + [name, type] = event.split(':') + + type = Ember.get(Travis, 'mappings')[name] + + if event == 'build:started' && data.build.commit + # TODO: commit should be a sideload record on build, not mixed with it + build = data.build + commit = { + id: build.commit_id + author_email: build.author_email + author_name: build.author_name + branch: build.branch + committed_at: build.committed_at + committer_email: build.committer_email + committer_name: build.committer_name + compare_url: build.compare_url + message: build.message + sha: build.commit + } + delete(data.build.commit) + @loadOrMerge(Travis.Commit, commit) + + + if event == 'job:log' + console.log 'store: received job:log event', data if Log.DEBUG + data = data.job + job = Travis.Job.find(data.id) + job.appendLog(number: parseInt(data.number), content: data._log, final: data.final) + else if data[type.singularName()] + @_loadOne(this, type, data) + else if data[type.pluralName()] + @_loadMany(this, type, data) + else + throw "can't load data for #{name}" unless type + + _loadOne: (store, type, json) -> + root = type.singularName() + reference = @loadOrMerge(type, json[root]) + unless reference.record + type.loadRecordForReference(reference) + + # we get other types of records only in a few situations and + # it's not always needed to update data, so I'm specyfing which + # things I want to update here: + if type == Travis.Build && (json.repository || json.repo) + data = json.repository || json.repo + reference = @loadOrMerge(Travis.Repo, data) + unless reference.record + Travis.Repo.loadRecordForReference(reference) + + loadOrMerge: (type, hash, options) -> + options ||= {} + + reference = type._getReferenceById(hash.id) + + if reference && options.skipIfExists + return + + reference = type._getOrCreateReferenceForId(hash.id) + if reference.record + reference.record.merge(hash) + else + if type.sideloadedData && type.sideloadedData[hash.id] + Ember.merge(type.sideloadedData[hash.id], hash) + else + type.load([hash]) + + reference toggleSidebar: -> $('body').toggleClass('maximized') diff --git a/assets/scripts/app/auth.coffee b/assets/scripts/app/auth.coffee index ea40fe3b..565a18c1 100644 --- a/assets/scripts/app/auth.coffee +++ b/assets/scripts/app/auth.coffee @@ -14,12 +14,14 @@ @set('state', 'signed-out') @set('user', undefined) if user = Travis.__container__.lookup('controller:currentUser').get('content') - if user.get('stateManager.currentPath') == 'rootState.loaded.updated.uncommitted' - user.send('rollback') - user.unloadRecord() + user.unload() Travis.__container__.lookup('controller:currentUser').set('content', null) if router = Travis.__container__.lookup('router:main') - router.send('afterSignOut') + try + router.send('afterSignOut') + catch e + throw e unless e.message =~ /There are no active handlers/ + signIn: (data) -> if data @@ -68,17 +70,16 @@ Travis.setLocale(data.user.locale || Travis.default_locale) Travis.trigger('user:signed_in', data.user) if router = Travis.__container__.lookup('router:main') - path = @readAfterSignInPath() Ember.run.next => try - router.send('afterSignIn', path) + router.send('afterSignIn') catch e throw e unless e =~ /There are no active handlers/ @refreshUserData(data.user) refreshUserData: (user) -> Travis.ajax.get "/users/#{user.id}", (data) => - Travis.store.loadIncomplete(Travis.User, data.user) + Travis.loadOrMerge(Travis.User, data.user) # if user is still signed in, update saved data if @signedIn() data.user.token = user.token @@ -93,20 +94,11 @@ storage.setItem('travis.user', JSON.stringify(data.user)) loadUser: (user) -> - store = @app.store - store.load(Travis.User, user.id, user) - user = store.find(Travis.User, user.id) + Travis.loadOrMerge(Travis.User, user) + user = Travis.User.find(user.id) user.get('permissions') user - storeAfterSignInPath: (path) -> - Travis.sessionStorage.setItem('travis.after_signin_path', path) - - readAfterSignInPath: -> - path = Travis.sessionStorage.getItem('travis.after_signin_path') - Travis.sessionStorage.removeItem('travis.after_signin_path') - path - receiveMessage: (event) -> if event.origin == @expectedOrigin() if event.data == 'redirect' diff --git a/assets/scripts/app/components.coffee b/assets/scripts/app/components.coffee new file mode 100644 index 00000000..da423edd --- /dev/null +++ b/assets/scripts/app/components.coffee @@ -0,0 +1,9 @@ +Travis.TravisSwitchComponent = Ember.Component.extend + tagName: 'a' + classNames: ['travis-switch'] + classNameBindings: ['active'] + + activeBinding: 'target.active' + + click: -> + @sendAction('action', @get('target')) diff --git a/assets/scripts/app/controllers.coffee b/assets/scripts/app/controllers.coffee index 475e997d..f1b4f520 100644 --- a/assets/scripts/app/controllers.coffee +++ b/assets/scripts/app/controllers.coffee @@ -1,11 +1,30 @@ require 'helpers' -require 'travis/ticker' Travis.Controller = Em.Controller.extend() Travis.TopController = Em.Controller.extend needs: ['currentUser'] userBinding: 'controllers.currentUser' + userName: (-> + @get('user.name') || @get('user.login') + ).property('user.login', 'user.name') + + gravatarUrl: (-> + "#{location.protocol}//www.gravatar.com/avatar/#{@get('user.gravatarId')}?s=48&d=mm" + ).property('user.gravatarId') + + signedIn: (-> + Travis.get('authState') == 'signed-in' + ).property('Travis.authState') + + signedOut: (-> + Travis.get('authState') == 'signed-out' + ).property('Travis.authState') + + signingIn: (-> + Travis.get('authState') == 'signing-in' + ).property('Travis.authState') + Travis.ApplicationController = Em.Controller.extend templateName: 'layouts/home' @@ -20,9 +39,15 @@ Travis.ProfileLayoutController = Em.Controller.extend() Travis.AuthLayoutController = Em.Controller.extend() Travis.AccountProfileController = Em.Controller.extend - needs: ['currentUser'] + needs: ['currentUser', 'repos'] userBinding: 'controllers.currentUser' +Travis.FirstSyncController = Em.Controller.extend + needs: ['currentUser'] + user: Ember.computed.alias('controllers.currentUser') + + isSyncing: Ember.computed.alias('user.isSyncing') + require 'controllers/accounts' require 'controllers/build' require 'controllers/builds' @@ -32,8 +57,6 @@ require 'controllers/job' require 'controllers/profile' require 'controllers/repos' require 'controllers/repo' -require 'controllers/running_jobs' -require 'controllers/sidebar' require 'controllers/stats' require 'controllers/current_user' require 'controllers/account_index' diff --git a/assets/scripts/spec/unit/log_request_spec.coffee b/assets/scripts/app/controllers/annotations.coffee similarity index 100% rename from assets/scripts/spec/unit/log_request_spec.coffee rename to assets/scripts/app/controllers/annotations.coffee diff --git a/assets/scripts/app/controllers/build.coffee b/assets/scripts/app/controllers/build.coffee index 493b710a..6e61ab6f 100644 --- a/assets/scripts/app/controllers/build.coffee +++ b/assets/scripts/app/controllers/build.coffee @@ -2,7 +2,8 @@ Travis.BuildController = Ember.Controller.extend needs: ['repo'] repoBinding: 'controllers.repo.repo' commitBinding: 'build.commit' - lineNumberBinding: 'controllers.repo.lineNumber' + currentUserBinding: 'controllers.repo.currentUser' + tabBinding: 'controllers.repo.tab' currentItemBinding: 'build' @@ -13,11 +14,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/builds.coffee b/assets/scripts/app/controllers/builds.coffee index 8bfb61da..76234a22 100644 --- a/assets/scripts/app/controllers/builds.coffee +++ b/assets/scripts/app/controllers/builds.coffee @@ -7,6 +7,7 @@ Travis.BuildsController = Em.ArrayController.extend repoBinding: 'controllers.repo.repo' tabBinding: 'controllers.repo.tab' isLoadedBinding: 'content.isLoaded' + isLoadingBinding: 'content.isLoading' showMore: -> id = @get('repo.id') diff --git a/assets/scripts/app/controllers/current_user.coffee b/assets/scripts/app/controllers/current_user.coffee index 548e06a5..c8226503 100644 --- a/assets/scripts/app/controllers/current_user.coffee +++ b/assets/scripts/app/controllers/current_user.coffee @@ -1,3 +1,17 @@ +delegate = (name, options) -> + options ||= options + -> + target = @get(options.to) + target[name].apply(target, arguments) + Travis.CurrentUserController = Em.ObjectController.extend sync: -> @get('content').sync() + + syncingDidChange: (-> + if (user = @get('content')) && user.get('isSyncing') && !user.get('syncedAt') + Ember.run.scheduleOnce 'routerTransitions', this, -> + @container.lookup('router:main').send('renderFirstSync') + ).observes('isSyncing', 'content') + + updateLocale: delegate('updateLocale', to: 'content') diff --git a/assets/scripts/app/controllers/flash.coffee b/assets/scripts/app/controllers/flash.coffee index 67ad6ae5..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: (-> @@ -17,8 +15,8 @@ Travis.FlashController = Ember.ArrayController.extend ).property('broadcasts.isLoaded', 'broadcasts.length') broadcasts: (-> - if @get('currentUser') then Travis.Broadcast.find() else Ember.A() - ).property('currentUser') + if @get('currentUser.id') then Travis.Broadcast.find() else Ember.A() + ).property('currentUser.id') loadFlashes: (msgs) -> for msg in msgs diff --git a/assets/scripts/app/controllers/job.coffee b/assets/scripts/app/controllers/job.coffee index 262081fd..193aab42 100644 --- a/assets/scripts/app/controllers/job.coffee +++ b/assets/scripts/app/controllers/job.coffee @@ -4,19 +4,12 @@ Travis.JobController = Em.Controller.extend jobBinding: 'controllers.repo.job' repoBinding: 'controllers.repo.repo' commitBinding: 'job.commit' - lineNumberBinding: 'controllers.repo.lineNumber' annotationsBinding: 'job.annotations' + currentUserBinding: 'controllers.repo.currentUser' + tabBinding: 'controllers.repo.tab' currentItemBinding: 'job' 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/profile.coffee b/assets/scripts/app/controllers/profile.coffee index 02800623..2415d05a 100644 --- a/assets/scripts/app/controllers/profile.coffee +++ b/assets/scripts/app/controllers/profile.coffee @@ -35,13 +35,16 @@ Travis.ProfileController = Travis.Controller.extend @reloadHooks() reloadHooks: -> - @set('allHooks', Travis.Hook.find(all: true, owner_name: @get('params.login') || @get('user.login'))) + # TODO: figure out why user is not available sometimes + @set('allHooks', Travis.Hook.find(all: true, owner_name: @get('params.login') || @get('user.login') || Travis.lookup('controller:currentUser').get('login'))) hooks: (-> + @reloadHooks() unless hooks = @get('allHooks') @get('allHooks').filter (hook) -> hook.get('admin') ).property('allHooks.length', 'allHooks') unAdminisetableHooks: (-> + @reloadHooks() unless hooks = @get('allHooks') @get('allHooks').filter (hook) -> !hook.get('admin') ).property('allHooks.length', 'allHooks') diff --git a/assets/scripts/app/controllers/repo.coffee b/assets/scripts/app/controllers/repo.coffee index ff9b2b77..88276d71 100644 --- a/assets/scripts/app/controllers/repo.coffee +++ b/assets/scripts/app/controllers/repo.coffee @@ -1,8 +1,9 @@ Travis.RepoController = Travis.Controller.extend - bindings: [] - needs: ['repos', 'currentUser'] + needs: ['repos', 'currentUser', 'build'] currentUserBinding: 'controllers.currentUser' + build: Ember.computed.alias('controllers.build.build') + slug: (-> @get('repo.slug') ).property('repo.slug') isLoading: (-> @get('repo.isLoading') ).property('repo.isLoading') @@ -11,22 +12,26 @@ Travis.RepoController = Travis.Controller.extend Visibility.every Travis.INTERVALS.updateTimes, @updateTimes.bind(this) updateTimes: -> - if builds = @get('builds') - builds.forEach (b) -> b.updateTimes() + Ember.run this, -> + if builds = @get('builds') + builds.forEach (b) -> b.updateTimes() - if build = @get('build') - build.updateTimes() + if build = @get('build') + build.updateTimes() - if build && jobs = build.get('jobs') - jobs.forEach (j) -> j.updateTimes() + if build && jobs = build.get('jobs') + jobs.forEach (j) -> j.updateTimes() activate: (action) -> + @stopObservingLastBuild() this["view#{$.camelize(action)}"]() viewIndex: -> + @observeLastBuild() @connectTab('current') viewCurrent: -> + @observeLastBuild() @connectTab('current') viewBuilds: -> @@ -44,15 +49,24 @@ Travis.RepoController = Travis.Controller.extend viewJob: -> @connectTab('job') + lastBuildDidChange: -> + Ember.run.scheduleOnce('data', this, @_lastBuildDidChange); + + _lastBuildDidChange: -> + build = @get('repo.lastBuild') + @set('build', build) + + stopObservingLastBuild: -> + @removeObserver('repo.lastBuild', this, 'lastBuildDidChange') + + observeLastBuild: -> + @lastBuildDidChange() + @addObserver('repo.lastBuild', this, 'lastBuildDidChange') + connectTab: (tab) -> # TODO: such implementation seems weird now, because we render # in the renderTemplate function in routes name = if tab == 'current' then 'build' else tab - viewClass = if name in ['builds', 'branches', 'pull_requests'] - Travis.BuildsView - else - Travis["#{$.camelize(name)}View"] - @set('tab', tab) urlGithub: (-> diff --git a/assets/scripts/app/controllers/repos.coffee b/assets/scripts/app/controllers/repos.coffee index 55a9df1f..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'] @@ -36,7 +36,6 @@ Travis.ReposController = Ember.ArrayController.extend Visibility.every Travis.INTERVALS.updateTimes, @updateTimes.bind(this) recentRepos: (-> - Travis.Repo.find() Travis.LimitedArray.create content: Em.ArrayProxy.extend(Em.SortableMixin).create( sortProperties: ['sortOrder'] @@ -64,7 +63,14 @@ Travis.ReposController = Ember.ArrayController.extend @set('content', @get('recentRepos')) viewOwned: -> - @set('content', Travis.Repo.accessibleBy(@get('currentUser.login'))) + @set('content', @get('userRepos')) + + userRepos: (-> + if login = @get('currentUser.login') + Travis.Repo.accessibleBy(login) + else + [] + ).property('currentUser.login') viewSearch: (params) -> @set('content', Travis.Repo.search(params.search)) @@ -83,3 +89,14 @@ Travis.ReposController = Ember.ArrayController.extend @searchLater = Ember.run.later(this, (-> @activate 'search', search: phrase ), 500) + + noReposMessage: (-> + tab = @get('tab') + + if tab == 'owned' + 'You don\'t have any repos set up on Travis CI' + else if tab == 'recent' + 'Repositories could not be loaded' + else + 'Could not find any repos' + ).property('tab') diff --git a/assets/scripts/app/controllers/running_jobs.coffee b/assets/scripts/app/controllers/running_jobs.coffee deleted file mode 100644 index 4d81a0e8..00000000 --- a/assets/scripts/app/controllers/running_jobs.coffee +++ /dev/null @@ -1,103 +0,0 @@ -Travis.RunningJobsController = Em.ArrayProxy.extend - Group: Em.Object.extend - slug: (-> @get('jobs.firstObject.repoSlug') ).property('jobs.firstObject.repoSlug') - - init: -> - @_super.apply this, arguments - @set 'jobs', [] - - @set 'sortedJobs', Em.ArrayProxy.extend(Em.SortableMixin, - sortProperties: ['number'] - ).create(content: @get('jobs')) - - willDestroy: -> - @get('sortedJobs').destroy() - - add: (job) -> - @get('jobs').pushObject(job) unless @get('jobs').contains job - @attach() - - remove: (job) -> - @get('jobs').removeObject(job) - @clean() - - attach: -> - @get('parent').addGroup(this) - - clean: -> - @get('parent').removeGroup(this) if @get('isEmpty') - - isEmpty: (-> - @get('jobs.length') == 0 - ).property('jobs.length') - - groups: [] - sortedGroups: (-> - Em.ArrayProxy.extend(Em.SortableMixin, - sortProperties: ['slug'] - ).create(content: @get('groups')) - ).property() - - groupsBySlug: {} - - init: -> - @_super.apply this, arguments - - jobs = Travis.Job.running() - @set 'content', jobs - @addedJobs jobs - - contentArrayWillChange: (array, index, removedCount, addedCount) -> - @_super.apply this, arguments - - if removedCount - @removedJobs array.slice(index, index + removedCount) - - contentArrayDidChange: (array, index, removedCount, addedCount) -> - @_super.apply this, arguments - - if addedCount - @addedJobs array.slice(index, index + addedCount) - - addedJobs: (jobs) -> - self = this - jobs.forEach (job) -> - self.waitForSlug(job, 'addJob') - - removedJobs: (jobs) -> - self = this - jobs.forEach (job) -> - self.waitForSlug(job, 'removeJob') - - addJob: (job) -> - slug = job.get('repoSlug') - group = @groupForSlug(slug) - group.add(job) - - removeJob: (job) -> - slug = job.get('repoSlug') - group = @groupForSlug(slug) - group.remove(job) - - waitForSlug: (job, callbackName) -> - callback = @[callbackName] - self = this - if job.get('repoSlug')? - callback.call self, job - else - observer = -> - if job.get('repoSlug')? - callback.call self, job - job.removeObserver 'repoSlug', observer - job.addObserver 'repoSlug', observer - - groupForSlug: (slug) -> - @groupsBySlug[slug] ||= @Group.create(slug: slug, parent: this) - - addGroup: (group) -> - @get('groups').pushObject group unless @get('groups').contains group - - removeGroup: (group) -> - @get('groups').removeObject group - delete @groupsBySlug[group.get('slug')] - group.destroy() diff --git a/assets/scripts/app/controllers/sidebar.coffee b/assets/scripts/app/controllers/sidebar.coffee deleted file mode 100644 index c1a37f2f..00000000 --- a/assets/scripts/app/controllers/sidebar.coffee +++ /dev/null @@ -1,45 +0,0 @@ -Travis.reopen - SidebarController: Em.ArrayController.extend - needs: ['runningJobs'] - jobsBinding: 'controllers.runningJobs' - - init: -> - @_super.apply this, arguments - @tickables = [] - - tick: -> - tickable.tick() for tickable in @tickables - - QueuesController: Em.ArrayController.extend - init: -> - @_super.apply this, arguments - - queues = for queue in Travis.QUEUES - Travis.LimitedArray.create - content: Travis.Job.queued(queue.name), limit: 20 - id: "queue_#{queue.name}" - name: queue.display - @set 'content', queues - - showAll: (queue) -> - queue.showAll() - - WorkersController: Em.ArrayController.extend - init: -> - @_super.apply this, arguments - @set 'content', Travis.Worker.find() - - groups: (-> - if content = @get 'arrangedContent' - groups = {} - for worker in content.toArray() - host = worker.get('host') - unless groups[host] - groups[host] = Em.ArrayProxy.extend(Em.SortableMixin, - content: [], - sortProperties: ['nameForSort'] - ).create() - groups[host].pushObject(worker) - - $.values(groups) - ).property('length') diff --git a/assets/scripts/app/helpers/handlebars.coffee b/assets/scripts/app/helpers/handlebars.coffee index d86aa80d..8dfda033 100644 --- a/assets/scripts/app/helpers/handlebars.coffee +++ b/assets/scripts/app/helpers/handlebars.coffee @@ -18,8 +18,9 @@ Ember.registerBoundHelper 'formatTime', (value, options) -> Ember.registerBoundHelper 'formatDuration', (duration, options) -> safe Travis.Helpers.timeInWords(duration) -Ember.registerBoundHelper 'formatCommit', (commit, options) -> +Ember.Handlebars.helper('formatCommit', (commit) -> safe Travis.Helpers.formatCommit(commit.get('sha'), commit.get('branch')) if commit +, 'sha', 'branch') Ember.registerBoundHelper 'formatSha', (sha, options) -> safe Travis.Helpers.formatSha(sha) @@ -27,8 +28,8 @@ Ember.registerBoundHelper 'formatSha', (sha, options) -> Ember.registerBoundHelper 'pathFrom', (url, options) -> safe Travis.Helpers.pathFrom(url) -Ember.registerBoundHelper 'formatMessage', (message, options) -> - safe Travis.Helpers.formatMessage(message, options) +Ember.Handlebars.helper 'formatMessage', (message, options) -> + safe Travis.Helpers.formatMessage(message, options.hash) Ember.registerBoundHelper 'formatConfig', (config, options) -> safe Travis.Helpers.formatConfig(config) diff --git a/assets/scripts/app/helpers/helpers.coffee b/assets/scripts/app/helpers/helpers.coffee index 2b68dbd4..209f333f 100644 --- a/assets/scripts/app/helpers/helpers.coffee +++ b/assets/scripts/app/helpers/helpers.coffee @@ -26,18 +26,21 @@ 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) -> message = message || '' message = message.split(/\n/)[0] if options.short - @_emojize(@_escape(message)).replace /\n/g, '
' + message = @_emojize(@_escape(message)) + if !!options.repo + message = @githubify(message, options.repo.get('owner'), options.repo.get('name')) + message.replace /\n/g, '
' pathFrom: (url) -> (url || '').split('/').pop() @@ -66,6 +69,35 @@ require 'config/emoij' result.push seconds + ' sec' if seconds > 0 if result.length > 0 then result.join(' ') else '-' + githubify: (text, owner, repo) -> + self = this + text = text.replace @_githubReferenceRegexp, (reference, matchedOwner, matchedRepo, matchedNumber) -> + self._githubReferenceLink(reference, { owner: owner, repo: repo }, { owner: matchedOwner, repo: matchedRepo, number: matchedNumber } ) + text = text.replace @_githubUserRegexp, (reference, username) -> + self._githubUserLink(reference, username) + text = text.replace @_githubCommitReferenceRegexp, (reference, matchedOwner, matchedRepo, matchedSHA) -> + self._githubCommitReferenceLink(reference, { owner: owner, repo: repo }, { owner: matchedOwner, repo: matchedRepo, sha: matchedSHA }) + text + + _githubReferenceRegexp: new RegExp("([\\w-]+)?\\/?([\\w-]+)?(?:#|gh-)(\\d+)", 'g') + + _githubReferenceLink: (reference, current, matched) -> + owner = matched.owner || current.owner + repo = matched.repo || current.repo + "#{reference}" + + _githubUserRegexp: new RegExp("\\B@([\\w-]+)", 'g') + + _githubUserLink: (reference, username) -> + "#{reference}" + + _githubCommitReferenceRegexp: new RegExp("([\\w-]+)?\\/([\\w-]+)?@([0-9A-Fa-f]+)", 'g') + + _githubCommitReferenceLink: (reference, current, matched) -> + owner = matched.owner || current.owner + repo = matched.repo || current.repo + "#{reference}" + _normalizeDateString: (string) -> if window.JHW string = string.replace('T', ' ').replace(/-/g, '/') @@ -93,4 +125,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/helpers/urls.coffee b/assets/scripts/app/helpers/urls.coffee index 8af4d14e..17b75597 100644 --- a/assets/scripts/app/helpers/urls.coffee +++ b/assets/scripts/app/helpers/urls.coffee @@ -3,22 +3,22 @@ "#{Travis.config.api_endpoint}/jobs/#{id}/log.txt?deansi=true" githubPullRequest: (slug, pullRequestNumber) -> - "http://github.com/#{slug}/pull/#{pullRequestNumber}" + "https://github.com/#{slug}/pull/#{pullRequestNumber}" githubCommit: (slug, sha) -> - "http://github.com/#{slug}/commit/#{sha}" + "https://github.com/#{slug}/commit/#{sha}" githubRepo: (slug) -> - "http://github.com/#{slug}" + "https://github.com/#{slug}" githubWatchers: (slug) -> - "http://github.com/#{slug}/watchers" + "https://github.com/#{slug}/watchers" githubNetwork: (slug) -> - "http://github.com/#{slug}/network" + "https://github.com/#{slug}/network" githubAdmin: (slug) -> - "http://github.com/#{slug}/settings/hooks#travis_minibucket" + "https://github.com/#{slug}/settings/hooks#travis_minibucket" statusImage: (slug, branch) -> "#{location.protocol}//#{location.host}/#{slug}.png" + if branch then "?branch=#{branch}" else '' diff --git a/assets/scripts/app/models/account.coffee b/assets/scripts/app/models/account.coffee index 4169903e..03c29f79 100644 --- a/assets/scripts/app/models/account.coffee +++ b/assets/scripts/app/models/account.coffee @@ -1,12 +1,18 @@ require 'travis/model' @Travis.Account = Travis.Model.extend - primaryKey: 'login' - login: DS.attr('string') - name: DS.attr('string') - type: DS.attr('string') - reposCount: DS.attr('number') + login: Ember.attr('string') + name: Ember.attr('string') + type: Ember.attr('string') + _reposCount: Ember.attr(Number, key: 'repos_count') urlGithub: (-> - "http://github.com/#{@get('login')}" + "https://github.com/#{@get('login')}" ).property() + + # TODO: maybe it would be good to add a "default" value for Ember.attr + reposCount: (-> + @get('_reposCount') || 0 + ).property('_reposCount') + +Travis.Account.primaryKey = 'login' diff --git a/assets/scripts/app/models/annotation.coffee b/assets/scripts/app/models/annotation.coffee index 1f776765..df299b5e 100644 --- a/assets/scripts/app/models/annotation.coffee +++ b/assets/scripts/app/models/annotation.coffee @@ -1,10 +1,10 @@ require 'travis/model' @Travis.Annotation = Travis.Model.extend - jobId: DS.attr('number') - description: DS.attr('string') - url: DS.attr('string') - image: DS.attr('object') - providerName: DS.attr('string') + jobId: Ember.attr('number') + description: Ember.attr('string') + url: Ember.attr('string') + image: Ember.attr('object') + providerName: Ember.attr('string') - job: DS.belongsTo('Travis.Job') + job: Ember.belongsTo('Travis.Job') diff --git a/assets/scripts/app/models/branch.coffee b/assets/scripts/app/models/branch.coffee index 3d45d52d..9f6e66f7 100644 --- a/assets/scripts/app/models/branch.coffee +++ b/assets/scripts/app/models/branch.coffee @@ -1,18 +1,18 @@ require 'travis/model' @Travis.Branch = Travis.Model.extend - repoId: DS.attr('number', key: 'repository_id') - commitId: DS.attr('number') - state: DS.attr('string') - number: DS.attr('number') - branch: DS.attr('string') - message: DS.attr('string') - result: DS.attr('number') - duration: DS.attr('number') - startedAt: DS.attr('string') - finishedAt: DS.attr('string') + repoId: Ember.attr('number', key: 'repository_id') + commitId: Ember.attr('number') + state: Ember.attr('string') + number: Ember.attr('number') + branch: Ember.attr('string') + message: Ember.attr('string') + result: Ember.attr('number') + duration: Ember.attr('number') + startedAt: Ember.attr('string') + finishedAt: Ember.attr('string') - commit: DS.belongsTo('Travis.Commit') + commit: Ember.belongsTo('Travis.Commit') repo: (-> Travis.Repo.find @get('repoId') if @get('repoId') diff --git a/assets/scripts/app/models/broadcast.coffee b/assets/scripts/app/models/broadcast.coffee index dab4fe4d..9f712631 100644 --- a/assets/scripts/app/models/broadcast.coffee +++ b/assets/scripts/app/models/broadcast.coffee @@ -1,7 +1,7 @@ require 'travis/model' @Travis.Broadcast = Travis.Model.extend - message: DS.attr('string') + message: Ember.attr('string') toObject: -> { type: 'broadcast', id: @get('id'), message: @get('message') } diff --git a/assets/scripts/app/models/build.coffee b/assets/scripts/app/models/build.coffee index 9906e770..667daee9 100644 --- a/assets/scripts/app/models/build.coffee +++ b/assets/scripts/app/models/build.coffee @@ -1,32 +1,31 @@ require 'travis/model' @Travis.Build = Travis.Model.extend Travis.DurationCalculations, - eventType: DS.attr('string') - repoId: DS.attr('number') - commitId: DS.attr('number') + repositoryId: Ember.attr('number') + commitId: Ember.attr('number') - state: DS.attr('string') - number: DS.attr('number') - branch: DS.attr('string') - message: DS.attr('string') - _duration: DS.attr('number') - _config: DS.attr('object') - startedAt: DS.attr('string') - finishedAt: DS.attr('string') - pullRequest: DS.attr('boolean') - pullRequestTitle: DS.attr('string') - pullRequestNumber: DS.attr('number') + state: Ember.attr('string') + number: Ember.attr(Number) + branch: Ember.attr('string') + message: Ember.attr('string') + _duration: Ember.attr(Number, key: 'duration') + _config: Ember.attr('object', key: 'config') + startedAt: Ember.attr('string') + finishedAt: Ember.attr('string') + pullRequest: Ember.attr('boolean') + pullRequestTitle: Ember.attr('string') + pullRequestNumber: Ember.attr(Number) - repo: DS.belongsTo('Travis.Repo') - commit: DS.belongsTo('Travis.Commit') - jobs: DS.hasMany('Travis.Job') + repo: Ember.belongsTo('Travis.Repo', key: 'repository_id') + commit: Ember.belongsTo('Travis.Commit') + jobs: Ember.hasMany('Travis.Job') config: (-> Travis.Helpers.compact(@get('_config')) ).property('_config') isPullRequest: (-> - @get('eventType') == 'pull_request' + @get('eventType') == 'pull_request' || @get('pullRequest') ).property('eventType') isMatrix: (-> @@ -46,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) -> @@ -58,26 +57,30 @@ 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: (-> - @get('state') == 'created' # TODO - ).property('state') + !@get('isFinished') && @get('jobs').filter( (j) -> j.get('canCancel') ).get('length') > 0 + ).property('isFinished', 'jobs.@each.canCancel') cancel: (-> - Travis.ajax.post "/builds/#{@get('id')}", _method: 'delete' + Travis.ajax.post "/builds/#{@get('id')}/cancel" ) requeue: -> Travis.ajax.post '/requests', build_id: @get('id') - isAttributeLoaded: (key) -> + isPropertyLoaded: (key) -> if ['_duration', 'finishedAt'].contains(key) && !@get('isFinished') return true else @_super(key) + formattedFinishedAt: (-> + if finishedAt = @get('finishedAt') + moment(finishedAt).format('lll') + ).property('finishedAt') @Travis.Build.reopenClass byRepoId: (id, parameters) -> diff --git a/assets/scripts/app/models/commit.coffee b/assets/scripts/app/models/commit.coffee index 18602b5c..bea75130 100644 --- a/assets/scripts/app/models/commit.coffee +++ b/assets/scripts/app/models/commit.coffee @@ -1,15 +1,14 @@ require 'travis/model' @Travis.Commit = Travis.Model.extend - buildId: DS.attr('number') - sha: DS.attr('string') - branch: DS.attr('string') - message: DS.attr('string') - compareUrl: DS.attr('string') - authorName: DS.attr('string') - authorEmail: DS.attr('string') - committerName: DS.attr('string') - committerEmail: DS.attr('string') - pullRequestNumber: DS.attr('number') + buildId: Ember.attr('number') + sha: Ember.attr('string') + branch: Ember.attr('string') + message: Ember.attr('string') + compareUrl: Ember.attr('string') + authorName: Ember.attr('string') + authorEmail: Ember.attr('string') + committerName: Ember.attr('string') + committerEmail: Ember.attr('string') - build: DS.belongsTo('Travis.Build') + build: Ember.belongsTo('Travis.Build') diff --git a/assets/scripts/app/models/event.coffee b/assets/scripts/app/models/event.coffee index 605fe7b3..357d9abd 100644 --- a/assets/scripts/app/models/event.coffee +++ b/assets/scripts/app/models/event.coffee @@ -1,11 +1,11 @@ require 'travis/model' @Travis.Event = Travis.Model.extend - event: DS.attr('string') - repoId: DS.attr('number', key: 'repository_id') - sourceId: DS.attr('number', key: 'source_id') - sourceType: DS.attr('string', key: 'source_type') - createdAt: DS.attr('string', key: 'created_at') + event: Ember.attr('string') + repoId: Ember.attr('number', key: 'repository_id') + sourceId: Ember.attr('number', key: 'source_id') + sourceType: Ember.attr('string', key: 'source_type') + createdAt: Ember.attr('string', key: 'created_at') event_: (-> @get('event') diff --git a/assets/scripts/app/models/extensions.coffee b/assets/scripts/app/models/extensions.coffee index be8f2b7a..013da4f5 100644 --- a/assets/scripts/app/models/extensions.coffee +++ b/assets/scripts/app/models/extensions.coffee @@ -7,6 +7,6 @@ Travis.DurationCalculations = Ember.Mixin.create ).property('_duration', 'finishedAt', 'startedAt') updateTimes: -> - unless ['rootState.loaded.reloading', 'rootState.loading'].contains @get('stateManager.currentState.path') + unless ['rootState.loaded.reloading', 'rootState.loading'].contains @get('stateManager.currentState.path') or @get('isFinished') @notifyPropertyChange '_duration' @notifyPropertyChange 'finished_at' diff --git a/assets/scripts/app/models/hook.coffee b/assets/scripts/app/models/hook.coffee index 13cc04d1..677708d1 100644 --- a/assets/scripts/app/models/hook.coffee +++ b/assets/scripts/app/models/hook.coffee @@ -1,11 +1,11 @@ require 'travis/model' @Travis.Hook = Travis.Model.extend - name: DS.attr('string') - ownerName: DS.attr('string') - description: DS.attr('string') - active: DS.attr('boolean') - admin: DS.attr('boolean') + name: Ember.attr('string') + ownerName: Ember.attr('string') + description: Ember.attr('string') + active: Ember.attr('boolean') + admin: Ember.attr('boolean') account: (-> @get('slug').split('/')[0] @@ -16,18 +16,14 @@ require 'travis/model' ).property('ownerName', 'name') urlGithub: (-> - "http://github.com/#{@get('slug')}" + "https://github.com/#{@get('slug')}" ).property() urlGithubAdmin: (-> - "http://github.com/#{@get('slug')}/settings/hooks#travis_minibucket" + "https://github.com/#{@get('slug')}/settings/hooks#travis_minibucket" ).property() toggle: -> return if @get('isSaving') - transaction = @get('store').transaction() - transaction.add this - @set 'active', !@get('active') - - transaction.commit() + @save() diff --git a/assets/scripts/app/models/job.coffee b/assets/scripts/app/models/job.coffee index 7378915b..d28eb522 100644 --- a/assets/scripts/app/models/job.coffee +++ b/assets/scripts/app/models/job.coffee @@ -1,39 +1,26 @@ require 'travis/model' @Travis.Job = Travis.Model.extend Travis.DurationCalculations, - repoId: DS.attr('number') - buildId: DS.attr('number') - commitId: DS.attr('number') - logId: DS.attr('number') + repoId: Ember.attr('string', key: 'repository_id') + buildId: Ember.attr('string') + commitId: Ember.attr('string') + logId: Ember.attr('string') - queue: DS.attr('string') - state: DS.attr('string') - number: DS.attr('string') - startedAt: DS.attr('string') - finishedAt: DS.attr('string') - allowFailure: DS.attr('boolean') + queue: Ember.attr('string') + state: Ember.attr('string') + number: Ember.attr('string') + startedAt: Ember.attr('string') + finishedAt: Ember.attr('string') + allowFailure: Ember.attr('boolean') - repositorySlug: DS.attr('string') - repo: DS.belongsTo('Travis.Repo') - build: DS.belongsTo('Travis.Build') - commit: DS.belongsTo('Travis.Commit') + repositorySlug: Ember.attr('string') + repo: Ember.belongsTo('Travis.Repo', key: 'repository_id') + build: Ember.belongsTo('Travis.Build') + commit: Ember.belongsTo('Travis.Commit') - annotations: DS.hasMany('Travis.Annotation') + annotations: Ember.hasMany('Travis.Annotation') - # this is a fake relationship just to get rid - # of ember data's bug: https://github.com/emberjs/data/issues/758 - # TODO: remove when this issue is fixed - fakeBuild: DS.belongsTo('Travis.Build') - - _config: DS.attr('object') - - repoSlugDidChange: (-> - if slug = @get('repoSlug') - @get('store').loadIncomplete(Travis.Repo, { - id: @get('repoId'), - slug: slug - }, { skipIfExists: true }) - ).observes('repoSlug') + _config: Ember.attr('object', key: 'config') log: ( -> @set('isLogAccessed', true) @@ -75,11 +62,11 @@ require 'travis/model' ).property('config', 'build.rawConfigKeys.length') canCancel: (-> - @get('state') == 'created' || @get('state') == 'queued' # TODO + !@get('isFinished') ).property('state') cancel: (-> - Travis.ajax.post "/jobs/#{@get('id')}", _method: 'delete' + Travis.ajax.post "/jobs/#{@get('id')}/cancel" ) requeue: -> @@ -105,7 +92,7 @@ require 'travis/model' Travis.pusher.unsubscribe "job-#{@get('id')}" ).observes('state') - isAttributeLoaded: (key) -> + isPropertyLoaded: (key) -> if ['finishedAt'].contains(key) && !@get('isFinished') return true else if key == 'startedAt' && @get('state') == 'created' @@ -117,19 +104,41 @@ require 'travis/model' @get('state') in ['passed', 'failed', 'errored', 'canceled'] ).property('state') + # TODO: such formattings should be done in controller, but in order + # to use it there easily, I would have to refactor job and build + # controllers + formattedFinishedAt: (-> + if finishedAt = @get('finishedAt') + moment(finishedAt).format('lll') + ).property('finishedAt') + @Travis.Job.reopenClass - queued: (queue) -> - @find() - Travis.store.filter this, (job) -> - queued = ['created', 'queued'].indexOf(job.get('state')) != -1 - # TODO: why queue is sometimes just common instead of build.common? - queued && (!queue || job.get('queue') == "builds.#{queue}" || job.get('queue') == queue) + queued: -> + filtered = Ember.FilteredRecordArray.create( + modelClass: Travis.Job + filterFunction: (job) -> + ['created', 'queued'].indexOf(job.get('state')) != -1 + filterProperties: ['state', 'queue'] + ) + + @fetch().then (array) -> + filtered.updateFilter() + filtered.set('isLoaded', true) + + filtered running: -> - @find(state: 'started') - Travis.store.filter this, (job) -> - job.get('state') == 'started' + filtered = Ember.FilteredRecordArray.create( + modelClass: Travis.Job + filterFunction: (job) -> + job.get('state') == 'started' + filterProperties: ['state'] + ) + + @fetch(state: 'started').then (array) -> + filtered.updateFilter() + filtered.set('isLoaded', true) + + filtered - findMany: (ids) -> - Travis.store.findMany this, ids diff --git a/assets/scripts/app/models/log.coffee b/assets/scripts/app/models/log.coffee index 63df7690..5671199d 100644 --- a/assets/scripts/app/models/log.coffee +++ b/assets/scripts/app/models/log.coffee @@ -15,6 +15,7 @@ require 'travis/chunk_buffer' fetch: -> console.log 'log model: fetching log' if Log.DEBUG + @setParts() handlers = json: (json) => @loadParts(json['log']['parts']) text: (text) => @loadText(text) @@ -45,7 +46,7 @@ Travis.Log.Request = Em.Object.extend Travis.ajax.ajax "/jobs/#{@id}/log?cors_hax=true", 'GET', dataType: 'text' headers: @HEADERS - success: (body, status, xhr) => @handle(body, status, xhr) + success: (body, status, xhr) => Ember.run(this, -> @handle(body, status, xhr)) handle: (body, status, xhr) -> if xhr.status == 204 diff --git a/assets/scripts/app/models/repo.coffee b/assets/scripts/app/models/repo.coffee index e1503b2a..f582fb10 100644 --- a/assets/scripts/app/models/repo.coffee +++ b/assets/scripts/app/models/repo.coffee @@ -2,16 +2,18 @@ require 'travis/expandable_record_array' require 'travis/model' @Travis.Repo = Travis.Model.extend - slug: DS.attr('string') - description: DS.attr('string') - lastBuildId: DS.attr('number') - lastBuildNumber: DS.attr('string') - lastBuildState: DS.attr('string') - lastBuildStartedAt: DS.attr('string') - lastBuildFinishedAt: DS.attr('string') - _lastBuildDuration: DS.attr('number') + id: Ember.attr('string') + slug: Ember.attr('string') + description: Ember.attr('string') + lastBuildId: Ember.attr('string') + lastBuildNumber: Ember.attr(Number) + lastBuildState: Ember.attr('string') + lastBuildStartedAt: Ember.attr('string') + lastBuildFinishedAt: Ember.attr('string') + githubLanguage: Ember.attr('string') + _lastBuildDuration: Ember.attr(Number, key: 'last_build_duration') - lastBuild: DS.belongsTo('Travis.Build') + lastBuild: Ember.belongsTo('Travis.Build', key: 'last_build_id') lastBuildHash: (-> { @@ -22,7 +24,9 @@ require 'travis/model' ).property('lastBuildId', 'lastBuildNumber') allBuilds: (-> - Travis.Build.find() + recordArray = Ember.RecordArray.create({ modelClass: Travis.Build, content: Ember.A([]) }) + Travis.Build.registerRecordArray(recordArray) + recordArray ).property() builds: (-> @@ -33,12 +37,11 @@ require 'travis/model' array = Travis.ExpandableRecordArray.create type: Travis.Build content: Ember.A([]) - store: @get('store') array.load(builds) id = @get('id') - array.observe(@get('allBuilds'), (build) -> build.get('isLoaded') && build.get('eventType') && build.get('repo.id') == id && !build.get('isPullRequest') ) + array.observe(@get('allBuilds'), (build) -> build.get('isLoaded') && build.get('repo.id') == id && !build.get('isPullRequest') ) array ).property() @@ -49,12 +52,11 @@ require 'travis/model' array = Travis.ExpandableRecordArray.create type: Travis.Build content: Ember.A([]) - store: @get('store') array.load(builds) id = @get('id') - array.observe(@get('allBuilds'), (build) -> build.get('isLoaded') && build.get('eventType') && build.get('repo.id') == id && build.get('isPullRequest') ) + array.observe(@get('allBuilds'), (build) -> build.get('isLoaded') && build.get('repo.id') == id && build.get('isPullRequest') ) array ).property() @@ -117,13 +119,28 @@ require 'travis/model' @find(search: query, orderBy: 'name') withLastBuild: -> - @filter( (repo) -> (!repo.get('incomplete') || repo.isAttributeLoaded('lastBuildId')) && repo.get('lastBuildId') ) + filtered = Ember.FilteredRecordArray.create( + modelClass: Travis.Repo + filterFunction: (repo) -> repo.get('lastBuildId') + filterProperties: ['lastBuildId'] + ) + + Travis.Repo.fetch().then (array) -> + filtered.updateFilter() + filtered.set('isLoaded', true) + + filtered bySlug: (slug) -> 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 242ffb93..553b26bc 100644 --- a/assets/scripts/app/models/user.coffee +++ b/assets/scripts/app/models/user.coffee @@ -2,17 +2,17 @@ require 'travis/ajax' require 'travis/model' @Travis.User = Travis.Model.extend - _name: DS.attr('string') - email: DS.attr('string') - login: DS.attr('string') - token: DS.attr('string') - locale: DS.attr('string') - gravatarId: DS.attr('string') - isSyncing: DS.attr('boolean') - syncedAt: DS.attr('string') - repoCount: DS.attr('number') + _name: Ember.attr('string', key: 'name') + email: Ember.attr('string') + login: Ember.attr('string') + token: Ember.attr('string') + locale: Ember.attr('string') + gravatarId: Ember.attr('string') + isSyncing: Ember.attr('boolean') + syncedAt: Ember.attr('string') + repoCount: Ember.attr('number') - # This is the only way I found to override the attribue created with DS.attr + # This is the only way I found to override the attribue created with Ember.attr name: Ember.computed( (key, value) -> if arguments.length == 1 @get('_name') || @get('login') @@ -27,34 +27,40 @@ require 'travis/model' Ember.run.next this, -> @poll() if @get('isSyncing') - Ember.run.next this, -> - transaction = @get('store').transaction() - transaction.add this - urlGithub: (-> "https://github.com/#{@get('login')}" ).property() + _rawPermissions: (-> + Travis.ajax.get('/users/permissions') + ).property() + permissions: (-> - unless @permissions - @permissions = Ember.ArrayProxy.create(content: []) - Travis.ajax.get('/users/permissions', (data) => @permissions.set('content', data.permissions)) - @permissions + permissions = Ember.ArrayProxy.create(content: []) + @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() updateLocale: (locale) -> - - transaction = @get('transaction') - transaction.commit() - - self = this - observer = -> - unless self.get('isSaving') - self.removeObserver 'isSaving', observer - transaction = self.get('store').transaction() - transaction.add self - - @addObserver 'isSaving', observer + @save() Travis.setLocale(locale) type: (-> @@ -71,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/models/worker.coffee b/assets/scripts/app/models/worker.coffee index c60a898f..c5bc8539 100644 --- a/assets/scripts/app/models/worker.coffee +++ b/assets/scripts/app/models/worker.coffee @@ -1,10 +1,10 @@ require 'travis/model' @Travis.Worker = Travis.Model.extend - state: DS.attr('string') - name: DS.attr('string') - host: DS.attr('string') - payload: DS.attr('object') + state: Ember.attr('string') + name: Ember.attr('string') + host: Ember.attr('string') + payload: Ember.attr('object') number: (-> @get('name').match(/\d+$/)[0] diff --git a/assets/scripts/app/pusher.coffee b/assets/scripts/app/pusher.coffee index 473982e9..c3b1f825 100644 --- a/assets/scripts/app/pusher.coffee +++ b/assets/scripts/app/pusher.coffee @@ -55,11 +55,11 @@ $.extend Travis.Pusher.prototype, # TODO remove job:requeued, once sf-restart-event has been merged # TODO this also needs to clear logs on build:created if matrix jobs are already loaded if event == 'job:created' || event == 'job:requeued' - if Travis.store.isInStore(Travis.Job, data.job.id) + if Travis.Job.isRecordLoaded(data.job.id) Travis.Job.find(data.job.id).clearLog() Ember.run.next -> - Travis.store.receive(event, data) + Travis.receive(event, data) processSavedCallbacks: -> while callback = @callbacksToProcess.shiftObject() @@ -78,7 +78,7 @@ $.extend Travis.Pusher.prototype, switch event when 'build:started', 'build:finished' data - when 'job:created', 'job:started', 'job:requeued', 'job:finished', 'job:log' + when 'job:created', 'job:started', 'job:requeued', 'job:finished', 'job:log', 'job:canceled' data.queue = data.queue.replace('builds.', '') if data.queue { job: data } when 'worker:added', 'worker:updated', 'worker:removed' diff --git a/assets/scripts/app/routes.coffee b/assets/scripts/app/routes.coffee index 93dda8af..711111a8 100644 --- a/assets/scripts/app/routes.coffee +++ b/assets/scripts/app/routes.coffee @@ -1,44 +1,9 @@ require 'travis/location' -require 'travis/line_number_parser' - -Travis.DontSetupModelForControllerMixin = Ember.Mixin.create - # I've override setup to *not* set controller's model - # this can be remove when this patch will be merged https://github.com/emberjs/ember.js/pull/2044 - # this will allow us to override setting up model for a controller - setup: (context) -> - isTop = undefined - unless @_redirected - isTop = true - @_redirected = [] - - @_checkingRedirect = true - depth = ++@_redirectDepth - - if context is `undefined` - @redirect() - else - @redirect context - - @_redirectDepth-- - @_checkingRedirect = false - - redirected = @_redirected - - @_redirected = null if isTop - - return false if redirected[depth] - - controller = @controllerFor(@routeName, context) - - @setupController controller, context - @renderTemplate controller, context Ember.Router.reopen location: (if testMode? then Ember.NoneLocation.create() else Travis.Location.create()) handleURL: (url) -> - Travis.autoSignIn() unless Travis.__container__.lookup('controller:currentUser').get('content') - url = url.replace(/#.*?$/, '') try @_super(url) @@ -51,29 +16,51 @@ 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 + error: (error) -> + if error == 'needs-auth' + authController = @container.lookup('controller:auth') || @generateController('auth') + authController.set('redirected', true) + @transitionTo('auth') + else + throw(error) + renderNoOwnedRepos: -> @render('no_owned_repos', outlet: 'main') + renderFirstSync: -> + @renderFirstSync() + afterSignIn: (path) -> @afterSignIn(path) afterSignOut: -> @afterSignOut() - afterSignIn: (path) -> - @routeToPath(path) + afterSignIn: -> + if transition = Travis.auth.get('afterSignInTransition') + Travis.auth.set('afterSignInTransition', null) + transition.retry() + else + @transitionTo('index.current') if @constructor == Travis.AuthRoute || @constructor.superclass == Travis.AuthRoute afterSignOut: -> - @routeToPath('/') + @transitionTo('index.current') - routeToPath: (path) -> - return unless path - @router.handleURL(path) - @router.location.setURL(path) + renderFirstSync: -> + @transitionTo 'first_sync' + + beforeModel: (transition) -> + Travis.autoSignIn() unless @signedIn() + + if !@signedIn() && @get('needsAuth') + Travis.auth.set('afterSignInTransition', transition) + Ember.RSVP.reject("needs-auth") + else + @_super.apply(this, arguments) signedIn: -> @controllerFor('currentUser').get('content') @@ -91,13 +78,6 @@ Ember.Route.reopen Travis.storeAfterSignInPath(path) @transitionTo('auth') -Travis.Router.reopen - transitionTo: -> - this.container.lookup('controller:repo').set('lineNumber', null) - - @_super.apply this, arguments - - Travis.Router.map -> @resource 'index', path: '/', -> @route 'current', path: '/' @@ -110,6 +90,7 @@ Travis.Router.map -> @resource 'branches', path: '/branches' @route 'getting_started' + @route 'first_sync' @route 'stats', path: '/stats' @route 'auth', path: '/auth' @route 'notFound', path: '/not-found' @@ -120,32 +101,17 @@ Travis.Router.map -> @route 'index', path: '/' @route 'profile', path: '/profile' -Travis.ApplicationRoute = Ember.Route.extend Travis.LineNumberParser, - setupController: -> - @_super.apply this, arguments - - this.controllerFor('repo').set('lineNumber', @fetchLineNumber()) - Travis.SetupLastBuild = Ember.Mixin.create setupController: -> - @lastBuildDidChange() - @controllerFor('repo').addObserver('repo.lastBuild', this, 'lastBuildDidChange') @repoDidLoad() @controllerFor('repo').addObserver('repo.isLoaded', this, 'repoDidLoad') - deactivate: -> - @_super.apply this, arguments - @controllerFor('repo').removeObserver('repo.lastBuild', this, 'lastBuildDidChange') - 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') - - lastBuildDidChange: -> - build = @controllerFor('repo').get('repo.lastBuild') - @controllerFor('build').set('build', build) + 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: -> @@ -159,11 +125,23 @@ Travis.GettingStartedRoute = Ember.Route.extend @render 'repos', outlet: 'left' @_super.apply(this, arguments) -Travis.IndexCurrentRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, Travis.SetupLastBuild, - renderDefaultTemplate: -> - @render 'repo' - @render 'build', outlet: 'pane', into: 'repo' +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() + @container.lookup('controller:application').connectLayout 'simple' + @_super.apply(this, arguments) + + renderTemplate: -> + @render 'top', outlet: 'top' + @_super.apply(this, arguments) + +Travis.IndexCurrentRoute = Ember.Route.extend Travis.SetupLastBuild, renderTemplate: -> @render 'repo' @render 'build', outlet: 'pane', into: 'repo' @@ -171,7 +149,8 @@ Travis.IndexCurrentRoute = Ember.Route.extend Travis.DontSetupModelForController setupController: -> @_super.apply this, arguments @currentRepoDidChange() - @container.lookup('controller:repo').activate('index') + + @controllerFor('repo').activate('index') @controllerFor('repos').addObserver('firstObject', this, 'currentRepoDidChange') deactivate: -> @@ -180,7 +159,7 @@ Travis.IndexCurrentRoute = Ember.Route.extend Travis.DontSetupModelForController currentRepoDidChange: -> @controllerFor('repo').set('repo', @controllerFor('repos').get('firstObject')) -Travis.AbstractBuildsRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, +Travis.AbstractBuildsRoute = Ember.Route.extend renderTemplate: -> @render 'builds', outlet: 'pane', into: 'repo' @@ -205,7 +184,7 @@ Travis.BuildsRoute = Travis.AbstractBuildsRoute.extend(contentType: 'builds') Travis.PullRequestsRoute = Travis.AbstractBuildsRoute.extend(contentType: 'pull_requests') Travis.BranchesRoute = Travis.AbstractBuildsRoute.extend(contentType: 'branches') -Travis.BuildRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, +Travis.BuildRoute = Ember.Route.extend renderTemplate: -> @render 'build', outlet: 'pane', into: 'repo' @@ -223,7 +202,10 @@ Travis.BuildRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, @controllerFor('build').set('build', model) repo.set('build', model) -Travis.JobRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, + model: (params) -> + Travis.Build.fetch(params.build_id) + +Travis.JobRoute = Ember.Route.extend renderTemplate: -> @render 'job', outlet: 'pane', into: 'repo' @@ -238,18 +220,23 @@ Travis.JobRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, repo = @controllerFor('repo') repo.set('job', model) repo.activate('job') - @controllerFor('build').set('build', model.get('build')) - repo.set('build', model.get('build')) -Travis.RepoIndexRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, Travis.SetupLastBuild, + if build = model.get('build') + @controllerFor('build').set('build', build) + repo.set('build', build) + + model: (params) -> + Travis.Job.fetch(params.job_id) + +Travis.RepoIndexRoute = Ember.Route.extend Travis.SetupLastBuild, setupController: (controller, model) -> @_super.apply this, arguments - @container.lookup('controller:repo').activate('current') + @controllerFor('repo').activate('current') renderTemplate: -> @render 'build', outlet: 'pane', into: 'repo' -Travis.RepoRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, +Travis.RepoRoute = Ember.Route.extend renderTemplate: -> @render 'repo' @@ -264,38 +251,21 @@ Travis.RepoRoute = Ember.Route.extend Travis.DontSetupModelForControllerMixin, [owner, name] = slug.split('/') { owner: owner, name: name } - deserialize: (params) -> + 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: -> $('body').attr('id', 'home') @render 'repos', outlet: 'left' - @render 'sidebar', outlet: 'right' @render 'top', outlet: 'top' @render 'flash', outlet: 'flash' @@ -303,6 +273,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') @@ -354,7 +328,7 @@ Travis.AccountRoute = Ember.Route.extend params = { login: account.get('login') } profileController.setParams(params) - deserialize: (params) -> + model: (params) -> controller = @container.lookup('controller:accounts') account = controller.findByLogin(params.login) @@ -373,7 +347,7 @@ Travis.AccountRoute = Ember.Route.extend proxy serialize: (account) -> - if account + if account && account.get { login: account.get('login') } else {} @@ -401,3 +375,6 @@ Travis.AuthRoute = Ember.Route.extend setupController: -> @container.lookup('controller:application').connectLayout('simple') + + deactivate: -> + @controllerFor('auth').set('redirected', false) diff --git a/assets/scripts/app/store.coffee b/assets/scripts/app/store.coffee deleted file mode 100644 index 101b5ae5..00000000 --- a/assets/scripts/app/store.coffee +++ /dev/null @@ -1,176 +0,0 @@ -require 'store/rest_adapter' - -coerceId = (id) -> if id == null then null else id+'' - -Travis.Store = DS.Store.extend - revision: 12 - adapter: Travis.RestAdapter.create() - - init: -> - @_super.apply this, arguments - @_loadedData = {} - @clientIdToComplete = {} - - load: (type, data, prematerialized) -> - result = @_super.apply this, arguments - - if result && result.clientId && @clientIdToComplete[result.clientId] == undefined - # I assume that everything that goes through load is complete record - # representation, incomplete hashes from pusher go through merge() - @clientIdToComplete[result.clientId] = true - - result - - # TODO use isUpdating once we've upgraded ember-data - loadMany: (type, ids, hashes) -> - result = @_super.apply this, arguments - array.set('isLoaded', true) for array in @typeMapFor(type).recordArrays - result - - merge: (type, data, incomplete) -> - id = coerceId data.id - - typeMap = @typeMapFor(type) - clientId = typeMap.idToCid[id] - record = @recordCache[clientId] - if record - @get('adapter').merge(this, record, data) - else - if (savedData = @clientIdToData[clientId]) && savedData.id? - $.extend(savedData, data) - else - result = @load(type, data, {id: data.id}) - - if result && result.clientId - clientId = result.clientId - if incomplete - @clientIdToComplete[result.clientId] = false - - { clientId: clientId, id: id } - - isInStore: (type, id) -> - !!@typeMapFor(type).idToCid[id] - - receive: (event, data) -> - [name, type] = event.split(':') - - mappings = @adapter.get('mappings') - type = mappings[name] - - - if event == 'build:started' && data.build.commit - # TODO: commit should be a sideload record on build, not mixed with it - build = data.build - commit = { - id: build.commit_id - author_email: build.author_email - author_name: build.author_name - branch: build.branch - committed_at: build.committed_at - committer_email: build.committer_email - committer_name: build.committer_name - compare_url: build.compare_url - message: build.message - sha: build.commit - } - delete(data.build.commit) - @loadIncomplete(Travis.Commit, commit) - - - if event == 'job:log' - console.log 'store: received job:log event', data if Log.DEBUG - data = data.job - job = @find(Travis.Job, data.id) - job.appendLog(number: parseInt(data.number), content: data._log, final: data.final) - else if data[type.singularName()] - @_loadOne(this, type, data) - else if data[type.pluralName()] - @_loadMany(this, type, data) - else - throw "can't load data for #{name}" unless type - - _loadOne: (store, type, json) -> - root = type.singularName() - # we get other types of records only in a few situations and - # it's not always needed to update data, so I'm specyfing which - # things I want to update here: - if type == Travis.Build && (json.repository || json.repo) - @loadIncomplete(Travis.Repo, json.repository || json.repo) - - result = @loadIncomplete(type, json[root]) - if result.id - @find(type, result.id) - - addLoadedData: (type, clientId, hash) -> - id = hash.id - @_loadedData[type.toString()] ||= {} - loadedData = (@_loadedData[type][clientId] ||= []) - - serializer = @get('adapter.serializer') - - Ember.get(type, 'attributes').forEach( (name, meta) -> - value = @extractAttribute(type, hash, name) - if value != undefined - loadedData.pushObject name unless loadedData.contains(name) - , serializer) - - Ember.get(type, 'relationshipsByName').forEach( (name, relationship) -> - key = @_keyForBelongsTo(type, relationship.key) - value = @extractBelongsTo(type, hash, key) - if value != undefined - loadedData.pushObject name unless loadedData.contains(name) - , serializer) - - isDataLoadedFor: (type, clientId, key) -> - if recordsData = @_loadedData[type.toString()] - if data = recordsData[clientId] - data.contains(key) - - loadIncomplete: (type, hash, options) -> - options ?= {} - - id = coerceId hash.id - - typeMap = @typeMapFor(type) - cidToData = @clientIdToData - clientId = typeMap.idToCid[id] - - if clientId && cidToData[clientId] && options.skipIfExists - return - - result = @merge(type, hash, true) - if result && result.clientId - @addLoadedData(type, result.clientId, hash) - #@_updateRelationships(type, hash) - - result - - materializeRecord: (type, clientId, id) -> - record = @_super.apply this, arguments - - if @clientIdToComplete[clientId] != undefined && !@clientIdToComplete[clientId] - record.set 'incomplete', true - else - record.set 'incomplete', false - - record - - _loadMany: (store, type, json) -> - root = type.pluralName() - @adapter.sideload(store, type, json, root) - @loadMany(type, json[root]) - - _updateRelationships: (type, data) -> - Em.get(type, 'relationshipsByName').forEach (key, meta) => - if meta.kind == 'belongsTo' - id = data["#{key}_id"] - if clientId = @typeMapFor(meta.type).idToCid[id] - if parent = this.findByClientId(meta.type, clientId, id) - dataProxy = parent.get('data') - if ids = dataProxy['hasMany'][type.pluralName()] - unless data.id in ids - state = parent.get('stateManager.currentState.path') - unless state == "rootState.loaded.materializing" - parent.send('materializingData') - ids.pushObject(data.id) - parent.notifyPropertyChange('data') diff --git a/assets/scripts/app/templates/auth/signin.hbs b/assets/scripts/app/templates/auth/signin.hbs index 867fa2b5..1a83cc2e 100644 --- a/assets/scripts/app/templates/auth/signin.hbs +++ b/assets/scripts/app/templates/auth/signin.hbs @@ -1,11 +1,16 @@ -{{#if view.signingIn}} -

Signing in ...

-

- Trying to authenticate with GitHub. -

-{{else}} -

Sign in

-

- Please sign in with GitHub. -

+{{#if redirected}} {{/if}} + + +

Hey, we're so glad you're here!

+

In order to view your repositories, please sign in.

+ +

+ + {{#if Travis.signingIn}} + Signing in... + {{else}} + Sign in with GitHub + {{/if}} + +

diff --git a/assets/scripts/app/templates/builds/list.hbs b/assets/scripts/app/templates/builds/list.hbs index 317594aa..5d73ca69 100644 --- a/assets/scripts/app/templates/builds/list.hbs +++ b/assets/scripts/app/templates/builds/list.hbs @@ -26,16 +26,16 @@ {{#if id}} - {{#linkTo "build" repo this}} + {{#link-to "build" repo this}} {{number}} - {{/linkTo}} + {{/link-to}} {{/if}} - {{{formatMessage commit.message short="true"}}} + {{{formatMessage commit.message short="true" repoBinding=build.repo}}} - + {{formatCommit commit}} @@ -44,15 +44,15 @@ {{#if view.isPullRequestsList}} - - #{{commit.pullRequestNumber}} + + #{{pullRequestNumber}} {{/if}} - + {{formatDuration duration}} - + {{formatTime finishedAt}} {{/view}} @@ -62,6 +62,9 @@ {{#if displayShowMoreButton}}

{{view view.ShowMoreButton}} + {{#if isLoading}} + + {{/if}}

{{/if}} {{else}} diff --git a/assets/scripts/app/templates/builds/show.hbs b/assets/scripts/app/templates/builds/show.hbs index ee75d356..969bac0a 100644 --- a/assets/scripts/app/templates/builds/show.hbs +++ b/assets/scripts/app/templates/builds/show.hbs @@ -8,49 +8,45 @@ {{#if build.id}} {{#if build.repo.slug}} - {{#linkTo build repo build}}{{build.number}}{{/linkTo}} + {{#link-to "build" repo build}}{{build.number}}{{/link-to}} {{/if}} {{/if}}
{{t builds.state}}
{{capitalize build.state}}
{{t builds.finished_at}}
-
{{formatTime build.finishedAt}}
+
{{formatTime build.finishedAt}}
{{t builds.duration}}
-
{{formatDuration build.duration}}
+
{{formatDuration build.duration}}
- {{#with build.commit}} + {{#with build}}
{{t builds.commit}}
-
{{formatCommit this}}
- {{#if ../build.pullRequest}} +
{{formatCommit commit}}
+ {{#if pullRequest}}
{{t builds.pull_request}}
-
#{{build.pullRequestNumber}} {{build.pullRequestTitle}}
+
#{{pullRequestNumber}} {{pullRequestTitle}}
{{else}} - {{#if compareUrl}} + {{#if commit.compareUrl}}
{{t builds.compare}}
-
{{pathFrom compareUrl}}
+
{{pathFrom commit.compareUrl}}
{{/if}} {{/if}} - {{#if authorName}} + {{#if commit.authorName}}
{{t builds.author}}
-
{{authorName}}
+
{{commit.authorName}}
{{/if}} - {{#if committerName}} + {{#if commit.committerName}}
{{t builds.committer}}
-
{{committerName}}
+
{{commit.committerName}}
{{/if}}
{{/with}} -
{{t builds.message}}
-
{{formatMessage build.commit.message}}
- {{#unless isMatrix}} -
{{t builds.config}}
-
{{formatConfig build.config}}
- {{/unless}} +
{{t builds.message}}
+
{{formatMessage build.commit.message repoBinding=build.repo}}
{{#unless build.isMatrix}} diff --git a/assets/scripts/app/templates/components/travis-switch.hbs b/assets/scripts/app/templates/components/travis-switch.hbs new file mode 100644 index 00000000..e7b20837 --- /dev/null +++ b/assets/scripts/app/templates/components/travis-switch.hbs @@ -0,0 +1,5 @@ +{{#if active}} + ON +{{else}} + OFF +{{/if}} diff --git a/assets/scripts/app/templates/first_sync.hbs b/assets/scripts/app/templates/first_sync.hbs new file mode 100644 index 00000000..16d217bb --- /dev/null +++ b/assets/scripts/app/templates/first_sync.hbs @@ -0,0 +1,29 @@ +
+ + +

One more thing

+

Just a few more seconds as we talk to GitHub to find out which repositories belong to you.

+ +
+
+

+ If you're part of an organization that already has repositories set up on Travis CI, we'll take you to the list once we're done. +

+
+ +
+

+ If you're not part of any existing organizations yet, we'll take you to a handy getting started guide to get you off the ground quickly. +

+
+
+
+ {{#unless isSyncing}} + +

Great news!

+

+ We've successfully synchronized your details from GitHub. We will redirect you to your profile in a few seconds. +

+ {{/unless}} +
+
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/list.hbs b/assets/scripts/app/templates/jobs/list.hbs index 71aca67c..22314a35 100644 --- a/assets/scripts/app/templates/jobs/list.hbs +++ b/assets/scripts/app/templates/jobs/list.hbs @@ -8,7 +8,7 @@ {{/if}} @@ -25,14 +25,14 @@ {{#if job.id}} {{#if job.repo.slug}} - {{#linkTo "job" repo job}}{{number}}{{/linkTo}} + {{#link-to "job" repo job}}{{number}}{{/link-to}} {{/if}} {{/if}} - - {{#each value in configValues}} @@ -45,7 +45,7 @@ {{#unless view.required}}
{{t jobs.allowed_failures}} - +
+ {{formatDuration duration}} + {{formatTime finishedAt}}
Source:
" + escapeText( source ) + "
"; + } + } + runLoggingCallbacks( "log", QUnit, details ); + config.current.assertions.push({ + result: result, + message: msg + }); + }, + + /** + * Assert that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * @name equal + * @function + * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); + */ + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected == actual, actual, expected, message ); + }, + + /** + * @name notEqual + * @function + */ + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected != actual, actual, expected, message ); + }, + + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name deepEqual + * @function + */ + deepEqual: function( actual, expected, message ) { + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notDeepEqual + * @function + */ + notDeepEqual: function( actual, expected, message ) { + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name strictEqual + * @function + */ + strictEqual: function( actual, expected, message ) { + QUnit.push( expected === actual, actual, expected, message ); + }, + + /** + * @name notStrictEqual + * @function + */ + notStrictEqual: function( actual, expected, message ) { + QUnit.push( expected !== actual, actual, expected, message ); + }, + + "throws": function( block, expected, message ) { + var actual, + expectedOutput = expected, + ok = false; + + // 'expected' is optional + if ( typeof expected === "string" ) { + message = expected; + expected = null; + } + + config.current.ignoreGlobalErrors = true; + try { + block.call( config.current.testEnvironment ); + } catch (e) { + actual = e; + } + config.current.ignoreGlobalErrors = false; + + if ( actual ) { + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + // expected is a regexp + } else if ( QUnit.objectType( expected ) === "regexp" ) { + ok = expected.test( errorString( actual ) ); + // expected is a constructor + } else if ( actual instanceof expected ) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + + QUnit.push( ok, actual, expectedOutput, message ); + } else { + QUnit.pushFailure( message, null, "No exception was thrown." ); + } + } +}; + +/** + * @deprecated since 1.8.0 + * Kept assertion helpers in root for backwards compatibility. + */ +extend( QUnit, assert ); + +/** + * @deprecated since 1.9.0 + * Kept root "raises()" for backwards compatibility. + * (Note that we don't introduce assert.raises). + */ +QUnit.raises = assert[ "throws" ]; + +/** + * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.equals = function() { + QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); +}; +QUnit.same = function() { + QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); +}; + +// We want access to the constructor's prototype +(function() { + function F() {} + F.prototype = QUnit; + QUnit = new F(); + // Make F QUnit's constructor so that we can add to the prototype later + QUnit.constructor = F; +}()); + +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true, + + // when enabled, show only failing tests + // gets persisted through sessionStorage and can be changed in UI via checkbox + hidepassed: false, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + // when enabled, all tests must call expect() + requireExpects: false, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: {}, + + // logging callback queues + begin: [], + done: [], + log: [], + testStart: [], + testDone: [], + moduleStart: [], + moduleDone: [] +}; + +// Export global variables, unless an 'exports' object exists, +// in that case we assume we're in CommonJS (dealt with on the bottom of the script) +if ( typeof exports === "undefined" ) { + extend( window, QUnit.constructor.prototype ); + + // Expose QUnit object + window.QUnit = QUnit; +} + +// Initialize more QUnit.config and QUnit.urlParams +(function() { + var i, + location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}, + current; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + urlParams[ current[ 0 ] ] = current[ 1 ]; + } + } + + QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName + config.filter = urlParams.filter; + + // Exact match of the module name + config.module = urlParams.module; + + config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = location.protocol === "file:"; +}()); + +// Extend QUnit object, +// these after set here because they should not be exposed as global functions +extend( QUnit, { + assert: assert, + + config: config, + + // Initialize the configuration options + init: function() { + extend( config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date(), + updateRate: 1000, + blocking: false, + autostart: true, + autorun: false, + filter: "", + queue: [], + semaphore: 1 + }); + + var tests, banner, result, + qunit = id( "qunit" ); + + if ( qunit ) { + qunit.innerHTML = + "

" + escapeText( document.title ) + "

" + + "

" + + "
" + + "

" + + "
    "; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
     "; + } + }, + + // Resets the test setup. Useful for tests that modify the DOM. + /* + DEPRECATED: Use multiple tests instead of resetting inside a test. + Use testStart or testDone for custom cleanup. + This method will throw an error in 2.0, and will be removed in 2.1 + */ + reset: function() { + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + fixture.innerHTML = config.fixture; + } + }, + + // Trigger an event on an element. + // @example triggerEvent( document.body, "click" ); + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent( "MouseEvents" ); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + + elem.dispatchEvent( event ); + } else if ( elem.fireEvent ) { + elem.fireEvent( "on" + type ); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) === type; + }, + + objectType: function( obj ) { + if ( typeof obj === "undefined" ) { + return "undefined"; + // consider: typeof null === object + } + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; + + switch ( type ) { + case "Number": + if ( isNaN(obj) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Date": + case "RegExp": + case "Function": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } + return undefined; + }, + + push: function( result, actual, expected, message ) { + if ( !config.current ) { + throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); + } + + var output, source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeText( message ) || ( result ? "okay" : "failed" ); + message = "" + message + ""; + output = message; + + if ( !result ) { + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); + output += ""; + + if ( actual !== expected ) { + output += ""; + output += ""; + } + + source = sourceFromStacktrace(); + + if ( source ) { + details.source = source; + output += ""; + } + + output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; + } + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + pushFailure: function( message, source, actual ) { + if ( !config.current ) { + throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + + var output, + details = { + module: config.current.module, + name: config.current.testName, + result: false, + message: message + }; + + message = escapeText( message ) || "error"; + message = "" + message + ""; + output = message; + + output += ""; + + if ( actual ) { + output += ""; + } + + if ( source ) { + details.source = source; + output += ""; + } + + output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: false, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var key, + querystring = "?"; + + for ( key in params ) { + if ( hasOwn.call( params, key ) ) { + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } + } + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); + }, + + extend: extend, + id: id, + addEvent: addEvent, + addClass: addClass, + hasClass: hasClass, + removeClass: removeClass + // load, equiv, jsDump, diff: Attached later +}); + +/** + * @deprecated: Created for backwards compatibility with test runner that set the hook function + * into QUnit.{hook}, instead of invoking it and passing the hook function. + * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. + * Doing this allows us to tell if the following methods have been overwritten on the actual + * QUnit object. + */ +extend( QUnit.constructor.prototype, { + + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: registerLoggingCallback( "begin" ), + + // done: { failed, passed, total, runtime } + done: registerLoggingCallback( "done" ), + + // log: { result, actual, expected, message } + log: registerLoggingCallback( "log" ), + + // testStart: { name } + testStart: registerLoggingCallback( "testStart" ), + + // testDone: { name, failed, passed, total, duration } + testDone: registerLoggingCallback( "testDone" ), + + // moduleStart: { name } + moduleStart: registerLoggingCallback( "moduleStart" ), + + // moduleDone: { name, failed, passed, total } + moduleDone: registerLoggingCallback( "moduleDone" ) +}); + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +QUnit.load = function() { + runLoggingCallbacks( "begin", QUnit, {} ); + + // Initialize the config, saving the execution queue + var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, + urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, + numModules = 0, + moduleNames = [], + moduleFilterHtml = "", + urlConfigHtml = "", + oldconfig = extend( {}, config ); + + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + len = config.urlConfig.length; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[i]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val, + tooltip: "[no tooltip available]" + }; + } + config[ val.id ] = QUnit.urlParams[ val.id ]; + urlConfigHtml += ""; + } + for ( i in config.modules ) { + if ( config.modules.hasOwnProperty( i ) ) { + moduleNames.push(i); + } + } + numModules = moduleNames.length; + moduleNames.sort( function( a, b ) { + return a.localeCompare( b ); + }); + moduleFilterHtml += ""; + + // `userAgent` initialized at top of scope + userAgent = id( "qunit-userAgent" ); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + + // `banner` initialized at top of scope + banner = id( "qunit-header" ); + if ( banner ) { + banner.innerHTML = "" + banner.innerHTML + " "; + } + + // `toolbar` initialized at top of scope + toolbar = id( "qunit-testrunner-toolbar" ); + if ( toolbar ) { + // `filter` initialized at top of scope + filter = document.createElement( "input" ); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + + addEvent( filter, "click", function() { + var tmp, + ol = document.getElementById( "qunit-tests" ); + + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace( / hidepass /, " " ); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); + } else { + sessionStorage.removeItem( "qunit-filter-passed-tests" ); + } + } + }); + + if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { + filter.checked = true; + // `ol` initialized at top of scope + ol = document.getElementById( "qunit-tests" ); + ol.className = ol.className + " hidepass"; + } + toolbar.appendChild( filter ); + + // `label` initialized at top of scope + label = document.createElement( "label" ); + label.setAttribute( "for", "qunit-filter-pass" ); + label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + + urlConfigCheckboxesContainer = document.createElement("span"); + urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; + urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" + // * Fallback from event.target to event.srcElement + addEvents( urlConfigCheckboxes, "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? true : undefined; + window.location = QUnit.url( params ); + }); + toolbar.appendChild( urlConfigCheckboxesContainer ); + + if (numModules > 1) { + moduleFilter = document.createElement( "span" ); + moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); + moduleFilter.innerHTML = moduleFilterHtml; + addEvent( moduleFilter.lastChild, "change", function() { + var selectBox = moduleFilter.getElementsByTagName("select")[0], + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + + window.location = QUnit.url({ + module: ( selectedModule === "" ) ? undefined : selectedModule, + // Remove any existing filters + filter: undefined, + testNumber: undefined + }); + }); + toolbar.appendChild(moduleFilter); + } + } + + // `main` initialized at top of scope + main = id( "qunit-fixture" ); + if ( main ) { + config.fixture = main.innerHTML; + } + + if ( config.autostart ) { + QUnit.start(); + } +}; + +addEvent( window, "load", QUnit.load ); + +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will suppress the default browser handler, +// returning false will let it run. +window.onerror = function ( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend( function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: validTest } ) ); + } + return false; + } + + return ret; +}; + +function done() { + config.autorun = true; + + // Log the last module results + if ( config.currentModule ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.currentModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + delete config.previousModule; + + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + runtime = +new Date() - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + "Tests completed in ", + runtime, + " milliseconds.
    ", + "", + passed, + " assertions of ", + config.stats.all, + " passed, ", + config.stats.bad, + " failed." + ].join( "" ); + + if ( banner ) { + banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( config.altertitle && typeof document !== "undefined" && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( config.stats.bad ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { + // `key` & `i` initialized at top of scope + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } + } + } + + // scroll back to top to show results + if ( window.scrollTo ) { + window.scrollTo(0, 0); + } + + runLoggingCallbacks( "done", QUnit, { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); +} + +/** @return Boolean: true if this test should be ran */ +function validTest( test ) { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = config.module && config.module.toLowerCase(), + fullName = (test.module + ": " + test.testName).toLowerCase(); + + // Internally-generated tests are always valid + if ( test.callback && test.callback.validTest === validTest ) { + delete test.callback.validTest; + return true; + } + + if ( config.testNumber ) { + return test.testNumber === config.testNumber; + } + + if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { + return false; + } + + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; + } + + // Otherwise, do the opposite + return !include; +} + +// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) +// Later Safari and IE10 are supposed to support error.stack as well +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 3 : offset; + + var stack, include, i; + + if ( e.stacktrace ) { + // Opera + return e.stacktrace.split( "\n" )[ offset + 3 ]; + } else if ( e.stack ) { + // Firefox, Chrome + stack = e.stack.split( "\n" ); + if (/^error$/i.test( stack[0] ) ) { + stack.shift(); + } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } + return stack[ offset ]; + } else if ( e.sourceURL ) { + // Safari, PhantomJS + // hopefully one day Safari provides actual stacktraces + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} +function sourceFromStacktrace( offset ) { + try { + throw new Error(); + } catch ( e ) { + return extractStacktrace( e, offset ); + } +} + +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { + if ( !s ) { + return ""; + } + s = s + ""; + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch( s ) { + case "'": + return "'"; + case "\"": + return """; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + } + }); +} + +function synchronize( callback, last ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +function process( last ) { + function next() { + process( last ); + } + var start = new Date().getTime(); + config.depth = config.depth ? config.depth + 1 : 1; + + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { + config.queue.shift()(); + } else { + setTimeout( next, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + if ( hasOwn.call( window, key ) ) { + // in Opera sometimes DOM element ids show up here, ignore them + if ( /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } + } + } +} + +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + + saveGlobal(); + + newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); + } + + deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice( i, 1 ); + i--; + break; + } + } + } + return result; +} + +function extend( a, b ) { + for ( var prop in b ) { + if ( hasOwn.call( b, prop ) ) { + // Avoid "Member not found" error in IE8 caused by messing with window.constructor + if ( !( prop === "constructor" && a === window ) ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else { + a[ prop ] = b[ prop ]; + } + } + } + } + + return a; +} + +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + // Standards-based browsers + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + // IE + } else { + elem.attachEvent( "on" + type, fn ); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); + } + // If possible, trim it for prettiness, but not necessarily + elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); +} + +function id( name ) { + return !!( typeof document !== "undefined" && document && document.getElementById ) && + document.getElementById( name ); +} + +function registerLoggingCallback( key ) { + return function( callback ) { + config[key].push( callback ); + }; +} + +// Supports deprecated method of completely overwriting logging callbacks +function runLoggingCallbacks( key, scope, args ) { + var i, callbacks; + if ( QUnit.hasOwnProperty( key ) ) { + QUnit[ key ].call(scope, args ); + } else { + callbacks = config[ key ]; + for ( i = 0; i < callbacks.length; i++ ) { + callbacks[ i ].call( scope, args ); + } + } +} + +// Test for equality any JavaScript type. +// Author: Philippe Rathé +QUnit.equiv = (function() { + + // Call the o related callback with the given arguments. + function bindCallbacks( o, callbacks, args ) { + var prop = QUnit.objectType( o ); + if ( prop ) { + if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { + return callbacks[ prop ].apply( callbacks, args ); + } else { + return callbacks[ prop ]; // or undefined + } + } + } + + // the real equiv function + var innerEquiv, + // stack to decide between skip/abort functions + callers = [], + // stack to avoiding loops from circular referencing + parents = [], + parentsB = [], + + getProto = Object.getPrototypeOf || function ( obj ) { + /*jshint camelcase:false */ + return obj.__proto__; + }, + callbacks = (function () { + + // for string, boolean, number and null + function useStrictEquality( b, a ) { + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { + // to catch short annotation VS 'new' annotation of a + // declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function( b ) { + return isNaN( b ); + }, + + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && + // the regex itself + a.source === b.source && + // and its modifiers + a.global === b.global && + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[callers.length - 1]; + return caller !== Object && typeof caller !== "undefined"; + }, + + "array": function( b, a ) { + var i, j, len, loop, aCircular, bCircular; + + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } + + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[j] === a[i]; + bCircular = parentsB[j] === b[i]; + if ( aCircular || bCircular ) { + if ( a[i] === b[i] || aCircular && bCircular ) { + loop = true; + } else { + parents.pop(); + parentsB.pop(); + return false; + } + } + } + if ( !loop && !innerEquiv(a[i], b[i]) ) { + parents.pop(); + parentsB.pop(); + return false; + } + } + parents.pop(); + parentsB.pop(); + return true; + }, + + "object": function( b, a ) { + /*jshint forin:false */ + var i, j, loop, aCircular, bCircular, + // Default to true + eq = true, + aProperties = [], + bProperties = []; + + // comparing constructors is more strict than using + // instanceof + if ( a.constructor !== b.constructor ) { + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || + ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { + return false; + } + } + + // stack constructor before traversing properties + callers.push( a.constructor ); + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + + // be strict: don't ensure hasOwnProperty and go deep + for ( i in a ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[j] === a[i]; + bCircular = parentsB[j] === b[i]; + if ( aCircular || bCircular ) { + if ( a[i] === b[i] || aCircular && bCircular ) { + loop = true; + } else { + eq = false; + break; + } + } + } + aProperties.push(i); + if ( !loop && !innerEquiv(a[i], b[i]) ) { + eq = false; + break; + } + } + + parents.pop(); + parentsB.pop(); + callers.pop(); // unstack, we are done + + for ( i in b ) { + bProperties.push( i ); // collect b's properties + } + + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + }()); + + innerEquiv = function() { // can take multiple arguments + var args = [].slice.apply( arguments ); + if ( args.length < 2 ) { + return true; // end transition + } + + return (function( a, b ) { + if ( a === b ) { + return true; // catch the most you can + } else if ( a === null || b === null || typeof a === "undefined" || + typeof b === "undefined" || + QUnit.objectType(a) !== QUnit.objectType(b) ) { + return false; // don't lose time with error prone cases + } else { + return bindCallbacks(a, callbacks, [ b, a ]); + } + + // apply transition with (1..n) arguments + }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); + }; + + return innerEquiv; +}()); + +/** + * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | + * http://flesler.blogspot.com Licensed under BSD + * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 + * + * @projectDescription Advanced and extensible data dumping for Javascript. + * @version 1.0.0 + * @author Ariel Flesler + * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} + */ +QUnit.jsDump = (function() { + function quote( str ) { + return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; + } + function literal( o ) { + return o + ""; + } + function join( pre, arr, post ) { + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if ( arr.join ) { + arr = arr.join( "," + s + inner ); + } + if ( !arr ) { + return pre + post; + } + return [ pre, inner + arr, base + post ].join(s); + } + function array( arr, stack ) { + var i = arr.length, ret = new Array(i); + this.up(); + while ( i-- ) { + ret[i] = this.parse( arr[i] , undefined , stack); + } + this.down(); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + jsDump = { + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { + stack = stack || [ ]; + var inStack, res, + parser = this.parsers[ type || this.typeOf(obj) ]; + + type = typeof parser; + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + (inStack - stack.length) + ")"; + } + if ( type === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( type === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj) ) { + type = "date"; + } else if ( QUnit.is( "function", obj) ) { + type = "function"; + } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + // native arrays + toString.call( obj ) === "[object Array]" || + // NodeList objects + ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; + }, + separator: function() { + return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; + }, + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); + } + return new Array( this.depth + ( extra || 0 ) ).join(chr); + }, + up: function( a ) { + this.depth += a || 1; + }, + down: function( a ) { + this.depth -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + depth: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + /*jshint forin:false */ + var ret = [ ], keys, key, val, i; + QUnit.jsDump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); + } + QUnit.jsDump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = QUnit.jsDump.HTML ? "<" : "<", + close = QUnit.jsDump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } + } + } + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } + + args = new Array(l); + while ( l-- ) { + // 97 is 'a' + args[l] = String.fromCharCode(97+l); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; + + return jsDump; +}()); + +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +/* + * Javascript Diff Algorithm + * By John Resig (http://ejohn.org/) + * Modified by Chu Alan "sprite" + * + * Released under the MIT license. + * + * More Info: + * http://ejohn.org/projects/javascript-diff-algorithm/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" + */ +QUnit.diff = (function() { + /*jshint eqeqeq:false, eqnull:true */ + function diff( o, n ) { + var i, + ns = {}, + os = {}; + + for ( i = 0; i < n.length; i++ ) { + if ( !hasOwn.call( ns, n[i] ) ) { + ns[ n[i] ] = { + rows: [], + o: null + }; + } + ns[ n[i] ].rows.push( i ); + } + + for ( i = 0; i < o.length; i++ ) { + if ( !hasOwn.call( os, o[i] ) ) { + os[ o[i] ] = { + rows: [], + n: null + }; + } + os[ o[i] ].rows.push( i ); + } + + for ( i in ns ) { + if ( hasOwn.call( ns, i ) ) { + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { + n[ ns[i].rows[0] ] = { + text: n[ ns[i].rows[0] ], + row: os[i].rows[0] + }; + o[ os[i].rows[0] ] = { + text: o[ os[i].rows[0] ], + row: ns[i].rows[0] + }; + } + } + } + + for ( i = 0; i < n.length - 1; i++ ) { + if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && + n[ i + 1 ] == o[ n[i].row + 1 ] ) { + + n[ i + 1 ] = { + text: n[ i + 1 ], + row: n[i].row + 1 + }; + o[ n[i].row + 1 ] = { + text: o[ n[i].row + 1 ], + row: i + 1 + }; + } + } + + for ( i = n.length - 1; i > 0; i-- ) { + if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && + n[ i - 1 ] == o[ n[i].row - 1 ]) { + + n[ i - 1 ] = { + text: n[ i - 1 ], + row: n[i].row - 1 + }; + o[ n[i].row - 1 ] = { + text: o[ n[i].row - 1 ], + row: i - 1 + }; + } + } + + return { + o: o, + n: n + }; + } + + return function( o, n ) { + o = o.replace( /\s+$/, "" ); + n = n.replace( /\s+$/, "" ); + + var i, pre, + str = "", + out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), + oSpace = o.match(/\s+/g), + nSpace = n.match(/\s+/g); + + if ( oSpace == null ) { + oSpace = [ " " ]; + } + else { + oSpace.push( " " ); + } + + if ( nSpace == null ) { + nSpace = [ " " ]; + } + else { + nSpace.push( " " ); + } + + if ( out.n.length === 0 ) { + for ( i = 0; i < out.o.length; i++ ) { + str += "" + out.o[i] + oSpace[i] + ""; + } + } + else { + if ( out.n[0].text == null ) { + for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { + str += "" + out.o[n] + oSpace[n] + ""; + } + } + + for ( i = 0; i < out.n.length; i++ ) { + if (out.n[i].text == null) { + str += "" + out.n[i] + nSpace[i] + ""; + } + else { + // `pre` initialized at top of scope + pre = ""; + + for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { + pre += "" + out.o[n] + oSpace[n] + ""; + } + str += " " + out.n[i].text + nSpace[i] + pre; + } + } + } + + return str; + }; +}()); + +// for CommonJS environments, export everything +if ( typeof exports !== "undefined" ) { + extend( exports, QUnit.constructor.prototype ); +} + +// get at whatever the global object is, like window in browsers +}( window )); diff --git a/assets/scripts/travis.coffee b/assets/scripts/travis.coffee index 152d8093..95770a69 100644 --- a/assets/scripts/travis.coffee +++ b/assets/scripts/travis.coffee @@ -32,7 +32,41 @@ Storage = Em.Object.extend clear: -> @set('storage', {}) -window.Travis = TravisApplication.create() +Ember.RecordArray.reopen + # TODO: ember.js changed a way ArrayProxies behave, so that check for content is done + # in _replace method. I should not be overriding it, because it's private, but + # there is no easy other way to do it at this point + _replace: (index, removedCount, records) -> + # in Travis it's sometimes the case that we add new records to RecordArrays + # from pusher before its content has loaded from an ajax query. In order to handle + # this case nicer I'm extending record array to buffer those records and push them + # to content when it's available + @bufferedRecords = [] unless @bufferedRecords + + if !@get('content') + for record in records + @bufferedRecords.pushObject(record) unless @bufferedRecords.contains(record) + + records = [] + + # call super only if there's anything more to add + if removedCount || records.length + @_super(index, removedCount, records) + + contentDidChange: (-> + if (content = @get('content')) && @bufferedRecords && @bufferedRecords.length + for record in @bufferedRecords + content.pushObject(record) unless content.contains(record) + @bufferedRecords = [] + ).observes('content') + +window.Travis = TravisApplication.create( + LOG_ACTIVE_GENERATION: true, + LOG_MODULE_RESOLVER: true, + LOG_TRANSITIONS: true, + LOG_TRANSITIONS_INTERNAL: true, + LOG_VIEW_LOOKUPS: true +) Travis.deferReadiness() @@ -41,11 +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' } @@ -77,10 +129,34 @@ $.extend Travis, storage )() -setupGoogleAnalytics() if Travis.config.ga_code +Travis.initializer + name: 'googleAnalytics' + + initialize: (container) -> + if Travis.config.ga_code + window._gaq = [] + _gaq.push(['_setAccount', Travis.config.ga_code]) + + ga = document.createElement('script') + ga.type = 'text/javascript' + ga.async = true + ga.src = 'https://ssl.google-analytics.com/ga.js' + s = document.getElementsByTagName('script')[0] + s.parentNode.insertBefore(ga, s) + +Travis.Router.reopen + didTransition: -> + @_super.apply @, arguments + + 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' require 'routes' require 'auth' require 'controllers' @@ -88,10 +164,10 @@ require 'helpers' require 'models' require 'pusher' require 'slider' -require 'store' require 'tailing' require 'templates' require 'views' +require 'components' require 'config/locales' diff --git a/assets/scripts/vendor/ember-data.js b/assets/scripts/vendor/ember-data.js deleted file mode 100644 index 4b83ba82..00000000 --- a/assets/scripts/vendor/ember-data.js +++ /dev/null @@ -1,8328 +0,0 @@ -// Last commit: 0c516e4 (2013-03-08 15:59:48 +0100) - - -(function() { -window.DS = Ember.Namespace.create({ - // this one goes past 11 - CURRENT_API_REVISION: 12 -}); - -})(); - - - -(function() { -var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred - Evented = Ember.Evented, // ember-runtime/mixins/evented - run = Ember.run, // ember-metal/run-loop - get = Ember.get; // ember-metal/accessors - -var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { - init: function() { - this._super.apply(this, arguments); - this.one('didLoad', function() { - run(this, 'resolve', this); - }); - - if (get(this, 'isLoaded')) { - this.trigger('didLoad'); - } - } -}); - -DS.LoadPromise = LoadPromise; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -var LoadPromise = DS.LoadPromise; // system/mixins/load_promise - -/** - A record array is an array that contains records of a certain type. The record - array materializes records as needed when they are retrieved for the first - time. You should not create record arrays yourself. Instead, an instance of - DS.RecordArray or its subclasses will be returned by your application's store - in response to queries. -*/ - -DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { - /** - The model type contained by this record array. - - @type DS.Model - */ - type: null, - - // The array of client ids backing the record array. When a - // record is requested from the record array, the record - // for the client id at the same index is materialized, if - // necessary, by the store. - content: null, - - isLoaded: false, - isUpdating: false, - - // The store that created this record array. - store: null, - - objectAtContent: function(index) { - var content = get(this, 'content'), - reference = content.objectAt(index), - store = get(this, 'store'); - - if (reference) { - return store.recordForReference(reference); - } - }, - - materializedObjectAt: function(index) { - var reference = get(this, 'content').objectAt(index); - if (!reference) { return; } - - if (get(this, 'store').recordIsMaterialized(reference)) { - return this.objectAt(index); - } - }, - - update: function() { - if (get(this, 'isUpdating')) { return; } - - var store = get(this, 'store'), - type = get(this, 'type'); - - store.fetchAll(type, this); - }, - - addReference: function(reference) { - get(this, 'content').addObject(reference); - }, - - removeReference: function(reference) { - get(this, 'content').removeObject(reference); - } -}); - -})(); - - - -(function() { -var get = Ember.get; - -DS.FilteredRecordArray = DS.RecordArray.extend({ - filterFunction: null, - isLoaded: true, - - replace: function() { - var type = get(this, 'type').toString(); - throw new Error("The result of a client-side filter (on " + type + ") is immutable."); - }, - - updateFilter: Ember.observer(function() { - var store = get(this, 'store'); - store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); - }, 'filterFunction') -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ - query: null, - - replace: function() { - var type = get(this, 'type').toString(); - throw new Error("The result of a server query (on " + type + ") is immutable."); - }, - - load: function(references) { - var store = get(this, 'store'), type = get(this, 'type'); - - this.beginPropertyChanges(); - set(this, 'content', Ember.A(references)); - set(this, 'isLoaded', true); - this.endPropertyChanges(); - - var self = this; - // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(function() { - self.trigger('didLoad'); - }); - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -/** - A ManyArray is a RecordArray that represents the contents of a has-many - relationship. - - The ManyArray is instantiated lazily the first time the relationship is - requested. - - ### Inverses - - Often, the relationships in Ember Data applications will have - an inverse. For example, imagine the following models are - defined: - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); - - If you created a new instance of `App.Post` and added - a `App.Comment` record to its `comments` has-many - relationship, you would expect the comment's `post` - property to be set to the post that contained - the has-many. - - We call the record to which a relationship belongs the - relationship's _owner_. -*/ -DS.ManyArray = DS.RecordArray.extend({ - init: function() { - this._super.apply(this, arguments); - this._changesToSync = Ember.OrderedSet.create(); - }, - - /** - @private - - The record to which this relationship belongs. - - @property {DS.Model} - */ - owner: null, - - // LOADING STATE - - isLoaded: false, - - loadingRecordsCount: function(count) { - this.loadingRecordsCount = count; - }, - - loadedRecord: function() { - this.loadingRecordsCount--; - if (this.loadingRecordsCount === 0) { - set(this, 'isLoaded', true); - this.trigger('didLoad'); - } - }, - - fetch: function() { - var references = get(this, 'content'), - store = get(this, 'store'), - type = get(this, 'type'), - owner = get(this, 'owner'); - - store.fetchUnloadedReferences(type, references, owner); - }, - - // Overrides Ember.Array's replace method to implement - replaceContent: function(index, removed, added) { - // Map the array of record objects into an array of client ids. - added = added.map(function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor)); - return get(record, '_reference'); - }, this); - - this._super(index, removed, added); - }, - - arrangedContentDidChange: function() { - this.fetch(); - }, - - arrayContentWillChange: function(index, removed, added) { - var owner = get(this, 'owner'), - name = get(this, 'name'); - - if (!owner._suspendedRelationships) { - // This code is the first half of code that continues inside - // of arrayContentDidChange. It gets or creates a change from - // the child object, adds the current owner as the old - // parent if this is the first time the object was removed - // from a ManyArray, and sets `newParent` to null. - // - // Later, if the object is added to another ManyArray, - // the `arrayContentDidChange` will set `newParent` on - // the change. - for (var i=index; i "created.uncommitted" - - The `DS.Model` states are themselves stateless. What we mean is that, - though each instance of a record also has a unique instance of a - `DS.StateManager`, the hierarchical states that each of *those* points - to is a shared data structure. For performance reasons, instead of each - record getting its own copy of the hierarchy of states, each state - manager points to this global, immutable shared instance. How does a - state know which record it should be acting on? We pass a reference to - the current state manager as the first parameter to every method invoked - on a state. - - The state manager passed as the first parameter is where you should stash - state about the record if needed; you should never store data on the state - object itself. If you need access to the record being acted on, you can - retrieve the state manager's `record` property. For example, if you had - an event handler `myEvent`: - - myEvent: function(manager) { - var record = manager.get('record'); - record.doSomething(); - } - - For more information about state managers in general, see the Ember.js - documentation on `Ember.StateManager`. - - ### Events, Flags, and Transitions - - A state may implement zero or more events, flags, or transitions. - - #### Events - - Events are named functions that are invoked when sent to a record. The - state manager will first look for a method with the given name on the - current state. If no method is found, it will search the current state's - parent, and then its grandparent, and so on until reaching the top of - the hierarchy. If the root is reached without an event handler being found, - an exception will be raised. This can be very helpful when debugging new - features. - - Here's an example implementation of a state with a `myEvent` event handler: - - aState: DS.State.create({ - myEvent: function(manager, param) { - console.log("Received myEvent with "+param); - } - }) - - To trigger this event: - - record.send('myEvent', 'foo'); - //=> "Received myEvent with foo" - - Note that an optional parameter can be sent to a record's `send()` method, - which will be passed as the second parameter to the event handler. - - Events should transition to a different state if appropriate. This can be - done by calling the state manager's `transitionTo()` method with a path to the - desired state. The state manager will attempt to resolve the state path - relative to the current state. If no state is found at that path, it will - attempt to resolve it relative to the current state's parent, and then its - parent, and so on until the root is reached. For example, imagine a hierarchy - like this: - - * created - * start <-- currentState - * inFlight - * updated - * inFlight - - If we are currently in the `start` state, calling - `transitionTo('inFlight')` would transition to the `created.inFlight` state, - while calling `transitionTo('updated.inFlight')` would transition to - the `updated.inFlight` state. - - Remember that *only events* should ever cause a state transition. You should - never call `transitionTo()` from outside a state's event handler. If you are - tempted to do so, create a new event and send that to the state manager. - - #### Flags - - Flags are Boolean values that can be used to introspect a record's current - state in a more user-friendly way than examining its state path. For example, - instead of doing this: - - var statePath = record.get('stateManager.currentPath'); - if (statePath === 'created.inFlight') { - doSomething(); - } - - You can say: - - if (record.get('isNew') && record.get('isSaving')) { - doSomething(); - } - - If your state does not set a value for a given flag, the value will - be inherited from its parent (or the first place in the state hierarchy - where it is defined). - - The current set of flags are defined below. If you want to add a new flag, - in addition to the area below, you will also need to declare it in the - `DS.Model` class. - - #### Transitions - - Transitions are like event handlers but are called automatically upon - entering or exiting a state. To implement a transition, just call a method - either `enter` or `exit`: - - myState: DS.State.create({ - // Gets called automatically when entering - // this state. - enter: function(manager) { - console.log("Entered myState"); - } - }) - - Note that enter and exit events are called once per transition. If the - current state changes, but changes to another child state of the parent, - the transition event on the parent will not be triggered. -*/ - -var stateProperty = Ember.computed(function(key) { - var parent = get(this, 'parentState'); - if (parent) { - return get(parent, key); - } -}).property(); - -var isEmptyObject = function(object) { - for (var name in object) { - if (object.hasOwnProperty(name)) { return false; } - } - - return true; -}; - -var hasDefinedProperties = function(object) { - for (var name in object) { - if (object.hasOwnProperty(name) && object[name]) { return true; } - } - - return false; -}; - -var didChangeData = function(manager) { - var record = get(manager, 'record'); - record.materializeData(); -}; - -var willSetProperty = function(manager, context) { - context.oldValue = get(get(manager, 'record'), context.name); - - var change = DS.AttributeChange.createChange(context); - get(manager, 'record')._changesToSync[context.attributeName] = change; -}; - -var didSetProperty = function(manager, context) { - var change = get(manager, 'record')._changesToSync[context.attributeName]; - change.value = get(get(manager, 'record'), context.name); - change.sync(); -}; - -// Whenever a property is set, recompute all dependent filters -var updateRecordArrays = function(manager) { - var record = manager.get('record'); - record.updateRecordArraysLater(); -}; - -DS.State = Ember.State.extend({ - isLoaded: stateProperty, - isReloading: stateProperty, - isDirty: stateProperty, - isSaving: stateProperty, - isDeleted: stateProperty, - isError: stateProperty, - isNew: stateProperty, - isValid: stateProperty, - - // For states that are substates of a - // DirtyState (updated or created), it is - // useful to be able to determine which - // type of dirty state it is. - dirtyType: stateProperty -}); - -// Implementation notes: -// -// Each state has a boolean value for all of the following flags: -// -// * isLoaded: The record has a populated `data` property. When a -// record is loaded via `store.find`, `isLoaded` is false -// until the adapter sets it. When a record is created locally, -// its `isLoaded` property is always true. -// * isDirty: The record has local changes that have not yet been -// saved by the adapter. This includes records that have been -// created (but not yet saved) or deleted. -// * isSaving: The record's transaction has been committed, but -// the adapter has not yet acknowledged that the changes have -// been persisted to the backend. -// * isDeleted: The record was marked for deletion. When `isDeleted` -// is true and `isDirty` is true, the record is deleted locally -// but the deletion was not yet persisted. When `isSaving` is -// true, the change is in-flight. When both `isDirty` and -// `isSaving` are false, the change has persisted. -// * isError: The adapter reported that it was unable to save -// local changes to the backend. This may also result in the -// record having its `isValid` property become false if the -// adapter reported that server-side validations failed. -// * isNew: The record was created on the client and the adapter -// did not yet report that it was successfully saved. -// * isValid: No client-side validations have failed and the -// adapter did not report any server-side validation failures. - -// The dirty state is a abstract state whose functionality is -// shared between the `created` and `updated` states. -// -// The deleted state shares the `isDirty` flag with the -// subclasses of `DirtyState`, but with a very different -// implementation. -// -// Dirty states have three child states: -// -// `uncommitted`: the store has not yet handed off the record -// to be saved. -// `inFlight`: the store has handed off the record to be saved, -// but the adapter has not yet acknowledged success. -// `invalid`: the record has invalid information and cannot be -// send to the adapter yet. -var DirtyState = DS.State.extend({ - initialState: 'uncommitted', - - // FLAGS - isDirty: true, - - // SUBSTATES - - // When a record first becomes dirty, it is `uncommitted`. - // This means that there are local pending changes, but they - // have not yet begun to be saved, and are not invalid. - uncommitted: DS.State.extend({ - // TRANSITIONS - enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameDirty(dirtyType, record); - }); - }, - - // EVENTS - willSetProperty: willSetProperty, - didSetProperty: didSetProperty, - - becomeDirty: Ember.K, - - willCommit: function(manager) { - manager.transitionTo('inFlight'); - }, - - becameClean: function(manager) { - var record = get(manager, 'record'), - dirtyType = get(this, 'dirtyType'); - - record.withTransaction(function(t) { - t.recordBecameClean(dirtyType, record); - }); - - manager.transitionTo('loaded.materializing'); - }, - - becameInvalid: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); - - manager.transitionTo('invalid'); - }, - - rollback: function(manager) { - get(manager, 'record').rollback(); - } - }), - - // Once a record has been handed off to the adapter to be - // saved, it is in the 'in flight' state. Changes to the - // record cannot be made during this window. - inFlight: DS.State.extend({ - // FLAGS - isSaving: true, - - // TRANSITIONS - enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); - }, - - // EVENTS - didCommit: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); - }); - - manager.transitionTo('saved'); - manager.send('invokeLifecycleCallbacks', dirtyType); - }, - - becameInvalid: function(manager, errors) { - var record = get(manager, 'record'); - - set(record, 'errors', errors); - - manager.transitionTo('invalid'); - manager.send('invokeLifecycleCallbacks'); - }, - - becameError: function(manager) { - manager.transitionTo('error'); - manager.send('invokeLifecycleCallbacks'); - } - }), - - // A record is in the `invalid` state when its client-side - // invalidations have failed, or if the adapter has indicated - // the the record failed server-side invalidations. - invalid: DS.State.extend({ - // FLAGS - isValid: false, - - exit: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameClean('inflight', record); - }); - }, - - // EVENTS - deleteRecord: function(manager) { - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - }, - - willSetProperty: willSetProperty, - - didSetProperty: function(manager, context) { - var record = get(manager, 'record'), - errors = get(record, 'errors'), - key = context.name; - - set(errors, key, null); - - if (!hasDefinedProperties(errors)) { - manager.send('becameValid'); - } - - didSetProperty(manager, context); - }, - - becomeDirty: Ember.K, - - rollback: function(manager) { - manager.send('becameValid'); - manager.send('rollback'); - }, - - becameValid: function(manager) { - manager.transitionTo('uncommitted'); - }, - - invokeLifecycleCallbacks: function(manager) { - var record = get(manager, 'record'); - record.trigger('becameInvalid', record); - } - }) -}); - -// The created and updated states are created outside the state -// chart so we can reopen their substates and add mixins as -// necessary. - -var createdState = DirtyState.create({ - dirtyType: 'created', - - // FLAGS - isNew: true -}); - -var updatedState = DirtyState.create({ - dirtyType: 'updated' -}); - -createdState.states.uncommitted.reopen({ - deleteRecord: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordIsMoving('created', record); - }); - - record.clearRelationships(); - manager.transitionTo('deleted.saved'); - } -}); - -createdState.states.uncommitted.reopen({ - rollback: function(manager) { - this._super(manager); - manager.transitionTo('deleted.saved'); - } -}); - -updatedState.states.uncommitted.reopen({ - deleteRecord: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordIsMoving('updated', record); - }); - - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - } -}); - -var states = { - rootState: Ember.State.create({ - // FLAGS - isLoaded: false, - isReloading: false, - isDirty: false, - isSaving: false, - isDeleted: false, - isError: false, - isNew: false, - isValid: true, - - // SUBSTATES - - // A record begins its lifecycle in the `empty` state. - // If its data will come from the adapter, it will - // transition into the `loading` state. Otherwise, if - // the record is being created on the client, it will - // transition into the `created` state. - empty: DS.State.create({ - // EVENTS - loadingData: function(manager) { - manager.transitionTo('loading'); - }, - - loadedData: function(manager) { - manager.transitionTo('loaded.created'); - } - }), - - // A record enters this state when the store askes - // the adapter for its data. It remains in this state - // until the adapter provides the requested data. - // - // Usually, this process is asynchronous, using an - // XHR to retrieve the data. - loading: DS.State.create({ - // EVENTS - loadedData: didChangeData, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing.firstTime'); - } - }), - - // A record enters this state when its data is populated. - // Most of a record's lifecycle is spent inside substates - // of the `loaded` state. - loaded: DS.State.create({ - initialState: 'saved', - - // FLAGS - isLoaded: true, - - // SUBSTATES - - materializing: DS.State.create({ - // FLAGS - isLoaded: false, - - // EVENTS - willSetProperty: Ember.K, - didSetProperty: Ember.K, - - didChangeData: didChangeData, - - finishedMaterializing: function(manager) { - manager.transitionTo('loaded.saved'); - }, - - // SUBSTATES - firstTime: DS.State.create({ - exit: function(manager) { - var record = get(manager, 'record'); - - Ember.run.once(function() { - record.trigger('didLoad'); - }); - } - }) - }), - - reloading: DS.State.create({ - // FLAGS - isReloading: true, - - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'), - store = get(record, 'store'); - - store.reloadRecord(record); - }, - - exit: function(manager) { - var record = get(manager, 'record'); - - once(record, 'trigger', 'didReload'); - }, - - // EVENTS - loadedData: didChangeData, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing'); - } - }), - - // If there are no local changes to a record, it remains - // in the `saved` state. - saved: DS.State.create({ - // EVENTS - willSetProperty: willSetProperty, - didSetProperty: didSetProperty, - - didChangeData: didChangeData, - loadedData: didChangeData, - - reloadRecord: function(manager) { - manager.transitionTo('loaded.reloading'); - }, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing'); - }, - - becomeDirty: function(manager) { - manager.transitionTo('updated'); - }, - - deleteRecord: function(manager) { - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - }, - - unloadRecord: function(manager) { - manager.transitionTo('deleted.saved'); - get(manager, 'record').clearRelationships(); - }, - - invokeLifecycleCallbacks: function(manager, dirtyType) { - var record = get(manager, 'record'); - if (dirtyType === 'created') { - record.trigger('didCreate', record); - } else { - record.trigger('didUpdate', record); - } - } - }), - - // A record is in this state after it has been locally - // created but before the adapter has indicated that - // it has been saved. - created: createdState, - - // A record is in this state if it has already been - // saved to the server, but there are new local changes - // that have not yet been saved. - updated: updatedState - }), - - // A record is in this state if it was deleted from the store. - deleted: DS.State.create({ - initialState: 'uncommitted', - dirtyType: 'deleted', - - // FLAGS - isDeleted: true, - isLoaded: true, - isDirty: true, - - // TRANSITIONS - setup: function(manager) { - var record = get(manager, 'record'), - store = get(record, 'store'); - - store.removeFromRecordArrays(record); - }, - - // SUBSTATES - - // When a record is deleted, it enters the `start` - // state. It will exit this state when the record's - // transaction starts to commit. - uncommitted: DS.State.create({ - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameDirty('deleted', record); - }); - }, - - // EVENTS - willCommit: function(manager) { - manager.transitionTo('inFlight'); - }, - - rollback: function(manager) { - get(manager, 'record').rollback(); - }, - - becomeDirty: Ember.K, - - becameClean: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('deleted', record); - }); - - manager.transitionTo('loaded.materializing'); - } - }), - - // After a record's transaction is committing, but - // before the adapter indicates that the deletion - // has saved to the server, a record is in the - // `inFlight` substate of `deleted`. - inFlight: DS.State.create({ - // FLAGS - isSaving: true, - - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'); - - record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight('deleted', record); - }); - }, - - // EVENTS - didCommit: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); - }); - - manager.transitionTo('saved'); - - manager.send('invokeLifecycleCallbacks'); - } - }), - - // Once the adapter indicates that the deletion has - // been saved, the record enters the `saved` substate - // of `deleted`. - saved: DS.State.create({ - // FLAGS - isDirty: false, - - setup: function(manager) { - var record = get(manager, 'record'), - store = get(record, 'store'); - - store.dematerializeRecord(record); - }, - - invokeLifecycleCallbacks: function(manager) { - var record = get(manager, 'record'); - record.trigger('didDelete', record); - } - }) - }), - - // If the adapter indicates that there was an unknown - // error saving a record, the record enters the `error` - // state. - error: DS.State.create({ - isError: true, - - // EVENTS - - invokeLifecycleCallbacks: function(manager) { - var record = get(manager, 'record'); - record.trigger('becameError', record); - } - }) - }) -}; - -DS.StateManager = Ember.StateManager.extend({ - record: null, - initialState: 'rootState', - states: states, - unhandledEvent: function(manager, originalEvent) { - var record = manager.get('record'), - contexts = [].slice.call(arguments, 2), - errorMessage; - errorMessage = "Attempted to handle event `" + originalEvent + "` "; - errorMessage += "on " + record.toString() + " while in state "; - errorMessage += get(manager, 'currentState.path') + ". Called with "; - errorMessage += arrayMap.call(contexts, function(context){ - return Ember.inspect(context); - }).join(', '); - throw new Ember.Error(errorMessage); - } -}); - -})(); - - - -(function() { -var LoadPromise = DS.LoadPromise; // system/mixins/load_promise - -var get = Ember.get, set = Ember.set, none = Ember.isNone, map = Ember.EnumerableUtils.map; - -var retrieveFromCurrentState = Ember.computed(function(key) { - return get(get(this, 'stateManager.currentState'), key); -}).property('stateManager.currentState'); - -DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { - isLoaded: retrieveFromCurrentState, - isReloading: retrieveFromCurrentState, - isDirty: retrieveFromCurrentState, - isSaving: retrieveFromCurrentState, - isDeleted: retrieveFromCurrentState, - isError: retrieveFromCurrentState, - isNew: retrieveFromCurrentState, - isValid: retrieveFromCurrentState, - - clientId: null, - id: null, - transaction: null, - stateManager: null, - errors: null, - - /** - Create a JSON representation of the record, using the serialization - strategy of the store's adapter. - - Available options: - - * `includeId`: `true` if the record's ID should be included in the - JSON representation. - - @param {Object} options - @returns {Object} an object whose values are primitive JSON values only - */ - serialize: function(options) { - var store = get(this, 'store'); - return store.serialize(this, options); - }, - - didLoad: Ember.K, - didReload: Ember.K, - didUpdate: Ember.K, - didCreate: Ember.K, - didDelete: Ember.K, - becameInvalid: Ember.K, - becameError: Ember.K, - - data: Ember.computed(function() { - if (!this._data) { - this.materializeData(); - } - - return this._data; - }).property(), - - materializeData: function() { - this.send('materializingData'); - - get(this, 'store').materializeData(this); - - this.suspendRelationshipObservers(function() { - this.notifyPropertyChange('data'); - }); - }, - - _data: null, - - init: function() { - this._super(); - - var stateManager = DS.StateManager.create({ record: this }); - set(this, 'stateManager', stateManager); - - this._setup(); - - stateManager.goToState('empty'); - }, - - _setup: function() { - this._relationshipChanges = {}; - this._changesToSync = {}; - }, - - send: function(name, context) { - return get(this, 'stateManager').send(name, context); - }, - - withTransaction: function(fn) { - var transaction = get(this, 'transaction'); - if (transaction) { fn(transaction); } - }, - - loadingData: function() { - this.send('loadingData'); - }, - - loadedData: function() { - this.send('loadedData'); - }, - - didChangeData: function() { - this.send('didChangeData'); - }, - - setProperty: function(key, value, oldValue) { - this.send('setProperty', { key: key, value: value, oldValue: oldValue }); - }, - - /** - Reload the record from the adapter. - - This will only work if the record has already finished loading - and has not yet been modified (`isLoaded` but not `isDirty`, - or `isSaving`). - */ - reload: function() { - this.send('reloadRecord'); - }, - - deleteRecord: function() { - this.send('deleteRecord'); - }, - - unloadRecord: function() { - Ember.assert("You can only unload a loaded, non-dirty record.", !get(this, 'isDirty')); - - this.send('unloadRecord'); - }, - - clearRelationships: function() { - this.eachRelationship(function(name, relationship) { - if (relationship.kind === 'belongsTo') { - set(this, name, null); - } else if (relationship.kind === 'hasMany') { - get(this, name).clear(); - } - }, this); - }, - - updateRecordArrays: function() { - var store = get(this, 'store'); - if (store) { - store.dataWasUpdated(this.constructor, get(this, '_reference'), this); - } - }, - - /** - If the adapter did not return a hash in response to a commit, - merge the changed attributes and relationships into the existing - saved data. - */ - adapterDidCommit: function() { - var attributes = get(this, 'data').attributes; - - get(this.constructor, 'attributes').forEach(function(name, meta) { - attributes[name] = get(this, name); - }, this); - - this.send('didCommit'); - this.updateRecordArraysLater(); - }, - - adapterDidDirty: function() { - this.send('becomeDirty'); - this.updateRecordArraysLater(); - }, - - dataDidChange: Ember.observer(function() { - var relationships = get(this.constructor, 'relationshipsByName'); - - this.updateRecordArraysLater(); - - relationships.forEach(function(name, relationship) { - if (relationship.kind === 'hasMany') { - this.hasManyDidChange(relationship.key); - } - }, this); - - this.send('finishedMaterializing'); - }, 'data'), - - hasManyDidChange: function(key) { - var cachedValue = this.cacheFor(key); - - if (cachedValue) { - var type = get(this.constructor, 'relationshipsByName').get(key).type; - var store = get(this, 'store'); - var ids = this._data.hasMany[key] || []; - - var references = map(ids, function(id) { - // if it was already a reference, return the reference - if (typeof id === 'object') { return id; } - return store.referenceForId(type, id); - }); - - set(cachedValue, 'content', Ember.A(references)); - } - }, - - updateRecordArraysLater: function() { - Ember.run.once(this, this.updateRecordArrays); - }, - - setupData: function(prematerialized) { - this._data = { - attributes: {}, - belongsTo: {}, - hasMany: {}, - id: null - }; - }, - - materializeId: function(id) { - set(this, 'id', id); - }, - - materializeAttributes: function(attributes) { - Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes); - this._data.attributes = attributes; - }, - - materializeAttribute: function(name, value) { - this._data.attributes[name] = value; - }, - - materializeHasMany: function(name, ids) { - this._data.hasMany[name] = ids; - }, - - materializeBelongsTo: function(name, id) { - this._data.belongsTo[name] = id; - }, - - rollback: function() { - this._setup(); - this.send('becameClean'); - - this.suspendRelationshipObservers(function() { - this.notifyPropertyChange('data'); - }); - }, - - toStringExtension: function() { - return get(this, 'id'); - }, - - /** - @private - - The goal of this method is to temporarily disable specific observers - that take action in response to application changes. - - This allows the system to make changes (such as materialization and - rollback) that should not trigger secondary behavior (such as setting an - inverse relationship or marking records as dirty). - - The specific implementation will likely change as Ember proper provides - better infrastructure for suspending groups of observers, and if Array - observation becomes more unified with regular observers. - */ - suspendRelationshipObservers: function(callback, binding) { - var observers = get(this.constructor, 'relationshipNames').belongsTo; - var self = this; - - try { - this._suspendedRelationships = true; - Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() { - Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() { - callback.call(binding || self); - }); - }); - } finally { - this._suspendedRelationships = false; - } - }, - - becameInFlight: function() { - }, - - // FOR USE DURING COMMIT PROCESS - - adapterDidUpdateAttribute: function(attributeName, value) { - - // If a value is passed in, update the internal attributes and clear - // the attribute cache so it picks up the new value. Otherwise, - // collapse the current value into the internal attributes because - // the adapter has acknowledged it. - if (value !== undefined) { - get(this, 'data.attributes')[attributeName] = value; - this.notifyPropertyChange(attributeName); - } else { - value = get(this, attributeName); - get(this, 'data.attributes')[attributeName] = value; - } - - this.updateRecordArraysLater(); - }, - - _reference: Ember.computed(function() { - return get(this, 'store').referenceForClientId(get(this, 'clientId')); - }), - - adapterDidInvalidate: function(errors) { - this.send('becameInvalid', errors); - }, - - adapterDidError: function() { - this.send('becameError'); - }, - - /** - @private - - Override the default event firing from Ember.Evented to - also call methods with the given name. - */ - trigger: function(name) { - Ember.tryInvoke(this, name, [].slice.call(arguments, 1)); - this._super.apply(this, arguments); - } -}); - -// Helper function to generate store aliases. -// This returns a function that invokes the named alias -// on the default store, but injects the class as the -// first parameter. -var storeAlias = function(methodName) { - return function() { - var store = get(DS, 'defaultStore'), - args = [].slice.call(arguments); - - args.unshift(this); - Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store); - return store[methodName].apply(store, args); - }; -}; - -DS.Model.reopenClass({ - isLoaded: storeAlias('recordIsLoaded'), - find: storeAlias('find'), - all: storeAlias('all'), - query: storeAlias('findQuery'), - filter: storeAlias('filter'), - - _create: DS.Model.create, - - create: function() { - throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); - }, - - createRecord: storeAlias('createRecord') -}); - -})(); - - - -(function() { -var get = Ember.get; -DS.Model.reopenClass({ - attributes: Ember.computed(function() { - var map = Ember.Map.create(); - - this.eachComputedProperty(function(name, meta) { - if (meta.isAttribute) { - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.toString(), name !== 'id'); - - meta.name = name; - map.set(name, meta); - } - }); - - return map; - }) -}); - -var AttributeChange = DS.AttributeChange = function(options) { - this.reference = options.reference; - this.store = options.store; - this.name = options.name; - this.oldValue = options.oldValue; -}; - -AttributeChange.createChange = function(options) { - return new AttributeChange(options); -}; - -AttributeChange.prototype = { - sync: function() { - this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); - - // TODO: Use this object in the commit process - this.destroy(); - }, - - destroy: function() { - delete this.store.recordForReference(this.reference)._changesToSync[this.name]; - } -}; - -DS.Model.reopen({ - eachAttribute: function(callback, binding) { - get(this.constructor, 'attributes').forEach(function(name, meta) { - callback.call(binding, name, meta); - }, binding); - }, - - attributeWillChange: Ember.beforeObserver(function(record, key) { - var reference = get(record, '_reference'), - store = get(record, 'store'); - - record.send('willSetProperty', { reference: reference, store: store, name: key }); - }), - - attributeDidChange: Ember.observer(function(record, key) { - record.send('didSetProperty', { name: key }); - }), - - getAttr: function(key, options) { - var attributes = get(this, 'data').attributes; - var value = attributes[key]; - - if (value === undefined) { - value = options.defaultValue ; - } - - return value; - } -}); - -DS.attr = function(type, options) { - options = options || {}; - - var meta = { - type: type, - isAttribute: true, - options: options - }; - - return Ember.computed(function(key, value, oldValue) { - var data; - - if (arguments.length > 1) { - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); - } else { - value = this.getAttr(key, options); - } - - return value; - // `data` is never set directly. However, it may be - // invalidated from the state manager's setData - // event. - }).property('data').meta(meta); -}; - - -})(); - - - -(function() { - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, - none = Ember.isNone; - -DS.Model.reopen({ - getBelongsTo: function(key, type, meta) { - var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - id = data[key]; - - if(!id) { - return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, id); - } - } -}); - -DS.belongsTo = function(type, options) { - Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); - - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; - - return Ember.computed(function(key, value) { - if (arguments.length === 2) { - return value === undefined ? null : value; - } - - return this.getBelongsTo(key, type, meta); - }).property('data').meta(meta); -}; - -/** - These observers observe all `belongsTo` relationships on the record. See - `relationships/ext` to see how these observers get their dependencies. - -*/ - -DS.Model.reopen({ - /** @private */ - belongsToWillChange: Ember.beforeObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var oldParent = get(record, key); - - var childReference = get(record, '_reference'), - store = get(record, 'store'); - if (oldParent){ - var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); - change.sync(); - this._changesToSync[key] = change; - } - } - }), - - /** @private */ - belongsToDidChange: Ember.immediateObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var newParent = get(record, key); - if(newParent){ - var childReference = get(record, '_reference'), - store = get(record, 'store'); - var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); - change.sync(); - if(this._changesToSync[key]){ - DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); - } - } - } - delete this._changesToSync[key]; - }) -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; -DS.Model.reopen({ - getHasMany: function(key, type, meta) { - var data = get(this, 'data').hasMany, - store = get(this, 'store'), - ids, relationship; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - ids = data[key]; - relationship = store.findMany(type, ids || [], this, meta); - set(relationship, 'owner', this); - set(relationship, 'name', key); - - return relationship; - } -}); - -var hasRelationship = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; - - return Ember.computed(function(key, value) { - return this.getHasMany(key, type, meta); - }).property().meta(meta); -}; - -DS.hasMany = function(type, options) { - Ember.assert("The type passed to DS.hasMany must be defined", !!type); - return hasRelationship(type, options); -}; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -/** - @private - - This file defines several extensions to the base `DS.Model` class that - add support for one-to-many relationships. -*/ - -DS.Model.reopen({ - // This Ember.js hook allows an object to be notified when a property - // is defined. - // - // In this case, we use it to be notified when an Ember Data user defines a - // belongs-to relationship. In that case, we need to set up observers for - // each one, allowing us to track relationship changes and automatically - // reflect changes in the inverse has-many array. - // - // This hook passes the class being set up, as well as the key and value - // being defined. So, for example, when the user does this: - // - // DS.Model.extend({ - // parent: DS.belongsTo(App.User) - // }); - // - // This hook would be called with "parent" as the key and the computed - // property returned by `DS.belongsTo` as the value. - didDefineProperty: function(proto, key, value) { - // Check if the value being set is a computed property. - if (value instanceof Ember.Descriptor) { - - // If it is, get the metadata for the relationship. This is - // populated by the `DS.belongsTo` helper when it is creating - // the computed property. - var meta = value.meta(); - - if (meta.isRelationship && meta.kind === 'belongsTo') { - Ember.addObserver(proto, key, null, 'belongsToDidChange'); - Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); - } - - if (meta.isAttribute) { - Ember.addObserver(proto, key, null, 'attributeDidChange'); - Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); - } - - meta.parentType = proto.constructor; - } - } -}); - -/** - These DS.Model extensions add class methods that provide relationship - introspection abilities about relationships. - - A note about the computed properties contained here: - - **These properties are effectively sealed once called for the first time.** - To avoid repeatedly doing expensive iteration over a model's fields, these - values are computed once and then cached for the remainder of the runtime of - your application. - - If your application needs to modify a class after its initial definition - (for example, using `reopen()` to add additional attributes), make sure you - do it before using your model with the store, which uses these properties - extensively. -*/ - -DS.Model.reopenClass({ - /** - For a given relationship name, returns the model type of the relationship. - - For example, if you define a model like this: - - App.Post = DS.Model.extend({ - comments: DS.hasMany(App.Comment) - }); - - Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. - - @param {String} name the name of the relationship - @return {subclass of DS.Model} the type of the relationship, or undefined - */ - typeForRelationship: function(name) { - var relationship = get(this, 'relationshipsByName').get(name); - return relationship && relationship.type; - }, - - /** - The model's relationships as a map, keyed on the type of the - relationship. The value of each entry is an array containing a descriptor - for each relationship with that type, describing the name of the relationship - as well as the type. - - For example, given the following model definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); - - This computed property would return a map describing these - relationships, like this: - - var relationships = Ember.get(App.Blog, 'relationships'); - associatons.get(App.User); - //=> [ { name: 'users', kind: 'hasMany' }, - // { name: 'owner', kind: 'belongsTo' } ] - relationships.get(App.Post); - //=> [ { name: 'posts', kind: 'hasMany' } ] - - @type Ember.Map - @readOnly - */ - relationships: Ember.computed(function() { - var map = new Ember.MapWithDefault({ - defaultValue: function() { return []; } - }); - - // Loop through each computed property on the class - this.eachComputedProperty(function(name, meta) { - - // If the computed property is a relationship, add - // it to the map. - if (meta.isRelationship) { - if (typeof meta.type === 'string') { - meta.type = Ember.get(Ember.lookup, meta.type); - } - - var relationshipsForType = map.get(meta.type); - - relationshipsForType.push({ name: name, kind: meta.kind }); - } - }); - - return map; - }), - - /** - A hash containing lists of the model's relationships, grouped - by the relationship kind. For example, given a model with this - definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - - posts: DS.hasMany(App.Post) - }); - - This property would contain the following: - - var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); - relationshipNames.hasMany; - //=> ['users', 'posts'] - relationshipNames.belongsTo; - //=> ['owner'] - - @type Object - @readOnly - */ - relationshipNames: Ember.computed(function() { - var names = { hasMany: [], belongsTo: [] }; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - names[meta.kind].push(name); - } - }); - - return names; - }), - - /** - An array of types directly related to a model. Each type will be - included once, regardless of the number of relationships it has with - the model. - - For example, given a model with this definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); - - This property would contain the following: - - var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); - //=> [ App.User, App.Post ] - - @type Ember.Array - @readOnly - */ - relatedTypes: Ember.computed(function() { - var type, - types = Ember.A([]); - - // Loop through each computed property on the class, - // and create an array of the unique types involved - // in relationships - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - type = meta.type; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - if (!types.contains(type)) { - types.push(type); - } - } - }); - - return types; - }), - - /** - A map whose keys are the relationships of a model and whose values are - relationship descriptors. - - For example, given a model with this - definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - - posts: DS.hasMany(App.Post) - }); - - This property would contain the following: - - var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); - relationshipsByName.get('users'); - //=> { key: 'users', kind: 'hasMany', type: App.User } - relationshipsByName.get('owner'); - //=> { key: 'owner', kind: 'belongsTo', type: App.User } - - @type Ember.Map - @readOnly - */ - relationshipsByName: Ember.computed(function() { - var map = Ember.Map.create(), type; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - meta.key = name; - type = meta.type; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - meta.type = type; - } - - map.set(name, meta); - } - }); - - return map; - }), - - /** - A map whose keys are the fields of the model and whose values are strings - describing the kind of the field. A model's fields are the union of all of its - attributes and relationships. - - For example: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - - posts: DS.hasMany(App.Post), - - title: DS.attr('string') - }); - - var fields = Ember.get(App.Blog, 'fields'); - fields.forEach(function(field, kind) { - console.log(field, kind); - }); - - // prints: - // users, hasMany - // owner, belongsTo - // posts, hasMany - // title, attribute - - @type Ember.Map - @readOnly - */ - fields: Ember.computed(function() { - var map = Ember.Map.create(), type; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - map.set(name, meta.kind); - } else if (meta.isAttribute) { - map.set(name, 'attribute'); - } - }); - - return map; - }), - - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - get(this, 'relationshipsByName').forEach(function(name, relationship) { - callback.call(binding, name, relationship); - }); - }, - - /** - Given a callback, iterates over each of the types related to a model, - invoking the callback with the related type's class. Each type will be - returned just once, regardless of how many different relationships it has - with a model. - - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelatedType: function(callback, binding) { - get(this, 'relatedTypes').forEach(function(type) { - callback.call(binding, type); - }); - } -}); - -DS.Model.reopen({ - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - this.constructor.eachRelationship(callback, binding); - } -}); - -/** - @private - - Helper method to look up the name of the inverse of a relationship. - - In a has-many relationship, there are always two sides: the `belongsTo` side - and the `hasMany` side. When one side changes, the other side should be updated - automatically. - - Given a model, the model of the inverse, and the kind of the relationship, this - helper returns the name of the relationship on the inverse. - - For example, imagine the following two associated models: - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); - - If the `post` property of a `Comment` was modified, Ember Data would invoke - this helper like this: - - DS._inverseNameFor(App.Comment, App.Post, 'hasMany'); - //=> 'comments' - - Ember Data uses the name of the relationship returned to reflect the changed - relationship on the other side. -*/ -DS._inverseRelationshipFor = function(modelType, inverseModelType) { - var relationshipMap = get(modelType, 'relationships'), - possibleRelationships = relationshipMap.get(inverseModelType), - possible, actual, oldValue; - - if (!possibleRelationships) { return; } - if (possibleRelationships.length > 1) { return; } - return possibleRelationships[0]; -}; - -/** - @private - - Given a model and a relationship name, returns the model type of - the named relationship. - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - DS._inverseTypeFor(App.Post, 'comments'); - //=> App.Comment - @param {DS.Model class} modelType - @param {String} relationshipName - @return {DS.Model class} -*/ -DS._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; -var forEach = Ember.EnumerableUtils.forEach; - -DS.RelationshipChange = function(options) { - this.parentReference = options.parentReference; - this.childReference = options.childReference; - this.firstRecordReference = options.firstRecordReference; - this.firstRecordKind = options.firstRecordKind; - this.firstRecordName = options.firstRecordName; - this.secondRecordReference = options.secondRecordReference; - this.secondRecordKind = options.secondRecordKind; - this.secondRecordName = options.secondRecordName; - this.store = options.store; - this.committed = {}; - this.changeType = options.changeType; -}; - -DS.RelationshipChangeAdd = function(options){ - DS.RelationshipChange.call(this, options); -}; - -DS.RelationshipChangeRemove = function(options){ - DS.RelationshipChange.call(this, options); -}; - -/** @private */ -DS.RelationshipChange.create = function(options) { - return new DS.RelationshipChange(options); -}; - -/** @private */ -DS.RelationshipChangeAdd.create = function(options) { - return new DS.RelationshipChangeAdd(options); -}; - -/** @private */ -DS.RelationshipChangeRemove.create = function(options) { - return new DS.RelationshipChangeRemove(options); -}; - -DS.OneToManyChange = {}; -DS.OneToNoneChange = {}; -DS.ManyToNoneChange = {}; -DS.OneToOneChange = {}; -DS.ManyToManyChange = {}; - -DS.RelationshipChange._createChange = function(options){ - if(options.changeType === "add"){ - return DS.RelationshipChangeAdd.create(options); - } - if(options.changeType === "remove"){ - return DS.RelationshipChangeRemove.create(options); - } -}; - - -DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ - var knownKey = knownSide.key, key, type, otherContainerType,assoc; - var knownContainerType = knownSide.kind; - var options = recordType.metaForProperty(knownKey).options; - var otherType = DS._inverseTypeFor(recordType, knownKey); - - if(options.inverse){ - key = options.inverse; - otherContainerType = get(otherType, 'relationshipsByName').get(key).kind; - } - else if(assoc = DS._inverseRelationshipFor(otherType, recordType)){ - key = assoc.name; - otherContainerType = assoc.kind; - } - if(!key){ - return knownContainerType === "belongsTo" ? "oneToNone" : "manyToNone"; - } - else{ - if(otherContainerType === "belongsTo"){ - return knownContainerType === "belongsTo" ? "oneToOne" : "manyToOne"; - } - else{ - return knownContainerType === "belongsTo" ? "oneToMany" : "manyToMany"; - } - } - -}; - -DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ - // Get the type of the child based on the child's client ID - var firstRecordType = firstRecordReference.type, key, changeType; - changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); - if (changeType === "oneToMany"){ - return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToOne"){ - return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); - } - else if (changeType === "oneToNone"){ - return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToNone"){ - return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "oneToOne"){ - return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToMany"){ - return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } -}; - -/** @private */ -DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - store: store, - changeType: options.changeType, - firstRecordName: key, - firstRecordKind: "belongsTo" - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - return change; -}; - -/** @private */ -DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: childReference, - childReference: parentReference, - secondRecordReference: childReference, - store: store, - changeType: options.changeType, - secondRecordName: options.key, - secondRecordKind: "hasMany" - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - return change; -}; - - -/** @private */ -DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; - - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - key = options.key; - - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "hasMany", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - - return change; -}; - -/** @private */ -DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; - - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = inverseBelongsToName(options.parentType, childType, options.key); - //DS.OneToOneChange.maintainInvariant( options, store, childReference, key ); - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } - - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "belongsTo", - store: store, - changeType: options.changeType, - firstRecordName: key - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - - return change; -}; - -DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key - }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; - -/** @private */ -DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; - - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = inverseBelongsToName(options.parentType, childType, options.key); - DS.OneToManyChange.maintainInvariant( options, store, childReference, key ); - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } - - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - - return change; -}; - - -DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key - }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; - -DS.OneToManyChange.ensureSameTransaction = function(changes, store){ - var records = Ember.A(); - forEach(changes, function(change){ - records.addObject(change.getSecondRecord()); - records.addObject(change.getFirstRecord()); - }); - var transaction = store.ensureSameTransaction(records); - forEach(changes, function(change){ - change.transaction = transaction; - }); -}; - -DS.RelationshipChange.prototype = { - - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; - - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } - - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } - - return name; - }, - - /** - Get the name of the relationship on the belongsTo side. - - @returns {String} - */ - getFirstRecordName: function() { - var name = this.firstRecordName, store = this.store, parent, child; - - if (!name) { - parent = this.secondRecordReference; - child = this.firstRecordReference; - if (!(child && parent)) { return; } - - name = DS._inverseRelationshipFor(child.type, parent.type).name; - - this.firstRecordName = name; - } - - return name; - }, - - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; - - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); - - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); - } - }, - - /** @private */ - getByReference: function(reference) { - var store = this.store; - - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } - - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); - } - }, - - getSecondRecord: function(){ - return this.getByReference(this.secondRecordReference); - }, - - /** @private */ - getFirstRecord: function() { - return this.getByReference(this.firstRecordReference); - }, - - /** - @private - - Make sure that all three parts of the relationship change are part of - the same transaction. If any of the three records is clean and in the - default transaction, and the rest are in a different transaction, move - them all into that transaction. - */ - ensureSameTransaction: function() { - var child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); - - var transaction = this.store.ensureSameTransaction([child, parentRecord]); - - this.transaction = transaction; - return transaction; - }, - - callChangeEvents: function(){ - var hasManyName = this.getSecondRecordName(), - belongsToName = this.getFirstRecordName(), - child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); - - var dirtySet = new Ember.OrderedSet(); - - // TODO: This implementation causes a race condition in key-value - // stores. The fix involves buffering changes that happen while - // a record is loading. A similar fix is required for other parts - // of ember-data, and should be done as new infrastructure, not - // a one-off hack. [tomhuda] - if (parentRecord && get(parentRecord, 'isLoaded')) { - this.store.recordHasManyDidChange(dirtySet, parentRecord, this); - } - - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } - - dirtySet.forEach(function(record) { - record.adapterDidDirty(); - }); - }, - - coalesce: function(){ - var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); - forEach(relationshipPairs, function(pair){ - var addedChange = pair["add"]; - var removedChange = pair["remove"]; - if(addedChange && removedChange) { - addedChange.destroy(); - removedChange.destroy(); - } - }); - } -}; - -DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); -DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); - -DS.RelationshipChangeAdd.prototype.changeType = "add"; -DS.RelationshipChangeAdd.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); - - //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); - //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); - - var transaction = this.ensureSameTransaction(); - transaction.relationshipBecameDirty(this); - - this.callChangeEvents(); - - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, firstRecord); - }); - - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).addObject(firstRecord); - }); - } - } - - if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, secondRecord); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).addObject(secondRecord); - }); - } - } - - this.coalesce(); -}; - -DS.RelationshipChangeRemove.prototype.changeType = "remove"; -DS.RelationshipChangeRemove.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); - - //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); - //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); - - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); - - this.callChangeEvents(); - - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, null); - }); - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).removeObject(firstRecord); - }); - } - } - - if (firstRecord && get(firstRecord, firstRecordName)) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, null); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).removeObject(secondRecord); - }); - } - } - - this.coalesce(); -}; - -function inverseBelongsToName(parentType, childType, hasManyName) { - // Get the options passed to the parent's DS.hasMany() - var options = parentType.metaForProperty(hasManyName).options; - var belongsToName; - - if (belongsToName = options.inverse) { - return belongsToName; - } - - return DS._inverseRelationshipFor(childType, parentType).name; -} - -function inverseHasManyName(parentType, childType, belongsToName) { - var options = childType.metaForProperty(belongsToName).options; - var hasManyName; - - if (hasManyName = options.inverse) { - return hasManyName; - } - - return DS._inverseRelationshipFor(parentType, childType).name; -} - -})(); - - - -(function() { - -})(); - - - -(function() { -var set = Ember.set; - -/** - This code registers an injection for Ember.Application. - - If an Ember.js developer defines a subclass of DS.Store on their application, - this code will automatically instantiate it and make it available on the - router. - - Additionally, after an application's controllers have been injected, they will - each have the store made available to them. - - For example, imagine an Ember.js application with the following classes: - - App.Store = DS.Store.extend({ - adapter: 'App.MyCustomAdapter' - }); - - App.PostsController = Ember.ArrayController.extend({ - // ... - }); - - When the application is initialized, `App.Store` will automatically be - instantiated, and the instance of `App.PostsController` will have its `store` - property set to that instance. - - Note that this code will only be run if the `ember-application` package is - loaded. If Ember Data is being used in an environment other than a - typical application (e.g., node.js where only `ember-runtime` is available), - this code will be ignored. -*/ - -Ember.onLoad('Ember.Application', function(Application) { - if (Application.registerInjection) { - Application.registerInjection({ - name: "store", - before: "controllers", - - // If a store subclass is defined, like App.Store, - // instantiate it and inject it into the router. - injection: function(app, stateManager, property) { - if (!stateManager) { return; } - if (property === 'Store') { - set(stateManager, 'store', app[property].create()); - } - } - }); - - Application.registerInjection({ - name: "giveStoreToControllers", - after: ['store','controllers'], - - // For each controller, set its `store` property - // to the DS.Store instance we created above. - injection: function(app, stateManager, property) { - if (!stateManager) { return; } - if (/^[A-Z].*Controller$/.test(property)) { - var controllerName = property.charAt(0).toLowerCase() + property.substr(1); - var store = stateManager.get('store'); - var controller = stateManager.get(controllerName); - if(!controller) { return; } - - controller.set('store', store); - } - } - }); - } else if (Application.initializer) { - Application.initializer({ - name: "store", - - initialize: function(container, application) { - container.register('store', 'main', application.Store); - - // Eagerly generate the store so defaultStore is populated. - // TODO: Do this in a finisher hook - container.lookup('store:main'); - } - }); - - Application.initializer({ - name: "injectStore", - - initialize: function(container) { - container.typeInjection('controller', 'store', 'store:main'); - container.typeInjection('route', 'store', 'store:main'); - } - }); - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, map = Ember.ArrayPolyfills.map, isNone = Ember.isNone; - -function mustImplement(name) { - return function() { - throw new Ember.Error("Your serializer " + this.toString() + " does not implement the required method " + name); - }; -} - -/** - A serializer is responsible for serializing and deserializing a group of - records. - - `DS.Serializer` is an abstract base class designed to help you build a - serializer that can read to and write from any serialized form. While most - applications will use `DS.JSONSerializer`, which reads and writes JSON, the - serializer architecture allows your adapter to transmit things like XML, - strings, or custom binary data. - - Typically, your application's `DS.Adapter` is responsible for both creating a - serializer as well as calling the appropriate methods when it needs to - materialize data or serialize a record. - - The serializer API is designed as a series of layered hooks that you can - override to customize any of the individual steps of serialization and - deserialization. - - The hooks are organized by the three responsibilities of the serializer: - - 1. Determining naming conventions - 2. Serializing records into a serialized form - 3. Deserializing records from a serialized form - - Because Ember Data lazily materializes records, the deserialization - step, and therefore the hooks you implement, are split into two phases: - - 1. Extraction, where the serialized forms for multiple records are - extracted from a single payload. The IDs of each record are also - extracted for indexing. - 2. Materialization, where a newly-created record has its attributes - and relationships initialized based on the serialized form loaded - by the adapter. - - Additionally, a serializer can convert values from their JavaScript - versions into their serialized versions via a declarative API. - - ## Naming Conventions - - One of the most common uses of the serializer is to map attribute names - from the serialized form to your `DS.Model`. For example, in your model, - you may have an attribute called `firstName`: - - ```javascript - App.Person = DS.Model.extend({ - firstName: DS.attr('string') - }); - ``` - - However, because the web API your adapter is communicating with is - legacy, it calls this attribute `FIRST_NAME`. - - You can determine the attribute name used in the serialized form - by implementing `keyForAttributeName`: - - ```javascript - keyForAttributeName: function(type, name) { - return name.underscore.toUpperCase(); - } - ``` - - If your attribute names are not predictable, you can re-map them - one-by-one using the `map` API: - - ```javascript - App.Person.map('App.Person', { - firstName: { key: '*API_USER_FIRST_NAME*' } - }); - ``` - - ## Serialization - - During the serialization process, a record or records are converted - from Ember.js objects into their serialized form. - - These methods are designed in layers, like a delicious 7-layer - cake (but with fewer layers). - - The main entry point for serialization is the `serialize` - method, which takes the record and options. - - The `serialize` method is responsible for: - - * turning the record's attributes (`DS.attr`) into - attributes on the JSON object. - * optionally adding the record's ID onto the hash - * adding relationships (`DS.hasMany` and `DS.belongsTo`) - to the JSON object. - - Depending on the backend, the serializer can choose - whether to include the `hasMany` or `belongsTo` - relationships on the JSON hash. - - For very custom serialization, you can implement your - own `serialize` method. In general, however, you will want - to override the hooks described below. - - ### Adding the ID - - The default `serialize` will optionally call your serializer's - `addId` method with the JSON hash it is creating, the - record's type, and the record's ID. The `serialize` method - will not call `addId` if the record's ID is undefined. - - Your adapter must specifically request ID inclusion by - passing `{ includeId: true }` as an option to `serialize`. - - NOTE: You may not want to include the ID when updating an - existing record, because your server will likely disallow - changing an ID after it is created, and the PUT request - itself will include the record's identification. - - By default, `addId` will: - - 1. Get the primary key name for the record by calling - the serializer's `primaryKey` with the record's type. - Unless you override the `primaryKey` method, this - will be `'id'`. - 2. Assign the record's ID to the primary key in the - JSON hash being built. - - If your backend expects a JSON object with the primary - key at the root, you can just override the `primaryKey` - method on your serializer subclass. - - Otherwise, you can override the `addId` method for - more specialized handling. - - ### Adding Attributes - - By default, the serializer's `serialize` method will call - `addAttributes` with the JSON object it is creating - and the record to serialize. - - The `addAttributes` method will then call `addAttribute` - in turn, with the JSON object, the record to serialize, - the attribute's name and its type. - - Finally, the `addAttribute` method will serialize the - attribute: - - 1. It will call `keyForAttributeName` to determine - the key to use in the JSON hash. - 2. It will get the value from the record. - 3. It will call `serializeValue` with the attribute's - value and attribute type to convert it into a - JSON-compatible value. For example, it will convert a - Date into a String. - - If your backend expects a JSON object with attributes as - keys at the root, you can just override the `serializeValue` - and `keyForAttributeName` methods in your serializer - subclass and let the base class do the heavy lifting. - - If you need something more specialized, you can probably - override `addAttribute` and let the default `addAttributes` - handle the nitty gritty. - - ### Adding Relationships - - By default, `serialize` will call your serializer's - `addRelationships` method with the JSON object that is - being built and the record being serialized. The default - implementation of this method is to loop over all of the - relationships defined on your record type and: - - * If the relationship is a `DS.hasMany` relationship, - call `addHasMany` with the JSON object, the record - and a description of the relationship. - * If the relationship is a `DS.belongsTo` relationship, - call `addBelongsTo` with the JSON object, the record - and a description of the relationship. - - The relationship description has the following keys: - - * `type`: the class of the associated information (the - first parameter to `DS.hasMany` or `DS.belongsTo`) - * `kind`: either `hasMany` or `belongsTo` - - The relationship description may get additional - information in the future if more capabilities or - relationship types are added. However, it will - remain backwards-compatible, so the mere existence - of new features should not break existing adapters. -*/ -DS.Serializer = Ember.Object.extend({ - init: function() { - this.mappings = Ember.Map.create(); - this.configurations = Ember.Map.create(); - this.globalConfigurations = {}; - }, - - extract: mustImplement('extract'), - extractMany: mustImplement('extractMany'), - - extractRecordRepresentation: function(loader, type, json, shouldSideload) { - var mapping = this.mappingForType(type); - var embeddedData, prematerialized = {}, reference; - - if (shouldSideload) { - reference = loader.sideload(type, json); - } else { - reference = loader.load(type, json); - } - - this.eachEmbeddedHasMany(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; - if (!isNone(embeddedData)) { - this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized); - } - }, this); - - this.eachEmbeddedBelongsTo(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; - if (!isNone(embeddedData)) { - this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized); - } - }, this); - - loader.prematerialize(reference, prematerialized); - - return reference; - }, - - extractEmbeddedHasMany: function(loader, relationship, array, parent, prematerialized) { - var references = map.call(array, function(item) { - if (!item) { return; } - - var reference = this.extractRecordRepresentation(loader, relationship.type, item, true); - - // If the embedded record should also be saved back when serializing the parent, - // make sure we set its parent since it will not have an ID. - var embeddedType = this.embeddedType(parent.type, relationship.key); - if (embeddedType === 'always') { - reference.parent = parent; - } - - return reference; - }, this); - - prematerialized[relationship.key] = references; - }, - - extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) { - var reference = this.extractRecordRepresentation(loader, relationship.type, data, true); - prematerialized[relationship.key] = reference; - - // If the embedded record should also be saved back when serializing the parent, - // make sure we set its parent since it will not have an ID. - var embeddedType = this.embeddedType(parent.type, relationship.key); - if (embeddedType === 'always') { - reference.parent = parent; - } - }, - - //....................... - //. SERIALIZATION HOOKS - //....................... - - /** - The main entry point for serializing a record. While you can consider this - a hook that can be overridden in your serializer, you will have to manually - handle serialization. For most cases, there are more granular hooks that you - can override. - - If overriding this method, these are the responsibilities that you will need - to implement yourself: - - * If the option hash contains `includeId`, add the record's ID to the serialized form. - By default, `serialize` calls `addId` if appropriate. - * Add the record's attributes to the serialized form. By default, `serialize` calls - `addAttributes`. - * Add the record's relationships to the serialized form. By default, `serialize` calls - `addRelationships`. - - @param {DS.Model} record the record to serialize - @param {Object} [options] a hash of options - @returns {any} the serialized form of the record - */ - serialize: function(record, options) { - options = options || {}; - - var serialized = this.createSerializedForm(), id; - - if (options.includeId) { - if (id = get(record, 'id')) { - this._addId(serialized, record.constructor, id); - } - } - - this.addAttributes(serialized, record); - this.addRelationships(serialized, record); - - return serialized; - }, - - /** - @private - - Given an attribute type and value, convert the value into the - serialized form using the transform registered for that type. - - @param {any} value the value to convert to the serialized form - @param {String} attributeType the registered type (e.g. `string` - or `boolean`) - @returns {any} the serialized form of the value - */ - serializeValue: function(value, attributeType) { - var transform = this.transforms ? this.transforms[attributeType] : null; - - Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); - return transform.serialize(value); - }, - - /** - A hook you can use to normalize IDs before adding them to the - serialized representation. - - Because the store coerces all IDs to strings for consistency, - this is the opportunity for the serializer to, for example, - convert numerical IDs back into number form. - - @param {String} id the id from the record - @returns {any} the serialized representation of the id - */ - serializeId: function(id) { - if (isNaN(id)) { return id; } - return +id; - }, - - /** - A hook you can use to change how attributes are added to the serialized - representation of a record. - - By default, `addAttributes` simply loops over all of the attributes of the - passed record, maps the attribute name to the key for the serialized form, - and invokes any registered transforms on the value. It then invokes the - more granular `addAttribute` with the key and transformed value. - - Since you can override `keyForAttributeName`, `addAttribute`, and register - custom tranforms, you should rarely need to override this hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - */ - addAttributes: function(data, record) { - record.eachAttribute(function(name, attribute) { - this._addAttribute(data, record, name, attribute.type); - }, this); - }, - - /** - A hook you can use to customize how the key/value pair is added to - the serialized data. - - @param {any} serialized the serialized form being built - @param {String} key the key to add to the serialized data - @param {any} value the value to add to the serialized data - */ - addAttribute: Ember.K, - - /** - A hook you can use to customize how the record's id is added to - the serialized data. - - The `addId` hook is called with: - - * the serialized representation being built - * the resolved primary key (taking configurations and the - `primaryKey` hook into consideration) - * the serialized id (after calling the `serializeId` hook) - - @param {any} data the serialized representation that is being built - @param {String} key the resolved primary key - @param {id} id the serialized id - */ - addId: Ember.K, - - /** - A hook you can use to change how relationships are added to the serialized - representation of a record. - - By default, `addAttributes` loops over all of the relationships of the - passed record, maps the relationship names to the key for the serialized form, - and then invokes the public `addBelongsTo` and `addHasMany` hooks. - - Since you can override `keyForBelongsTo`, `keyForHasMany`, `addBelongsTo`, - `addHasMany`, and register mappings, you should rarely need to override this - hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - */ - addRelationships: function(data, record) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'belongsTo') { - this._addBelongsTo(data, record, name, relationship); - } else if (relationship.kind === 'hasMany') { - this._addHasMany(data, record, name, relationship); - } - }, this); - }, - - /** - A hook you can use to add a `belongsTo` relationship to the - serialized representation. - - The specifics of this hook are very adapter-specific, so there - is no default implementation. You can see `DS.JSONSerializer` - for an example of an implementation of the `addBelongsTo` hook. - - The `belongsTo` relationship object has the following properties: - - * **type** a subclass of DS.Model that is the type of the - relationship. This is the first parameter to DS.belongsTo - * **options** the options passed to the call to DS.belongsTo - * **kind** always `belongsTo` - - Additional properties may be added in the future. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} key the key for the serialized object - @param {Object} relationship an object representing the relationship - */ - addBelongsTo: Ember.K, - - /** - A hook you can use to add a `hasMany` relationship to the - serialized representation. - - The specifics of this hook are very adapter-specific, so there - is no default implementation. You may not need to implement this, - for example, if your backend only expects relationships on the - child of a one to many relationship. - - The `hasMany` relationship object has the following properties: - - * **type** a subclass of DS.Model that is the type of the - relationship. This is the first parameter to DS.hasMany - * **options** the options passed to the call to DS.hasMany - * **kind** always `hasMany` - - Additional properties may be added in the future. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} key the key for the serialized object - @param {Object} relationship an object representing the relationship - */ - addHasMany: Ember.K, - - /** - NAMING CONVENTIONS - - The most commonly overridden APIs of the serializer are - the naming convention methods: - - * `keyForAttributeName`: converts a camelized attribute name - into a key in the adapter-provided data hash. For example, - if the model's attribute name was `firstName`, and the - server used underscored names, you would return `first_name`. - * `primaryKey`: returns the key that should be used to - extract the id from the adapter-provided data hash. It is - also used when serializing a record. - */ - - /** - A hook you can use in your serializer subclass to customize - how an unmapped attribute name is converted into a key. - - By default, this method returns the `name` parameter. - - For example, if the attribute names in your JSON are underscored, - you will want to convert them into JavaScript conventional - camelcase: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - - keyForAttributeName: function(type, name) { - return name.camelize(); - } - }); - ``` - - @param {DS.Model subclass} type the type of the record with - the attribute name `name` - @param {String} name the attribute name to convert into a key - - @returns {String} the key - */ - keyForAttributeName: function(type, name) { - return name; - }, - - /** - A hook you can use in your serializer to specify a conventional - primary key. - - By default, this method will return the string `id`. - - In general, you should not override this hook to specify a special - primary key for an individual type; use `configure` instead. - - For example, if your primary key is always `__id__`: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - primaryKey: function(type) { - return '__id__'; - } - }); - ``` - - In another example, if the primary key always includes the - underscored version of the type before the string `id`: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - primaryKey: function(type) { - // If the type is `BlogPost`, this will return - // `blog_post_id`. - var typeString = type.toString().split(".")[1].underscore(); - return typeString + "_id"; - } - }); - ``` - - @param {DS.Model subclass} type - @returns {String} the primary key for the type - */ - primaryKey: function(type) { - return "id"; - }, - - /** - A hook you can use in your serializer subclass to customize - how an unmapped `belongsTo` relationship is converted into - a key. - - By default, this method calls `keyForAttributeName`, so if - your naming convention is uniform across attributes and - relationships, you can use the default here and override - just `keyForAttributeName` as needed. - - For example, if the `belongsTo` names in your JSON always - begin with `BT_` (e.g. `BT_posts`), you can strip out the - `BT_` prefix:" - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - keyForBelongsTo: function(type, name) { - return name.match(/^BT_(.*)$/)[1].camelize(); - } - }); - ``` - - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - keyForBelongsTo: function(type, name) { - return this.keyForAttributeName(type, name); - }, - - /** - A hook you can use in your serializer subclass to customize - how an unmapped `hasMany` relationship is converted into - a key. - - By default, this method calls `keyForAttributeName`, so if - your naming convention is uniform across attributes and - relationships, you can use the default here and override - just `keyForAttributeName` as needed. - - For example, if the `hasMany` names in your JSON always - begin with the "table name" for the current type (e.g. - `post_comments`), you can strip out the prefix:" - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - keyForHasMany: function(type, name) { - // if your App.BlogPost has many App.BlogComment, the key from - // the server would look like: `blog_post_blog_comments` - // - // 1. Convert the type into a string and underscore the - // second part (App.BlogPost -> blog_post) - // 2. Extract the part after `blog_post_` (`blog_comments`) - // 3. Underscore it, to become `blogComments` - var typeString = type.toString().split(".")[1].underscore(); - return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize(); - } - }); - ``` - - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - keyForHasMany: function(type, name) { - return this.keyForAttributeName(type, name); - }, - - //......................... - //. MATERIALIZATION HOOKS - //......................... - - materialize: function(record, serialized, prematerialized) { - var id; - if (Ember.isNone(get(record, 'id'))) { - if (prematerialized && prematerialized.hasOwnProperty('id')) { - id = prematerialized.id; - } else { - id = this.extractId(record.constructor, serialized); - } - record.materializeId(id); - } - - this.materializeAttributes(record, serialized, prematerialized); - this.materializeRelationships(record, serialized, prematerialized); - }, - - deserializeValue: function(value, attributeType) { - var transform = this.transforms ? this.transforms[attributeType] : null; - - Ember.assert("You tried to use a attribute type (" + attributeType + ") that has not been registered", transform); - return transform.deserialize(value); - }, - - materializeAttributes: function(record, serialized, prematerialized) { - record.eachAttribute(function(name, attribute) { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeAttribute(name, prematerialized[name]); - } else { - this.materializeAttribute(record, serialized, name, attribute.type); - } - }, this); - }, - - materializeAttribute: function(record, serialized, attributeName, attributeType) { - var value = this.extractAttribute(record.constructor, serialized, attributeName); - value = this.deserializeValue(value, attributeType); - - record.materializeAttribute(attributeName, value); - }, - - materializeRelationships: function(record, hash, prematerialized) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'hasMany') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeHasMany(name, prematerialized[name]); - } else { - this.materializeHasMany(name, record, hash, relationship, prematerialized); - } - } else if (relationship.kind === 'belongsTo') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeBelongsTo(name, prematerialized[name]); - } else { - this.materializeBelongsTo(name, record, hash, relationship, prematerialized); - } - } - }, this); - }, - - materializeHasMany: function(name, record, hash, relationship) { - var key = this._keyForHasMany(record.constructor, relationship.key); - record.materializeHasMany(name, this.extractHasMany(record.constructor, hash, key)); - }, - - materializeBelongsTo: function(name, record, hash, relationship) { - var key = this._keyForBelongsTo(record.constructor, relationship.key); - record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key)); - }, - - _extractEmbeddedRelationship: function(type, hash, name, relationshipType) { - var key = this['_keyFor' + relationshipType](type, name); - - if (this.embeddedType(type, name)) { - return this['extractEmbedded' + relationshipType](type, hash, key); - } - }, - - _extractEmbeddedBelongsTo: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo'); - }, - - _extractEmbeddedHasMany: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'HasMany'); - }, - - /** - @private - - This method is called to get the primary key for a given - type. - - If a primary key configuration exists for this type, this - method will return the configured value. Otherwise, it will - call the public `primaryKey` hook. - - @param {DS.Model subclass} type - @returns {String} the primary key for the type - */ - _primaryKey: function(type) { - var config = this.configurationForType(type), - primaryKey = config && config.primaryKey; - - if (primaryKey) { - return primaryKey; - } else { - return this.primaryKey(type); - } - }, - - /** - @private - - This method looks up the key for the attribute name and transforms the - attribute's value using registered transforms. - - Specifically: - - 1. Look up the key for the attribute name. If available, this will use - any registered mappings. Otherwise, it will invoke the public - `keyForAttributeName` hook. - 2. Get the value from the record using the `attributeName`. - 3. Transform the value using registered transforms for the `attributeType`. - 4. Invoke the public `addAttribute` hook with the hash, key, and - transformed value. - - @param {any} data the serialized representation being built - @param {DS.Model} record the record to serialize - @param {String} attributeName the name of the attribute on the record - @param {String} attributeType the type of the attribute (e.g. `string` - or `boolean`) - */ - _addAttribute: function(data, record, attributeName, attributeType) { - var key = this._keyForAttributeName(record.constructor, attributeName); - var value = get(record, attributeName); - - this.addAttribute(data, key, this.serializeValue(value, attributeType)); - }, - - /** - @private - - This method looks up the primary key for the `type` and invokes - `serializeId` on the `id`. - - It then invokes the public `addId` hook with the primary key and - the serialized id. - - @param {any} data the serialized representation that is being built - @param {Ember.Model subclass} type - @param {any} id the materialized id from the record - */ - _addId: function(hash, type, id) { - var primaryKey = this._primaryKey(type); - - this.addId(hash, primaryKey, this.serializeId(id)); - }, - - /** - @private - - This method is called to get a key used in the data from - an attribute name. It first checks for any mappings before - calling the public hook `keyForAttributeName`. - - @param {DS.Model subclass} type the type of the record with - the attribute name `name` - @param {String} name the attribute name to convert into a key - - @returns {String} the key - */ - _keyForAttributeName: function(type, name) { - return this._keyFromMappingOrHook('keyForAttributeName', type, name); - }, - - /** - @private - - This method is called to get a key used in the data from - a belongsTo relationship. It first checks for any mappings before - calling the public hook `keyForBelongsTo`. - - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - _keyForBelongsTo: function(type, name) { - return this._keyFromMappingOrHook('keyForBelongsTo', type, name); - }, - - keyFor: function(description) { - var type = description.parentType, - name = description.key; - - switch (description.kind) { - case 'belongsTo': - return this._keyForBelongsTo(type, name); - case 'hasMany': - return this._keyForHasMany(type, name); - } - }, - - /** - @private - - This method is called to get a key used in the data from - a hasMany relationship. It first checks for any mappings before - calling the public hook `keyForHasMany`. - - @param {DS.Model subclass} type the type of the record with - the `hasMany` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - _keyForHasMany: function(type, name) { - return this._keyFromMappingOrHook('keyForHasMany', type, name); - }, - /** - @private - - This method converts the relationship name to a key for serialization, - and then invokes the public `addBelongsTo` hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} name the relationship name - @param {Object} relationship an object representing the relationship - */ - _addBelongsTo: function(data, record, name, relationship) { - var key = this._keyForBelongsTo(record.constructor, name); - this.addBelongsTo(data, record, key, relationship); - }, - - /** - @private - - This method converts the relationship name to a key for serialization, - and then invokes the public `addHasMany` hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} name the relationship name - @param {Object} relationship an object representing the relationship - */ - _addHasMany: function(data, record, name, relationship) { - var key = this._keyForHasMany(record.constructor, name); - this.addHasMany(data, record, key, relationship); - }, - - /** - @private - - An internal method that handles checking whether a mapping - exists for a particular attribute or relationship name before - calling the public hooks. - - If a mapping is found, and the mapping has a key defined, - use that instead of invoking the hook. - - @param {String} publicMethod the public hook to invoke if - a mapping is not found (e.g. `keyForAttributeName`) - @param {DS.Model subclass} type the type of the record with - the attribute or relationship name. - @param {String} name the attribute or relationship name to - convert into a key - */ - _keyFromMappingOrHook: function(publicMethod, type, name) { - var key = this.mappingOption(type, name, 'key'); - - if (key) { - return key; - } else { - return this[publicMethod](type, name); - } - }, - - /** - TRANSFORMS - */ - - registerTransform: function(type, transform) { - this.transforms[type] = transform; - }, - - registerEnumTransform: function(type, objects) { - var transform = { - deserialize: function(deserialized) { - return Ember.A(objects).objectAt(deserialized); - }, - serialize: function(serialized) { - return Ember.EnumerableUtils.indexOf(objects, serialized); - }, - values: objects - }; - this.registerTransform(type, transform); - }, - - /** - MAPPING CONVENIENCE - */ - - map: function(type, mappings) { - this.mappings.set(type, mappings); - }, - - configure: function(type, configuration) { - if (type && !configuration) { - Ember.merge(this.globalConfigurations, type); - return; - } - - var config = Ember.create(this.globalConfigurations); - Ember.merge(config, configuration); - - this.configurations.set(type, config); - }, - - mappingForType: function(type) { - this._reifyMappings(); - return this.mappings.get(type) || {}; - }, - - configurationForType: function(type) { - this._reifyConfigurations(); - return this.configurations.get(type) || this.globalConfigurations; - }, - - _reifyMappings: function() { - if (this._didReifyMappings) { return; } - - var mappings = this.mappings, - reifiedMappings = Ember.Map.create(); - - mappings.forEach(function(key, mapping) { - if (typeof key === 'string') { - var type = Ember.get(Ember.lookup, key); - Ember.assert("Could not find model at path " + key, type); - - reifiedMappings.set(type, mapping); - } else { - reifiedMappings.set(key, mapping); - } - }); - - this.mappings = reifiedMappings; - - this._didReifyMappings = true; - }, - - _reifyConfigurations: function() { - if (this._didReifyConfigurations) { return; } - - var configurations = this.configurations, - reifiedConfigurations = Ember.Map.create(); - - configurations.forEach(function(key, mapping) { - if (typeof key === 'string' && key !== 'plurals') { - var type = Ember.get(Ember.lookup, key); - Ember.assert("Could not find model at path " + key, type); - - reifiedConfigurations.set(type, mapping); - } else { - reifiedConfigurations.set(key, mapping); - } - }); - - this.configurations = reifiedConfigurations; - - this._didReifyConfigurations = true; - }, - - mappingOption: function(type, name, option) { - var mapping = this.mappingForType(type)[name]; - - return mapping && mapping[option]; - }, - - configOption: function(type, option) { - var config = this.configurationForType(type); - - return config[option]; - }, - - // EMBEDDED HELPERS - - embeddedType: function(type, name) { - return this.mappingOption(type, name, 'embedded'); - }, - - eachEmbeddedRecord: function(record, callback, binding) { - this.eachEmbeddedBelongsToRecord(record, callback, binding); - this.eachEmbeddedHasManyRecord(record, callback, binding); - }, - - eachEmbeddedBelongsToRecord: function(record, callback, binding) { - var type = record.constructor; - - this.eachEmbeddedBelongsTo(record.constructor, function(name, relationship, embeddedType) { - var embeddedRecord = get(record, name); - if (embeddedRecord) { callback.call(binding, embeddedRecord, embeddedType); } - }); - }, - - eachEmbeddedHasManyRecord: function(record, callback, binding) { - var type = record.constructor; - - this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) { - var array = get(record, name); - for (var i=0, l=get(array, 'length'); i types) before sideloading. - // We can't do this conversion immediately here, because `configure` - // may be called before certain types have been defined. - this.sideloadMapping.normalized = false; - - delete configuration.sideloadAs; - } - - this._super.apply(this, arguments); - }, - - addId: function(data, key, id) { - data[key] = id; - }, - - /** - A hook you can use to customize how the key/value pair is added to - the serialized data. - - @param {any} hash the JSON hash being built - @param {String} key the key to add to the serialized data - @param {any} value the value to add to the serialized data - */ - addAttribute: function(hash, key, value) { - hash[key] = value; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - - extractAttribute: function(type, hash, attributeName) { - var key = this._keyForAttributeName(type, attributeName); - return hash[key]; - }, - - extractId: function(type, hash) { - var primaryKey = this._primaryKey(type); - - if (hash.hasOwnProperty(primaryKey)) { - // Ensure that we coerce IDs to strings so that record - // IDs remain consistent between application runs; especially - // if the ID is serialized and later deserialized from the URL, - // when type information will have been lost. - return hash[primaryKey]+''; - } else { - return null; - } - }, - - extractHasMany: function(type, hash, key) { - return hash[key]; - }, - - extractBelongsTo: function(type, hash, key) { - return hash[key]; - }, - - addBelongsTo: function(hash, record, key, relationship) { - var type = record.constructor, - name = relationship.key, - value = null, - embeddedChild; - - if (this.embeddedType(type, name)) { - if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { includeId: true }); - } - - hash[key] = value; - } else { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } - } - }, - - /** - Adds a has-many relationship to the JSON hash being built. - - The default REST semantics are to only add a has-many relationship if it - is embedded. If the relationship was initially loaded by ID, we assume that - that was done as a performance optimization, and that changes to the - has-many should be saved as foreign key changes on the child's belongs-to - relationship. - - @param {Object} hash the JSON being built - @param {DS.Model} record the record being serialized - @param {String} key the JSON key into which the serialized relationship - should be saved - @param {Object} relationship metadata about the relationship being serialized - */ - addHasMany: function(hash, record, key, relationship) { - var type = record.constructor, - name = relationship.key, - serializedHasMany = [], - manyArray, embeddedType; - - // If the has-many is not embedded, there is nothing to do. - embeddedType = this.embeddedType(type, name); - if (embeddedType !== 'always') { return; } - - // Get the DS.ManyArray for the relationship off the record - manyArray = get(record, name); - - // Build up the array of serialized records - manyArray.forEach(function (record) { - serializedHasMany.push(this.serialize(record, { includeId: true })); - }, this); - - // Set the appropriate property of the serialized JSON to the - // array of serialized embedded records - hash[key] = serializedHasMany; - }, - - // EXTRACTION - - extract: function(loader, json, type, record) { - var root = this.rootForType(type); - - this.sideload(loader, type, json, root); - this.extractMeta(loader, type, json); - - if (json[root]) { - if (record) { loader.updateId(record, json[root]); } - this.extractRecordRepresentation(loader, type, json[root]); - } - }, - - extractMany: function(loader, json, type, records) { - var root = this.rootForType(type); - root = this.pluralize(root); - - this.sideload(loader, type, json, root); - this.extractMeta(loader, type, json); - - if (json[root]) { - var objects = json[root], references = []; - if (records) { records = records.toArray(); } - - for (var i = 0; i < objects.length; i++) { - if (records) { loader.updateId(records[i], objects[i]); } - var reference = this.extractRecordRepresentation(loader, type, objects[i]); - references.push(reference); - } - - loader.populateArray(references); - } - }, - - extractMeta: function(loader, type, json) { - var meta = json[this.configOption(type, 'meta')], since; - if (!meta) { return; } - - if (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); - } - }, - - /** - @private - - Iterates over the `json` payload and attempts to load any data - included alongside `root`. - - The keys expected for sideloaded data are based upon the types related - to the root model. Recursion is used to ensure that types related to - related types can be loaded as well. Any custom keys specified by - `sideloadAs` mappings will also be respected. - - @param {DS.Store subclass} loader - @param {DS.Model subclass} type - @param {Object} json - @param {String} root - */ - sideload: function(loader, type, json, root) { - var sideloadedType; - - this.normalizeSideloadMappings(); - this.configureSideloadMappingForType(type); - - for (var prop in json) { - if (!json.hasOwnProperty(prop) || - prop === root || - prop === this.configOption(type, 'meta')) { - continue; - } - - sideloadedType = this.sideloadMapping.get(prop); - Ember.assert("Your server returned a hash with the key " + prop + - " but you have no mapping for it", - !!sideloadedType); - - this.loadValue(loader, sideloadedType, json[prop]); - } - }, - - /** - @private - - Iterates over all the `sideloadAs` mappings and converts any that are - strings to their equivalent types. - - This is an optimization used to avoid performing lookups for every - call to `sideload`. - */ - normalizeSideloadMappings: function() { - if (! this.sideloadMapping.normalized) { - this.sideloadMapping.forEach(function(key, value) { - if (typeof value === 'string') { - this.sideloadMapping.set(key, get(Ember.lookup, value)); - } - }, this); - this.sideloadMapping.normalized = true; - } - }, - - /** - @private - - Configures possible sideload mappings for the types related to a - particular model. This recursive method ensures that sideloading - works for related models as well. - - @param {DS.Model subclass} type - @param {Ember.A} configured an array of types that have already been configured - */ - configureSideloadMappingForType: function(type, configured) { - if (!configured) {configured = Ember.A([]);} - configured.pushObject(type); - - type.eachRelatedType(function(relatedType) { - if (!configured.contains(relatedType)) { - var root = this.sideloadMappingForType(relatedType); - if (!root) { - root = this.defaultSideloadRootForType(relatedType); - this.sideloadMapping.set(root, relatedType); - } - this.configureSideloadMappingForType(relatedType, configured); - } - }, this); - }, - - loadValue: function(loader, type, value) { - if (value instanceof Array) { - for (var i=0; i < value.length; i++) { - loader.sideload(type, value[i]); - } - } else { - loader.sideload(type, value); - } - }, - - // HELPERS - - // define a plurals hash in your subclass to define - // special-case pluralization - pluralize: function(name) { - var plurals = this.configurations.get('plurals'); - return (plurals && plurals[name]) || name + "s"; - }, - - // use the same plurals hash to determine - // special-case singularization - singularize: function(name) { - var plurals = this.configurations.get('plurals'); - if (plurals) { - for (var i in plurals) { - if (plurals[i] === name) { - return i; - } - } - } - if (name.lastIndexOf('s') === name.length - 1) { - return name.substring(0, name.length - 1); - } else { - return name; - } - }, - - /** - @private - - Determines the singular root name for a particular type. - - This is an underscored, lowercase version of the model name. - For example, the type `App.UserGroup` will have the root - `user_group`. - - @param {DS.Model subclass} type - @returns {String} name of the root element - */ - rootForType: function(type) { - var typeString = type.toString(); - - Ember.assert("Your model must not be anonymous. It was " + type, typeString.charAt(0) !== '('); - - // use the last part of the name as the URL - var parts = typeString.split("."); - var name = parts[parts.length - 1]; - return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1); - }, - - /** - @private - - Determines the root name mapped to a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element, if any is registered - */ - sideloadMappingForType: function(type) { - this.sideloadMapping.forEach(function(key, value) { - if (type === value) { - return key; - } - }); - }, - - /** - @private - - The default root name for a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element - */ - defaultSideloadRootForType: function(type) { - return this.pluralize(this.rootForType(type)); - } -}); - -})(); - - - -(function() { -function loaderFor(store) { - return { - load: function(type, data, prematerialized) { - return store.load(type, data, prematerialized); - }, - - loadMany: function(type, array) { - return store.loadMany(type, array); - }, - - updateId: function(record, data) { - return store.updateId(record, data); - }, - - populateArray: Ember.K, - - sideload: function(type, data) { - return store.load(type, data); - }, - - sideloadMany: function(type, array) { - return store.loadMany(type, array); - }, - - prematerialize: function(reference, prematerialized) { - store.prematerialize(reference, prematerialized); - }, - - sinceForType: function(type, since) { - store.sinceForType(type, since); - } - }; -} - -DS.loaderFor = loaderFor; - -/** - An adapter is an object that receives requests from a store and - translates them into the appropriate action to take against your - persistence layer. The persistence layer is usually an HTTP API, but may - be anything, such as the browser's local storage. - - ### Creating an Adapter - - First, create a new subclass of `DS.Adapter`: - - App.MyAdapter = DS.Adapter.extend({ - // ...your code here - }); - - To tell your store which adapter to use, set its `adapter` property: - - App.store = DS.Store.create({ - revision: 3, - adapter: App.MyAdapter.create() - }); - - `DS.Adapter` is an abstract base class that you should override in your - application to customize it for your backend. The minimum set of methods - that you should implement is: - - * `find()` - * `createRecord()` - * `updateRecord()` - * `deleteRecord()` - - To improve the network performance of your application, you can optimize - your adapter by overriding these lower-level methods: - - * `findMany()` - * `createRecords()` - * `updateRecords()` - * `deleteRecords()` - * `commit()` -*/ - -var get = Ember.get, set = Ember.set, merge = Ember.merge; - -DS.Adapter = Ember.Object.extend(DS._Mappable, { - - init: function() { - var serializer = get(this, 'serializer'); - - if (Ember.Object.detect(serializer)) { - serializer = serializer.create(); - set(this, 'serializer', serializer); - } - - this._attributesMap = this.createInstanceMapFor('attributes'); - this._configurationsMap = this.createInstanceMapFor('configurations'); - - this._outstandingOperations = new Ember.MapWithDefault({ - defaultValue: function() { return 0; } - }); - - this._dependencies = new Ember.MapWithDefault({ - defaultValue: function() { return new Ember.OrderedSet(); } - }); - - this.registerSerializerTransforms(this.constructor, serializer, {}); - this.registerSerializerMappings(serializer); - }, - - /** - Loads a payload for a record into the store. - - This method asks the serializer to break the payload into - constituent parts, and then loads them into the store. For example, - if you have a payload that contains embedded records, they will be - extracted by the serializer and loaded into the store. - - For example: - - ```javascript - adapter.load(store, App.Person, { - id: 123, - firstName: "Yehuda", - lastName: "Katz", - occupations: [{ - id: 345, - title: "Tricycle Mechanic" - }] - }); - ``` - - This will load the payload for the `App.Person` with ID `123` and - the embedded `App.Occupation` with ID `345`. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - load: function(store, type, payload) { - var loader = loaderFor(store); - get(this, 'serializer').extractRecordRepresentation(loader, type, payload); - }, - - /** - Acknowledges that the adapter has finished creating a record. - - Your adapter should call this method from `createRecord` when - it has saved a new record to its persistent storage and received - an acknowledgement. - - If the persistent storage returns a new payload in response to the - creation, and you want to update the existing record with the - new information, pass the payload as the fourth parameter. - - For example, the `RESTAdapter` saves newly created records by - making an Ajax request. When the server returns, the adapter - calls didCreateRecord. If the server returns a response body, - it is passed as the payload. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didCreateRecord: function(store, type, record, payload) { - store.didSaveRecord(record); - - if (payload) { - var loader = DS.loaderFor(store); - - loader.load = function(type, data, prematerialized) { - store.updateId(record, data); - return store.load(type, data, prematerialized); - }; - - get(this, 'serializer').extract(loader, payload, type); - } - }, - - /** - Acknowledges that the adapter has finished creating several records. - - Your adapter should call this method from `createRecords` when it - has saved multiple created records to its persistent storage - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - creation, and you want to update the existing record with the - new information, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didCreateRecords: function(store, type, records, payload) { - records.forEach(function(record) { - store.didSaveRecord(record); - }, this); - - if (payload) { - var loader = DS.loaderFor(store); - get(this, 'serializer').extractMany(loader, payload, type, records); - } - }, - - /** - @private - - Acknowledges that the adapter has finished updating or deleting a record. - - Your adapter should call this method from `updateRecord` or `deleteRecord` - when it has updated or deleted a record to its persistent storage and - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - update or delete, and you want to update the existing record with the - new information, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didSaveRecord: function(store, type, record, payload) { - store.didSaveRecord(record); - - var serializer = get(this, 'serializer'), - mappings = serializer.mappingForType(type); - - serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { - if (embeddedType === 'load') { return; } - - this.didSaveRecord(store, embeddedRecord.constructor, embeddedRecord); - }, this); - - if (payload) { - var loader = DS.loaderFor(store); - serializer.extract(loader, payload, type); - } - }, - - /** - Acknowledges that the adapter has finished updating a record. - - Your adapter should call this method from `updateRecord` when it - has updated a record to its persistent storage and received an - acknowledgement. - - If the persistent storage returns a new payload in response to the - update, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didUpdateRecord: function() { - this.didSaveRecord.apply(this, arguments); - }, - - /** - Acknowledges that the adapter has finished deleting a record. - - Your adapter should call this method from `deleteRecord` when it - has deleted a record from its persistent storage and received an - acknowledgement. - - If the persistent storage returns a new payload in response to the - deletion, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didDeleteRecord: function() { - this.didSaveRecord.apply(this, arguments); - }, - - /** - Acknowledges that the adapter has finished updating or deleting - multiple records. - - Your adapter should call this method from its `updateRecords` or - `deleteRecords` when it has updated or deleted multiple records - to its persistent storage and received an acknowledgement. - - If the persistent storage returns a new payload in response to the - creation, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} records - @param {any} payload - */ - didSaveRecords: function(store, type, records, payload) { - records.forEach(function(record) { - store.didSaveRecord(record); - }, this); - - if (payload) { - var loader = DS.loaderFor(store); - get(this, 'serializer').extractMany(loader, payload, type); - } - }, - - /** - Acknowledges that the adapter has finished updating multiple records. - - Your adapter should call this method from its `updateRecords` when - it has updated multiple records to its persistent storage and - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - update, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} records - @param {any} payload - */ - didUpdateRecords: function() { - this.didSaveRecords.apply(this, arguments); - }, - - /** - Acknowledges that the adapter has finished updating multiple records. - - Your adapter should call this method from its `deleteRecords` when - it has deleted multiple records to its persistent storage and - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - deletion, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} records - @param {any} payload - */ - didDeleteRecords: function() { - this.didSaveRecords.apply(this, arguments); - }, - - /** - Loads the response to a request for a record by ID. - - Your adapter should call this method from its `find` method - with the response from the backend. - - You should pass the same ID to this method that was given - to your find method so that the store knows which record - to associate the new data with. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - @param {String} id - */ - didFindRecord: function(store, type, payload, id) { - var loader = DS.loaderFor(store); - - loader.load = function(type, data, prematerialized) { - prematerialized = prematerialized || {}; - prematerialized.id = id; - - return store.load(type, data, prematerialized); - }; - - get(this, 'serializer').extract(loader, payload, type); - }, - - /** - Loads the response to a request for all records by type. - - You adapter should call this method from its `findAll` - method with the response from the backend. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - didFindAll: function(store, type, payload) { - var loader = DS.loaderFor(store), - serializer = get(this, 'serializer'); - - store.didUpdateAll(type); - - serializer.extractMany(loader, payload, type); - }, - - /** - Loads the response to a request for records by query. - - Your adapter should call this method from its `findQuery` - method with the response from the backend. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - @param {DS.AdapterPopulatedRecordArray} recordArray - */ - didFindQuery: function(store, type, payload, recordArray) { - var loader = DS.loaderFor(store); - - loader.populateArray = function(data) { - recordArray.load(data); - }; - - get(this, 'serializer').extractMany(loader, payload, type); - }, - - /** - Loads the response to a request for many records by ID. - - You adapter should call this method from its `findMany` - method with the response from the backend. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - didFindMany: function(store, type, payload) { - var loader = DS.loaderFor(store); - - get(this, 'serializer').extractMany(loader, payload, type); - }, - - /** - Notifies the store that a request to the backend returned - an error. - - Your adapter should call this method to indicate that the - backend returned an error for a request. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - */ - didError: function(store, type, record) { - store.recordWasError(record); - }, - - dirtyRecordsForAttributeChange: function(dirtySet, record, attributeName, newValue, oldValue) { - if (newValue !== oldValue) { - // If this record is embedded, add its parent - // to the dirty set. - this.dirtyRecordsForRecordChange(dirtySet, record); - } - }, - - dirtyRecordsForRecordChange: function(dirtySet, record) { - dirtySet.add(record); - }, - - dirtyRecordsForBelongsToChange: function(dirtySet, child) { - this.dirtyRecordsForRecordChange(dirtySet, child); - }, - - dirtyRecordsForHasManyChange: function(dirtySet, parent) { - this.dirtyRecordsForRecordChange(dirtySet, parent); - }, - - /** - @private - - This method recursively climbs the superclass hierarchy and - registers any class-registered transforms on the adapter's - serializer. - - Once it registers a transform for a given type, it ignores - subsequent transforms for the same attribute type. - - @param {Class} klass the DS.Adapter subclass to extract the - transforms from - @param {DS.Serializer} serializer the serializer to register - the transforms onto - @param {Object} seen a hash of attributes already seen - */ - registerSerializerTransforms: function(klass, serializer, seen) { - var transforms = klass._registeredTransforms, superclass, prop; - - for (prop in transforms) { - if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; } - seen[prop] = true; - - serializer.registerTransform(prop, transforms[prop]); - } - - if (superclass = klass.superclass) { - this.registerSerializerTransforms(superclass, serializer, seen); - } - }, - - /** - @private - - This method recursively climbs the superclass hierarchy and - registers any class-registered mappings on the adapter's - serializer. - - @param {Class} klass the DS.Adapter subclass to extract the - transforms from - @param {DS.Serializer} serializer the serializer to register the - mappings onto - */ - registerSerializerMappings: function(serializer) { - var mappings = this._attributesMap, - configurations = this._configurationsMap; - - mappings.forEach(serializer.map, serializer); - configurations.forEach(serializer.configure, serializer); - }, - - /** - The `find()` method is invoked when the store is asked for a record that - has not previously been loaded. In response to `find()` being called, you - should query your persistence layer for a record with the given ID. Once - found, you can asynchronously call the store's `load()` method to load - the record. - - Here is an example `find` implementation: - - find: function(store, type, id) { - var url = type.url; - url = url.fmt(id); - - jQuery.getJSON(url, function(data) { - // data is a hash of key/value pairs. If your server returns a - // root, simply do something like: - // store.load(type, id, data.person) - store.load(type, id, data); - }); - } - */ - find: null, - - serializer: DS.JSONSerializer, - - registerTransform: function(attributeType, transform) { - get(this, 'serializer').registerTransform(attributeType, transform); - }, - - /** - A public method that allows you to register an enumerated - type on your adapter. This is useful if you want to utilize - a text representation of an integer value. - - Eg: Say you want to utilize "low","medium","high" text strings - in your app, but you want to persist those as 0,1,2 in your backend. - You would first register the transform on your adapter instance: - - adapter.registerEnumTransform('priority', ['low', 'medium', 'high']); - - You would then refer to the 'priority' DS.attr in your model: - App.Task = DS.Model.extend({ - priority: DS.attr('priority') - }); - - And lastly, you would set/get the text representation on your model instance, - but the transformed result will be the index number of the type. - - App: myTask.get('priority') => 'low' - Server Response / Load: { myTask: {priority: 0} } - - @param {String} type of the transform - @param {Array} array of String objects to use for the enumerated values. - This is an ordered list and the index values will be used for the transform. - */ - registerEnumTransform: function(attributeType, objects) { - get(this, 'serializer').registerEnumTransform(attributeType, objects); - }, - - /** - If the globally unique IDs for your records should be generated on the client, - implement the `generateIdForRecord()` method. This method will be invoked - each time you create a new record, and the value returned from it will be - assigned to the record's `primaryKey`. - - Most traditional REST-like HTTP APIs will not use this method. Instead, the ID - of the record will be set by the server, and your adapter will update the store - with the new ID when it calls `didCreateRecord()`. Only implement this method if - you intend to generate record IDs on the client-side. - - The `generateIdForRecord()` method will be invoked with the requesting store as - the first parameter and the newly created record as the second parameter: - - generateIdForRecord: function(store, record) { - var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); - return uuid; - } - */ - generateIdForRecord: null, - - materialize: function(record, data, prematerialized) { - get(this, 'serializer').materialize(record, data, prematerialized); - }, - - serialize: function(record, options) { - return get(this, 'serializer').serialize(record, options); - }, - - extractId: function(type, data) { - return get(this, 'serializer').extractId(type, data); - }, - - groupByType: function(enumerable) { - var map = Ember.MapWithDefault.create({ - defaultValue: function() { return Ember.OrderedSet.create(); } - }); - - enumerable.forEach(function(item) { - map.get(item.constructor).add(item); - }); - - return map; - }, - - commit: function(store, commitDetails) { - this.save(store, commitDetails); - }, - - save: function(store, commitDetails) { - var adapter = this; - - function filter(records) { - var filteredSet = Ember.OrderedSet.create(); - - records.forEach(function(record) { - if (adapter.shouldSave(record)) { - filteredSet.add(record); - } - }); - - return filteredSet; - } - - this.groupByType(commitDetails.created).forEach(function(type, set) { - this.createRecords(store, type, filter(set)); - }, this); - - this.groupByType(commitDetails.updated).forEach(function(type, set) { - this.updateRecords(store, type, filter(set)); - }, this); - - this.groupByType(commitDetails.deleted).forEach(function(type, set) { - this.deleteRecords(store, type, filter(set)); - }, this); - }, - - shouldSave: Ember.K, - - createRecords: function(store, type, records) { - records.forEach(function(record) { - this.createRecord(store, type, record); - }, this); - }, - - updateRecords: function(store, type, records) { - records.forEach(function(record) { - this.updateRecord(store, type, record); - }, this); - }, - - deleteRecords: function(store, type, records) { - records.forEach(function(record) { - this.deleteRecord(store, type, record); - }, this); - }, - - findMany: function(store, type, ids) { - ids.forEach(function(id) { - this.find(store, type, id); - }, this); - } -}); - -DS.Adapter.reopenClass({ - registerTransform: function(attributeType, transform) { - var registeredTransforms = this._registeredTransforms || {}; - - registeredTransforms[attributeType] = transform; - - this._registeredTransforms = registeredTransforms; - }, - - map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { - var existingValue = map.get(key); - - merge(existingValue, newValue); - }), - - configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) { - var existingValue = map.get(key); - - // If a mapping configuration is provided, peel it off and apply it - // using the DS.Adapter.map API. - var mappings = newValue && newValue.mappings; - if (mappings) { - this.map(key, mappings); - delete newValue.mappings; - } - - merge(existingValue, newValue); - }), - - resolveMapConflict: function(oldValue, newValue, mappingsKey) { - merge(newValue, oldValue); - - return newValue; - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -DS.FixtureSerializer = DS.Serializer.extend({ - deserializeValue: function(value, attributeType) { - return value; - }, - - serializeValue: function(value, attributeType) { - return value; - }, - - addId: function(data, key, id) { - data[key] = id; - }, - - addAttribute: function(hash, key, value) { - hash[key] = value; - }, - - addBelongsTo: function(hash, record, key, relationship) { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } - }, - - addHasMany: function(hash, record, key, relationship) { - var ids = get(record, relationship.key).map(function(item) { - return item.get('id'); - }); - - hash[relationship.key] = ids; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - - extract: function(loader, fixture, type, record) { - if (record) { loader.updateId(record, fixture); } - this.extractRecordRepresentation(loader, type, fixture); - }, - - extractMany: function(loader, fixtures, type, records) { - var objects = fixtures, references = []; - if (records) { records = records.toArray(); } - - for (var i = 0; i < objects.length; i++) { - if (records) { loader.updateId(records[i], objects[i]); } - var reference = this.extractRecordRepresentation(loader, type, objects[i]); - references.push(reference); - } - - loader.populateArray(references); - }, - - extractId: function(type, hash) { - var primaryKey = this._primaryKey(type); - - if (hash.hasOwnProperty(primaryKey)) { - // Ensure that we coerce IDs to strings so that record - // IDs remain consistent between application runs; especially - // if the ID is serialized and later deserialized from the URL, - // when type information will have been lost. - return hash[primaryKey]+''; - } else { - return null; - } - }, - - extractAttribute: function(type, hash, attributeName) { - var key = this._keyForAttributeName(type, attributeName); - return hash[key]; - }, - - extractHasMany: function(type, hash, key) { - return hash[key]; - }, - - extractBelongsTo: function(type, hash, key) { - return hash[key]; - } -}); - -})(); - - - -(function() { -var get = Ember.get, fmt = Ember.String.fmt; - -/** - `DS.FixtureAdapter` is an adapter that loads records from memory. - Its primarily used for development and testing. You can also use - `DS.FixtureAdapter` while working on the API but are not ready to - integrate yet. It is a fully functioning adapter. All CRUD methods - are implemented. You can also implement query logic that a remote - system would do. Its possible to do develop your entire application - with `DS.FixtureAdapter`. - -*/ -DS.FixtureAdapter = DS.Adapter.extend({ - - simulateRemoteResponse: true, - - latency: 50, - - serializer: DS.FixtureSerializer, - - /* - Implement this method in order to provide data associated with a type - */ - fixturesForType: function(type) { - if (type.FIXTURES) { - var fixtures = Ember.A(type.FIXTURES); - return fixtures.map(function(fixture){ - if(!fixture.id){ - throw new Error(fmt('the id property must be defined for fixture %@', [fixture])); - } - fixture.id = fixture.id + ''; - return fixture; - }); - } - return null; - }, - - /* - Implement this method in order to query fixtures data - */ - queryFixtures: function(fixtures, query, type) { - Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.'); - }, - - updateFixtures: function(type, fixture) { - if(!type.FIXTURES) { - type.FIXTURES = []; - } - - var fixtures = type.FIXTURES; - - this.deleteLoadedFixture(type, fixture); - - fixtures.push(fixture); - }, - - /* - Implement this method in order to provide provide json for CRUD methods - */ - mockJSON: function(type, record) { - return this.serialize(record, { includeId: true }); - }, - - /* - Adapter methods - */ - generateIdForRecord: function(store, record) { - return Ember.guidFor(record); - }, - - find: function(store, type, id) { - var fixtures = this.fixturesForType(type), - fixture; - - Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures); - - if (fixtures) { - fixture = Ember.A(fixtures).findProperty('id', id); - } - - if (fixture) { - this.simulateRemoteCall(function() { - this.didFindRecord(store, type, fixture, id); - }, this); - } - }, - - findMany: function(store, type, ids) { - var fixtures = this.fixturesForType(type); - - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - if (fixtures) { - fixtures = fixtures.filter(function(item) { - return ids.indexOf(item.id) !== -1; - }); - } - - if (fixtures) { - this.simulateRemoteCall(function() { - this.didFindMany(store, type, fixtures); - }, this); - } - }, - - findAll: function(store, type) { - var fixtures = this.fixturesForType(type); - - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - this.simulateRemoteCall(function() { - this.didFindAll(store, type, fixtures); - }, this); - }, - - findQuery: function(store, type, query, array) { - var fixtures = this.fixturesForType(type); - - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - fixtures = this.queryFixtures(fixtures, query, type); - - if (fixtures) { - this.simulateRemoteCall(function() { - this.didFindQuery(store, type, fixtures, array); - }, this); - } - }, - - createRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); - - this.updateFixtures(type, fixture); - - this.simulateRemoteCall(function() { - this.didCreateRecord(store, type, record, fixture); - }, this); - }, - - updateRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); - - this.updateFixtures(type, fixture); - - this.simulateRemoteCall(function() { - this.didUpdateRecord(store, type, record, fixture); - }, this); - }, - - deleteRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); - - this.deleteLoadedFixture(type, fixture); - - this.simulateRemoteCall(function() { - this.didDeleteRecord(store, type, record); - }, this); - }, - - /* - @private - */ - deleteLoadedFixture: function(type, record) { - var id = this.extractId(type, record); - - var existingFixture = this.findExistingFixture(type, record); - - if(existingFixture) { - var index = type.FIXTURES.indexOf(existingFixture); - type.FIXTURES.splice(index, 1); - return true; - } - }, - - findExistingFixture: function(type, record) { - var fixtures = this.fixturesForType(type); - var id = this.extractId(type, record); - - return this.findFixtureById(fixtures, id); - }, - - findFixtureById: function(fixtures, id) { - var adapter = this; - - return Ember.A(fixtures).find(function(r) { - if(''+get(r, 'id') === ''+id) { - return true; - } else { - return false; - } - }); - }, - - simulateRemoteCall: function(callback, context) { - if (get(this, 'simulateRemoteResponse')) { - // Schedule with setTimeout - Ember.run.later(context, callback, get(this, 'latency')); - } else { - // Asynchronous, but at the of the runloop with zero latency - Ember.run.once(context, callback); - } - } -}); - -})(); - - - -(function() { -DS.RESTSerializer = DS.JSONSerializer.extend({ - keyForAttributeName: function(type, name) { - return Ember.String.decamelize(name); - }, - - keyForBelongsTo: function(type, name) { - var key = this.keyForAttributeName(type, name); - - if (this.embeddedType(type, name)) { - return key; - } - - return key + "_id"; - }, - - keyForHasMany: function(type, name) { - var key = this.keyForAttributeName(type, name); - - if (this.embeddedType(type, name)) { - return key; - } - - return this.singularize(key) + "_ids"; - } -}); - -})(); - - - -(function() { -/*global jQuery*/ - -var get = Ember.get, set = Ember.set, merge = Ember.merge; - -/** - The REST adapter allows your store to communicate with an HTTP server by - transmitting JSON via XHR. Most Ember.js apps that consume a JSON API - should use the REST adapter. - - This adapter is designed around the idea that the JSON exchanged with - the server should be conventional. - - ## JSON Structure - - The REST adapter expects the JSON returned from your server to follow - these conventions. - - ### Object Root - - The JSON payload should be an object that contains the record inside a - root property. For example, in response to a `GET` request for - `/posts/1`, the JSON should look like this: - - ```js - { - "post": { - title: "I'm Running to Reform the W3C's Tag", - author: "Yehuda Katz" - } - } - ``` - - ### Conventional Names - - Attribute names in your JSON payload should be the underscored versions of - the attributes in your Ember.js models. - - For example, if you have a `Person` model: - - ```js - App.Person = DS.Model.extend({ - firstName: DS.attr('string'), - lastName: DS.attr('string'), - occupation: DS.attr('string') - }); - ``` - - The JSON returned should look like this: - - ```js - { - "person": { - "first_name": "Barack", - "last_name": "Obama", - "occupation": "President" - } - } - ``` -*/ -DS.RESTAdapter = DS.Adapter.extend({ - bulkCommit: false, - since: 'since', - - serializer: DS.RESTSerializer, - - init: function() { - this._super.apply(this, arguments); - }, - - shouldSave: function(record) { - var reference = get(record, '_reference'); - - return !reference.parent; - }, - - createRecord: function(store, type, record) { - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record, { includeId: true }); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - - dirtyRecordsForRecordChange: function(dirtySet, record) { - this._dirtyTree(dirtySet, record); - }, - - dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) { - var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName); - - if (embeddedType === 'always') { - relationship.childReference.parent = relationship.parentReference; - this._dirtyTree(dirtySet, record); - } - }, - - _dirtyTree: function(dirtySet, record) { - dirtySet.add(record); - - get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { - if (embeddedType !== 'always') { return; } - if (dirtySet.has(embeddedRecord)) { return; } - this._dirtyTree(dirtySet, embeddedRecord); - }, this); - - var reference = record.get('_reference'); - - if (reference.parent) { - var store = get(record, 'store'); - var parent = store.recordForReference(reference.parent); - this._dirtyTree(dirtySet, parent); - } - }, - - createRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(this.serialize(record, { includeId: true })); - }, this); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecords(store, type, records, json); - }); - } - }); - }, - - updateRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record); - - this.ajax(this.buildURL(root, id), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - - updateRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(this.serialize(record, { includeId: true })); - }, this); - - this.ajax(this.buildURL(root, "bulk"), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); - }, - - deleteRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); - - this.ajax(this.buildURL(root, id), "DELETE", { - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - } - }); - }, - - deleteRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root), - serializer = get(this, 'serializer'); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(serializer.serializeId( get(record, 'id') )); - }); - - this.ajax(this.buildURL(root, 'bulk'), "DELETE", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); - }, - - find: function(store, type, id) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root, id), "GET", { - success: function(json) { - Ember.run(this, function(){ - this.didFindRecord(store, type, json, id); - }); - } - }); - }, - - findAll: function(store, type, since) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root), "GET", { - data: this.sinceQuery(since), - success: function(json) { - Ember.run(this, function(){ - this.didFindAll(store, type, json); - }); - } - }); - }, - - findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root), "GET", { - data: query, - success: function(json) { - Ember.run(this, function(){ - this.didFindQuery(store, type, json, recordArray); - }); - } - }); - }, - - findMany: function(store, type, ids, owner) { - var root = this.rootForType(type); - ids = this.serializeIds(ids); - - this.ajax(this.buildURL(root), "GET", { - data: {ids: ids}, - success: function(json) { - Ember.run(this, function(){ - this.didFindMany(store, type, json); - }); - } - }); - }, - - /** - @private - - This method serializes a list of IDs using `serializeId` - - @returns {Array} an array of serialized IDs - */ - serializeIds: function(ids) { - var serializer = get(this, 'serializer'); - - return Ember.EnumerableUtils.map(ids, function(id) { - return serializer.serializeId(id); - }); - }, - - didError: function(store, type, record, xhr) { - if (xhr.status === 422) { - var data = JSON.parse(xhr.responseText); - store.recordWasInvalid(record, data['errors']); - } else { - this._super.apply(this, arguments); - } - }, - - ajax: function(url, type, hash) { - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.contentType = 'application/json; charset=utf-8'; - hash.context = this; - - if (hash.data && type !== 'GET') { - hash.data = JSON.stringify(hash.data); - } - - jQuery.ajax(hash); - }, - - url: "", - - rootForType: function(type) { - var serializer = get(this, 'serializer'); - return serializer.rootForType(type); - }, - - pluralize: function(string) { - var serializer = get(this, 'serializer'); - return serializer.pluralize(string); - }, - - buildURL: function(record, suffix) { - var url = [this.url]; - - Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/"); - Ember.assert("Record URL (" + record + ") must not start with slash", !record || record.toString().charAt(0) !== "/"); - Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/"); - - if (this.namespace !== undefined) { - url.push(this.namespace); - } - - url.push(this.pluralize(record)); - if (suffix !== undefined) { - url.push(suffix); - } - - return url.join("/"); - }, - - sinceQuery: function(since) { - var query = {}; - query[get(this, 'since')] = since; - return since ? query : null; - } -}); - - -})(); - - - -(function() { -var camelize = Ember.String.camelize, - get = Ember.get, - registeredTransforms; - -var passthruTransform = { - serialize: function(value) { return value; }, - deserialize: function(value) { return value; } -}; - -var defaultTransforms = { - string: passthruTransform, - boolean: passthruTransform, - number: passthruTransform -}; - -function camelizeKeys(json) { - var value; - - for (var prop in json) { - value = json[prop]; - delete json[prop]; - json[camelize(prop)] = value; - } -} - -function munge(json, callback) { - callback(json); -} - -function applyTransforms(json, type, transformType) { - var transforms = registeredTransforms[transformType]; - - Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); - - get(type, 'attributes').forEach(function(name, attribute) { - var attributeType = attribute.type, - value = json[name]; - - var transform = transforms[attributeType] || defaultTransforms[attributeType]; - - Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); - - json[name] = transform.deserialize(value); - }); -} - -function ObjectProcessor(json, type, store) { - this.json = json; - this.type = type; - this.store = store; -} - -ObjectProcessor.prototype = { - load: function() { - this.store.load(this.type, {}, this.json); - }, - - camelizeKeys: function() { - camelizeKeys(this.json); - return this; - }, - - munge: function(callback) { - munge(this.json, callback); - return this; - }, - - applyTransforms: function(transformType) { - applyTransforms(this.json, this.type, transformType); - return this; - } -}; - -function processorFactory(store, type) { - return function(json) { - return new ObjectProcessor(json, type, store); - }; -} - -function ArrayProcessor(json, type, array, store) { - this.json = json; - this.type = type; - this.array = array; - this.store = store; -} - -ArrayProcessor.prototype = { - load: function() { - var store = this.store, - type = this.type; - - var references = this.json.map(function(object) { - return store.load(type, {}, object); - }); - - this.array.load(references); - }, - - camelizeKeys: function() { - this.json.forEach(camelizeKeys); - return this; - }, - - munge: function(callback) { - this.json.forEach(function(object) { - munge(object, callback); - }); - return this; - }, - - applyTransforms: function(transformType) { - var type = this.type; - - this.json.forEach(function(object) { - applyTransforms(object, type, transformType); - }); - - return this; - } -}; - -function arrayProcessorFactory(store, type, array) { - return function(json) { - return new ArrayProcessor(json, type, array, store); - }; -} - -DS.BasicAdapter = DS.Adapter.extend({ - find: function(store, type, id) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); - - sync.find(id, processorFactory(store, type)); - }, - - findQuery: function(store, type, query, recordArray) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); - - sync.query(query, arrayProcessorFactory(store, type, recordArray)); - } -}); - -DS.registerTransforms = function(kind, object) { - registeredTransforms[kind] = object; -}; - -DS.clearTransforms = function() { - registeredTransforms = {}; -}; - -DS.clearTransforms(); - -})(); - - - -(function() { - -})(); - - - -(function() { -//Copyright (C) 2011 by Living Social, Inc. - -//Permission is hereby granted, free of charge, to any person obtaining a copy of -//this software and associated documentation files (the "Software"), to deal in -//the Software without restriction, including without limitation the rights to -//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -//of the Software, and to permit persons to whom the Software is furnished to do -//so, subject to the following conditions: - -//The above copyright notice and this permission notice shall be included in all -//copies or substantial portions of the Software. - -//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -//SOFTWARE. - -})(); - diff --git a/assets/scripts/vendor/ember-model.js b/assets/scripts/vendor/ember-model.js new file mode 100644 index 00000000..b04a12ab --- /dev/null +++ b/assets/scripts/vendor/ember-model.js @@ -0,0 +1,1695 @@ +(function() { + +var VERSION = '0.0.10'; + +if (Ember.libraries) { + Ember.libraries.register('Ember Model', VERSION); +} + + +})(); + +(function() { + +function mustImplement(message) { + var fn = function() { + var className = this.constructor.toString(); + + throw new Error(message.replace('{{className}}', className)); + }; + fn.isUnimplemented = true; + return fn; +} + +Ember.Adapter = Ember.Object.extend({ + 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() { + +var get = Ember.get; + +Ember.FixtureAdapter = Ember.Adapter.extend({ + _findData: function(klass, id) { + var fixtures = klass.FIXTURES, + idAsString = id.toString(), + primaryKey = get(klass, 'primaryKey'), + data = Ember.A(fixtures).find(function(el) { return (el[primaryKey]).toString() === idAsString; }); + + return data; + }, + + find: function(record, id) { + var data = this._findData(record.constructor, id); + + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run.later(this, function() { + Ember.run(record, record.load, id, data); + resolve(record); + }, 0); + }); + }, + + findMany: function(klass, records, ids) { + var fixtures = klass.FIXTURES, + requestedData = []; + + for (var i = 0, l = ids.length; i < l; i++) { + requestedData.push(this._findData(klass, ids[i])); + } + + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run.later(this, function() { + Ember.run(records, records.load, klass, requestedData); + resolve(records); + }, 0); + }); + }, + + findAll: function(klass, records) { + var fixtures = klass.FIXTURES; + + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run.later(this, function() { + Ember.run(records, records.load, klass, fixtures); + resolve(records); + }, 0); + }); + }, + + createRecord: function(record) { + var klass = record.constructor, + fixtures = klass.FIXTURES; + + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run.later(this, function() { + fixtures.push(klass.findFromCacheOrLoad(record.toJSON())); + record.didCreateRecord(); + resolve(record); + }, 0); + }); + }, + + saveRecord: function(record) { + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run.later(this, function() { + record.didSaveRecord(); + resolve(record); + }, 0); + }); + }, + + deleteRecord: function(record) { + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run.later(this, function() { + record.didDeleteRecord(); + resolve(record); + }, 0); + }); + } +}); + + +})(); + +(function() { + +var get = Ember.get, + set = Ember.set; + +Ember.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { + isLoaded: false, + isLoading: Ember.computed.not('isLoaded'), + + load: function(klass, data) { + set(this, 'content', this.materializeData(klass, data)); + this.notifyLoaded(); + }, + + loadForFindMany: function(klass) { + var content = get(this, '_ids').map(function(id) { return klass.cachedRecordForId(id); }); + set(this, 'content', Ember.A(content)); + this.notifyLoaded(); + }, + + notifyLoaded: function() { + set(this, 'isLoaded', true); + this.trigger('didLoad'); + }, + + materializeData: function(klass, data) { + return Ember.A(data.map(function(el) { + return klass.findFromCacheOrLoad(el); // FIXME + })); + }, + + reload: function() { + var modelClass = this.get('modelClass'), + self = this, + promises; + + set(this, 'isLoaded', false); + 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(); + }); + } + } +}); + + +})(); + +(function() { + +var get = Ember.get; + +Ember.FilteredRecordArray = Ember.RecordArray.extend({ + init: function() { + if (!get(this, 'modelClass')) { + throw new Error('FilteredRecordArrays must be created with a modelClass'); + } + if (!get(this, 'filterFunction')) { + throw new Error('FilteredRecordArrays must be created with a filterFunction'); + } + if (!get(this, 'filterProperties')) { + throw new Error('FilteredRecordArrays must be created with filterProperties'); + } + + var modelClass = get(this, 'modelClass'); + modelClass.registerRecordArray(this); + + this.registerObservers(); + this.updateFilter(); + + this._super(); + }, + + updateFilter: function() { + var self = this, + results = []; + get(this, 'modelClass').forEachCachedRecord(function(record) { + if (self.filterFunction(record)) { + results.push(record); + } + }); + this.set('content', Ember.A(results)); + }, + + updateFilterForRecord: function(record) { + var results = get(this, 'content'), + filterMatches = this.filterFunction(record); + if (filterMatches && !results.contains(record)) { + results.pushObject(record); + } else if(!filterMatches) { + results.removeObject(record); + } + }, + + registerObservers: function() { + var self = this; + get(this, 'modelClass').forEachCachedRecord(function(record) { + self.registerObserversOnRecord(record); + }); + }, + + registerObserversOnRecord: function(record) { + var self = this, + filterProperties = get(this, 'filterProperties'); + + for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { + record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); + } + } +}); + + +})(); + +(function() { + +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'); + + if (!content.length) { return; } + + return this.materializeRecord(idx); + }, + + save: function() { + // TODO: loop over dirty records only + return Ember.RSVP.all(this.map(function(record) { + return record.save(); + })); + }, + + replaceContent: function(index, removed, added) { + added = Ember.EnumerableUtils.map(added, function(record) { + return record._reference; + }, 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(); + } +}); + +Ember.HasManyArray = Ember.ManyArray.extend({ + materializeRecord: function(idx) { + var klass = get(this, 'modelClass'), + content = get(this, 'content'), + reference = content.objectAt(idx), + record; + + if (reference.record) { + record = reference.record; + } else { + record = klass.find(reference.id); + } + + return record; + }, + + toJSON: function() { + var ids = [], content = this.get('content'); + + content.forEach(function(reference) { + if (reference.id) { + ids.push(reference.id); + } + }); + + return ids; + } +}); + +Ember.EmbeddedHasManyArray = Ember.ManyArray.extend({ + create: function(attrs) { + var klass = get(this, 'modelClass'), + record = klass.create(attrs); + + this.pushObject(record); + + return record; // FIXME: inject parent's id + }, + + materializeRecord: function(idx) { + var klass = get(this, 'modelClass'), + primaryKey = get(klass, 'primaryKey'), + content = get(this, 'content'), + reference = content.objectAt(idx), + attrs = reference.data; + + if (reference.record) { + return reference.record; + } else { + var record = klass.create({ _reference: reference }); + reference.record = record; + if (attrs) { + record.load(attrs[primaryKey], attrs); + } + return record; + } + }, + + toJSON: function() { + return this.map(function(record) { + return record.toJSON(); + }); + } +}); + + +})(); + +(function() { + +var get = Ember.get, + set = Ember.set, + setProperties = Ember.setProperties, + meta = Ember.meta, + underscore = Ember.String.underscore; + +function contains(array, element) { + for (var i = 0, l = array.length; i < l; i++) { + if (array[i] === element) { return true; } + } + return false; +} + +function concatUnique(toArray, fromArray) { + var e; + for (var i = 0, l = fromArray.length; i < l; i++) { + e = fromArray[i]; + if (!contains(toArray, e)) { toArray.push(e); } + } + return toArray; +} + +function hasCachedValue(object, key) { + var objectMeta = meta(object, false); + if (objectMeta) { + return key in objectMeta.cache; + } +} + +Ember.run.queues.push('data'); + +Ember.Model = Ember.Object.extend(Ember.Evented, { + isLoaded: true, + isLoading: Ember.computed.not('isLoaded'), + isNew: true, + isDeleted: false, + _dirtyAttributes: null, + + /** + Called when attribute is accessed. + + @method getAttr + @param key {String} key which is being accessed + @param value {Object} value, which will be returned from getter by default + */ + getAttr: function(key, value) { + return value; + }, + + isDirty: function() { + var dirtyAttributes = get(this, '_dirtyAttributes'); + return dirtyAttributes && dirtyAttributes.length !== 0 || false; + }.property('_dirtyAttributes.length'), + + _relationshipBecameDirty: function(name) { + var dirtyAttributes = get(this, '_dirtyAttributes'); + if (!dirtyAttributes.contains(name)) { dirtyAttributes.pushObject(name); } + }, + + _relationshipBecameClean: function(name) { + var dirtyAttributes = get(this, '_dirtyAttributes'); + dirtyAttributes.removeObject(name); + }, + + dataKey: function(key) { + var camelizeKeys = get(this.constructor, 'camelizeKeys'); + var meta = this.constructor.metaForProperty(key); + if (meta.options && meta.options.key) { + return camelizeKeys ? underscore(meta.options.key) : meta.options.key; + } + return camelizeKeys ? underscore(key) : key; + }, + + init: function() { + this._createReference(); + if (!this._dirtyAttributes) { + set(this, '_dirtyAttributes', []); + } + this._super(); + }, + + _createReference: function() { + var reference = this._reference, + id = this.getPrimaryKey(); + + if (!reference) { + 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) { + reference.id = id; + } + + return reference; + }, + + getPrimaryKey: function() { + return get(this, get(this.constructor, 'primaryKey')); + }, + + load: function(id, hash) { + 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(); + this.trigger('didLoad'); + }, + + didDefineProperty: function(proto, key, value) { + if (value instanceof Ember.Descriptor) { + var meta = value.meta(); + var klass = proto.constructor; + + if (meta.isAttribute) { + if (!klass._attributes) { klass._attributes = []; } + klass._attributes.push(key); + } else if (meta.isRelationship) { + if (!klass._relationships) { klass._relationships = []; } + klass._relationships.push(key); + meta.relationshipKey = key; + } + } + }, + + serializeHasMany: function(key, meta) { + return this.get(key).toJSON(); + }, + + serializeBelongsTo: function(key, meta) { + if (meta.options.embedded) { + var record = this.get(key); + return record ? record.toJSON() : null; + } else { + var primaryKey = get(meta.getType(), 'primaryKey'); + return this.get(key + '.' + primaryKey); + } + }, + + toJSON: function() { + var key, meta, + json = {}, + attributes = this.constructor.getAttributes(), + relationships = this.constructor.getRelationships(), + properties = attributes ? this.getProperties(attributes) : {}, + rootKey = get(this.constructor, 'rootKey'); + + for (key in properties) { + meta = this.constructor.metaForProperty(key); + if (meta.type && meta.type.serialize) { + json[this.dataKey(key)] = meta.type.serialize(properties[key]); + } else if (meta.type && Ember.Model.dataTypes[meta.type]) { + json[this.dataKey(key)] = Ember.Model.dataTypes[meta.type].serialize(properties[key]); + } else { + json[this.dataKey(key)] = properties[key]; + } + } + + if (relationships) { + var data, relationshipKey; + + for(var i = 0; i < relationships.length; i++) { + key = relationships[i]; + meta = this.constructor.metaForProperty(key); + relationshipKey = meta.options.key || key; + + if (meta.kind === 'belongsTo') { + data = this.serializeBelongsTo(key, meta); + } else { + data = this.serializeHasMany(key, meta); + } + + json[relationshipKey] = data; + + } + } + + if (rootKey) { + var jsonRoot = {}; + jsonRoot[rootKey] = json; + return jsonRoot; + } else { + return json; + } + }, + + save: function() { + var adapter = this.constructor.adapter; + set(this, 'isSaving', true); + if (get(this, 'isNew')) { + return adapter.createRecord(this); + } else if (get(this, 'isDirty')) { + return adapter.saveRecord(this); + } else { // noop, return a resolved promise + var self = this, + promise = new Ember.RSVP.Promise(function(resolve, reject) { + resolve(self); + }); + set(this, 'isSaving', false); + return promise; + } + }, + + reload: function() { + this.getWithDefault('_dirtyAttributes', []).clear(); + return this.constructor.reload(this.get(get(this.constructor, 'primaryKey'))); + }, + + revert: function() { + this.getWithDefault('_dirtyAttributes', []).clear(); + this.notifyPropertyChange('_data'); + }, + + didCreateRecord: function() { + var primaryKey = get(this.constructor, 'primaryKey'), + id = get(this, primaryKey); + + set(this, 'isNew', false); + + set(this, '_dirtyAttributes', []); + this.constructor.addToRecordArrays(this); + this.trigger('didCreateRecord'); + this.didSaveRecord(); + }, + + didSaveRecord: function() { + set(this, 'isSaving', false); + this.trigger('didSaveRecord'); + if (this.get('isDirty')) { this._copyDirtyAttributesToData(); } + }, + + deleteRecord: function() { + return this.constructor.adapter.deleteRecord(this); + }, + + didDeleteRecord: function() { + this.constructor.removeFromRecordArrays(this); + set(this, 'isDeleted', true); + this.trigger('didDeleteRecord'); + }, + + _copyDirtyAttributesToData: function() { + if (!this._dirtyAttributes) { return; } + var dirtyAttributes = this._dirtyAttributes, + data = get(this, '_data'), + key; + + if (!data) { + data = {}; + set(this, '_data', data); + } + for (var i = 0, l = dirtyAttributes.length; i < l; i++) { + // TODO: merge Object.create'd object into prototype + key = dirtyAttributes[i]; + data[this.dataKey(key)] = this.cacheFor(key); + } + set(this, '_dirtyAttributes', []); + }, + + dataDidChange: Ember.observer(function() { + this._reloadHasManys(); + }, '_data'), + + _registerHasManyArray: function(array) { + if (!this._hasManyArrays) { this._hasManyArrays = Ember.A([]); } + + this._hasManyArrays.pushObject(array); + }, + + _reloadHasManys: function() { + if (!this._hasManyArrays) { return; } + 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); + } + }, + + _getHasManyContent: function(key, type, embedded) { + var content = get(this, '_data.' + key); + + if (content) { + var mapFunction, primaryKey, reference; + if (embedded) { + primaryKey = get(type, 'primaryKey'); + mapFunction = function(attrs) { + reference = type._getOrCreateReferenceForId(attrs[primaryKey]); + reference.data = attrs; + return reference; + }; + } else { + mapFunction = function(id) { return type._getOrCreateReferenceForId(id); }; + } + content = Ember.EnumerableUtils.map(content, mapFunction); + } + + return Ember.A(content || []); + } +}); + +Ember.Model.reopenClass({ + primaryKey: 'id', + + adapter: Ember.Adapter.create(), + + _clientIdCounter: 1, + + 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._findFetchAll(false); + } else if (Ember.isArray(id)) { + return this._findFetchMany(id, false); + } else if (typeof id === 'object') { + return this._findFetchQuery(id, false); + } else { + return this._findFetchById(id, false); + } + }, + + findQuery: function(params) { + return this._findFetchQuery(params, false); + }, + + 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); + + if (this._currentBatchIds) { + concatUnique(this._currentBatchIds, ids); + this._currentBatchRecordArrays.push(records); + } else { + this._currentBatchIds = concatUnique([], ids); + 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 isFetch ? deferred : records; + }, + + findAll: function() { + 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}); + + var promise = this.adapter.findAll(this, 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, + _currentBatchDeferreds: null, + + reload: function(id) { + var record = this.cachedRecordForId(id); + record.set('isLoaded', false); + return this._fetchById(record, id); + }, + + _fetchById: function(record, id) { + var adapter = get(this, 'adapter'), + deferred; + + if (adapter.findMany && !adapter.findMany.isUnimplemented) { + if (this._currentBatchIds) { + if (!contains(this._currentBatchIds, id)) { this._currentBatchIds.push(id); } + } else { + this._currentBatchIds = [id]; + 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); + + return deferred; + } else { + return adapter.find(record, id); + } + }, + + _executeBatch: function() { + var batchIds = this._currentBatchIds, + batchRecordArrays = this._currentBatchRecordArrays || [], + batchDeferreds = this._currentBatchDeferreds, + self = this, + requestIds = [], + promise, + i; + + 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}); + 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); + } + } + }); + }, + + getCachedReferenceRecord: function(id){ + var ref = this._getReferenceById(id); + if(ref) return ref.record; + return undefined; + }, + + cachedRecordForId: function(id) { + var record = this.getCachedReferenceRecord(id); + + if (!record) { + var primaryKey = get(this, 'primaryKey'), + attrs = {isLoaded: false}; + attrs[primaryKey] = id; + record = this.create(attrs); + var sideloadedData = this.sideloadedData && this.sideloadedData[id]; + if (sideloadedData) { + record.load(id, sideloadedData); + } + } + + return record; + }, + + + addToRecordArrays: function(record) { + if (this._findAllRecordArray) { + this._findAllRecordArray.pushObject(record); + } + if (this.recordArrays) { + this.recordArrays.forEach(function(recordArray) { + if (recordArray instanceof Ember.FilteredRecordArray) { + recordArray.registerObserversOnRecord(record); + recordArray.updateFilterForRecord(record); + } else { + recordArray.pushObject(record); + } + }); + } + }, + + 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); + } + if (this.recordArrays) { + this.recordArrays.forEach(function(recordArray) { + recordArray.removeObject(record); + }); + } + }, + + // FIXME + findFromCacheOrLoad: function(data) { + var record; + if (!data[get(this, 'primaryKey')]) { + record = this.create({isLoaded: false}); + } else { + record = this.cachedRecordForId(data[get(this, 'primaryKey')]); + } + // set(record, 'data', data); + record.load(data[get(this, 'primaryKey')], data); + return record; + }, + + registerRecordArray: function(recordArray) { + if (!this.recordArrays) { this.recordArrays = []; } + this.recordArrays.push(recordArray); + }, + + unregisterRecordArray: function(recordArray) { + if (!this.recordArrays) { return; } + Ember.A(this.recordArrays).removeObject(recordArray); + }, + + forEachCachedRecord: function(callback) { + if (!this._referenceCache) { this._referenceCache = {}; } + var ids = Object.keys(this._referenceCache); + ids.map(function(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], + primaryKey = hash[get(this, 'primaryKey')], + record = this.getCachedReferenceRecord(primaryKey); + + if (record) { + record.load(primaryKey, hash); + } else { + this.sideloadedData[primaryKey] = hash; + } + } + }, + + _getReferenceById: function(id) { + if (!this._referenceCache) { this._referenceCache = {}; } + return this._referenceCache[id]; + }, + + _getOrCreateReferenceForId: function(id) { + var reference = this._getReferenceById(id); + + if (!reference) { + reference = this._createReference(id); + } + + return reference; + }, + + _createReference: function(id) { + if (!this._referenceCache) { this._referenceCache = {}; } + + 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++ + }; + + this._cacheReference(reference); + + return reference; + }, + + _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; + } + } +}); + + +})(); + +(function() { + +var get = Ember.get; + +Ember.hasMany = function(type, options) { + options = options || {}; + + var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }, + key = options.key; + + return Ember.computed(function() { + if (typeof type === "string") { + type = Ember.get(Ember.lookup, type); + } + + return this.getHasMany(key, type, meta); + }).property().meta(meta); +}; + +Ember.Model.reopen({ + getHasMany: function(key, type, meta) { + var embedded = meta.options.embedded, + collectionClass = embedded ? Ember.EmbeddedHasManyArray : Ember.HasManyArray; + + var collection = collectionClass.create({ + parent: this, + modelClass: type, + content: this._getHasManyContent(key, type, embedded), + embedded: embedded, + key: key, + relationshipKey: meta.relationshipKey + }); + + this._registerHasManyArray(collection); + + return collection; + } +}); + + +})(); + +(function() { + +var get = Ember.get, + set = Ember.set; + +function getType() { + if (typeof this.type === "string") { + this.type = Ember.get(Ember.lookup, this.type); + } + return this.type; +} + +Ember.belongsTo = function(type, options) { + options = options || {}; + + var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo', getType: getType }, + relationshipKey = options.key; + + return Ember.computed(function(key, value, oldValue) { + type = meta.getType(); + + 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 { + return this.getBelongsTo(relationshipKey, type, meta); + } + }).property('_data').meta(meta); +}; + +Ember.Model.reopen({ + getBelongsTo: function(key, type, meta) { + var idOrAttrs = get(this, '_data.' + key), + record; + + if (Ember.isNone(idOrAttrs)) { + return null; + } + + if (meta.options.embedded) { + var primaryKey = get(type, 'primaryKey'), + id = idOrAttrs[primaryKey]; + record = type.create({ isLoaded: false, id: id }); + record.load(id, idOrAttrs); + } else { + record = type.find(idOrAttrs); + } + + return record; + } +}); + + +})(); + +(function() { + +var get = Ember.get, + set = Ember.set, + meta = Ember.meta; + +Ember.Model.dataTypes = {}; + +Ember.Model.dataTypes[Date] = { + deserialize: function(string) { + if (!string) { return null; } + return new Date(string); + }, + serialize: function (date) { + if (!date) { return null; } + return date.toISOString(); + }, + isEqual: function(obj1, obj2) { + if (obj1 instanceof Date) { obj1 = this.serialize(obj1); } + if (obj2 instanceof Date) { obj2 = this.serialize(obj2); } + return obj1 === obj2; + } +}; + +Ember.Model.dataTypes[Number] = { + deserialize: function(string) { + if (!string && string !== 0) { return null; } + return Number(string); + }, + serialize: function (number) { + if (!number && number !== 0) { return null; } + return Number(number); + } +}; + +function deserialize(value, type) { + if (type && type.deserialize) { + return type.deserialize(value); + } else if (type && Ember.Model.dataTypes[type]) { + return Ember.Model.dataTypes[type].deserialize(value); + } else { + 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, + dirtyAttributes = get(this, '_dirtyAttributes'), + createdDirtyAttributes = false; + + if (!dirtyAttributes) { + dirtyAttributes = []; + createdDirtyAttributes = true; + } + + if (arguments.length === 2) { + if (beingCreated) { + if (!data) { + data = {}; + set(this, '_data', data); + } + dataValue = data[dataKey] = 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)); + }).property('_data').meta({isAttribute: true, type: type, options: options}); +}; + + +})(); + +(function() { + +var get = Ember.get; + +Ember.RESTAdapter = Ember.Adapter.extend({ + find: function(record, id) { + var url = this.buildURL(record.constructor, id), + self = this; + + return this.ajax(url).then(function(data) { + self.didFind(record, id, data); + return record; + }); + }, + + didFind: function(record, id, data) { + var rootKey = get(record.constructor, 'rootKey'), + dataToLoad = rootKey ? data[rootKey] : data; + + record.load(id, dataToLoad); + }, + + findAll: function(klass, records) { + var url = this.buildURL(klass), + self = this; + + return this.ajax(url).then(function(data) { + self.didFindAll(klass, records, data); + return records; + }); + }, + + didFindAll: function(klass, records, data) { + var collectionKey = get(klass, 'collectionKey'), + dataToLoad = collectionKey ? data[collectionKey] : data; + + records.load(klass, dataToLoad); + }, + + findQuery: function(klass, records, params) { + var url = this.buildURL(klass), + self = this; + + return this.ajax(url, params).then(function(data) { + self.didFindQuery(klass, records, params, data); + return records; + }); + }, + + didFindQuery: function(klass, records, params, data) { + var collectionKey = get(klass, 'collectionKey'), + dataToLoad = collectionKey ? data[collectionKey] : data; + + records.load(klass, dataToLoad); + }, + + createRecord: function(record) { + var url = this.buildURL(record.constructor), + self = this; + + return this.ajax(url, record.toJSON(), "POST").then(function(data) { + self.didCreateRecord(record, data); + return record; + }); + }, + + didCreateRecord: function(record, data) { + var rootKey = get(record.constructor, 'rootKey'), + primaryKey = get(record.constructor, 'primaryKey'), + dataToLoad = rootKey ? data[rootKey] : data; + record.load(dataToLoad[primaryKey], dataToLoad); + record.didCreateRecord(); + }, + + saveRecord: function(record) { + var primaryKey = get(record.constructor, 'primaryKey'), + url = this.buildURL(record.constructor, get(record, primaryKey)), + self = this; + + 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; + }); + }, + + didSaveRecord: function(record, data) { + record.didSaveRecord(); + }, + + deleteRecord: function(record) { + var primaryKey = get(record.constructor, 'primaryKey'), + url = this.buildURL(record.constructor, get(record, primaryKey)), + self = this; + + return this.ajax(url, record.toJSON(), "DELETE").then(function(data) { // TODO: Some APIs may or may not return data + self.didDeleteRecord(record, data); + }); + }, + + didDeleteRecord: function(record, data) { + record.didDeleteRecord(); + }, + + 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 (!Ember.isEmpty(id)) { + return urlRoot + "/" + id + ".json"; + } else { + return urlRoot + ".json"; + } + }, + + ajaxSettings: function(url, method) { + return { + url: url, + type: method, + dataType: "json" + }; + }, + + _ajax: function(url, params, method, settings) { + if (!settings) { + settings = this.ajaxSettings(url, method); + } + + return new Ember.RSVP.Promise(function(resolve, reject) { + if (params) { + if (method === "GET") { + settings.data = params; + } else { + settings.contentType = "application/json; charset=utf-8"; + settings.data = JSON.stringify(params); + } + } + + settings.success = function(json) { + Ember.run(null, resolve, json); + }; + + settings.error = function(jqXHR, textStatus, errorThrown) { + // https://github.com/ebryn/ember-model/issues/202 + if (jqXHR) { + jqXHR.then = null; + } + + Ember.run(null, reject, jqXHR); + }; + + + Ember.$.ajax(settings); + }); + } +}); + + +})(); + +(function() { + +var get = Ember.get; + +Ember.LoadPromise = Ember.Object.extend(Ember.DeferredMixin, { + init: function() { + this._super.apply(this, arguments); + + var target = get(this, 'target'); + + if (get(target, 'isLoaded') && !get(target, 'isNew')) { + this.resolve(target); + } else { + target.one('didLoad', this, function() { + this.resolve(target); + }); + } + } +}); + +Ember.loadPromise = function(target) { + if (Ember.isNone(target)) { + return null; + } else if (target.then) { + return target; + } else { + return Ember.LoadPromise.create({target: 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 d658dfdf..dc4bb500 100644 --- a/assets/scripts/vendor/ember.js +++ b/assets/scripts/vendor/ember.js @@ -1,5 +1,12 @@ -// Version: v1.0.0-rc.3-206-gc9bbf8e -// Last commit: c9bbf8e (2013-05-13 16:35:33 +0200) +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 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.3.1 + */ (function() { @@ -24,7 +31,20 @@ if ('undefined' === typeof Ember) { } } -Ember.ENV = 'undefined' === typeof ENV ? {} : ENV; +// This needs to be kept in sync with the logic in +// `packages/ember-metal/lib/core.js`. +// +// This is duplicated here to ensure that `Ember.ENV` +// is setup even if `Ember` is not loaded yet. +if (Ember.ENV) { + // do nothing if Ember.ENV is already setup +} else if ('undefined' !== typeof EmberENV) { + Ember.ENV = EmberENV; +} else if('undefined' !== typeof ENV) { + Ember.ENV = ENV; +} else { + Ember.ENV = {}; +} if (!('MANDATORY_SETTER' in Ember.ENV)) { Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist @@ -49,7 +69,14 @@ if (!('MANDATORY_SETTER' in Ember.ENV)) { falsy, an exception will be thrown. */ Ember.assert = function(desc, test) { - if (!test) throw new Error("assertion failed: "+desc); + if (!test) { + Ember.Logger.assert(test, desc); + } + + if (Ember.testing && !test) { + // when testing, ensure test failures when assertions fail + throw new Ember.Error("Assertion Failed: " + desc); + } }; @@ -95,12 +122,12 @@ Ember.debug = function(message) { will be displayed. */ Ember.deprecate = function(message, test) { - if (Ember && Ember.TESTING_DEPRECATION) { return; } + if (Ember.TESTING_DEPRECATION) { return; } if (arguments.length === 1) { test = false; } if (test) { return; } - if (Ember && Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Ember.Error(message); } var error; @@ -131,15 +158,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) { @@ -149,14 +182,33 @@ 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.3-206-gc9bbf8e -// Last commit: c9bbf8e (2013-05-13 16:35:33 +0200) +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 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.3.1 + */ (function() { -var define, requireModule; +var define, requireModule, require, requirejs; (function() { var registry = {}, seen = {}; @@ -165,10 +217,16 @@ var define, requireModule; registry[name] = { deps: deps, callback: callback }; }; - requireModule = function(name) { + requirejs = require = requireModule = function(name) { + requirejs._eak_seen = registry; + if (seen[name]) { return seen[name]; } seen[name] = {}; + if (!registry[name]) { + throw new Error("Could not find module " + name); + } + var mod = registry[name], deps = mod.deps, callback = mod.callback, @@ -179,16 +237,32 @@ var define, requireModule; if (deps[i] === 'exports') { reified.push(exports = {}); } else { - reified.push(requireModule(deps[i])); + reified.push(requireModule(resolve(deps[i]))); } } var value = callback.apply(this, reified); return seen[name] = exports || value; + + function resolve(child) { + if (child.charAt(0) !== '.') { return child; } + var parts = child.split("/"); + var parentBase = name.split("/").slice(0, -1); + + for (var i=0, l=parts.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); } }, @@ -1579,6 +1870,1024 @@ var utils = Ember.EnumerableUtils = { +(function() { +/** +@module ember-metal +*/ + +var META_KEY = Ember.META_KEY, get; + +var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + +var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; +var HAS_THIS = /^this[\.\*]/; +var FIRST_KEY = /^([^\.\*]+)/; + +// .......................................................... +// GET AND SET +// +// If we are on a platform that supports accessors we can use those. +// Otherwise simulate accessors by looking up the property directly on the +// object. + +/** + Gets the value of a property on an object. If the property is computed, + the function will be invoked. If the property is not defined but the + object implements the `unknownProperty` method then that will be invoked. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to retrieve a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + On all newer browsers, you only need to use this method to retrieve + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. + + Note that if the object itself is `undefined`, this method will throw + an error. + + @method get + @for Ember + @param {Object} obj The object to retrieve from. + @param {String} keyName The property key to retrieve + @return {Object} the property value or `null`. +*/ +get = function get(obj, keyName) { + // Helpers that operate with 'this' within an #each + if (keyName === '') { + return obj; + } + + if (!keyName && 'string'===typeof obj) { + keyName = obj; + obj = null; + } + + Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); + + if (obj === null || keyName.indexOf('.') !== -1) { + return getPath(obj, keyName); + } + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; + if (desc) { + return desc.get(obj, keyName); + } else { + if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { + ret = meta.values[keyName]; + } else { + ret = obj[keyName]; + } + + if (ret === undefined && + 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { + return obj.unknownProperty(keyName); + } + + return ret; + } +}; + +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.get = get; + Ember.config.overrideAccessors(); + get = Ember.get; +} + +/** + Normalizes a target/path pair to reflect that actual target/path that should + be observed, etc. This takes into account passing in global property + paths (i.e. a path beginning with a captial letter not defined on the + target) and * separators. + + @private + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. +*/ +var normalizeTuple = Ember.normalizeTuple = function(target, path) { + var hasThis = HAS_THIS.test(path), + isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), + key; + + if (!target || isGlobal) target = Ember.lookup; + if (hasThis) path = path.slice(5); + + if (target === Ember.lookup) { + key = path.match(FIRST_KEY)[0]; + target = get(target, key); + path = path.slice(key.length+1); + } + + // must return some kind of path to be valid else other things will break. + if (!path || path.length===0) throw new Ember.Error('Invalid Path'); + + return [ target, path ]; +}; + +var getPath = Ember._getPath = function(root, path) { + var hasThis, parts, tuple, idx, len; + + // If there is no root and path is a key name, return that + // property from the global object. + // E.g. get('Ember') -> Ember + if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } + + // detect complicated paths and normalize them + hasThis = HAS_THIS.test(path); + + if (!root || hasThis) { + tuple = normalizeTuple(root, path); + root = tuple[0]; + path = tuple[1]; + tuple.length = 0; + } + + parts = path.split("."); + len = parts.length; + for (idx = 0; root != null && idx < len; idx++) { + root = get(root, parts[idx], true); + if (root && root.isDestroyed) { return undefined; } + } + return root; +}; + +Ember.getWithDefault = function(root, key, defaultValue) { + var value = get(root, key); + + if (value === undefined) { return defaultValue; } + return value; +}; + + +Ember.get = get; + +})(); + + + +(function() { +/** +@module ember-metal +*/ + +var o_create = Ember.create, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY, + a_slice = [].slice, + /* listener flags */ + ONCE = 1, SUSPENDED = 2; + +/* + The event system uses a series of nested hashes to store listeners on an + object. When a listener is registered, or when an event arrives, these + hashes are consulted to determine which target and action pair to invoke. + + The hashes are stored in the object's meta hash, and look like this: + + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": [ // variable name: `actions` + target, method, flags + ] + } + } + +*/ + +function indexOf(array, target, method) { + var index = -1; + for (var i = 0, l = array.length; i < l; i += 3) { + if (target === array[i] && method === array[i+1]) { index = i; break; } + } + return index; +} + +function actionsFor(obj, eventName) { + var meta = metaFor(obj, true), + actions; + + if (!meta.listeners) { meta.listeners = {}; } + + if (!meta.hasOwnProperty('listeners')) { + // setup inherited copy of the listeners object + meta.listeners = o_create(meta.listeners); + } + + actions = meta.listeners[eventName]; + + // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype + if (actions && !meta.listeners.hasOwnProperty(eventName)) { + actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); + } else if (!actions) { + actions = meta.listeners[eventName] = []; + } + + return actions; +} + +function actionsUnion(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push(target, method, flags); + } + } +} + +function actionsDiff(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName], + diffActions = []; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex !== -1) { continue; } + + otherActions.push(target, method, flags); + diffActions.push(target, method, flags); + } + + return diffActions; +} + +/** + Add an event listener + + @method addListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Boolean} once A flag whether a function should only be called once +*/ +function addListener(obj, eventName, target, method, once) { + Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + flags = 0; + + if (once) flags |= ONCE; + + if (actionIndex !== -1) { return; } + + actions.push(target, method, flags); + + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } +} + +/** + Remove an event listener + + Arguments should match those passed to `Ember.addListener`. + + @method removeListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` +*/ +function removeListener(obj, eventName, target, method) { + Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + function _removeListener(target, method) { + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); + + // action doesn't exist, give up silently + if (actionIndex === -1) { return; } + + actions.splice(actionIndex, 3); + + if ('function' === typeof obj.didRemoveListener) { + obj.didRemoveListener(eventName, target, method); + } + } + + if (method) { + _removeListener(target, method); + } else { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + _removeListener(actions[i], actions[i+1]); + } + } +} + +/** + Suspend listener during callback. + + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. + + @private + @method suspendListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback +*/ +function suspendListener(obj, eventName, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { + actions[actionIndex+2] |= SUSPENDED; // mark the action as suspended + } + + function tryable() { return callback.call(target); } + function finalizer() { if (actionIndex !== -1) { actions[actionIndex+2] &= ~SUSPENDED; } } + + return Ember.tryFinally(tryable, finalizer); +} + +/** + Suspends multiple listeners during a callback. + + @private + @method suspendListeners + @for Ember + @param obj + @param {Array} eventName Array of event names + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback +*/ +function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var suspendedActions = [], + actionsList = [], + eventName, actions, i, l; + + for (i=0, l=eventNames.length; i= 0; i -= 3) { // looping in reverse for once listeners + var target = actions[i], method = actions[i+1], flags = actions[i+2]; + if (!method) { continue; } + if (flags & SUSPENDED) { continue; } + if (flags & ONCE) { removeListener(obj, eventName, target, method); } + if (!target) { target = obj; } + if ('string' === typeof method) { method = target[method]; } + if (params) { + method.apply(target, params); + } else { + method.call(target); + } + } + return true; +} + +/** + @private + @method hasListeners + @for Ember + @param obj + @param {String} eventName +*/ +function hasListeners(obj, eventName) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + return !!(actions && actions.length); +} + +/** + @private + @method listenersFor + @for Ember + @param obj + @param {String} eventName +*/ +function listenersFor(obj, eventName) { + var ret = []; + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return ret; } + + for (var i = 0, l = actions.length; i < l; i += 3) { + var target = actions[i], + method = actions[i+1]; + ret.push([target, method]); + } + + return ret; +} + +/** + Define a property as a function that should be executed when + a specified event or events are triggered. + + + ``` javascript + var Job = Ember.Object.extend({ + logCompleted: Ember.on('completed', function(){ + console.log('Job completed!'); + }) + }); + var job = Job.create(); + Ember.sendEvent(job, 'completed'); // Logs "Job completed!" + ``` + + @method on + @for Ember + @param {String} eventNames* + @param {Function} func + @return func +*/ +Ember.on = function(){ + var func = a_slice.call(arguments, -1)[0], + events = a_slice.call(arguments, 0, -1); + func.__ember_listens__ = events; + return func; +}; + +Ember.addListener = addListener; +Ember.removeListener = removeListener; +Ember._suspendListener = suspendListener; +Ember._suspendListeners = suspendListeners; +Ember.sendEvent = sendEvent; +Ember.hasListeners = hasListeners; +Ember.watchedEvents = watchedEvents; +Ember.listenersFor = listenersFor; +Ember.listenersDiff = actionsDiff; +Ember.listenersUnion = actionsUnion; + +})(); + + + +(function() { +var guidFor = Ember.guidFor, + sendEvent = Ember.sendEvent; + +/* + this.observerSet = { + [senderGuid]: { // variable name: `keySet` + [keyName]: listIndex + } + }, + this.observers = [ + { + sender: obj, + keyName: keyName, + eventName: eventName, + listeners: [ + [target, method, flags] + ] + }, + ... + ] +*/ +var ObserverSet = Ember._ObserverSet = function() { + this.clear(); +}; + +ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = guidFor(sender), + keySet = observerSet[senderGuid], + index; + + if (!keySet) { + observerSet[senderGuid] = keySet = {}; + } + index = keySet[keyName]; + if (index === undefined) { + index = observers.push({ + sender: sender, + keyName: keyName, + eventName: eventName, + listeners: [] + }) - 1; + keySet[keyName] = index; + } + return observers[index].listeners; +}; + +ObserverSet.prototype.flush = function() { + var observers = this.observers, i, len, observer, sender; + this.clear(); + for (i=0, len=observers.length; i < len; ++i) { + observer = observers[i]; + sender = observer.sender; + if (sender.isDestroying || sender.isDestroyed) { continue; } + sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); + } +}; + +ObserverSet.prototype.clear = function() { + this.observerSet = {}; + this.observers = []; +}; +})(); + + + +(function() { +var metaFor = Ember.meta, + guidFor = Ember.guidFor, + tryFinally = Ember.tryFinally, + sendEvent = Ember.sendEvent, + listenersUnion = Ember.listenersUnion, + listenersDiff = Ember.listenersDiff, + ObserverSet = Ember._ObserverSet, + beforeObserverSet = new ObserverSet(), + observerSet = new ObserverSet(), + deferred = 0; + +// .......................................................... +// PROPERTY CHANGES +// + +/** + This function is called just before an object property is about to change. + It will notify any before observers and prepare caches among other things. + + 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.propertyDidChange()` which you should call just + after the property value changes. + + @method propertyWillChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} +*/ +function propertyWillChange(obj, keyName) { + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; + + if (!watching) { return; } + if (proto === obj) { return; } + if (desc && desc.willChange) { desc.willChange(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. + It will notify any observers and clear caches among other things. + + 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 + before the property value changes. + + @method propertyDidChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} +*/ +function propertyDidChange(obj, keyName) { + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; + + if (proto === obj) { return; } + + // shouldn't this mean that we're watching this key? + if (desc && desc.didChange) { desc.didChange(obj, keyName); } + if (!watching && keyName !== 'length') { return; } + + dependentKeysDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); + notifyObservers(obj, keyName); +} +Ember.propertyDidChange = propertyDidChange; + +var WILL_SEEN, DID_SEEN; + +// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) +function dependentKeysWillChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } + + var seen = WILL_SEEN, top = !seen; + if (top) { seen = WILL_SEEN = {}; } + iterDeps(propertyWillChange, obj, depKey, seen, meta); + if (top) { WILL_SEEN = null; } +} + +// called whenever a property has just changed to update dependent keys +function dependentKeysDidChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } + + var seen = DID_SEEN, top = !seen; + if (top) { seen = DID_SEEN = {}; } + iterDeps(propertyDidChange, obj, depKey, seen, meta); + if (top) { DID_SEEN = null; } +} + +function iterDeps(method, obj, depKey, seen, meta) { + var guid = guidFor(obj); + if (!seen[guid]) seen[guid] = {}; + if (seen[guid][depKey]) return; + seen[guid][depKey] = true; + + var deps = meta.deps; + deps = deps && deps[depKey]; + if (deps) { + for(var key in deps) { + var desc = meta.descs[key]; + if (desc && desc._suspended === obj) continue; + method(obj, key); + } + } +} + +function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = [], + i, l; + + 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); +}; + +/** + @method beginPropertyChanges + @chainable + @private +*/ +function beginPropertyChanges() { + deferred++; +} + +Ember.beginPropertyChanges = beginPropertyChanges; + +/** + @method endPropertyChanges + @private +*/ +function endPropertyChanges() { + deferred--; + if (deferred<=0) { + beforeObserverSet.clear(); + observerSet.flush(); + } +} + +Ember.endPropertyChanges = endPropertyChanges; + +/** + Make a series of property changes together in an + exception-safe way. + + ```javascript + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); + }); + ``` + + @method changeProperties + @param {Function} callback + @param [binding] +*/ +Ember.changeProperties = function(cb, binding) { + beginPropertyChanges(); + tryFinally(cb, endPropertyChanges, binding); +}; + +function notifyBeforeObservers(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':before', listeners, diff; + if (deferred) { + listeners = beforeObserverSet.add(obj, keyName, eventName); + diff = listenersDiff(obj, eventName, listeners); + sendEvent(obj, eventName, [obj, keyName], diff); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } +} + +function notifyObservers(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':change', listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + listenersUnion(obj, eventName, listeners); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } +} + +})(); + + + +(function() { +// META_KEY +// _getPath +// propertyWillChange, propertyDidChange + +var META_KEY = Ember.META_KEY, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/, + getPath = Ember._getPath; + +/** + 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 `setUnknownProperty` + method then that will be invoked as well. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to set a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + 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 `setUnknownProperty` handler. Otherwise you can ignore this + method. + + @method set + @for Ember + @param {Object} obj The object to modify. + @param {String} keyName The property key to set + @param {Object} value The value to set + @return {Object} the passed value. +*/ +var set = function set(obj, keyName, value, tolerant) { + if (typeof obj === 'string') { + Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); + value = keyName; + keyName = obj; + obj = null; + } + + Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); + + if (!obj || keyName.indexOf('.') !== -1) { + return setPath(obj, keyName, value, tolerant); + } + + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], + isUnknown, currentValue; + if (desc) { + desc.set(obj, keyName, value); + } else { + isUnknown = 'object' === typeof obj && !(keyName in obj); + + // setUnknownProperty is called if `obj` is an object, + // the property does not already exist, and the + // `setUnknownProperty` method exists on the object + if (isUnknown && 'function' === typeof obj.setUnknownProperty) { + obj.setUnknownProperty(keyName, value); + } else if (meta && meta.watching[keyName] > 0) { + if (MANDATORY_SETTER) { + currentValue = meta.values[keyName]; + } else { + currentValue = obj[keyName]; + } + // only trigger a change if the value has changed + if (value !== currentValue) { + Ember.propertyWillChange(obj, keyName); + if (MANDATORY_SETTER) { + if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) { + Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } + } else { + obj[keyName] = value; + } + Ember.propertyDidChange(obj, keyName); + } + } else { + obj[keyName] = value; + } + } + return value; +}; + +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.set = set; + Ember.config.overrideAccessors(); + set = Ember.set; +} + +function setPath(root, path, value, tolerant) { + var keyName; + + // get the last part of the path + keyName = path.slice(path.lastIndexOf('.') + 1); + + // get the first part of the part + path = (path === keyName) ? keyName : path.slice(0, path.length-(keyName.length+1)); + + // unless the path is this, look up the first part to + // get the root + if (path !== 'this') { + root = getPath(root, path); + } + + if (!keyName || keyName.length === 0) { + throw new Ember.Error('Property set failed: You passed an empty path'); + } + + if (!root) { + if (tolerant) { return; } + else { throw new Ember.Error('Property set failed: object in path "'+path+'" could not be found or was destroyed.'); } + } + + return set(root, keyName, value); +} + +Ember.set = set; + +/** + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + chain is `undefined`, `null`, or destroyed. + + This is primarily used when syncing bindings, which may try to update after + an object has been destroyed. + + @method trySet + @for Ember + @param {Object} obj The object to modify. + @param {String} path The property path to set + @param {Object} value The value to set +*/ +Ember.trySet = function(root, path, value) { + return set(root, path, value, true); +}; + +})(); + + + (function() { /** @module ember-metal @@ -1603,7 +2912,8 @@ var utils = Ember.EnumerableUtils = { Map is mocked out to look like an Ember object, so you can do `Ember.Map.create()` for symmetry with other Ember classes. */ -var guidFor = Ember.guidFor, +var set = Ember.set, + guidFor = Ember.guidFor, indexOf = Ember.ArrayPolyfills.indexOf; var copy = function(obj) { @@ -1622,6 +2932,7 @@ var copyMap = function(original, newObject) { newObject.keys = keys; newObject.values = values; + newObject.length = original.length; return newObject; }; @@ -1781,6 +3092,16 @@ 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. @@ -1810,6 +3131,7 @@ Map.prototype = { keys.add(key); values[guid] = value; + set(this, 'length', keys.list.length); }, /** @@ -1829,6 +3151,7 @@ Map.prototype = { if (values.hasOwnProperty(guid)) { keys.remove(key); delete values[guid]; + set(this, 'length', keys.list.length); return true; } else { return false; @@ -1945,980 +3268,142 @@ MapWithDefault.prototype.copy = function() { (function() { -/** -@module ember-metal -*/ - -var META_KEY = Ember.META_KEY, get; - -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; - -var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; -var HAS_THIS = /^this[\.\*]/; -var FIRST_KEY = /^([^\.\*]+)/; - -// .......................................................... -// GET AND SET -// -// If we are on a platform that supports accessors we can use those. -// Otherwise simulate accessors by looking up the property directly on the -// object. - -/** - Gets the value of a property on an object. If the property is computed, - the function will be invoked. If the property is not defined but the - object implements the `unknownProperty` method then that will be invoked. - - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to retrieve a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to retrieve - properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this - method. - - Note that if the object itself is `undefined`, this method will throw - an error. - - @method get - @for Ember - @param {Object} obj The object to retrieve from. - @param {String} keyName The property key to retrieve - @return {Object} the property value or `null`. -*/ -get = function get(obj, keyName) { - // Helpers that operate with 'this' within an #each - if (keyName === '') { - return obj; +function consoleMethod(name) { + var consoleObj, logToConsole; + if (Ember.imports.console) { + consoleObj = Ember.imports.console; + } else if (typeof console !== 'undefined') { + consoleObj = console; } - if (!keyName && 'string'===typeof obj) { - keyName = obj; - obj = null; - } - - Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); - - if (obj === null || keyName.indexOf('.') !== -1) { - return getPath(obj, keyName); - } - - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; - if (desc) { - return desc.get(obj, keyName); - } else { - if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { - ret = meta.values[keyName]; - } else { - ret = obj[keyName]; - } - - if (ret === undefined && - 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { - return obj.unknownProperty(keyName); - } - - return ret; - } -}; - -// Currently used only by Ember Data tests -if (Ember.config.overrideAccessors) { - Ember.get = get; - Ember.config.overrideAccessors(); - get = Ember.get; -} - -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} - -// assumes path is already normalized -function normalizeTuple(target, path) { - var hasThis = HAS_THIS.test(path), - isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), - key; - - if (!target || isGlobal) target = Ember.lookup; - if (hasThis) path = path.slice(5); - - if (target === Ember.lookup) { - key = firstKey(path); - target = get(target, key); - path = path.slice(key.length+1); - } - - // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); - - return [ target, path ]; -} - -var getPath = Ember._getPath = function(root, path) { - var hasThis, parts, tuple, idx, len; - - // If there is no root and path is a key name, return that - // property from the global object. - // E.g. get('Ember') -> Ember - if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } - - // detect complicated paths and normalize them - hasThis = HAS_THIS.test(path); - - if (!root || hasThis) { - tuple = normalizeTuple(root, path); - root = tuple[0]; - path = tuple[1]; - tuple.length = 0; - } - - parts = path.split("."); - len = parts.length; - for (idx=0; root !== undefined && root !== null && idx= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - flags = actions[i][2], - actionIndex = indexOf(otherActions, target, method); - - if (actionIndex === -1) { - otherActions.push([target, method, flags]); - } - } -} - -function actionsDiff(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName], - diffActions = []; - - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - flags = actions[i][2], - actionIndex = indexOf(otherActions, target, method); - - if (actionIndex !== -1) { continue; } - - otherActions.push([target, method, flags]); - diffActions.push([target, method, flags]); - } - - return diffActions; -} - -/** - Add an event listener - - @method addListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Boolean} once A flag whether a function should only be called once -*/ -function addListener(obj, eventName, target, method, once) { - Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); - - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method), - flags = 0; - - if (once) flags |= ONCE; - - if (actionIndex !== -1) { return; } - - actions.push([target, method, flags]); - - if ('function' === typeof obj.didAddListener) { - obj.didAddListener(eventName, target, method); - } -} - -/** - Remove an event listener - - Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} - - @method removeListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` -*/ -function removeListener(obj, eventName, target, method) { - Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); - - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - function _removeListener(target, method) { - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); - - // action doesn't exist, give up silently - if (actionIndex === -1) { return; } - - actions.splice(actionIndex, 1); - - if ('function' === typeof obj.didRemoveListener) { - obj.didRemoveListener(eventName, target, method); - } - } + var method = typeof consoleObj === 'object' ? consoleObj[name] : null; if (method) { - _removeListener(target, method); - } else { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; - - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - _removeListener(actions[i][0], actions[i][1]); - } - } -} - -/** - @private - - Suspend listener during callback. - - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. - - @method suspendListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback -*/ -function suspendListener(obj, eventName, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method), - action; - - if (actionIndex !== -1) { - action = actions[actionIndex].slice(); // copy it, otherwise we're modifying a shared object - action[2] |= SUSPENDED; // mark the action as suspended - actions[actionIndex] = action; // replace the shared object with our copy - } - - function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[2] &= ~SUSPENDED; } } - - return Ember.tryFinally(tryable, finalizer); -} - -/** - @private - - Suspend listener during callback. - - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. - - @method suspendListener - @for Ember - @param obj - @param {Array} eventName Array of event names - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback -*/ -function suspendListeners(obj, eventNames, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var suspendedActions = [], - eventName, actions, action, i, l; - - for (i=0, l=eventNames.length; i= 0; i--) { // looping in reverse for once listeners - var action = actions[i]; - if (!action) { continue; } - var target = action[0], method = action[1], flags = action[2]; - if (flags & SUSPENDED) { continue; } - if (flags & ONCE) { removeListener(obj, eventName, target, method); } - if (!target) { target = obj; } - if ('string' === typeof method) { method = target[method]; } - if (params) { - method.apply(target, params); + // Older IE doesn't support apply, but Chrome needs it + if (method.apply) { + logToConsole = function() { + method.apply(consoleObj, arguments); + }; + logToConsole.displayName = 'console.' + name; + return logToConsole; } else { - method.call(target); - } - } - return true; -} - -/** - @private - @method hasListeners - @for Ember - @param obj - @param {String} eventName -*/ -function hasListeners(obj, eventName) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; - - return !!(actions && actions.length); -} - -/** - @private - @method listenersFor - @for Ember - @param obj - @param {String} eventName -*/ -function listenersFor(obj, eventName) { - var ret = []; - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; - - if (!actions) { return ret; } - - for (var i = 0, l = actions.length; i < l; i++) { - var target = actions[i][0], - method = actions[i][1]; - ret.push([target, method]); - } - - return ret; -} - -Ember.addListener = addListener; -Ember.removeListener = removeListener; -Ember._suspendListener = suspendListener; -Ember._suspendListeners = suspendListeners; -Ember.sendEvent = sendEvent; -Ember.hasListeners = hasListeners; -Ember.watchedEvents = watchedEvents; -Ember.listenersFor = listenersFor; -Ember.listenersDiff = actionsDiff; -Ember.listenersUnion = actionsUnion; - -})(); - - - -(function() { -var guidFor = Ember.guidFor, - sendEvent = Ember.sendEvent; - -/* - this.observerSet = { - [senderGuid]: { // variable name: `keySet` - [keyName]: listIndex - } - }, - this.observers = [ - { - sender: obj, - keyName: keyName, - eventName: eventName, - listeners: [ - [target, method, flags] - ] - }, - ... - ] -*/ -var ObserverSet = Ember._ObserverSet = function() { - this.clear(); -}; - -ObserverSet.prototype.add = function(sender, keyName, eventName) { - var observerSet = this.observerSet, - observers = this.observers, - senderGuid = guidFor(sender), - keySet = observerSet[senderGuid], - index; - - if (!keySet) { - observerSet[senderGuid] = keySet = {}; - } - index = keySet[keyName]; - if (index === undefined) { - index = observers.push({ - sender: sender, - keyName: keyName, - eventName: eventName, - listeners: [] - }) - 1; - keySet[keyName] = index; - } - return observers[index].listeners; -}; - -ObserverSet.prototype.flush = function() { - var observers = this.observers, i, len, observer, sender; - this.clear(); - for (i=0, len=observers.length; i < len; ++i) { - observer = observers[i]; - sender = observer.sender; - if (sender.isDestroying || sender.isDestroyed) { continue; } - sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); - } -}; - -ObserverSet.prototype.clear = function() { - this.observerSet = {}; - this.observers = []; -}; -})(); - - - -(function() { -var metaFor = Ember.meta, - guidFor = Ember.guidFor, - tryFinally = Ember.tryFinally, - sendEvent = Ember.sendEvent, - listenersUnion = Ember.listenersUnion, - listenersDiff = Ember.listenersDiff, - ObserverSet = Ember._ObserverSet, - beforeObserverSet = new ObserverSet(), - observerSet = new ObserverSet(), - deferred = 0; - -// .......................................................... -// PROPERTY CHANGES -// - -/** - This function is called just before an object property is about to change. - It will notify any before observers and prepare caches among other things. - - 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.propertyDidChange()` which you should call just - after the property value changes. - - @method propertyWillChange - @for Ember - @param {Object} obj The object with the property that will change - @param {String} keyName The property key (or path) that will change. - @return {void} -*/ -var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; - - if (!watching) { return; } - if (proto === obj) { return; } - if (desc && desc.willChange) { desc.willChange(obj, keyName); } - dependentKeysWillChange(obj, keyName, m); - chainsWillChange(obj, keyName, m); - notifyBeforeObservers(obj, keyName); -}; - -/** - This function is called just after an object property has changed. - It will notify any observers and clear caches among other things. - - 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 - before the property value changes. - - @method propertyDidChange - @for Ember - @param {Object} obj The object with the property that will change - @param {String} keyName The property key (or path) that will change. - @return {void} -*/ -var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; - - if (proto === obj) { return; } - - // shouldn't this mean that we're watching this key? - if (desc && desc.didChange) { desc.didChange(obj, keyName); } - if (!watching && keyName !== 'length') { return; } - - dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); - notifyObservers(obj, keyName); -}; - -var WILL_SEEN, DID_SEEN; - -// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) -function dependentKeysWillChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } - - var seen = WILL_SEEN, top = !seen; - if (top) { seen = WILL_SEEN = {}; } - iterDeps(propertyWillChange, obj, depKey, seen, meta); - if (top) { WILL_SEEN = null; } -} - -// called whenever a property has just changed to update dependent keys -function dependentKeysDidChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } - - var seen = DID_SEEN, top = !seen; - if (top) { seen = DID_SEEN = {}; } - iterDeps(propertyDidChange, obj, depKey, seen, meta); - if (top) { DID_SEEN = null; } -} - -function iterDeps(method, obj, depKey, seen, meta) { - var guid = guidFor(obj); - if (!seen[guid]) seen[guid] = {}; - if (seen[guid][depKey]) return; - seen[guid][depKey] = true; - - var deps = meta.deps; - deps = deps && deps[depKey]; - if (deps) { - for(var key in deps) { - var desc = meta.descs[key]; - if (desc && desc._suspended === obj) continue; - method(obj, key); + return function() { + var message = Array.prototype.join.call(arguments, ', '); + method(message); + }; } } } -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; } - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); - } -}; - -var chainsDidChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - // looping in reverse because the chainWatchers array can be modified inside didChange - for (var i = nodes.length - 1; i >= 0; i--) { - nodes[i].didChange(arg); - } -}; - -Ember.overrideChains = function(obj, keyName, m) { - chainsDidChange(obj, keyName, m, true); -}; - -/** - @method beginPropertyChanges - @chainable -*/ -var beginPropertyChanges = Ember.beginPropertyChanges = function() { - deferred++; -}; - -/** - @method endPropertyChanges -*/ -var endPropertyChanges = Ember.endPropertyChanges = function() { - deferred--; - if (deferred<=0) { - beforeObserverSet.clear(); - observerSet.flush(); - } -}; - -/** - Make a series of property changes together in an - exception-safe way. - - ```javascript - Ember.changeProperties(function() { - obj1.set('foo', mayBlowUpWhenSet); - obj2.set('bar', baz); - }); - ``` - - @method changePropertiess - @param {Function} callback - @param [binding] -*/ -Ember.changeProperties = function(cb, binding){ - beginPropertyChanges(); - tryFinally(cb, endPropertyChanges, binding); -}; - -var notifyBeforeObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } - - var eventName = keyName + ':before', listeners, diff; - if (deferred) { - listeners = beforeObserverSet.add(obj, keyName, eventName); - diff = listenersDiff(obj, eventName, listeners); - sendEvent(obj, eventName, [obj, keyName], diff); - } else { - sendEvent(obj, eventName, [obj, keyName]); - } -}; - -var notifyObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } - - var eventName = keyName + ':change', listeners; - if (deferred) { - listeners = observerSet.add(obj, keyName, eventName); - listenersUnion(obj, eventName, listeners); - } else { - sendEvent(obj, eventName, [obj, keyName]); - } -}; -})(); - - - -(function() { -// META_KEY -// _getPath -// propertyWillChange, propertyDidChange - -var META_KEY = Ember.META_KEY, - MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, - IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/, - getPath = Ember._getPath; - -/** - 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` - method then that will be invoked as well. - - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - 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 - method. - - @method set - @for Ember - @param {Object} obj The object to modify. - @param {String} keyName The property key to set - @param {Object} value The value to set - @return {Object} the passed value. -*/ -var set = function set(obj, keyName, value, tolerant) { - if (typeof obj === 'string') { - Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); - value = keyName; - keyName = obj; - obj = null; - } - - if (!obj || keyName.indexOf('.') !== -1) { - return setPath(obj, keyName, value, tolerant); - } - - Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); - Ember.assert('calling set on destroyed object', !obj.isDestroyed); - - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], - isUnknown, currentValue; - if (desc) { - desc.set(obj, keyName, value); - } else { - isUnknown = 'object' === typeof obj && !(keyName in obj); - - // setUnknownProperty is called if `obj` is an object, - // the property does not already exist, and the - // `setUnknownProperty` method exists on the object - if (isUnknown && 'function' === typeof obj.setUnknownProperty) { - obj.setUnknownProperty(keyName, value); - } else if (meta && meta.watching[keyName] > 0) { - if (MANDATORY_SETTER) { - currentValue = meta.values[keyName]; - } else { - currentValue = obj[keyName]; - } - // only trigger a change if the value has changed - if (value !== currentValue) { - Ember.propertyWillChange(obj, keyName); - if (MANDATORY_SETTER) { - if (currentValue === undefined && !(keyName in obj)) { - Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter - } else { - meta.values[keyName] = value; - } - } else { - obj[keyName] = value; - } - Ember.propertyDidChange(obj, keyName); - } - } else { - obj[keyName] = value; +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); } } - return value; -}; - -// Currently used only by Ember Data tests -if (Ember.config.overrideAccessors) { - Ember.set = set; - Ember.config.overrideAccessors(); - set = Ember.set; } -function setPath(root, path, value, tolerant) { - var keyName; - - // get the last part of the path - keyName = path.slice(path.lastIndexOf('.') + 1); - - // get the first part of the part - path = path.slice(0, path.length-(keyName.length+1)); - - // unless the path is this, look up the first part to - // get the root - if (path !== 'this') { - root = getPath(root, path); - } - - if (!keyName || keyName.length === 0) { - throw new 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.'); } - } - - 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 - chain is `undefined`, `null`, or destroyed. + Inside Ember-Metal, simply uses the methods from `imports.console`. + Override this to provide more robust logging functionality. - This is primarily used when syncing bindings, which may try to update after - an object has been destroyed. - - @method trySet - @for Ember - @param {Object} obj The object to modify. - @param {String} path The property path to set - @param {Object} value The value to set + @class Logger + @namespace Ember */ -Ember.trySet = function(root, path, value) { - return set(root, path, value, true); +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 trace. + 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 }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); + })(); @@ -2968,8 +3453,6 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { }; /** - @private - NOTE: This is a low-level method used by other parts of the API. You almost never want to call this method directly. Instead you should use `Ember.mixin()` to define new properties. @@ -3003,6 +3486,7 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { }).property('firstName', 'lastName')); ``` + @private @method defineProperty @for Ember @param {Object} obj the object to define this property on. This may be a prototype. @@ -3039,7 +3523,6 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { } else { obj[keyName] = undefined; // make enumerable } - desc.setup(obj, keyName); } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { @@ -3080,6 +3563,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; @@ -3089,19 +3613,28 @@ 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 target - @param {Hash} properties - @return target + @param self + @param {Object} hash + @return self */ Ember.setProperties = function(self, hash) { - changeProperties(function(){ + changeProperties(function() { for(var prop in hash) { if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } } }); return self; }; + })(); @@ -3116,13 +3649,11 @@ Ember.watchKey = function(obj, keyName) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching, desc; + var m = metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { watching[keyName] = 1; - desc = m.descs[keyName]; - if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } if ('function' === typeof obj.willWatchProperty) { obj.willWatchProperty(keyName); @@ -3132,7 +3663,7 @@ Ember.watchKey = function(obj, keyName) { m.values[keyName] = obj[keyName]; o_defineProperty(obj, keyName, { configurable: true, - enumerable: true, + enumerable: obj.propertyIsEnumerable(keyName), set: Ember.MANDATORY_SETTER_FUNCTION, get: Ember.DEFAULT_GETTER_FUNCTION(keyName) }); @@ -3144,13 +3675,10 @@ Ember.watchKey = function(obj, keyName) { Ember.unwatchKey = function(obj, keyName) { - var m = metaFor(obj), watching = m.watching, desc; + var m = metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; - desc = m.descs[keyName]; - - if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } if ('function' === typeof obj.didUnwatchProperty) { obj.didUnwatchProperty(keyName); @@ -3159,16 +3687,25 @@ Ember.unwatchKey = function(obj, keyName) { if (MANDATORY_SETTER && keyName in obj) { o_defineProperty(obj, keyName, { configurable: true, - enumerable: true, - writable: true, - value: m.values[keyName] + enumerable: obj.propertyIsEnumerable(keyName), + set: function(val) { + // redefine to set as enumerable + o_defineProperty(obj, keyName, { + configurable: true, + writable: true, + enumerable: true, + value: val + }); + delete m.values[keyName]; + }, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) }); - delete m.values[keyName]; } } else if (watching[keyName] > 1) { watching[keyName]--; } }; + })(); @@ -3181,8 +3718,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) { @@ -3237,10 +3772,6 @@ var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) unwatchKey(obj, keyName); }; -function isProto(pvalue) { - return metaFor(pvalue, false).proto === pvalue; -} - // A ChainNode watches a single key on an object. If you provide a starting // value for the key then the node won't actually watch it. For a root node // pass null for parent and key and object for value. @@ -3275,10 +3806,32 @@ var ChainNode = Ember._ChainNode = function(parent, key, value) { var ChainNodePrototype = ChainNode.prototype; +function lazyGet(obj, key) { + if (!obj) return undefined; + + var meta = metaFor(obj, false); + // check if object meant only to be a prototype + if (meta.proto === obj) return undefined; + + if (key === "@each") return get(obj, key); + + // if a CP only return cached value + var desc = meta.descs[key]; + if (desc && desc._cacheable) { + if (key in meta.cache) { + return meta.cache[key]; + } else { + return undefined; + } + } + + return get(obj, key); +} + ChainNodePrototype.value = function() { if (this._value === undefined && this._watching) { var obj = this._parent.value(); - this._value = (obj && !isProto(obj)) ? get(obj, this._key) : undefined; + this._value = lazyGet(obj, this._key); } return this._value; }; @@ -3398,42 +3951,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(); @@ -3455,14 +4016,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) { @@ -3471,9 +4033,16 @@ Ember.finishChains = function(obj) { if (chains.value() !== obj) { m.chains = chains = chains.copy(obj); } - chains.didChange(true); + chains.didChange(null); } }; + +})(); + + + +(function() { + })(); @@ -3547,27 +4116,26 @@ function isKeyName(path) { } /** - @private - Starts watching a property on an object. Whenever the property changes, invokes `Ember.propertyWillChange` and `Ember.propertyDidChange`. This is the primitive used by observers and dependent keys; usually you will never call this method directly but instead use higher level methods like `Ember.addObserver()` + @private @method watch @for Ember @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); + if (isKeyName(_keyPath)) { + watchKey(obj, _keyPath); } else { - watchPath(obj, keyPath); + watchPath(obj, _keyPath); } }; @@ -3578,24 +4146,23 @@ 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); + if (isKeyName(_keyPath)) { + unwatchKey(obj, _keyPath); } else { - unwatchPath(obj, keyPath); + unwatchPath(obj, _keyPath); } }; /** - @private - Call on an object when you first beget it from another object. This will setup any chained watchers on the object instance as needed. This method is safe to call multiple times. + @private @method rewatch @for Ember @param obj @@ -3605,7 +4172,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. @@ -3678,6 +4245,7 @@ var get = Ember.get, watch = Ember.watch, unwatch = Ember.unwatch; + // .......................................................... // DEPENDENT KEYS // @@ -3754,6 +4322,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 @@ -3772,28 +4415,15 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/* - Call on a computed property to explicitly change it's cacheable mode. +/** + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. - Please use `.volatile` over this method. + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. - ```javascript - MyApp.president = Ember.Object.create({ - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // By default, Ember will return the value of this property - // without re-executing this function. - }.property('firstName', 'lastName') - - initials: function() { - return this.get('firstName')[0] + this.get('lastName')[0]; - - // This function will be executed every time this property - // is requested. - }.property('firstName', 'lastName').cacheable(false) - }); - ``` + However, if a property is properly observable, there is no reason to disable + caching. @method cacheable @param {Boolean} aFlag optional set to `false` to disable caching @@ -3810,11 +4440,11 @@ ComputedPropertyPrototype.cacheable = function(aFlag) { mode the computed property will not automatically cache the return value. ```javascript - MyApp.outsideService = Ember.Object.create({ + MyApp.outsideService = Ember.Object.extend({ value: function() { return OutsideService.getValue(); }.property().volatile() - }); + }).create(); ``` @method volatile @@ -3830,12 +4460,14 @@ ComputedPropertyPrototype.volatile = function() { mode the computed property will throw an error when set. ```javascript - MyApp.person = Ember.Object.create({ + MyApp.Person = Ember.Object.extend({ guid: function() { return 'guid-guid-guid'; }.property().readOnly() }); + MyApp.person = MyApp.Person.create(); + MyApp.person.set('guid', 'new-guid'); // will throw an exception ``` @@ -3853,7 +4485,7 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { arguments containing key paths that this computed property depends on. ```javascript - MyApp.president = Ember.Object.create({ + MyApp.President = Ember.Object.extend({ fullName: Ember.computed(function() { return this.get('firstName') + ' ' + this.get('lastName'); @@ -3861,6 +4493,12 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { // and lastName }).property('firstName', 'lastName') }); + + MyApp.president = MyApp.President.create({ + firstName: 'Barack', + lastName: 'Obama', + }); + MyApp.president.get('fullName'); // Barack Obama ``` @method property @@ -3869,10 +4507,12 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { @chainable */ ComputedPropertyPrototype.property = function() { - var args = []; - for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); - } + var args; + + + args = a_slice.call(arguments); + + this._dependentKeys = args; return this; }; @@ -3911,25 +4551,6 @@ ComputedPropertyPrototype.meta = function(meta) { } }; -/* impl descriptor API */ -ComputedPropertyPrototype.willWatch = function(obj, keyName) { - // watch already creates meta for this instance - var meta = obj[META_KEY]; - Ember.assert('watch should have setup meta to be writable', meta.source === obj); - if (!(keyName in meta.cache)) { - addDependentKeys(this, obj, keyName, meta); - } -}; - -ComputedPropertyPrototype.didUnwatch = function(obj, keyName) { - var meta = obj[META_KEY]; - Ember.assert('unwatch should have setup meta to be writable', meta.source === obj); - if (!(keyName in meta.cache)) { - // unwatch already creates meta for this instance - removeDependentKeys(this, obj, keyName, meta); - } -}; - /* impl descriptor API */ ComputedPropertyPrototype.didChange = function(obj, keyName) { // _suspended is set via a CP.set to ensure we don't clear @@ -3938,31 +4559,76 @@ ComputedPropertyPrototype.didChange = function(obj, keyName) { var meta = metaFor(obj); if (keyName in meta.cache) { delete meta.cache[keyName]; - if (!meta.watching[keyName]) { - removeDependentKeys(this, obj, keyName, meta); - } + removeDependentKeys(this, obj, keyName, meta); } } }; -/* impl descriptor API */ +function finishChains(chainNodes) +{ + for (var i=0, l=chainNodes.length; i 1) { set(this, dependentKey, value); return value; @@ -4350,6 +5279,67 @@ Ember.computed.alias = function(dependentKey) { }; /** + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + data flow, `computed.oneWay` only provides an aliased `get`. The `set` will + not mutate the upstream property, rather causes the current property to + become the value set. This causes the downstream property to permentantly + diverge from the upstream property. + + Example + + ```javascript + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.oneWay('firstName') + }); + + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # 'TeddyBear' + + user.get('firstName'); + # 'Teddy' + ``` + + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. +*/ +Ember.computed.oneWay = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }); +}; + + +/** + A computed property that acts like a standard getter and setter, + but returns the value at the provided `defaultPath` if the + property itself has not been set to a value + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + wishList: Ember.computed.defaultTo('favoriteFood') + }); + var hamster = Hamster.create({favoriteFood: 'Banana'}); + hamster.get('wishList'); // 'Banana' + hamster.set('wishList', 'More Unit Tests'); + hamster.get('wishList'); // 'More Unit Tests' + hamster.get('favoriteFood'); // 'Banana' + ``` + @method computed.defaultTo @for Ember @param {String} defaultPath @@ -4365,6 +5355,7 @@ Ember.computed.defaultTo = function(defaultPath) { }); }; + })(); @@ -4375,8 +5366,8 @@ Ember.computed.defaultTo = function(defaultPath) { @module ember-metal */ -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; +var AFTER_OBSERVERS = ':change', + BEFORE_OBSERVERS = ':before'; function changeEvent(keyName) { return keyName+AFTER_OBSERVERS; @@ -4393,9 +5384,10 @@ function beforeEvent(keyName) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.addObserver = function(obj, path, target, method) { - Ember.addListener(obj, changeEvent(path), target, method); - Ember.watch(obj, path); +Ember.addObserver = function(obj, _path, target, method) { + Ember.addListener(obj, changeEvent(_path), target, method); + Ember.watch(obj, _path); + return this; }; @@ -4410,9 +5402,10 @@ Ember.observersFor = function(obj, path) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.removeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); +Ember.removeObserver = function(obj, _path, target, method) { + Ember.unwatch(obj, _path); + Ember.removeListener(obj, changeEvent(_path), target, method); + return this; }; @@ -4423,9 +5416,10 @@ Ember.removeObserver = function(obj, path, target, method) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.addBeforeObserver = function(obj, path, target, method) { - Ember.addListener(obj, beforeEvent(path), target, method); - Ember.watch(obj, path); +Ember.addBeforeObserver = function(obj, _path, target, method) { + Ember.addListener(obj, beforeEvent(_path), target, method); + Ember.watch(obj, _path); + return this; }; @@ -4464,193 +5458,684 @@ Ember.beforeObserversFor = function(obj, path) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.removeBeforeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, beforeEvent(path), target, method); +Ember.removeBeforeObserver = function(obj, _path, target, method) { + Ember.unwatch(obj, _path); + Ember.removeListener(obj, beforeEvent(_path), target, method); + return this; }; + })(); (function() { -// Ember.Logger -// Ember.watch.flushPending -// Ember.beginPropertyChanges, Ember.endPropertyChanges -// Ember.guidFor, Ember.tryFinally - -/** -@module ember-metal -*/ - -// .......................................................... -// HELPERS -// - -var slice = [].slice, - forEach = Ember.ArrayPolyfills.forEach; - -// invokes passed params - normalizing so you can pass target/func, -// target/string or just func -function invoke(target, method, args, ignore) { - - if (method === undefined) { - method = target; - target = undefined; - } - - if ('string' === typeof method) { method = target[method]; } - if (args && ignore > 0) { - args = args.length > ignore ? slice.call(args, ignore) : null; - } - - return Ember.handleErrors(function() { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - }, this); -} - - -// .......................................................... -// RUNLOOP -// - -/** -Ember RunLoop (Private) - -@class RunLoop -@namespace Ember -@private -@constructor -*/ -var RunLoop = function(prev) { - this._prev = prev || null; - this.onceTimers = {}; -}; - -RunLoop.prototype = { - /** - @method end - */ - end: function() { - this.flush(); - }, - - /** - @method prev - */ - prev: function() { - return this._prev; - }, - - // .......................................................... - // Delayed Actions - // - - /** - @method schedule - @param {String} queueName - @param target - @param method - */ - schedule: function(queueName, target, method) { - var queues = this._queues, queue; - if (!queues) { queues = this._queues = {}; } - queue = queues[queueName]; - if (!queue) { queue = queues[queueName] = []; } - - var args = arguments.length > 3 ? slice.call(arguments, 3) : null; - queue.push({ target: target, method: method, args: args }); - return this; - }, - - /** - @method flush - @param {String} queueName - */ - flush: function(queueName) { - var queueNames, idx, len, queue, log; - - if (!this._queues) { return this; } // nothing to do - - function iter(item) { - invoke(item.target, item.method, item.args); +define("backburner/queue", + ["exports"], + function(__exports__) { + "use strict"; + function Queue(daq, name, options) { + this.daq = daq; + this.name = name; + this.options = options; + this._queue = []; } - function tryable() { - forEach.call(queue, iter); - } + Queue.prototype = { + daq: null, + name: null, + options: null, + _queue: null, - Ember.watch.flushPending(); // make sure all chained watchers are setup + push: function(target, method, args, stack) { + var queue = this._queue; + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - if (queueName) { - while (this._queues && (queue = this._queues[queueName])) { - this._queues[queueName] = null; + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; - // the sync phase is to allow property changes to propagate. don't - // invoke observers until that is finished. - if (queueName === 'sync') { - log = Ember.LOG_BINDINGS; - if (log) { Ember.Logger.log('Begin: Flush Sync Queue'); } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - Ember.beginPropertyChanges(); - - Ember.tryFinally(tryable, Ember.endPropertyChanges); - - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } - - } else { - forEach.call(queue, iter); - } - } - - } else { - queueNames = Ember.run.queues; - len = queueNames.length; - idx = 0; - - outerloop: - while (idx < len) { - queueName = queueNames[idx]; - queue = this._queues && this._queues[queueName]; - delete this._queues[queueName]; - - if (queue) { - // the sync phase is to allow property changes to propagate. don't - // invoke observers until that is finished. - if (queueName === 'sync') { - log = Ember.LOG_BINDINGS; - if (log) { Ember.Logger.log('Begin: Flush Sync Queue'); } - - Ember.beginPropertyChanges(); - - Ember.tryFinally(tryable, Ember.endPropertyChanges); - - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } - } else { - forEach.call(queue, iter); + if (currentTarget === target && currentMethod === method) { + queue[i+2] = args; // replace args + queue[i+3] = stack; // replace stack + return {queue: this, target: target, method: method}; // TODO: test this code path } } - // Loop through prior queues - for (var i = 0; i <= idx; i++) { - if (this._queues && this._queues[queueNames[i]]) { - // Start over at the first queue with contents - idx = i; + this._queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, + + // TODO: remove me, only being used for Ember.run.sync + flush: function() { + var queue = this._queue, + options = this.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, i, l = queue.length; + + if (l && before) { before(); } + for (i = 0; i < l; i += 4) { + target = queue[i]; + method = queue[i+1]; + args = queue[i+2]; + stack = queue[i+3]; // Debugging assistance + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } + if (l && after) { after(); } + + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); + } else { + this._queue.length = 0; + } + }, + + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; + + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; + } + } + + // if not found in current queue + // could be in the queue that is being flushed + queue = this._queueBeingFlushed; + if (!queue) { + return; + } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + // don't mess with array during flush + // just nullify the method + queue[i+1] = null; + return true; + } + } + } + }; + + + __exports__.Queue = Queue; + }); + +define("backburner/deferred_action_queues", + ["backburner/queue","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Queue = __dependency1__.Queue; + + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; + + var queueName; + for (var i = 0, l = queueNames.length; i < l; i++) { + queueName = queueNames[i]; + queues[queueName] = new Queue(this, queueName, options[queueName]); + } + } + + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, + + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; + + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } + + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, + + flush: function() { + var queues = this.queues, + queueNames = this.queueNames, + queueName, queue, queueItems, priorQueueNameIndex, + queueNameIndex = 0, numberOfQueues = queueNames.length; + + outerloop: + while (queueNameIndex < numberOfQueues) { + queueName = queueNames[queueNameIndex]; + queue = queues[queueName]; + queueItems = queue._queueBeingFlushed = queue._queue.slice(); + queue._queue = []; + + var options = queue.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, + queueIndex = 0, numberOfQueueItems = queueItems.length; + + if (numberOfQueueItems && before) { before(); } + while (queueIndex < numberOfQueueItems) { + target = queueItems[queueIndex]; + method = queueItems[queueIndex+1]; + args = queueItems[queueIndex+2]; + stack = queueItems[queueIndex+3]; // Debugging assistance + + if (typeof method === 'string') { method = target[method]; } + + // method could have been nullified / canceled during flush + if (method) { + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } + + queueIndex += 4; + } + queue._queueBeingFlushed = null; + if (numberOfQueueItems && after) { after(); } + + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; continue outerloop; } + + queueNameIndex++; + } + } + }; + + function indexOfPriorQueueWithActions(daq, currentQueueIndex) { + var queueName, queue; + + for (var i = 0, l = currentQueueIndex; i <= l; i++) { + queueName = daq.queueNames[i]; + queue = daq.queues[queueName]; + if (queue._queue.length) { return i; } + } + + return -1; + } + + + __exports__.DeferredActionQueues = DeferredActionQueues; + }); + +define("backburner", + ["backburner/deferred_action_queues","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DeferredActionQueues = __dependency1__.DeferredActionQueues; + + var slice = [].slice, + pop = [].pop, + throttlers = [], + debouncees = [], + timers = [], + autorun, laterTimer, laterTimerExpiresAt, + global = this, + NUMBER = /\d+/; + + function isCoercableNumber(number) { + return typeof number === 'number' || NUMBER.test(number); + } + + function Backburner(queueNames, options) { + this.queueNames = queueNames; + this.options = options || {}; + if (!this.options.defaultQueue) { + this.options.defaultQueue = queueNames[0]; + } + this.instanceStack = []; + } + + Backburner.prototype = { + queueNames: null, + options: null, + currentInstance: null, + instanceStack: null, + + begin: function() { + var onBegin = this.options && this.options.onBegin, + previousInstance = this.currentInstance; + + if (previousInstance) { + this.instanceStack.push(previousInstance); } - idx++; + this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); + if (onBegin) { + onBegin(this.currentInstance, previousInstance); + } + }, + + end: function() { + var onEnd = this.options && this.options.onEnd, + currentInstance = this.currentInstance, + nextInstance = null; + + try { + currentInstance.flush(); + } finally { + this.currentInstance = null; + + if (this.instanceStack.length) { + nextInstance = this.instanceStack.pop(); + this.currentInstance = nextInstance; + } + + if (onEnd) { + onEnd(currentInstance, nextInstance); + } + } + }, + + run: function(target, method /*, args */) { + var ret; + this.begin(); + + if (!method) { + method = target; + target = null; + } + + if (typeof method === 'string') { + method = target[method]; + } + + // Prevent Safari double-finally. + var finallyAlreadyCalled = false; + try { + if (arguments.length > 2) { + ret = method.apply(target, slice.call(arguments, 2)); + } else { + ret = method.call(target); + } + } finally { + if (!finallyAlreadyCalled) { + finallyAlreadyCalled = true; + this.end(); + } + } + return ret; + }, + + defer: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } + + if (typeof method === 'string') { + method = target[method]; + } + + var stack = this.DEBUG ? new Error() : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, false, stack); + }, + + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } + + if (typeof method === 'string') { + method = target[method]; + } + + var stack = this.DEBUG ? new Error() : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, true, stack); + }, + + setTimeout: function() { + var args = slice.call(arguments); + var length = args.length; + var method, wait, target; + var self = this; + var methodOrTarget, methodOrWait, methodOrArgs; + + if (length === 0) { + return; + } else if (length === 1) { + method = args.shift(); + wait = 0; + } else if (length === 2) { + methodOrTarget = args[0]; + methodOrWait = args[1]; + + if (typeof methodOrWait === 'function' || typeof methodOrTarget[methodOrWait] === 'function') { + target = args.shift(); + method = args.shift(); + wait = 0; + } else if (isCoercableNumber(methodOrWait)) { + method = args.shift(); + wait = args.shift(); + } else { + method = args.shift(); + wait = 0; + } + } else { + var last = args[args.length - 1]; + + if (isCoercableNumber(last)) { + wait = args.pop(); + } + + methodOrTarget = args[0]; + methodOrArgs = args[1]; + + if (typeof methodOrArgs === 'function' || (typeof methodOrArgs === 'string' && + methodOrTarget !== null && + methodOrArgs in methodOrTarget)) { + target = args.shift(); + method = args.shift(); + } else { + method = args.shift(); + } + } + + var executeAt = (+new Date()) + parseInt(wait, 10); + + if (typeof method === 'string') { + method = target[method]; + } + + function fn() { + method.apply(target, args); + } + + // find position to insert - TODO: binary search + var i, l; + for (i = 0, l = timers.length; i < l; i += 2) { + if (executeAt < timers[i]) { break; } + } + + timers.splice(i, 0, executeAt, fn); + + updateLaterTimer(self, executeAt, wait); + + return fn; + }, + + throttle: function(target, method /* , args, wait */) { + var self = this, + args = arguments, + wait = parseInt(pop.call(args), 10), + throttler, + index, + timer; + + index = findThrottler(target, method); + if (index > -1) { return throttlers[index]; } // throttled + + timer = global.setTimeout(function() { + self.run.apply(self, args); + + var index = findThrottler(target, method); + if (index > -1) { throttlers.splice(index, 1); } + }, wait); + + throttler = [target, method, timer]; + + throttlers.push(throttler); + + return throttler; + }, + + debounce: function(target, method /* , args, wait, [immediate] */) { + var self = this, + args = arguments, + immediate = pop.call(args), + wait, + index, + debouncee, + timer; + + 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); + + if (index > -1) { + debouncee = debouncees[index]; + debouncees.splice(index, 1); + clearTimeout(debouncee[2]); + } + + timer = global.setTimeout(function() { + if (!immediate) { + self.run.apply(self, args); + } + var index = findDebouncee(target, method); + if (index > -1) { + debouncees.splice(index, 1); + } + }, wait); + + if (immediate && index === -1) { + self.run.apply(self, args); + } + + debouncee = [target, method, timer]; + + debouncees.push(debouncee); + + return debouncee; + }, + + cancelTimers: function() { + var i, len; + + for (i = 0, len = throttlers.length; i < len; i++) { + clearTimeout(throttlers[i][2]); + } + throttlers = []; + + for (i = 0, len = debouncees.length; i < len; i++) { + clearTimeout(debouncees[i][2]); + } + debouncees = []; + + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + timers = []; + + if (autorun) { + clearTimeout(autorun); + autorun = null; + } + }, + + hasTimers: function() { + return !!timers.length || autorun; + }, + + cancel: function(timer) { + var timerType = typeof timer; + + if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + return timer.queue.cancel(timer); + } else if (timerType === 'function') { // we're cancelling a setTimeout + for (var i = 0, l = timers.length; i < l; i += 2) { + if (timers[i + 1] === timer) { + timers.splice(i, 2); // remove the two elements + return true; + } + } + } else if (window.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce + return this._cancelItem(findThrottler, throttlers, timer) || + this._cancelItem(findDebouncee, debouncees, timer); + } else { + return; // timer was null or not a timer + } + }, + + _cancelItem: function(findMethod, array, timer){ + var item, + index; + + if (timer.length < 3) { return false; } + + index = findMethod(timer[0], timer[1]); + + if(index > -1) { + + item = array[index]; + + if(item[2] === timer[2]){ + array.splice(index, 1); + clearTimeout(timer[2]); + return true; + } + } + + return false; + } + + }; + + Backburner.prototype.schedule = Backburner.prototype.defer; + Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; + Backburner.prototype.later = Backburner.prototype.setTimeout; + + function createAutorun(backburner) { + backburner.begin(); + autorun = global.setTimeout(function() { + autorun = null; + backburner.end(); + }); + } + + function updateLaterTimer(self, executeAt, wait) { + if (!laterTimer || executeAt < laterTimerExpiresAt) { + if (laterTimer) { + clearTimeout(laterTimer); + } + laterTimer = global.setTimeout(function() { + laterTimer = null; + laterTimerExpiresAt = null; + executeTimers(self); + }, wait); + laterTimerExpiresAt = executeAt; } } - return this; - } + function executeTimers(self) { + var now = +new Date(), + time, fns, i, l; + self.run(function() { + // TODO: binary search + for (i = 0, l = timers.length; i < l; i += 2) { + time = timers[i]; + if (time > now) { break; } + } + + fns = timers.splice(0, i); + + for (i = 1, l = fns.length; i < l; i += 2) { + self.schedule(self.options.defaultQueue, null, fns[i]); + } + }); + + if (timers.length) { + updateLaterTimer(self, timers[0], timers[0] - now); + } + } + + function findDebouncee(target, method) { + var debouncee, + index = -1; + + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { + index = i; + break; + } + } + + return index; + } + + function findThrottler(target, method) { + var throttler, + index = -1; + + for (var i = 0, l = throttlers.length; i < l; i++) { + throttler = throttlers[i]; + if (throttler[0] === target && throttler[1] === method) { + index = i; + break; + } + } + + return index; + } + + + __exports__.Backburner = Backburner; + }); +})(); + + + +(function() { +var onBegin = function(current) { + Ember.run.currentRunLoop = current; }; -Ember.RunLoop = RunLoop; +var onEnd = function(current, next) { + Ember.run.currentRunLoop = next; +}; + +var Backburner = requireModule('backburner').Backburner, + backburner = new Backburner(['sync', 'actions', 'destroy'], { + sync: { + before: Ember.beginPropertyChanges, + after: Ember.endPropertyChanges + }, + defaultQueue: 'actions', + onBegin: onBegin, + onEnd: onEnd + }), + slice = [].slice; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -4667,7 +6152,7 @@ Ember.RunLoop = RunLoop; call. ```javascript - Ember.run(function(){ + Ember.run(function() { // code to be execute within a RunLoop }); ``` @@ -4684,20 +6169,75 @@ Ember.RunLoop = RunLoop; @return {Object} return value from invoking the passed function. */ Ember.run = function(target, method) { - var args = arguments; - run.begin(); + var ret; - function tryable() { - if (target || method) { - return invoke(target, method, args, 2); + if (Ember.onerror) { + try { + ret = backburner.run.apply(backburner, arguments); + } catch (e) { + Ember.onerror(e); } + } else { + ret = backburner.run.apply(backburner, arguments); } - return Ember.tryFinally(tryable, run.end); + return ret; }; +/** + If no run-loop is present, it creates a new one. If a run loop is + present it will queue itself to run on the existing run-loops action + queue. + + Please note: This is not for normal usage, and should be used sparingly. + + If invoked when not within a run loop: + + ```javascript + Ember.run.join(function() { + // creates a new run-loop + }); + ``` + + Alternatively, if called within an existing run loop: + + ```javascript + Ember.run(function() { + // creates a new run-loop + Ember.run.join(function() { + // joins with the existing run-loop, and queues for invocation on + // the existing run-loops action queue. + }); + }); + ``` + + @method join + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} Return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.join = function(target, method) { + if (!Ember.run.currentRunLoop) { + return Ember.run.apply(Ember.run, arguments); + } + + var args = slice.call(arguments); + args.unshift('actions'); + Ember.run.schedule.apply(Ember.run, args); +}; + +Ember.run.backburner = backburner; + var run = Ember.run; +Ember.run.currentRunLoop = null; + +Ember.run.queues = backburner.queueNames; /** Begins a new RunLoop. Any deferred actions invoked after the begin will @@ -4714,7 +6254,7 @@ var run = Ember.run; @return {void} */ Ember.run.begin = function() { - run.currentRunLoop = new RunLoop(run.currentRunLoop); + backburner.begin(); }; /** @@ -4732,12 +6272,7 @@ Ember.run.begin = function() { @return {void} */ Ember.run.end = function() { - Ember.assert('must have a current run loop', run.currentRunLoop); - - function tryable() { run.currentRunLoop.end(); } - function finalizer() { run.currentRunLoop = run.currentRunLoop.prev(); } - - Ember.tryFinally(tryable, finalizer); + backburner.end(); }; /** @@ -4750,7 +6285,6 @@ Ember.run.end = function() { @type Array @default ['sync', 'actions', 'destroy'] */ -Ember.run.queues = ['sync', 'actions', 'destroy']; /** Adds the passed target/method and any optional arguments to the named @@ -4763,17 +6297,18 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; the `Ember.run.queues` property. ```javascript - Ember.run.schedule('sync', this, function(){ + Ember.run.schedule('sync', this, function() { // this will be executed in the first RunLoop queue, when bindings are synced console.log("scheduled on sync queue"); }); - Ember.run.schedule('actions', this, function(){ + Ember.run.schedule('actions', this, function() { // this will be executed in the 'actions' queue, after bindings have synced. console.log("scheduled on actions queue"); }); - // Note the functions will be run in order based on the run queues order. Output would be: + // Note the functions will be run in order based on the run queues order. + // Output would be: // scheduled on sync queue // scheduled on actions queue ``` @@ -4789,57 +6324,18 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; @return {void} */ Ember.run.schedule = function(queue, target, method) { - var loop = run.autorun(); - loop.schedule.apply(loop, arguments); + checkAutoRun(); + backburner.schedule.apply(backburner, arguments); }; -var scheduledAutorun; -function autorun() { - scheduledAutorun = null; - if (run.currentRunLoop) { run.end(); } -} - // Used by global test teardown Ember.run.hasScheduledTimers = function() { - return !!(scheduledAutorun || scheduledLater); + return backburner.hasTimers(); }; // Used by global test teardown Ember.run.cancelTimers = function () { - if (scheduledAutorun) { - clearTimeout(scheduledAutorun); - scheduledAutorun = null; - } - if (scheduledLater) { - clearTimeout(scheduledLater); - scheduledLater = null; - } - timers = {}; -}; - -/** - Begins a new RunLoop if necessary and schedules a timer to flush the - RunLoop at a later time. This method is used by parts of Ember to - ensure the RunLoop always finishes. You normally do not need to call this - method directly. Instead use `Ember.run()` - - @method autorun - @example - Ember.run.autorun(); - @return {Ember.RunLoop} the new current RunLoop -*/ -Ember.run.autorun = function() { - if (!run.currentRunLoop) { - Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); - - run.begin(); - - if (!scheduledAutorun) { - scheduledAutorun = setTimeout(autorun, 1); - } - } - - return run.currentRunLoop; + backburner.cancelTimers(); }; /** @@ -4859,59 +6355,11 @@ Ember.run.autorun = function() { @return {void} */ Ember.run.sync = function() { - run.autorun(); - run.currentRunLoop.flush('sync'); + if (backburner.currentInstance) { + backburner.currentInstance.queues.sync.flush(); + } }; -// .......................................................... -// TIMERS -// - -var timers = {}; // active timers... - -function sortByExpires(timerA, timerB) { - var a = timerA.expires, - b = timerB.expires; - - if (a > b) { return 1; } - if (a < b) { return -1; } - return 0; -} - -var scheduledLater, scheduledLaterExpires; -function invokeLaterTimers() { - scheduledLater = null; - run(function() { - var now = (+ new Date()), earliest = -1; - var timersToBeInvoked = []; - for (var key in timers) { - if (!timers.hasOwnProperty(key)) { continue; } - var timer = timers[key]; - if (timer && timer.expires) { - if (now >= timer.expires) { - delete timers[key]; - timersToBeInvoked.push(timer); - } else { - if (earliest < 0 || (timer.expires < earliest)) { earliest = timer.expires; } - } - } - } - - forEach.call(timersToBeInvoked.sort(sortByExpires), function(timer) { - invoke(timer.target, timer.method, timer.args, 2); - }); - - // schedule next timeout to fire when the earliest timer expires - if (earliest > 0) { - // To allow overwrite `setTimeout` as stub from test code. - // The assignment to `window.setTimeout` doesn't equal to `setTimeout` in older IE. - // So `window` is required. - scheduledLater = window.setTimeout(invokeLaterTimers, earliest - now); - scheduledLaterExpires = earliest; - } - }); -} - /** Invokes the passed target/method and optional arguments after a specified period if time. The last parameter of this method must always be a number @@ -4923,7 +6371,7 @@ function invokeLaterTimers() { together, which is often more efficient than using a real setTimeout. ```javascript - Ember.run.later(myContext, function(){ + Ember.run.later(myContext, function() { // code here will execute within a RunLoop in about 500ms with this == myContext }, 500); ``` @@ -4936,77 +6384,12 @@ function invokeLaterTimers() { @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. @return {String} a string you can use to cancel the timer in - {{#crossLink "Ember/run.cancel"}}{{/crossLink}} later. + `Ember.run.cancel` later. */ Ember.run.later = function(target, method) { - var args, expires, timer, guid, wait; - - // setTimeout compatibility... - if (arguments.length===2 && 'function' === typeof target) { - wait = method; - method = target; - target = undefined; - args = [target, method]; - } else { - args = slice.call(arguments); - wait = args.pop(); - } - - expires = (+ new Date()) + wait; - timer = { target: target, method: method, expires: expires, args: args }; - guid = Ember.guidFor(timer); - timers[guid] = timer; - - if(scheduledLater && expires < scheduledLaterExpires) { - // Cancel later timer (then reschedule earlier timer below) - clearTimeout(scheduledLater); - scheduledLater = null; - } - - if (!scheduledLater) { - // Schedule later timers to be run. - scheduledLater = setTimeout(invokeLaterTimers, wait); - scheduledLaterExpires = expires; - } - - return guid; + return backburner.later.apply(backburner, arguments); }; -function invokeOnceTimer(guid, onceTimers) { - if (onceTimers[this.tguid]) { delete onceTimers[this.tguid][this.mguid]; } - if (timers[guid]) { invoke(this.target, this.method, this.args); } - delete timers[guid]; -} - -function scheduleOnce(queue, target, method, args) { - var tguid = Ember.guidFor(target), - mguid = Ember.guidFor(method), - onceTimers = run.autorun().onceTimers, - guid = onceTimers[tguid] && onceTimers[tguid][mguid], - timer; - - if (guid && timers[guid]) { - timers[guid].args = args; // replace args - } else { - timer = { - target: target, - method: method, - args: args, - tguid: tguid, - mguid: mguid - }; - - guid = Ember.guidFor(timer); - timers[guid] = timer; - if (!onceTimers[tguid]) { onceTimers[tguid] = {}; } - onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once - - run.schedule(queue, timer, invokeOnceTimer, guid, onceTimers); - } - - return guid; -} - /** Schedule a function to run one time during the current RunLoop. This is equivalent to calling `scheduleOnce` with the "actions" queue. @@ -5020,7 +6403,10 @@ function scheduleOnce(queue, target, method, args) { @return {Object} timer */ Ember.run.once = function(target, method) { - return scheduleOnce('actions', target, method, slice.call(arguments, 2)); + checkAutoRun(); + var args = slice.call(arguments); + args.unshift('actions'); + return backburner.scheduleOnce.apply(backburner, args); }; /** @@ -5033,11 +6419,11 @@ Ember.run.once = function(target, method) { calls. ```javascript - Ember.run(function(){ + Ember.run(function() { var sayHi = function() { console.log('hi'); } Ember.run.scheduleOnce('afterRender', myContext, sayHi); Ember.run.scheduleOnce('afterRender', myContext, sayHi); - // doFoo will only be executed once, in the afterRender queue of the RunLoop + // sayHi will only be executed once, in the afterRender queue of the RunLoop }); ``` @@ -5068,24 +6454,26 @@ Ember.run.once = function(target, method) { @return {Object} timer */ Ember.run.scheduleOnce = function(queue, target, method) { - return scheduleOnce(queue, target, method, slice.call(arguments, 3)); + checkAutoRun(); + return backburner.scheduleOnce.apply(backburner, arguments); }; /** - Schedules an item to run from within a separate run loop, after - control has been returned to the system. This is equivalent to calling + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling `Ember.run.later` with a wait time of 1ms. ```javascript - Ember.run.next(myContext, function(){ - // code to be executed in the next run loop, which will be scheduled after the current one + Ember.run.next(myContext, function() { + // code to be executed in the next run loop, + // which will be scheduled after the current one }); ``` Multiple operations scheduled with `Ember.run.next` will coalesce into the same later run loop, along with any other operations scheduled by `Ember.run.later` that expire right around the same - time that `Ember.run.next` operations will fire. + time that `Ember.run.next` operations will fire. Note that there are often alternatives to using `Ember.run.next`. For instance, if you'd like to schedule an operation to happen @@ -5111,13 +6499,13 @@ Ember.run.scheduleOnce = function(queue, target, method) { One benefit of the above approach compared to using `Ember.run.next` is that you will be able to perform DOM/CSS operations before unprocessed - elements are rendered to the screen, which may prevent flickering or + elements are rendered to the screen, which may prevent flickering or other artifacts caused by delaying processing until after rendering. - The other major benefit to the above approach is that `Ember.run.next` - introduces an element of non-determinism, which can make things much - harder to test, due to its reliance on `setTimeout`; it's much harder - to guarantee the order of scheduled operations when they are scheduled + The other major benefit to the above approach is that `Ember.run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled outside of the current run loop, i.e. with `Ember.run.next`. @method next @@ -5130,8 +6518,8 @@ Ember.run.scheduleOnce = function(queue, target, method) { */ Ember.run.next = function() { var args = slice.call(arguments); - args.push(1); // 1 millisecond wait - return run.later.apply(this, args); + args.push(1); + return backburner.later.apply(backburner, args); }; /** @@ -5139,17 +6527,17 @@ Ember.run.next = function() { `Ember.run.once()`, or `Ember.run.next()`. ```javascript - var runNext = Ember.run.next(myContext, function(){ + var runNext = Ember.run.next(myContext, function() { // will not be executed }); Ember.run.cancel(runNext); - var runLater = Ember.run.later(myContext, function(){ + var runLater = Ember.run.later(myContext, function() { // will not be executed }, 500); Ember.run.cancel(runLater); - var runOnce = Ember.run.once(myContext, function(){ + var runOnce = Ember.run.once(myContext, function() { // will not be executed }); Ember.run.cancel(runOnce); @@ -5160,9 +6548,93 @@ Ember.run.next = function() { @return {void} */ Ember.run.cancel = function(timer) { - delete timers[timer]; + return backburner.cancel(timer); }; +/** + Delay calling the target method until the debounce period has elapsed + with no additional debounce calls. If `debounce` is called again before + the specified time has elapsed, the timer is reset and the entire period + must pass again before the target method is called. + + This method should be used when an event may be called multiple times + but the action should only be called once when the event is done firing. + A common example is for scroll events where you only want updates to + happen once scrolling has ceased. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + Ember.run.debounce(myContext, myFunc, 150); + + // less than 150ms passes + + Ember.run.debounce(myContext, myFunc, 150); + + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'debounce ran.' one time. + ``` + + @method debounce + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} wait Number of milliseconds to wait. + @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval. + @return {void} +*/ +Ember.run.debounce = function() { + return backburner.debounce.apply(backburner, arguments); +}; + +/** + Ensure that the target method is never called more frequently than + the specified spacing period. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'throttle'}; + + Ember.run.throttle(myContext, myFunc, 150); + + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'throttle ran.' twice, 150ms apart. + ``` + + @method throttle + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} spacing Number of milliseconds to space out requests. + @return {void} +*/ +Ember.run.throttle = function() { + return backburner.throttle.apply(backburner, arguments); +}; + +// Make sure it's not an autorun during testing +function checkAutoRun() { + if (!Ember.run.currentRunLoop) { + Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); + } +} + })(); @@ -5238,7 +6710,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); @@ -5446,8 +6918,8 @@ function mixinProperties(to, from) { mixinProperties(Binding, { - /** - See {{#crossLink "Ember.Binding/from"}}{{/crossLink}} + /* + See `Ember.Binding.from`. @method from @static @@ -5457,8 +6929,8 @@ mixinProperties(Binding, { return binding.from.apply(binding, arguments); }, - /** - See {{#crossLink "Ember.Binding/to"}}{{/crossLink}} + /* + See `Ember.Binding.to`. @method to @static @@ -5475,13 +6947,14 @@ mixinProperties(Binding, { This means that if you change the "to" side directly, the "from" side may have a different value. - See {{#crossLink "Binding/oneWay"}}{{/crossLink}} + See `Binding.oneWay`. @method oneWay @param {String} from from path. @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); @@ -5536,7 +7009,7 @@ mixinProperties(Binding, { You should consider using one way bindings anytime you have an object that may be created frequently and you do not intend to change a property; only - to monitor it for changes. (such as in the example above). + to monitor it for changes (such as in the example above). ## Adding Bindings Manually @@ -5646,7 +7119,8 @@ Ember.oneWay = function(obj, to, from) { (function() { /** -@module ember-metal +@module ember +@submodule ember-metal */ var Mixin, REQUIRED, Alias, @@ -5658,6 +7132,7 @@ var Mixin, REQUIRED, Alias, defineProperty = Ember.defineProperty, guidFor = Ember.guidFor; + function mixinsMeta(obj) { var m = Ember.meta(obj, true), ret = m.mixins; if (!ret) { @@ -5705,13 +7180,13 @@ function mixinProperties(mixinsMeta, mixin) { } } -function concatenatedProperties(props, values, base) { +function concatenatedMixinProperties(concatProp, props, values, base) { var concats; // reset before adding each new mixin to pickup concats from previous - concats = values.concatenatedProperties || base.concatenatedProperties; - if (props.concatenatedProperties) { - concats = concats ? concats.concat(props.concatenatedProperties) : props.concatenatedProperties; + concats = values[concatProp] || base[concatProp]; + if (props[concatProp]) { + concats = concats ? concats.concat(props[concatProp]) : props[concatProp]; } return concats; @@ -5778,7 +7253,28 @@ function applyConcatenatedProperties(obj, key, value, values) { } } -function addNormalizedProperty(base, key, value, meta, descs, values, concats) { +function applyMergedProperties(obj, key, value, values) { + var baseValue = values[key] || obj[key]; + + if (!baseValue) { return value; } + + var newBase = Ember.merge({}, baseValue); + for (var prop in value) { + if (!value.hasOwnProperty(prop)) { continue; } + + var propValue = value[prop]; + if (isMethod(propValue)) { + // TODO: support for Computed Properties, etc? + newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); + } else { + newBase[prop] = propValue; + } + } + + return newBase; +} + +function addNormalizedProperty(base, key, value, meta, descs, values, concats, mergings) { if (value instanceof Ember.Descriptor) { if (value === REQUIRED && descs[key]) { return CONTINUE; } @@ -5791,11 +7287,14 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { 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) || key === 'concatenatedProperties') { + 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; @@ -5804,7 +7303,7 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { } function mergeMixins(mixins, m, descs, values, base, keys) { - var mixin, props, key, concats, meta; + var mixin, props, key, concats, mergings, meta; function removeKeys(keyName) { delete descs[keyName]; @@ -5813,19 +7312,22 @@ function mergeMixins(mixins, m, descs, values, base, keys) { for(var i=0, l=mixins.length; i this.changingFrom ? 'green' : 'red'; + // logic + } + }), + + friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }) + }); + ``` + + 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; + + + 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; }; @@ -6311,6 +7911,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 @@ -6322,153 +7971,232 @@ Ember Metal })(); (function() { -define("rsvp/all", - ["rsvp/defer","exports"], +/** + @class RSVP + @module RSVP + */ +define("rsvp/all", + ["./promise","exports"], function(__dependency1__, __exports__) { "use strict"; - var defer = __dependency1__.defer; + var Promise = __dependency1__["default"]; - function all(promises) { - var results = [], deferred = defer(), remaining = promises.length; + /** + This is a convenient alias for `RSVP.Promise.all`. - if (remaining === 0) { - deferred.resolve([]); - } + @method all + @for RSVP + @param {Array} array Array of promises. + @param {String} label An optional label. This is useful + for tooling. + @static + */ + __exports__["default"] = function all(array, label) { + return Promise.all(array, label); + }; + }); +define("rsvp/all_settled", + ["./promise","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var isArray = __dependency2__.isArray; + var isNonThenable = __dependency2__.isNonThenable; - var resolver = function(index) { - return function(value) { - resolveAll(index, value); - }; - }; + /** + `RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing + a fail-fast method, it waits until all the promises have returned and + shows you all the results. This is useful if you want to handle multiple + promises' failure states together as a set. - var resolveAll = function(index, value) { - results[index] = value; - if (--remaining === 0) { - deferred.resolve(results); + Returns a promise that is fulfilled when all the given promises have been + settled. The return promise is fulfilled with an array of the states of + the promises passed into the `promises` array argument. + + Each state object will either indicate fulfillment or rejection, and + provide the corresponding value or reason. The states will take one of + the following formats: + + ```javascript + { state: 'fulfilled', value: value } + or + { state: 'rejected', reason: reason } + ``` + + Example: + + ```javascript + var promise1 = RSVP.Promise.resolve(1); + var promise2 = RSVP.Promise.reject(new Error('2')); + var promise3 = RSVP.Promise.reject(new Error('3')); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.allSettled(promises).then(function(array){ + // array == [ + // { state: 'fulfilled', value: 1 }, + // { state: 'rejected', reason: Error }, + // { state: 'rejected', reason: Error } + // ] + // Note that for the second item, reason.message will be "2", and for the + // third item, reason.message will be "3". + }, function(error) { + // Not run. (This block would only be called if allSettled had failed, + // for instance if passed an incorrect argument type.) + }); + ``` + + @method allSettled + @for RSVP + @param {Array} promises + @param {String} label - optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with an array of the settled + states of the constituent promises. + @static + */ + + __exports__["default"] = function allSettled(entries, label) { + return new Promise(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to allSettled.'); } - }; - var rejectAll = function(error) { - deferred.reject(error); - }; + var remaining = entries.length; + var entry; - for (var i = 0; i < promises.length; i++) { - if (promises[i] && typeof promises[i].then === 'function') { - promises[i].then(resolver(i), rejectAll); - } else { - resolveAll(i, promises[i]); + if (remaining === 0) { + resolve([]); + return; } - } - return deferred.promise; - } - __exports__.all = all; - }); + var results = new Array(remaining); -define("rsvp/async", - ["exports"], - function(__exports__) { - "use strict"; - var browserGlobal = (typeof window !== 'undefined') ? window : {}; + function fulfilledResolver(index) { + return function(value) { + resolveAll(index, fulfilled(value)); + }; + } - var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; - var async; + function rejectedResolver(index) { + return function(reason) { + resolveAll(index, rejected(reason)); + }; + } - if (typeof process !== 'undefined' && - {}.toString.call(process) === '[object process]') { - async = function(callback, binding) { - process.nextTick(function() { - callback.call(binding); - }); - }; - } else if (BrowserMutationObserver) { - var queue = []; + function resolveAll(index, value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + } - var observer = new BrowserMutationObserver(function() { - var toProcess = queue.slice(); - queue = []; + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; - toProcess.forEach(function(tuple) { - var callback = tuple[0], binding = tuple[1]; - callback.call(binding); - }); - }); - - var element = document.createElement('div'); - observer.observe(element, { attributes: true }); - - // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661 - window.addEventListener('unload', function(){ - observer.disconnect(); - observer = null; - }); - - async = function(callback, binding) { - queue.push([callback, binding]); - element.setAttribute('drainQueue', 'drainQueue'); - }; - } else { - async = function(callback, binding) { - setTimeout(function() { - callback.call(binding); - }, 1); - }; - } - - - __exports__.async = async; - }); - -define("rsvp/config", - ["rsvp/async","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var async = __dependency1__.async; - - var config = {}; - config.async = async; - - __exports__.config = config; - }); - -define("rsvp/defer", - ["rsvp/promise","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; - - function defer() { - var deferred = {}; - - var promise = new Promise(function(resolve, reject) { - deferred.resolve = resolve; - deferred.reject = reject; - }); - - deferred.promise = promise; - return deferred; - } - - __exports__.defer = defer; - }); - -define("rsvp/events", - ["exports"], - function(__exports__) { - "use strict"; - var Event = function(type, options) { - this.type = type; - - for (var option in options) { - if (!options.hasOwnProperty(option)) { continue; } - - this[option] = options[option]; - } + if (isNonThenable(entry)) { + resolveAll(index, fulfilled(entry)); + } else { + Promise.cast(entry).then(fulfilledResolver(index), rejectedResolver(index)); + } + } + }, label); }; + function fulfilled(value) { + return { state: 'fulfilled', value: value }; + } + + function rejected(reason) { + return { state: 'rejected', reason: reason }; + } + }); +define("rsvp/config", + ["./events","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var EventTarget = __dependency1__["default"]; + + var config = { + instrument: false + }; + + EventTarget.mixin(config); + + function configure(name, value) { + if (name === 'onerror') { + // handle for legacy users that expect the actual + // error to be passed to their function added via + // `RSVP.configure('onerror', someFunctionHere);` + config.on('error', value); + return; + } + + if (arguments.length === 2) { + config[name] = value; + } else { + return config[name]; + } + } + + __exports__.config = config; + __exports__.configure = configure; + }); +define("rsvp/defer", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + `RSVP.defer` returns an object similar to jQuery's `$.Deferred`. + `RSVP.defer` should be used when porting over code reliant on `$.Deferred`'s + interface. New code should use the `RSVP.Promise` constructor instead. + + The object returned from `RSVP.defer` is a plain object with three properties: + + * promise - an `RSVP.Promise`. + * reject - a function that causes the `promise` property on this object to + become rejected + * resolve - a function that causes the `promise` property on this object to + become fulfilled. + + Example: + + ```javascript + var deferred = RSVP.defer(); + + deferred.resolve("Success!"); + + defered.promise.then(function(value){ + // value here is "Success!" + }); + ``` + + @method defer + @for RSVP + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Object} + */ + + __exports__["default"] = function defer(label) { + var deferred = { }; + + deferred.promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }, label); + + return deferred; + }; + }); +define("rsvp/events", + ["exports"], + function(__exports__) { + "use strict"; var indexOf = function(callbacks, callback) { for (var i=0, l=callbacks.length; i 1; + }; + + RSVP.filter(promises, filterFn).then(function(result){ + // result is [ 2, 3 ] + }); + ``` + + If any of the `promises` given to `RSVP.filter` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + var filterFn = function(item){ + return item > 1; + }; + + RSVP.filter(promises, filterFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` + + `RSVP.filter` will also wait for any promises returned from `filterFn`. + For instance, you may want to fetch a list of users then return a subset + of those users based on some asynchronous operation: + + ```javascript + + var alice = { name: 'alice' }; + var bob = { name: 'bob' }; + var users = [ alice, bob ]; + + var promises = users.map(function(user){ + return RSVP.resolve(user); + }); + + var filterFn = function(user){ + // Here, Alice has permissions to create a blog post, but Bob does not. + return getPrivilegesForUser(user).then(function(privs){ + return privs.can_create_blog_post === true; + }); + }; + RSVP.filter(promises, filterFn).then(function(users){ + // true, because the server told us only Alice can create a blog post. + users.length === 1; + // false, because Alice is the only user present in `users` + users[0] === bob; + }); + ``` + + @method filter + @for RSVP + @param {Array} promises + @param {Function} filterFn - function to be called on each resolved value to + filter the final results. + @param {String} label optional string describing the promise. Useful for + tooling. + @return {Promise} + */ + function filter(promises, filterFn, label) { + return all(promises, label).then(function(values){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to filter.'); + } + + if (!isFunction(filterFn)){ + throw new TypeError("You must pass a function to filter's second argument."); + } + + return map(promises, filterFn, label).then(function(filterResults){ + var i, + valuesLen = values.length, + filtered = []; + + for (i = 0; i < valuesLen; i++){ + if(filterResults[i]) filtered.push(values[i]); + } + return filtered; + }); + }); } - function hash(promises) { - var results = {}, deferred = defer(), remaining = size(promises); - - if (remaining === 0) { - deferred.resolve({}); - } - - var resolver = function(prop) { - return function(value) { - resolveAll(prop, value); - }; - }; - - var resolveAll = function(prop, value) { - results[prop] = value; - if (--remaining === 0) { - deferred.resolve(results); - } - }; - - var rejectAll = function(error) { - deferred.reject(error); - }; - - for (var prop in promises) { - if (promises[prop] && typeof promises[prop].then === 'function') { - promises[prop].then(resolver(prop), rejectAll); - } else { - resolveAll(prop, promises[prop]); - } - } - - return deferred.promise; - } - - __exports__.hash = hash; + __exports__["default"] = filter; }); - -define("rsvp/node", - ["rsvp/promise","rsvp/all","exports"], +define("rsvp/hash", + ["./promise","./utils","exports"], function(__dependency1__, __dependency2__, __exports__) { "use strict"; - var Promise = __dependency1__.Promise; - var all = __dependency2__.all; + var Promise = __dependency1__["default"]; + var isNonThenable = __dependency2__.isNonThenable; + var keysOf = __dependency2__.keysOf; + + /** + `RSVP.hash` is similar to `RSVP.all`, but takes an object instead of an array + for its `promises` argument. + + Returns a promise that is fulfilled when all the given promises have been + fulfilled, or rejected if any of them become rejected. The returned promise + is fulfilled with a hash that has the same key names as the `promises` object + argument. If any of the values in the object are not promises, they will + simply be copied over to the fulfilled object. + + Example: + + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + yourPromise: RSVP.resolve(2), + theirPromise: RSVP.resolve(3), + notAPromise: 4 + }; + + RSVP.hash(promises).then(function(hash){ + // hash here is an object that looks like: + // { + // myPromise: 1, + // yourPromise: 2, + // theirPromise: 3, + // notAPromise: 4 + // } + }); + ```` + + If any of the `promises` given to `RSVP.hash` are rejected, the first promise + that is rejected will be given as the reason to the rejection handler. + + Example: + + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + rejectedPromise: RSVP.reject(new Error("rejectedPromise")), + anotherRejectedPromise: RSVP.reject(new Error("anotherRejectedPromise")), + }; + + RSVP.hash(promises).then(function(hash){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "rejectedPromise" + }); + ``` + + An important note: `RSVP.hash` is intended for plain JavaScript objects that + are just a set of keys and values. `RSVP.hash` will NOT preserve prototype + chains. + + Example: + + ```javascript + function MyConstructor(){ + this.example = RSVP.resolve("Example"); + } + + MyConstructor.prototype = { + protoProperty: RSVP.resolve("Proto Property") + }; + + var myObject = new MyConstructor(); + + RSVP.hash(myObject).then(function(hash){ + // protoProperty will not be present, instead you will just have an + // object that looks like: + // { + // example: "Example" + // } + // + // hash.hasOwnProperty('protoProperty'); // false + // 'undefined' === typeof hash.protoProperty + }); + ``` + + @method hash + @for RSVP + @param {Object} promises + @param {String} label optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all properties of `promises` + have been fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function hash(object, label) { + return new Promise(function(resolve, reject){ + var results = {}; + var keys = keysOf(object); + var remaining = keys.length; + var entry, property; + + if (remaining === 0) { + resolve(results); + return; + } + + function fulfilledTo(property) { + return function(value) { + results[property] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } + + function onRejection(reason) { + remaining = 0; + reject(reason); + } + + for (var i = 0; i < keys.length; i++) { + property = keys[i]; + entry = object[property]; + + if (isNonThenable(entry)) { + results[property] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Promise.cast(entry).then(fulfilledTo(property), onRejection); + } + } + }); + }; + }); +define("rsvp/instrument", + ["./config","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var now = __dependency2__.now; + + __exports__["default"] = function instrument(eventName, promise, child) { + // instrumentation should not disrupt normal usage. + try { + config.trigger(eventName, { + guid: promise._guidKey + promise._id, + eventName: eventName, + detail: promise._detail, + childGuid: child && promise._guidKey + child._id, + label: promise._label, + timeStamp: now(), + stack: new Error(promise._label).stack + }); + } catch(error) { + setTimeout(function(){ + throw error; + }, 0); + } + }; + }); +define("rsvp/map", + ["./promise","./all","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var all = __dependency2__["default"]; + var isArray = __dependency3__.isArray; + var isFunction = __dependency3__.isFunction; + + /** + `RSVP.map` is similar to JavaScript's native `map` method, except that it + waits for all promises to become fulfilled before running the `mapFn` on + each item in given to `promises`. `RSVP.map` returns a promise that will + become fulfilled with the result of running `mapFn` on the values the promises + become fulfilled with. + + For example: + + ```javascript + + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; + + var mapFn = function(item){ + return item + 1; + }; + + RSVP.map(promises, mapFn).then(function(result){ + // result is [ 2, 3, 4 ] + }); + ``` + + If any of the `promises` given to `RSVP.map` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + var mapFn = function(item){ + return item + 1; + }; + + RSVP.map(promises, mapFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` + + `RSVP.map` will also wait if a promise is returned from `mapFn`. For example, + say you want to get all comments from a set of blog posts, but you need + the blog posts first becuase they contain a url to those comments. + + ```javscript + + var mapFn = function(blogPost){ + // getComments does some ajax and returns an RSVP.Promise that is fulfilled + // with some comments data + return getComments(blogPost.comments_url); + }; + + // getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled + // with some blog post data + RSVP.map(getBlogPosts(), mapFn).then(function(comments){ + // comments is the result of asking the server for the comments + // of all blog posts returned from getBlogPosts() + }); + ``` + + @method map + @for RSVP + @param {Array} promises + @param {Function} mapFn function to be called on each fulfilled promise. + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with the result of calling + `mapFn` on each fulfilled promise or value when they become fulfilled. + The promise will be rejected if any of the given `promises` become rejected. + @static + */ + __exports__["default"] = function map(promises, mapFn, label) { + return all(promises, label).then(function(results){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to map.'); + } + + if (!isFunction(mapFn)){ + throw new TypeError("You must pass a function to map's second argument."); + } + + + var resultLen = results.length, + mappedResults = [], + i; + + for (i = 0; i < resultLen; i++){ + mappedResults.push(mapFn(results[i])); + } + + return all(mappedResults, label); + }); + }; + }); +define("rsvp/node", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + var slice = Array.prototype.slice; function makeNodeCallbackFor(resolve, reject) { return function (error, value) { if (error) { reject(error); } else if (arguments.length > 2) { - resolve(Array.prototype.slice.call(arguments, 1)); + resolve(slice.call(arguments, 1)); } else { resolve(value); } }; } - function denodeify(nodeFunc) { + /** + `RSVP.denodeify` takes a "node-style" function and returns a function that + will return an `RSVP.Promise`. You can use `denodeify` in Node.js or the + browser when you'd prefer to use promises over using callbacks. For example, + `denodeify` transforms the following: + + ```javascript + var fs = require('fs'); + + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + handleData(data); + }); + ``` + + into: + + ```javascript + var fs = require('fs'); + + var readFile = RSVP.denodeify(fs.readFile); + + readFile('myfile.txt').then(handleData, handleError); + ``` + + Using `denodeify` makes it easier to compose asynchronous operations instead + of using callbacks. For example, instead of: + + ```javascript + var fs = require('fs'); + var log = require('some-async-logger'); + + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + fs.writeFile('myfile2.txt', data, function(err){ + if (err) throw err; + log('success', function(err) { + if (err) throw err; + }); + }); + }); + ``` + + You can chain the operations together using `then` from the returned promise: + + ```javascript + var fs = require('fs'); + var denodeify = RSVP.denodeify; + var readFile = denodeify(fs.readFile); + var writeFile = denodeify(fs.writeFile); + var log = denodeify(require('some-async-logger')); + + readFile('myfile.txt').then(function(data){ + return writeFile('myfile2.txt', data); + }).then(function(){ + return log('SUCCESS'); + }).then(function(){ + // success handler + }, function(reason){ + // rejection handler + }); + ``` + + @method denodeify + @for RSVP + @param {Function} nodeFunc a "node-style" function that takes a callback as + its last argument. The callback expects an error to be passed as its first + argument (if an error occurred, otherwise null), and the value from the + operation as its second argument ("function(err, value){ }"). + @param {Any} binding optional argument for binding the "this" value when + calling the `nodeFunc` function. + @return {Function} a function that wraps `nodeFunc` to return an + `RSVP.Promise` + @static + */ + __exports__["default"] = function denodeify(nodeFunc, binding) { return function() { - var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; + var nodeArgs = slice.call(arguments), resolve, reject; + var thisArg = this || binding; - var promise = new Promise(function(nodeResolve, nodeReject) { - resolve = nodeResolve; - reject = nodeReject; + return new Promise(function(resolve, reject) { + Promise.all(nodeArgs).then(function(nodeArgs) { + try { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + nodeFunc.apply(thisArg, nodeArgs); + } catch(e) { + reject(e); + } + }); }); - - all(nodeArgs).then(function(nodeArgs) { - nodeArgs.push(makeNodeCallbackFor(resolve, reject)); - - try { - nodeFunc.apply(this, nodeArgs); - } catch(e) { - reject(e); - } - }); - - return promise; }; - } - - __exports__.denodeify = denodeify; + }; }); - -define("rsvp/promise", - ["rsvp/config","rsvp/events","exports"], - function(__dependency1__, __dependency2__, __exports__) { +define("rsvp/promise", + ["./config","./events","./instrument","./utils","./promise/cast","./promise/all","./promise/race","./promise/resolve","./promise/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { "use strict"; var config = __dependency1__.config; - var EventTarget = __dependency2__.EventTarget; + var EventTarget = __dependency2__["default"]; + var instrument = __dependency3__["default"]; + var objectOrFunction = __dependency4__.objectOrFunction; + var isFunction = __dependency4__.isFunction; + var now = __dependency4__.now; + var cast = __dependency5__["default"]; + var all = __dependency6__["default"]; + var race = __dependency7__["default"]; + var Resolve = __dependency8__["default"]; + var Reject = __dependency9__["default"]; - function objectOrFunction(x) { - return isFunction(x) || (typeof x === "object" && x !== null); - } + var guidKey = 'rsvp_' + now() + '-'; + var counter = 0; - function isFunction(x){ - return typeof x === "function"; - } + function noop() {} - var Promise = function(resolver) { - var promise = this, - resolved = false; + __exports__["default"] = Promise; - if (typeof resolver !== 'function') { - throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); + + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise’s eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. Similarly, a + rejection reason is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error("getJSON: `" + url + "` failed with status: [" + this.status + "]"); + } + } + }; + }); } - if (!(promise instanceof Promise)) { - return new Promise(resolver); + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class RSVP.Promise + @param {function} + @param {String} label optional string for labeling the promise. + Useful for tooling. + @constructor + */ + function Promise(resolver, label) { + if (!isFunction(resolver)) { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); } - var resolvePromise = function(value) { - if (resolved) { return; } - resolved = true; + if (!(this instanceof Promise)) { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + this._id = counter++; + this._label = label; + this._subscribers = []; + + if (config.instrument) { + instrument('created', this); + } + + if (noop !== resolver) { + invokeResolver(resolver, this); + } + } + + function invokeResolver(resolver, promise) { + function resolvePromise(value) { resolve(promise, value); - }; + } - var rejectPromise = function(value) { - if (resolved) { return; } - resolved = true; - reject(promise, value); - }; - - this.on('promise:resolved', function(event) { - this.trigger('success', { detail: event.detail }); - }, this); - - this.on('promise:failed', function(event) { - this.trigger('error', { detail: event.detail }); - }, this); + function rejectPromise(reason) { + reject(promise, reason); + } try { resolver(resolvePromise, rejectPromise); } catch(e) { rejectPromise(e); } + } + + Promise.cast = cast; + Promise.all = all; + Promise.race = race; + Promise.resolve = Resolve; + Promise.reject = Reject; + + var PENDING = void 0; + var SEALED = 0; + var FULFILLED = 1; + var REJECTED = 2; + + function subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + subscribers[length] = child; + subscribers[length + FULFILLED] = onFulfillment; + subscribers[length + REJECTED] = onRejection; + } + + function publish(promise, settled) { + var child, callback, subscribers = promise._subscribers, detail = promise._detail; + + if (config.instrument) { + instrument(settled === FULFILLED ? 'fulfilled' : 'rejected', promise); + } + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + invokeCallback(settled, child, callback, detail); + } + + promise._subscribers = null; + } + + Promise.prototype = { + constructor: Promise, + + _id: undefined, + _guidKey: guidKey, + _label: undefined, + + _state: undefined, + _detail: undefined, + _subscribers: undefined, + + _onerror: function (reason) { + config.trigger('error', reason); + }, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, "downstream" + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return "default name"; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `"default name"` + }); + + findUser().then(function (user) { + throw new Error("Found user, but still unhappy"); + }, function (reason) { + throw new Error("`findUser` rejected and we're unhappy"); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be "Found user, but still unhappy". + // If `findUser` rejected, `reason` will be "`findUser` rejected and we're unhappy". + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException("Upstream error"); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + var result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + var author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection, label) { + var promise = this; + this._onerror = null; + + var thenPromise = new this.constructor(noop, label); + + if (this._state) { + var callbacks = arguments; + config.async(function invokePromiseCallback() { + invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail); + }); + } else { + subscribe(this, thenPromise, onFulfillment, onRejection); + } + + if (config.instrument) { + instrument('chained', promise, thenPromise); + } + + return thenPromise; + }, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error("couldn't find that author"); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection, label) { + return this.then(null, onRejection, label); + }, + + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves + + Synchronous example: + + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); + } + return new Author(); + } + + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` + + Asynchronous example: + + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` + + @method finally + @param {Function} callback + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'finally': function(callback, label) { + var constructor = this.constructor; + + return this.then(function(value) { + return constructor.cast(callback()).then(function(){ + return value; + }); + }, function(reason) { + return constructor.cast(callback()).then(function(){ + throw reason; + }); + }, label); + } }; - var invokeCallback = function(type, promise, callback, event) { + function invokeCallback(settled, promise, callback, detail) { var hasCallback = isFunction(callback), value, error, succeeded, failed; if (hasCallback) { try { - value = callback(event.detail); + value = callback(detail); succeeded = true; } catch(e) { failed = true; error = e; } } else { - value = event.detail; + value = detail; succeeded = true; } @@ -6730,44 +9441,53 @@ define("rsvp/promise", resolve(promise, value); } else if (failed) { reject(promise, error); - } else if (type === 'resolve') { + } else if (settled === FULFILLED) { resolve(promise, value); - } else if (type === 'reject') { + } else if (settled === REJECTED) { reject(promise, value); } - }; + } - Promise.prototype = { - constructor: Promise, + function handleThenable(promise, value) { + var then = null, + resolved; - then: function(done, fail) { - var thenPromise = new Promise(function() {}); - - if (this.isFulfilled) { - config.async(function() { - invokeCallback('resolve', thenPromise, done, { detail: this.fulfillmentValue }); - }, this); + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); } - if (this.isRejected) { - config.async(function() { - invokeCallback('reject', thenPromise, fail, { detail: this.rejectedReason }); - }, this); + if (objectOrFunction(value)) { + then = value.then; + + if (isFunction(then)) { + then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; + + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + if (resolved) { return true; } + resolved = true; + + reject(promise, val); + }, 'derived from: ' + (promise._label || ' unknown promise')); + + return true; + } } - - this.on('promise:resolved', function(event) { - invokeCallback('resolve', thenPromise, done, event); - }); - - this.on('promise:failed', function(event) { - invokeCallback('reject', thenPromise, fail, event); - }); - - return thenPromise; + } catch (error) { + if (resolved) { return true; } + reject(promise, error); + return true; } - }; - EventTarget.mixin(Promise.prototype); + return false; + } function resolve(promise, value) { if (promise === value) { @@ -6777,164 +9497,679 @@ define("rsvp/promise", } } - function handleThenable(promise, value) { - var then = null; + function fulfill(promise, value) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = value; - if (objectOrFunction(value)) { - try { - then = value.then; - } catch(e) { - reject(promise, e); - return true; - } + config.async(publishFulfillment, promise); + } - if (isFunction(then)) { - try { - then.call(value, function(val) { - if (value !== val) { - resolve(promise, val); - } else { - fulfill(promise, val); - } - }, function(val) { - reject(promise, val); - }); - } catch (e) { - reject(promise, e); - } - return true; - } + function reject(promise, reason) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = reason; + + config.async(publishRejection, promise); + } + + function publishFulfillment(promise) { + publish(promise, promise._state = FULFILLED); + } + + function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._detail); } - return false; + publish(promise, promise._state = REJECTED); } - - function fulfill(promise, value) { - config.async(function() { - promise.trigger('promise:resolved', { detail: value }); - promise.isFulfilled = true; - promise.fulfillmentValue = value; - }); - } - - function reject(promise, value) { - config.async(function() { - promise.trigger('promise:failed', { detail: value }); - promise.isRejected = true; - promise.rejectedReason = value; - }); - } - - - __exports__.Promise = Promise; }); - -define("rsvp/reject", - ["rsvp/promise","exports"], +define("rsvp/promise/all", + ["../utils","exports"], function(__dependency1__, __exports__) { "use strict"; - var Promise = __dependency1__.Promise; + var isArray = __dependency1__.isArray; + var isNonThenable = __dependency1__.isNonThenable; + /** + `RSVP.Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. - function objectOrFunction(x) { - return typeof x === "function" || (typeof x === "object" && x !== null); - } + Example: + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; - function reject(reason) { - return new Promise(function (resolve, reject) { - reject(reason); + RSVP.Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; }); - } + ``` + If any of the `promises` given to `RSVP.all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: - __exports__.reject = reject; - }); + Example: -define("rsvp/resolve", - ["rsvp/promise","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + RSVP.Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` - function objectOrFunction(x) { - return typeof x === "function" || (typeof x === "object" && x !== null); - } + @method all + @for RSVP.Promise + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function all(entries, label) { - function resolve(thenable){ - var promise = new Promise(function(resolve, reject){ - var then; + /*jshint validthis:true */ + var Constructor = this; - try { - if ( objectOrFunction(thenable) ) { - then = thenable.then; - - if (typeof then === "function") { - then.call(thenable, resolve, reject); - } else { - resolve(thenable); - } - - } else { - resolve(thenable); - } - - } catch(error) { - reject(error); + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to all.'); } + + var remaining = entries.length; + var results = new Array(remaining); + var entry, pending = true; + + if (remaining === 0) { + resolve(results); + return; + } + + function fulfillmentAt(index) { + return function(value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } + + function onRejection(reason) { + remaining = 0; + reject(reason); + } + + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; + if (isNonThenable(entry)) { + results[index] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Constructor.cast(entry).then(fulfillmentAt(index), onRejection); + } + } + }, label); + }; + }); +define("rsvp/promise/cast", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.cast` coerces its argument to a promise, or returns the + argument if it is already a promise which shares a constructor with the caster. + + Example: + + ```javascript + var promise = RSVP.Promise.resolve(1); + var casted = RSVP.Promise.cast(promise); + + console.log(promise === casted); // true + ``` + + In the case of a promise whose constructor does not match, it is assimilated. + The resulting promise will fulfill or reject based on the outcome of the + promise being casted. + + Example: + + ```javascript + var thennable = $.getJSON('/api/foo'); + var casted = RSVP.Promise.cast(thennable); + + console.log(thennable === casted); // false + console.log(casted instanceof RSVP.Promise) // true + + casted.then(function(data) { + // data is the value getJSON fulfills with + }); + ``` + + In the case of a non-promise, a promise which will fulfill with that value is + returned. + + Example: + + ```javascript + var value = 1; // could be a number, boolean, string, undefined... + var casted = RSVP.Promise.cast(value); + + console.log(value === casted); // false + console.log(casted instanceof RSVP.Promise) // true + + casted.then(function(val) { + val === value // => true + }); + ``` + + `RSVP.Promise.cast` is similar to `RSVP.Promise.resolve`, but `RSVP.Promise.cast` differs in the + following ways: + + * `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you + have something that could either be a promise or a value. RSVP.resolve + will have the same effect but will create a new promise wrapper if the + argument is a promise. + * `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to + promises of the exact class specified, so that the resulting object's `then` is + ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise). + + @method cast + @param {Object} object to be casted + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise + @static + */ + + __exports__["default"] = function cast(object, label) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + return new Constructor(function(resolve) { + resolve(object); + }, label); + }; + }); +define("rsvp/promise/race", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /* global toString */ + + var isArray = __dependency1__.isArray; + var isFunction = __dependency1__.isFunction; + var isNonThenable = __dependency1__.isNonThenable; + + /** + `RSVP.Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); }); - return promise; - } + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 2"); + }, 100); + }); + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // result === "promise 2" because it was resolved before promise1 + // was resolved. + }); + ``` - __exports__.resolve = resolve; + `RSVP.Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); + + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error("promise 2")); + }, 100); + }); + + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === "promise2" because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + RSVP.Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @param {Array} promises array of promises to observe + @param {String} label optional string for describing the promise returned. + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. + @static + */ + __exports__["default"] = function race(entries, label) { + /*jshint validthis:true */ + var Constructor = this, entry; + + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to race.'); + } + + var pending = true; + + function onFulfillment(value) { if (pending) { pending = false; resolve(value); } } + function onRejection(reason) { if (pending) { pending = false; reject(reason); } } + + for (var i = 0; i < entries.length; i++) { + entry = entries[i]; + if (isNonThenable(entry)) { + pending = false; + resolve(entry); + return; + } else { + Constructor.cast(entry).then(onFulfillment, onRejection); + } + } + }, label); + }; }); - -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__) { +define("rsvp/promise/reject", + ["exports"], + function(__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; + /** + `RSVP.Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: - function configure(name, value) { - config[name] = value; + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + var promise = RSVP.Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @param {Any} reason value that the returned promise will be rejected with. + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. + @static + */ + __exports__["default"] = function reject(reason, label) { + /*jshint validthis:true */ + var Constructor = this; + + return new Constructor(function (resolve, reject) { + reject(reason); + }, label); + }; + }); +define("rsvp/promise/resolve", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + var promise = RSVP.Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @param {Any} value value that the returned promise will be resolved with + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` + @static + */ + __exports__["default"] = function resolve(value, label) { + /*jshint validthis:true */ + var Constructor = this; + + return new Constructor(function(resolve, reject) { + resolve(value); + }, label); + }; + }); +define("rsvp/race", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.race`. + + @method race + @param {Array} array Array of promises. + @param {String} label An optional label. This is useful + for tooling. + @static + */ + __exports__["default"] = function race(array, label) { + return Promise.race(array, label); + }; + }); +define("rsvp/reject", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.reject`. + + @method reject + @for RSVP + @param {Any} reason value that the returned promise will be rejected with. + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. + @static + */ + __exports__["default"] = function reject(reason, label) { + return Promise.reject(reason, label); + }; + }); +define("rsvp/resolve", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.resolve`. + + @method resolve + @for RSVP + @param {Any} value value that the returned promise will be resolved with + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` + @static + */ + __exports__["default"] = function resolve(value, label) { + return Promise.resolve(value, label); + }; + }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.rethrow` will rethrow an error on the next turn of the JavaScript event + loop in order to aid debugging. + + Promises A+ specifies that any exceptions that occur with a promise must be + caught by the promises implementation and bubbled to the last handler. For + this reason, it is recommended that you always specify a second rejection + handler function to `then`. However, `RSVP.rethrow` will throw the exception + outside of the promise, so it bubbles up to your console if in the browser, + or domain/cause uncaught exception in Node. `rethrow` will also throw the + error again so the error can be handled by the promise per the spec. + + ```javascript + function throws(){ + throw new Error('Whoops!'); + } + + var promise = new RSVP.Promise(function(resolve, reject){ + throws(); + }); + + promise.catch(RSVP.rethrow).then(function(){ + // Code here doesn't run because the promise became rejected due to an + // error! + }, function (err){ + // handle the error here + }); + ``` + + The 'Whoops' error will be thrown on the next turn of the event loop + and you can watch for it in your console. You can also handle it using a + rejection handler given to `.then` or `.catch` on the returned promise. + + @method rethrow + @for RSVP + @param {Error} reason reason the promise became rejected. + @throws Error + @static + */ + __exports__["default"] = function rethrow(reason) { + setTimeout(function() { + throw reason; + }); + throw reason; + }; + }); +define("rsvp/utils", + ["exports"], + function(__exports__) { + "use strict"; + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); } + __exports__.objectOrFunction = objectOrFunction;function isFunction(x) { + return typeof x === "function"; + } + + __exports__.isFunction = isFunction;function isNonThenable(x) { + return !objectOrFunction(x); + } + + __exports__.isNonThenable = isNonThenable;function isArray(x) { + return Object.prototype.toString.call(x) === "[object Array]"; + } + + __exports__.isArray = isArray;// Date.now is not available in browsers < IE9 + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility + var now = Date.now || function() { return new Date().getTime(); }; + __exports__.now = now; + var keysOf = Object.keys || function(object) { + var result = []; + + for (var prop in object) { + result.push(prop); + } + + return result; + }; + __exports__.keysOf = keysOf; + }); +define("rsvp", + ["./rsvp/promise","./rsvp/events","./rsvp/node","./rsvp/all","./rsvp/all_settled","./rsvp/race","./rsvp/hash","./rsvp/rethrow","./rsvp/defer","./rsvp/config","./rsvp/map","./rsvp/resolve","./rsvp/reject","./rsvp/filter","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var EventTarget = __dependency2__["default"]; + var denodeify = __dependency3__["default"]; + var all = __dependency4__["default"]; + var allSettled = __dependency5__["default"]; + var race = __dependency6__["default"]; + var hash = __dependency7__["default"]; + var rethrow = __dependency8__["default"]; + var defer = __dependency9__["default"]; + var config = __dependency10__.config; + var configure = __dependency10__.configure; + var map = __dependency11__["default"]; + var resolve = __dependency12__["default"]; + var reject = __dependency13__["default"]; + var filter = __dependency14__["default"]; + + function async(callback, arg) { + config.async(callback, arg); + } + + function on() { + config.on.apply(config, arguments); + } + + function off() { + config.off.apply(config, arguments); + } + + // Set up instrumentation through `window.__PROMISE_INTRUMENTATION__` + if (typeof window !== 'undefined' && typeof window.__PROMISE_INSTRUMENTATION__ === 'object') { + var callbacks = window.__PROMISE_INSTRUMENTATION__; + configure('instrument', true); + for (var eventName in callbacks) { + if (callbacks.hasOwnProperty(eventName)) { + on(eventName, callbacks[eventName]); + } + } + } __exports__.Promise = Promise; __exports__.EventTarget = EventTarget; __exports__.all = all; + __exports__.allSettled = allSettled; + __exports__.race = race; __exports__.hash = hash; + __exports__.rethrow = rethrow; __exports__.defer = defer; __exports__.denodeify = denodeify; __exports__.configure = configure; + __exports__.on = on; + __exports__.off = off; __exports__.resolve = resolve; __exports__.reject = reject; + __exports__.async = async; + __exports__.map = map; + __exports__.filter = filter; }); })(); (function() { +/** +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 + @private +*/ + +/* + 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. function InheritingDict(parent) { this.parent = parent; this.dict = {}; } InheritingDict.prototype = { + + /** + @property parent + @type InheritingDict + @default null + */ + + parent: null, + + /** + Object used to store the current nodes data. + + @property dict + @type Object + @default Object + */ + dict: null, + + /** + Retrieve the value given a key, if the value is present at the current + level use it, otherwise walk up the parent hierarchy and try again. If + no matching key is found, return undefined. + + @method get + @param {String} key + @return {any} + */ get: function(key) { var dict = this.dict; @@ -6947,10 +10182,36 @@ define("container", } }, + /** + Set the given value for the given key, at the current level. + + @method set + @param {String} key + @param {Any} value + */ set: function(key, value) { this.dict[key] = value; }, + /** + Delete the given key + + @method remove + @param {String} key + */ + remove: function(key) { + delete this.dict[key]; + }, + + /** + Check for the existence of given a key, if the key is present at the current + level return true, otherwise walk up the parent hierarchy and try again. If + no matching key is found, return false. + + @method has + @param {String} key + @return {Boolean} + */ has: function(key) { var dict = this.dict; @@ -6965,6 +10226,13 @@ define("container", return false; }, + /** + Iterate and invoke a callback for each local key-value pair. + + @method eachLocal + @param {Function} callback + @param {Object} binding + */ eachLocal: function(callback, binding) { var dict = this.dict; @@ -6976,56 +10244,295 @@ define("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 = []; this.resolver = parent && parent.resolver || function() {}; + this.registry = new InheritingDict(parent && parent.registry); this.cache = new InheritingDict(parent && parent.cache); + this.factoryCache = new InheritingDict(parent && parent.cache); this.typeInjections = new InheritingDict(parent && parent.typeInjections); this.injections = {}; + + this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); + this.factoryInjections = {}; + this._options = new InheritingDict(parent && parent._options); this._typeOptions = new InheritingDict(parent && parent._typeOptions); } Container.prototype = { + + /** + @property parent + @type Container + @default null + */ + parent: null, + + /** + @property children + @type Array + @default [] + */ + children: null, + + /** + @property resolver + @type function + */ + resolver: null, + + /** + @property registry + @type InheritingDict + */ + registry: null, + + /** + @property cache + @type InheritingDict + */ + cache: null, + + /** + @property typeInjections + @type InheritingDict + */ + typeInjections: null, + + /** + @property injections + @type Object + @default {} + */ + injections: null, + + /** + @private + + @property _options + @type InheritingDict + @default null + */ + _options: null, + + /** + @private + + @property _typeOptions + @type InheritingDict + */ + _typeOptions: null, + + /** + Returns a new child of the current container. These children are configured + to correctly inherit from the current container. + + @method child + @return {Container} + */ child: function() { var container = new Container(this); this.children.push(container); return container; }, + /** + Sets a key-value pair on the current container. If a parent container, + has the same key, once set on a child, the parent and child will diverge + as expected. + + @method set + @param {Object} object + @param {String} key + @param {any} value + */ set: function(object, key, value) { object[key] = value; }, - register: function(type, name, factory, options) { - var fullName; + /** + Registers a factory for later injection. - 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; + Example: + + ```javascript + var container = new Container(); + + container.register('model:user', Person, {singleton: false }); + container.register('fruit:favorite', Orange); + container.register('communication:main', Email, {singleton: false}); + ``` + + @method register + @param {String} fullName + @param {Function} factory + @param {Object} options + */ + register: function(fullName, factory, options) { + if (fullName.indexOf(':') === -1) { + throw new TypeError("malformed fullName, expected: `type:name` got: " + fullName + ""); + } + + 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 || {}); }, + /** + Unregister a fullName + + ```javascript + var container = new Container(); + container.register('model:user', User); + + container.lookup('model:user') instanceof User //=> true + + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + ``` + + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + var normalizedName = this.normalize(fullName); + + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); + this._options.remove(normalizedName); + }, + + /** + Given a fullName return the corresponding factory. + + By default `resolve` will retrieve the factory from + its container's registry. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + container.resolve('api:twitter') // => Twitter + ``` + + Optionally the container can be provided with a custom resolver. + If provided, `resolve` will first provide the custom resolver + the oppertunity to resolve the fullName, otherwise it will fallback + to the registry. + + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; + + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` + + @method resolve + @param {String} fullName + @return {Function} fullName's factory + */ resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); }, + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. + + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. + + @method describe + @param {String} fullName + @return {string} described fullName + */ + describe: function(fullName) { + return fullName; + }, + + /** + A hook to enable custom fullName normalization behaviour + + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ normalize: function(fullName) { 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. + + The default behaviour is for lookup to return a singleton instance. + The singleton is scoped to the container, allowing multiple containers + to all have their own locally scoped singletons. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + var twitter = container.lookup('api:twitter'); + + twitter instanceof Twitter; // => true + + // by default the container will return singletons + var twitter2 = container.lookup('api:twitter'); + twitter instanceof Twitter; // => true + + twitter === twitter2; //=> true + ``` + + If singletons are not wanted an optional flag can be provided at lookup. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); + + twitter === twitter2; //=> false + ``` + + @method lookup + @param {String} fullName + @param {Object} options + @return {any} + */ lookup: function(fullName, options) { fullName = this.normalize(fullName); @@ -7037,7 +10544,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); @@ -7046,35 +10553,160 @@ define("container", return value; }, + /** + Given a fullName return the corresponding factory. + + @method lookupFactory + @param {String} fullName + @return {any} + */ + lookupFactory: function(fullName) { + return factoryFor(this, fullName); + }, + + /** + Given a fullName check if the container is aware of its factory + or singleton instance. + + @method has + @param {String} fullName + @return {Boolean} + */ has: function(fullName) { if (this.cache.has(fullName)) { return true; } - return !!factoryFor(this, fullName); + return !!this.resolve(fullName); }, + /** + Allow registering options for all factories of a type. + + ```javascript + var container = new Container(); + + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); + + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); + + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); + + twitter === twitter2; // => false + + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); + + facebook === facebook2; // => false + ``` + + @method optionsForType + @param {String} type + @param {Object} options + */ optionsForType: function(type, options) { if (this.parent) { illegalChildOperation('optionsForType'); } this._typeOptions.set(type, options); }, + /** + @method options + @param {String} type + @param {Object} options + */ options: function(type, options) { this.optionsForType(type, options); }, + /** + Used only via `injection`. + + Provides a specialized form of injection, specifically enabling + all objects of one type to be injected with a reference to another + object. + + For example, provided each object of type `controller` needed a `router`. + one would do the following: + + ```javascript + var container = new Container(); + + container.register('router:main', Router); + container.register('controller:user', UserController); + container.register('controller:post', PostController); + + container.typeInjection('controller', 'router', 'router:main'); + + var user = container.lookup('controller:user'); + var post = container.lookup('controller:post'); + + user.router instanceof Router; //=> true + post.router instanceof Router; //=> true + + // both controllers share the same router + user.router === post.router; //=> true + ``` + + @private + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ typeInjection: function(type, property, fullName) { if (this.parent) { illegalChildOperation('typeInjection'); } - var injections = this.typeInjections.get(type); - if (!injections) { - injections = []; - this.typeInjections.set(type, injections); - } - injections.push({ property: property, fullName: fullName }); + addTypeInjection(this.typeInjections, type, property, fullName); }, + /** + Defines injection rules. + + These rules are used to inject dependencies onto objects when they + are instantiated. + + Two forms of injections are possible: + + * Injecting one fullName on another fullName + * Injecting one fullName on a type + + Example: + + ```javascript + var container = new Container(); + + container.register('source:main', Source); + container.register('model:user', User); + container.register('model:post', Post); + + // injecting one fullName on another fullName + // eg. each user model gets a post model + container.injection('model:user', 'post', 'model:post'); + + // injecting one fullName on another type + container.injection('model', 'source', 'source:main'); + + var user = container.lookup('model:user'); + var post = container.lookup('model:post'); + + user.source instanceof Source; //=> true + post.source instanceof Source; //=> true + + user.post instanceof Post; //=> true + + // and both models share the same source + user.source === post.source; //=> true + ``` + + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ injection: function(factoryName, property, injectionName) { if (this.parent) { illegalChildOperation('injection'); } @@ -7082,12 +10714,111 @@ define("container", return this.typeInjection(factoryName, property, injectionName); } - var injections = this.injections[factoryName] = this.injections[factoryName] || []; - injections.push({ property: property, fullName: injectionName }); + addInjection(this.injections, factoryName, property, injectionName); }, + + /** + Used only via `factoryInjection`. + + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. + + For example, provided each factory of type `model` needed a `store`. + one would do the following: + + ```javascript + var container = new Container(); + + container.register('store:main', SomeStore); + + container.factoryTypeInjection('model', 'store', 'store:main'); + + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); + + UserFactory.store instanceof SomeStore; //=> true + ``` + + @private + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } + + addTypeInjection(this.factoryTypeInjections, type, property, fullName); + }, + + /** + Defines factory injection rules. + + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. + + These rules are used to inject objects onto factories when they + are looked up. + + Two forms of injections are possible: + + * Injecting one fullName on another fullName + * Injecting one fullName on a type + + Example: + + ```javascript + var container = new Container(); + + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); + + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); + + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); + + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); + + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false + + PostFactory.store instanceof Store; //=> true + PostFactory.secondaryStore instanceof OtherStore; //=> true + + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` + + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(factoryName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } + + if (factoryName.indexOf(':') === -1) { + return this.factoryTypeInjection(factoryName, property, injectionName); + } + + addInjection(this.factoryInjections, factoryName, property, injectionName); + }, + + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. + + @method destroy + */ destroy: function() { - this.isDestroyed = true; for (var i=0, l=this.children.length; i= 0) return; + if (!obj.hasOwnProperty(key)) return; + + array.push(key); + }; + Ember.keys = function(obj) { - var ret = []; - for(var key in obj) { - if (obj.hasOwnProperty(key)) { ret.push(key); } + 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.prototype.constructor.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 - - @class RSVP - @namespace Ember - @constructor -*/ -Ember.RSVP = requireModule('rsvp'); - })(); @@ -7531,7 +11329,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); @@ -7582,9 +11380,9 @@ Ember.String = { // first, replace any ORDERED replacements. var idx = 0; // the current index for non-numerical replacements return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { - argIndex = (argIndex) ? parseInt(argIndex,0) - 1 : idx++ ; + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; s = formats[argIndex]; - return ((s === null) ? '(null)' : (s === undefined) ? '' : s).toString(); + return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); }) ; }, @@ -7657,7 +11455,7 @@ Ember.String = { }, /** - Replaces underscores or spaces with dashes. + Replaces underscores, spaces, or camelCase with dashes. ```javascript 'innerHTML'.dasherize(); // 'inner-html' @@ -7757,21 +11555,24 @@ Ember.String = { /** Returns the Capitalized form of a string - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` @method capitalize - @param {String} str - @return {String} + @param {String} str The string to capitalize. + @return {String} The capitalized string. */ capitalize: function(str) { return str.charAt(0).toUpperCase() + str.substr(1); } - }; + + })(); @@ -7794,10 +11595,11 @@ var fmt = Ember.String.fmt, capitalize = Ember.String.capitalize, classify = Ember.String.classify; + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/fmt"}}{{/crossLink}} + See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). @method fmt @for String @@ -7807,7 +11609,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/w"}}{{/crossLink}} + See [Ember.String.w](/api/classes/Ember.String.html#method_w). @method w @for String @@ -7817,7 +11619,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/loc"}}{{/crossLink}} + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc). @method loc @for String @@ -7827,7 +11629,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/camelize"}}{{/crossLink}} + See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). @method camelize @for String @@ -7837,7 +11639,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/decamelize"}}{{/crossLink}} + See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). @method decamelize @for String @@ -7847,7 +11649,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/dasherize"}}{{/crossLink}} + See [Ember.String.dasherize](/api/classes/Ember.String.html#method_dasherize). @method dasherize @for String @@ -7857,7 +11659,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/underscore"}}{{/crossLink}} + See [Ember.String.underscore](/api/classes/Ember.String.html#method_underscore). @method underscore @for String @@ -7867,7 +11669,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/classify"}}{{/crossLink}} + See [Ember.String.classify](/api/classes/Ember.String.html#method_classify). @method classify @for String @@ -7877,7 +11679,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/capitalize"}}{{/crossLink}} + See [Ember.String.capitalize](/api/classes/Ember.String.html#method_capitalize). @method capitalize @for String @@ -7886,6 +11688,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { return capitalize(this); }; + } @@ -7899,2083 +11702,10 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { @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.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" - ``` - - 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.create({ - firstName: "Barack", - lastName: "Obama", - - 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 {{#crossLink "Ember.ComputedProperty"}}{{/crossLink}}, - {{#crossLink "Ember/computed"}}{{/crossLink}} - - @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.create({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` - - See {{#crossLink "Ember.Observable/observes"}}{{/crossLink}} - - @method observes - @for Function - */ - Function.prototype.observes = function() { - this.__ember_observes__ = a_slice.call(arguments); - 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: - - ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property is about to change - }.observesBefore('value') - }); - ``` - - See {{#crossLink "Ember.Observable/observesBefore"}}{{/crossLink}} - - @method observesBefore - @for Function - */ - Function.prototype.observesBefore = function() { - this.__ember_observesBefore__ = a_slice.call(arguments); - return this; - }; - -} - - -})(); - - - -(function() { - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -// .......................................................... -// HELPERS -// - -var get = Ember.get, set = Ember.set; -var a_slice = Array.prototype.slice; -var a_indexOf = Ember.EnumerableUtils.indexOf; - -var contexts = []; - -function popCtx() { - return contexts.length===0 ? {} : contexts.pop(); -} - -function pushCtx(ctx) { - contexts.push(ctx); - return null; -} - -function iter(key, value) { - var valueProvided = arguments.length === 2; - - function i(item) { - var cur = get(item, key); - return valueProvided ? value===cur : !!cur; - } - return i ; -} - -/** - This mixin defines the common interface implemented by enumerable objects - in Ember. Most of these methods follow the standard Array iteration - API defined up to JavaScript 1.8 (excluding language-specific features that - cannot be emulated in older versions of JavaScript). - - This mixin is applied automatically to the Array class on page load, so you - can use any of these methods on simple arrays. If Array already implements - one of these methods, the mixin will not override them. - - ## Writing Your Own Enumerable - - To make your own custom class enumerable, you need two items: - - 1. You must have a length property. This property should change whenever - the number of items in your enumerable object changes. If you using this - with an `Ember.Object` subclass, you should be sure to change the length - property using `set().` - - 2. If you must implement `nextObject().` See documentation. - - Once you have these two methods implement, apply the `Ember.Enumerable` mixin - to your class and you will be able to enumerate the contents of your object - like any other collection. - - ## Using Ember Enumeration with Other Libraries - - Many other libraries provide some kind of iterator or enumeration like - facility. This is often where the most common API conflicts occur. - Ember's API is designed to be as friendly as possible with other - libraries by implementing only methods that mostly correspond to the - JavaScript 1.8 API. - - @class Enumerable - @namespace Ember - @extends Ember.Mixin - @since Ember 0.9 -*/ -Ember.Enumerable = Ember.Mixin.create({ - - // compatibility - isEnumerable: true, - - /** - Implement this method to make your class enumerable. - - This method will be call repeatedly during enumeration. The index value - will always begin with 0 and increment monotonically. You don't have to - rely on the index value to determine what object to return, but you should - always check the value and start from the beginning when you see the - requested index is 0. - - The `previousObject` is the object that was returned from the last call - to `nextObject` for the current iteration. This is a useful way to - manage iteration if you are tracing a linked list, for example. - - Finally the context parameter will always contain a hash you can use as - a "scratchpad" to maintain any other state you need in order to iterate - properly. The context object is reused and is not reset between - iterations so make sure you setup the context with a fresh state whenever - the index parameter is 0. - - Generally iterators will continue to call `nextObject` until the index - reaches the your current length-1. If you run out of data before this - time for some reason, you should simply return undefined. - - The default implementation of this method simply looks up the index. - This works great on any Array-like objects. - - @method nextObject - @param {Number} index the current index of the iteration - @param {Object} previousObject the value returned by the last call to - `nextObject`. - @param {Object} context a context object you can use to maintain state. - @return {Object} the next object in the iteration or undefined - */ - nextObject: Ember.required(Function), - - /** - Helper method returns the first object from a collection. This is usually - used by bindings and other parts of the framework to extract a single - object if the enumerable contains only one item. - - If you override this method, you should implement it so that it will - always return the same value each time it is called. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return `undefined`. - - ```javascript - var arr = ["a", "b", "c"]; - arr.get('firstObject'); // "a" - - var arr = []; - arr.get('firstObject'); // undefined - ``` - - @property firstObject - @return {Object} the object or undefined - */ - firstObject: Ember.computed(function() { - if (get(this, 'length')===0) return undefined ; - - // handle generic enumerables - var context = popCtx(), ret; - ret = this.nextObject(0, null, context); - pushCtx(context); - return ret ; - }).property('[]'), - - /** - Helper method returns the last object from a collection. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return `undefined`. - - ```javascript - var arr = ["a", "b", "c"]; - arr.get('lastObject'); // "c" - - var arr = []; - arr.get('lastObject'); // undefined - ``` - - @property lastObject - @return {Object} the last object or undefined - */ - lastObject: Ember.computed(function() { - var len = get(this, 'length'); - if (len===0) return undefined ; - var context = popCtx(), idx=0, cur, last = null; - do { - last = cur; - cur = this.nextObject(idx++, last, context); - } while (cur !== undefined); - pushCtx(context); - return last; - }).property('[]'), - - /** - Returns `true` if the passed object can be found in the receiver. The - default version will iterate through the enumerable until the object - is found. You may want to override this with a more efficient version. - - ```javascript - var arr = ["a", "b", "c"]; - arr.contains("a"); // true - arr.contains("z"); // false - ``` - - @method contains - @param {Object} obj The object to search for. - @return {Boolean} `true` if object is found in enumerable. - */ - contains: function(obj) { - return this.find(function(item) { return item===obj; }) !== undefined; - }, - - /** - Iterates through the enumerable, calling the passed function on each - item. This method corresponds to the `forEach()` method defined in - JavaScript 1.6. - - The callback method you provide should have the following signature (all - parameters are optional): - - ```javascript - function(item, index, enumerable); - ``` - - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. - - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. - - @method forEach - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Object} receiver - */ - forEach: function(callback, target) { - if (typeof callback !== "function") throw new TypeError() ; - var len = get(this, 'length'), last = null, context = popCtx(); - - if (target === undefined) target = null; - - for(var idx=0;idx1) args = a_slice.call(arguments, 1); - - this.forEach(function(x, idx) { - var method = x && x[methodName]; - if ('function' === typeof method) { - ret[idx] = args ? method.apply(x, args) : method.call(x); - } - }, this); - - return ret; - }, - - /** - Simply converts the enumerable into a genuine array. The order is not - guaranteed. Corresponds to the method implemented by Prototype. - - @method toArray - @return {Array} the enumerable as an array. - */ - toArray: function() { - var ret = Ember.A([]); - this.forEach(function(o, idx) { ret[idx] = o; }); - return ret ; - }, - - /** - Returns a copy of the array with all null and undefined elements removed. - - ```javascript - var arr = ["a", null, "c", undefined]; - arr.compact(); // ["a", "c"] - ``` - - @method compact - @return {Array} the array without null and undefined elements. - */ - compact: function() { - return this.filter(function(value) { return value != null; }); - }, - - /** - Returns a new enumerable that excludes the passed value. The default - implementation returns an array regardless of the receiver type unless - the receiver does not contain the value. - - ```javascript - var arr = ["a", "b", "a", "c"]; - arr.without("a"); // ["b", "c"] - ``` - - @method without - @param {Object} value - @return {Ember.Enumerable} - */ - without: function(value) { - if (!this.contains(value)) return this; // nothing to do - var ret = Ember.A([]); - this.forEach(function(k) { - if (k !== value) ret[ret.length] = k; - }) ; - return ret ; - }, - - /** - Returns a new enumerable that contains only unique values. The default - implementation returns an array regardless of the receiver type. - - ```javascript - var arr = ["a", "a", "b", "b"]; - arr.uniq(); // ["a", "b"] - ``` - - @method uniq - @return {Ember.Enumerable} - */ - uniq: function() { - var ret = Ember.A([]); - this.forEach(function(k){ - if (a_indexOf(ret, k)<0) ret.push(k); - }); - return ret; - }, - - /** - This property will trigger anytime the enumerable's content changes. - You can observe this property to be notified of changes to the enumerables - content. - - For plain enumerables, this property is read only. `Ember.Array` overrides - this method. - - @property [] - @type Ember.Array - @return this - */ - '[]': Ember.computed(function(key, value) { - return this; - }), - - // .......................................................... - // ENUMERABLE OBSERVERS - // - - /** - Registers an enumerable observer. Must implement `Ember.EnumerableObserver` - mixin. - - @method addEnumerableObserver - @param {Object} target - @param {Hash} [opts] - @return this - */ - addEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; - - var hasObservers = get(this, 'hasEnumerableObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.addListener(this, '@enumerable:before', target, willChange); - Ember.addListener(this, '@enumerable:change', target, didChange); - if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, - - /** - Removes a registered enumerable observer. - - @method removeEnumerableObserver - @param {Object} target - @param {Hash} [opts] - @return this - */ - removeEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; - - var hasObservers = get(this, 'hasEnumerableObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.removeListener(this, '@enumerable:before', target, willChange); - Ember.removeListener(this, '@enumerable:change', target, didChange); - if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, - - /** - Becomes true whenever the array currently has observers watching changes - on the array. - - @property hasEnumerableObservers - @type Boolean - */ - hasEnumerableObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); - }), - - - /** - Invoke this method just before the contents of your enumerable will - change. You can either omit the parameters completely or pass the objects - to be removed or added if available or just a count. - - @method enumerableContentWillChange - @param {Ember.Enumerable|Number} removing An enumerable of the objects to - be removed or the number of items to be removed. - @param {Ember.Enumerable|Number} adding An enumerable of the objects to be - added or the number of items to be added. - @chainable - */ - enumerableContentWillChange: function(removing, adding) { - - var removeCnt, addCnt, hasDelta; - - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; - - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding,'length'); - else addCnt = adding = -1; - - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - - if (removing === -1) removing = null; - if (adding === -1) adding = null; - - Ember.propertyWillChange(this, '[]'); - if (hasDelta) Ember.propertyWillChange(this, 'length'); - Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); - - return this; - }, - - /** - Invoke this method when the contents of your enumerable has changed. - This will notify any observers watching for content changes. If your are - implementing an ordered enumerable (such as an array), also pass the - start and end values where the content changed so that it can be used to - notify range observers. - - @method enumerableContentDidChange - @param {Number} [start] optional start offset for the content change. - For unordered enumerables, you should always pass -1. - @param {Ember.Enumerable|Number} removing An enumerable of the objects to - be removed or the number of items to be removed. - @param {Ember.Enumerable|Number} adding An enumerable of the objects to - be added or the number of items to be added. - @chainable - */ - enumerableContentDidChange: function(removing, adding) { - var removeCnt, addCnt, hasDelta; - - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; - - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding, 'length'); - else addCnt = adding = -1; - - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - - if (removing === -1) removing = null; - if (adding === -1) adding = null; - - Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); - if (hasDelta) Ember.propertyDidChange(this, 'length'); - Ember.propertyDidChange(this, '[]'); - - return this ; - } - -}) ; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -// .......................................................... -// HELPERS -// - -var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; - -// .......................................................... -// ARRAY -// -/** - This module implements Observer-friendly Array-like behavior. This mixin is - picked up by the Array class as well as other controllers, etc. that want to - appear to be arrays. - - Unlike `Ember.Enumerable,` this mixin defines methods specifically for - collections that provide index-ordered access to their contents. When you - are designing code that needs to accept any kind of Array-like object, you - should use these methods instead of Array primitives because these will - properly notify observers of changes to the array. - - Although these methods are efficient, they do add a layer of indirection to - your application so it is a good idea to use them only when you need the - flexibility of using both true JavaScript arrays and "virtual" arrays such - as controllers and collections. - - You can use the methods defined in this module to access and modify array - contents in a KVO-friendly way. You can also be notified whenever the - membership if an array changes by changing the syntax of the property to - `.observes('*myProperty.[]')`. - - To support `Ember.Array` in your own class, you must override two - primitives to use it: `replace()` and `objectAt()`. - - Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` - mixin. All `Ember.Array`-like objects are also enumerable. - - @class Array - @namespace Ember - @extends Ember.Mixin - @uses Ember.Enumerable - @since Ember 0.9.0 -*/ -Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { - - // compatibility - isSCArray: true, - - /** - Your array must support the `length` property. Your replace methods should - set this property whenever it changes. - - @property {Number} length - */ - length: Ember.required(), - - /** - Returns the object at the given `index`. If the given `index` is negative - or is greater or equal than the array length, returns `undefined`. - - This is one of the primitives you must implement to support `Ember.Array`. - If your object supports retrieving the value of an array item using `get()` - (i.e. `myArray.get(0)`), then you do not need to implement this method - yourself. - - ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectAt(0); // "a" - arr.objectAt(3); // "d" - arr.objectAt(-1); // undefined - arr.objectAt(4); // undefined - arr.objectAt(5); // undefined - ``` - - @method objectAt - @param {Number} idx The index of the item to return. - @return {*} item at index or undefined - */ - objectAt: function(idx) { - if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; - return get(this, idx); - }, - - /** - This returns the objects at the specified indexes, using `objectAt`. - - ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] - arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] - ``` - - @method objectsAt - @param {Array} indexes An array of indexes of items to return. - @return {Array} - */ - objectsAt: function(indexes) { - var self = this; - return map(indexes, function(idx){ return self.objectAt(idx); }); - }, - - // overrides Ember.Enumerable version - nextObject: function(idx) { - return this.objectAt(idx); - }, - - /** - This is the handler for the special array content property. If you get - this property, it will return this. If you set this property it a new - array, it will replace the current content. - - This property overrides the default property defined in `Ember.Enumerable`. - - @property [] - @return this - */ - '[]': Ember.computed(function(key, value) { - if (value !== undefined) this.replace(0, get(this, 'length'), value) ; - return this ; - }), - - firstObject: Ember.computed(function() { - return this.objectAt(0); - }), - - lastObject: Ember.computed(function() { - return this.objectAt(get(this, 'length')-1); - }), - - // optimized version from Enumerable - contains: function(obj){ - return this.indexOf(obj) >= 0; - }, - - // Add any extra methods to Ember.Array that are native to the built-in Array. - /** - Returns a new array that is a slice of the receiver. This implementation - uses the observable array methods to retrieve the objects for the new - slice. - - ```javascript - var arr = ['red', 'green', 'blue']; - arr.slice(0); // ['red', 'green', 'blue'] - arr.slice(0, 2); // ['red', 'green'] - arr.slice(1, 100); // ['green', 'blue'] - ``` - - @method slice - @param {Integer} beginIndex (Optional) index to begin slicing from. - @param {Integer} endIndex (Optional) index to end the slice at. - @return {Array} New array with specified slice - */ - slice: function(beginIndex, endIndex) { - var ret = Ember.A([]); - var length = get(this, 'length') ; - if (isNone(beginIndex)) beginIndex = 0 ; - if (isNone(endIndex) || (endIndex > length)) endIndex = length ; - - if (beginIndex < 0) beginIndex = length + beginIndex; - if (endIndex < 0) endIndex = length + endIndex; - - while(beginIndex < endIndex) { - ret[ret.length] = this.objectAt(beginIndex++) ; - } - return ret ; - }, - - /** - Returns the index of the given object's first occurrence. - If no `startAt` argument is given, the starting location to - search is 0. If it's negative, will count backward from - the end of the array. Returns -1 if no match is found. - - ```javascript - var arr = ["a", "b", "c", "d", "a"]; - arr.indexOf("a"); // 0 - arr.indexOf("z"); // -1 - arr.indexOf("a", 2); // 4 - arr.indexOf("a", -1); // 4 - arr.indexOf("b", 3); // -1 - arr.indexOf("a", 100); // -1 - ``` - - @method indexOf - @param {Object} object the item to search for - @param {Number} startAt optional starting location to search, default 0 - @return {Number} index or -1 if not found - */ - indexOf: function(object, startAt) { - var idx, len = get(this, 'length'); - - if (startAt === undefined) startAt = 0; - if (startAt < 0) startAt += len; - - for(idx=startAt;idx= len) startAt = len-1; - if (startAt < 0) startAt += len; - - for(idx=startAt;idx>=0;idx--) { - if (this.objectAt(idx) === object) return idx ; - } - return -1; - }, - - // .......................................................... - // ARRAY OBSERVERS - // - - /** - Adds an array observer to the receiving array. The array observer object - normally must implement two methods: - - * `arrayWillChange(start, removeCount, addCount)` - This method will be - called just before the array is modified. - * `arrayDidChange(start, removeCount, addCount)` - This method will be - called just after the array is modified. - - Both callbacks will be passed the starting index of the change as well a - a count of the items to be removed and added. You can use these callbacks - to optionally inspect the array during the change, clear caches, or do - any other bookkeeping necessary. - - In addition to passing a target, you can also include an options hash - which you can use to override the method names that will be invoked on the - target. - - @method addArrayObserver - @param {Object} target The observer object. - @param {Hash} opts Optional hash of configuration options including - `willChange` and `didChange` option. - @return {Ember.Array} receiver - */ - addArrayObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'arrayWillChange', - didChange = (opts && opts.didChange) || 'arrayDidChange'; - - var hasObservers = get(this, 'hasArrayObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); - Ember.addListener(this, '@array:before', target, willChange); - Ember.addListener(this, '@array:change', target, didChange); - if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); - return this; - }, - - /** - Removes an array observer from the object if the observer is current - registered. Calling this method multiple times with the same object will - have no effect. - - @method removeArrayObserver - @param {Object} target The object observing the array. - @param {Hash} opts Optional hash of configuration options including - `willChange` and `didChange` option. - @return {Ember.Array} receiver - */ - removeArrayObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'arrayWillChange', - didChange = (opts && opts.didChange) || 'arrayDidChange'; - - var hasObservers = get(this, 'hasArrayObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); - Ember.removeListener(this, '@array:before', target, willChange); - Ember.removeListener(this, '@array:change', target, didChange); - if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); - return this; - }, - - /** - Becomes true whenever the array currently has observers watching changes - on the array. - - @property Boolean - */ - hasArrayObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); - }), - - /** - If you are implementing an object that supports `Ember.Array`, call this - method just before the array content changes to notify any observers and - invalidate any related properties. Pass the starting index of the change - as well as a delta of the amounts to change. - - @method arrayContentWillChange - @param {Number} startIdx The starting index in the array that will change. - @param {Number} removeAmt The number of items that will be removed. If you - pass `null` assumes 0 - @param {Number} addAmt The number of items that will be added. If you - pass `null` assumes 0. - @return {Ember.Array} receiver - */ - arrayContentWillChange: function(startIdx, removeAmt, addAmt) { - - // if no args are passed assume everything changes - if (startIdx===undefined) { - startIdx = 0; - removeAmt = addAmt = -1; - } else { - if (removeAmt === undefined) removeAmt=-1; - if (addAmt === undefined) addAmt=-1; - } - - // Make sure the @each proxy is set up if anyone is observing @each - if (Ember.isWatching(this, '@each')) { get(this, '@each'); } - - Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); - - var removing, lim; - if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { - removing = []; - lim = startIdx+removeAmt; - for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { - adding = []; - lim = startIdx+addAmt; - for(var idx=startIdx;idx b` - - Default implementation raises an exception. - - @method compare - @param a {Object} the first object to compare - @param b {Object} the second object to compare - @return {Integer} the result of the comparison - */ - compare: Ember.required(Function) - -}); - - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - - -var get = Ember.get, set = Ember.set; - -/** - Implements some standard methods for copying an object. Add this mixin to - any object you create that can create a copy of itself. This mixin is - added automatically to the built-in array. - - You should generally implement the `copy()` method to return a copy of the - receiver. - - Note that `frozenCopy()` will only work if you also implement - `Ember.Freezable`. - - @class Copyable - @namespace Ember - @extends Ember.Mixin - @since Ember 0.9 -*/ -Ember.Copyable = Ember.Mixin.create( -/** @scope Ember.Copyable.prototype */ { - - /** - Override to return a copy of the receiver. Default implementation raises - an exception. - - @method copy - @param {Boolean} deep if `true`, a deep copy of the object should be made - @return {Object} copy of receiver - */ - copy: Ember.required(Function), - - /** - If the object implements `Ember.Freezable`, then this will return a new - copy if the object is not frozen and the receiver if the object is frozen. - - Raises an exception if you try to call this method on a object that does - not support freezing. - - You should use this method whenever you want a copy of a freezable object - since a freezable object can simply return itself without actually - consuming more memory. - - @method frozenCopy - @return {Object} copy of receiver or receiver - */ - frozenCopy: function() { - if (Ember.Freezable && Ember.Freezable.detect(this)) { - return get(this, 'isFrozen') ? this : this.copy().freeze(); - } else { - throw new Error(Ember.String.fmt("%@ does not support freezing", [this])); - } - } -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - -var get = Ember.get, set = Ember.set; - -/** - The `Ember.Freezable` mixin implements some basic methods for marking an - object as frozen. Once an object is frozen it should be read only. No changes - may be made the internal state of the object. - - ## Enforcement - - To fully support freezing in your subclass, you must include this mixin and - override any method that might alter any property on the object to instead - raise an exception. You can check the state of an object by checking the - `isFrozen` property. - - Although future versions of JavaScript may support language-level freezing - object objects, that is not the case today. Even if an object is freezable, - it is still technically possible to modify the object, even though it could - break other parts of your application that do not expect a frozen object to - change. It is, therefore, very important that you always respect the - `isFrozen` property on all freezable objects. - - ## Example Usage - - The example below shows a simple object that implement the `Ember.Freezable` - protocol. - - ```javascript - Contact = Ember.Object.extend(Ember.Freezable, { - firstName: null, - lastName: null, - - // swaps the names - swapNames: function() { - if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; - var tmp = this.get('firstName'); - this.set('firstName', this.get('lastName')); - this.set('lastName', tmp); - return this; - } - - }); - - c = Context.create({ firstName: "John", lastName: "Doe" }); - c.swapNames(); // returns c - c.freeze(); - c.swapNames(); // EXCEPTION - ``` - - ## Copying - - Usually the `Ember.Freezable` protocol is implemented in cooperation with the - `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will - return a frozen object, if the object implements this method as well. - - @class Freezable - @namespace Ember - @extends Ember.Mixin - @since Ember 0.9 -*/ -Ember.Freezable = Ember.Mixin.create( -/** @scope Ember.Freezable.prototype */ { - - /** - Set to `true` when the object is frozen. Use this property to detect - whether your object is frozen or not. - - @property isFrozen - @type Boolean - */ - isFrozen: false, - - /** - Freezes the object. Once this method has been called the object should - no longer allow any properties to be edited. - - @method freeze - @return {Object} receiver - */ - freeze: function() { - if (get(this, 'isFrozen')) return this; - set(this, 'isFrozen', true); - return this; - } - -}); - -Ember.FROZEN_ERROR = "Frozen object cannot be modified."; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var forEach = Ember.EnumerableUtils.forEach; - -/** - This mixin defines the API for modifying generic enumerables. These methods - can be applied to an object regardless of whether it is ordered or - unordered. - - Note that an Enumerable can change even if it does not implement this mixin. - For example, a MappedEnumerable cannot be directly modified but if its - underlying enumerable changes, it will change also. - - ## Adding Objects - - To add an object to an enumerable, use the `addObject()` method. This - method will only add the object to the enumerable if the object is not - already present and is of a type supported by the enumerable. - - ```javascript - set.addObject(contact); - ``` - - ## Removing Objects - - To remove an object from an enumerable, use the `removeObject()` method. This - will only remove the object if it is present in the enumerable, otherwise - this method has no effect. - - ```javascript - set.removeObject(contact); - ``` - - ## Implementing In Your Own Code - - If you are implementing an object and want to support this API, just include - this mixin in your class and implement the required methods. In your unit - tests, be sure to apply the Ember.MutableEnumerableTests to your object. - - @class MutableEnumerable - @namespace Ember - @extends Ember.Mixin - @uses Ember.Enumerable -*/ -Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { - - /** - __Required.__ You must implement this method to apply this mixin. - - Attempts to add the passed object to the receiver if the object is not - already present in the collection. If the object is present, this method - has no effect. - - If the passed object is of a type not supported by the receiver, - then this method should raise an exception. - - @method addObject - @param {Object} object The object to add to the enumerable. - @return {Object} the passed object - */ - addObject: Ember.required(Function), - - /** - Adds each object in the passed enumerable to the receiver. - - @method addObjects - @param {Ember.Enumerable} objects the objects to add. - @return {Object} receiver - */ - addObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.addObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; - }, - - /** - __Required.__ You must implement this method to apply this mixin. - - Attempts to remove the passed object from the receiver collection if the - object is present in the collection. If the object is not present, - this method has no effect. - - If the passed object is of a type not supported by the receiver, - then this method should raise an exception. - - @method removeObject - @param {Object} object The object to remove from the enumerable. - @return {Object} the passed object - */ - removeObject: Ember.required(Function), - - - /** - Removes each object in the passed enumerable from the receiver. - - @method removeObjects - @param {Ember.Enumerable} objects the objects to remove - @return {Object} receiver - */ - removeObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.removeObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; - } - -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ -// .......................................................... -// CONSTANTS -// - -var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; -var EMPTY = []; - -// .......................................................... -// HELPERS -// - -var get = Ember.get, set = Ember.set; - -/** - This mixin defines the API for modifying array-like objects. These methods - can be applied only to a collection that keeps its items in an ordered set. - - Note that an Array can change even if it does not implement this mixin. - For example, one might implement a SparseArray that cannot be directly - modified, but if its underlying enumerable changes, it will change also. - - @class MutableArray - @namespace Ember - @extends Ember.Mixin - @uses Ember.Array - @uses Ember.MutableEnumerable -*/ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, - /** @scope Ember.MutableArray.prototype */ { - - /** - __Required.__ You must implement this method to apply this mixin. - - This is one of the primitives you must implement to support `Ember.Array`. - You should replace amt objects started at idx with the objects in the - passed array. You should also call `this.enumerableContentDidChange()` - - @method replace - @param {Number} idx Starting index in the array to replace. If - idx >= length, then append to the end of the array. - @param {Number} amt Number of elements that should be removed from - the array, starting at *idx*. - @param {Array} objects An array of zero or more objects that should be - inserted into the array at *idx* - */ - replace: Ember.required(), - - /** - Remove all elements from self. This is useful if you - want to reuse an existing array without having to recreate it. - - ```javascript - var colors = ["red", "green", "blue"]; - color.length(); // 3 - colors.clear(); // [] - colors.length(); // 0 - ``` - - @method clear - @return {Ember.Array} An empty Array. - */ - clear: function () { - var len = get(this, 'length'); - if (len === 0) return this; - this.replace(0, len, EMPTY); - return this; - }, - - /** - This will use the primitive `replace()` method to insert an object at the - specified index. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] - colors.insertAt(5, "orange"); // Error: Index out of range - ``` - - @method insertAt - @param {Number} idx index of insert the object at. - @param {Object} object object to insert - @return this - */ - insertAt: function(idx, object) { - if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; - this.replace(idx, 0, [object]) ; - return this ; - }, - - /** - Remove an object at the specified index using the `replace()` primitive - method. You can pass either a single index, or a start and a length. - - If you pass a start and length that is beyond the - length this method will throw an `Ember.OUT_OF_RANGE_EXCEPTION` - - ```javascript - var colors = ["red", "green", "blue", "yellow", "orange"]; - colors.removeAt(0); // ["green", "blue", "yellow", "orange"] - colors.removeAt(2, 2); // ["green", "blue"] - colors.removeAt(4, 2); // Error: Index out of range - ``` - - @method removeAt - @param {Number} start index, start of range - @param {Number} len length of passing range - @return {Object} receiver - */ - removeAt: function(start, len) { - if ('number' === typeof start) { - - if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); - } - - // fast case - if (len === undefined) len = 1; - this.replace(start, len, EMPTY); - } - - return this ; - }, - - /** - Push the object onto the end of the array. Works just like `push()` but it - is KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObject("black"); // ["red", "green", "blue", "black"] - colors.pushObject(["yellow", "orange"]); // ["red", "green", "blue", "black", ["yellow", "orange"]] - ``` - - @method pushObject - @param {*} obj object to push - @return {*} the same obj passed as param - */ - pushObject: function(obj) { - this.insertAt(get(this, 'length'), obj) ; - return obj ; - }, - - /** - Add the objects in the passed numerable to the end of the array. Defers - notifying observers of the change until all objects are added. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObjects("black"); // ["red", "green", "blue", "black"] - colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"] - ``` - - @method pushObjects - @param {Ember.Enumerable} objects the objects to add - @return {Ember.Array} receiver - */ - pushObjects: function(objects) { - this.replace(get(this, 'length'), 0, objects); - return this; - }, - - /** - Pop object from array or nil if none are left. Works just like `pop()` but - it is KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.popObject(); // "blue" - console.log(colors); // ["red", "green"] - ``` - - @method popObject - @return object - */ - popObject: function() { - var len = get(this, 'length') ; - if (len === 0) return null ; - - var ret = this.objectAt(len-1) ; - this.removeAt(len-1, 1) ; - return ret ; - }, - - /** - Shift an object from start of array or nil if none are left. Works just - like `shift()` but it is KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.shiftObject(); // "red" - console.log(colors); // ["green", "blue"] - ``` - - @method shiftObject - @return object - */ - shiftObject: function() { - if (get(this, 'length') === 0) return null ; - var ret = this.objectAt(0) ; - this.removeAt(0) ; - return ret ; - }, - - /** - Unshift an object to start of array. Works just like `unshift()` but it is - KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"] - colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"] - ``` - - @method unshiftObject - @param {*} obj object to unshift - @return {*} the same obj passed as param - */ - unshiftObject: function(obj) { - this.insertAt(0, obj) ; - return obj ; - }, - - /** - Adds the named objects to the beginning of the array. Defers notifying - observers until all objects have been added. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObjects(["black", "white"]); // ["black", "white", "red", "green", "blue"] - colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function - ``` - - @method unshiftObjects - @param {Ember.Enumerable} objects the objects to add - @return {Ember.Array} receiver - */ - unshiftObjects: function(objects) { - this.replace(0, 0, objects); - return this; - }, - - /** - Reverse objects in the array. Works just like `reverse()` but it is - KVO-compliant. - - @method reverseObjects - @return {Ember.Array} receiver - */ - reverseObjects: function() { - var len = get(this, 'length'); - if (len === 0) return this; - var objects = this.toArray().reverse(); - this.replace(0, len, objects); - return this; - }, - - /** - Replace all the the receiver's content with content of the argument. - If argument is an empty array receiver will be cleared. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.setObjects(["black", "white"]); // ["black", "white"] - colors.setObjects([]); // [] - ``` - - @method setObjects - @param {Ember.Array} objects array whose content will be used for replacing - the content of the receiver - @return {Ember.Array} receiver with the new content - */ - setObjects: function(objects) { - if (objects.length === 0) return this.clear(); - - var len = get(this, 'length'); - this.replace(0, len, objects); - return this; - }, - - // .......................................................... - // IMPLEMENT Ember.MutableEnumerable - // - - removeObject: function(obj) { - var loc = get(this, 'length') || 0; - while(--loc >= 0) { - var curObject = this.objectAt(loc) ; - if (curObject === obj) this.removeAt(loc) ; - } - return this ; - }, - - addObject: function(obj) { - if (!this.contains(obj)) this.pushObject(obj); - return this ; - } - -}); - - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, set = Ember.set; +var get = Ember.get, + set = Ember.set, + slice = Array.prototype.slice, + getProperties = Ember.getProperties; /** ## Overview @@ -10041,9 +11771,8 @@ var get = Ember.get, set = Ember.set; @class Observable @namespace Ember - @extends Ember.Mixin */ -Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { +Ember.Observable = Ember.Mixin.create({ /** Retrieves the value of a property from the object. @@ -10063,7 +11792,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { ```javascript fullName: function() { - return this.getEach('firstName', 'lastName').compact().join(' '); + return this.get('firstName') + ' ' + this.get('lastName'); }.property('firstName', 'lastName') ``` @@ -10106,15 +11835,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @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; + return getProperties.apply(null, [this].concat(slice.call(arguments))); }, /** @@ -10122,7 +11843,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { 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. + properties, the `setUnknownProperty()` method and property observers. ### Computed Properties @@ -10136,9 +11857,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { ### 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 + 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 `unknownProperty()` returns + are not predefined on the object. If `setUnknownProperty()` returns undefined, then `set()` will simply set the value on the object. ### Property Observers @@ -10170,9 +11891,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { return this; }, + /** - To set multiple properties at once, call `setProperties` - with a Hash: + Sets a list of properties at once. These properties are set inside + a single `beginPropertyChanges` and `endPropertyChanges` batch, so + observers will be buffered. ```javascript record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); @@ -10241,7 +11964,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @param {String} keyName The property key that is about to change. @return {Ember.Observable} */ - propertyWillChange: function(keyName){ + propertyWillChange: function(keyName) { Ember.propertyWillChange(this, keyName); return this; }, @@ -10292,8 +12015,8 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This is the core method used to register an observer for a property. - Once you call this method, anytime the key's value is set, your observer - will be notified. Note that the observers are triggered anytime the + 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. @@ -10368,29 +12091,6 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { 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`. @@ -10423,7 +12123,8 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { */ incrementProperty: function(keyName, increment) { if (Ember.isNone(increment)) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)+increment); + 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); }, @@ -10442,7 +12143,8 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { */ decrementProperty: function(keyName, decrement) { if (Ember.isNone(decrement)) { decrement = 1; } - set(this, keyName, (get(this, keyName) || 0)-decrement); + 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); }, @@ -10483,403 +12185,19 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { } }); - })(); (function() { /** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, set = Ember.set; - -/** -`Ember.TargetActionSupport` is a mixin that can be included in a class -to add a `triggerAction` method with semantics similar to the Handlebars -`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is -usually the best choice. This mixin is most often useful when you are -doing more complex event handling in View objects. - -See also `Ember.ViewTargetActionSupport`, which has -view-aware defaults for target and actionContext. - -@class TargetActionSupport -@namespace Ember -@extends Ember.Mixin -*/ -Ember.TargetActionSupport = Ember.Mixin.create({ - target: null, - action: null, - actionContext: null, - - targetObject: Ember.computed(function() { - var target = get(this, 'target'); - - if (Ember.typeOf(target) === "string") { - var value = get(this, target); - if (value === undefined) { value = get(Ember.lookup, target); } - return value; - } else { - return target; - } - }).property('target'), - - actionContextObject: Ember.computed(function() { - var actionContext = get(this, 'actionContext'); - - if (Ember.typeOf(actionContext) === "string") { - var value = get(this, actionContext); - if (value === undefined) { value = get(Ember.lookup, actionContext); } - return value; - } else { - return actionContext; - } - }).property('actionContext'), - - /** - Send an "action" with an "actionContext" to a "target". The action, actionContext - and target will be retrieved from properties of the object. For example: - - ```javascript - App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { - target: Ember.computed.alias('controller'), - action: 'save', - actionContext: Ember.computed.alias('context'), - click: function(){ - this.triggerAction(); // Sends the `save` action, along with the current context - // to the current controller - } - }); - ``` - - The `target`, `action`, and `actionContext` can be provided as properties of - an optional object argument to `triggerAction` as well. - - ```javascript - App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { - click: function(){ - this.triggerAction({ - action: 'save', - target: this.get('controller'), - actionContext: this.get('context'), - }); // Sends the `save` action, along with the current context - // to the current controller - } - }); - ``` - - The `actionContext` defaults to the object you mixing `TargetActionSupport` into. - But `target` and `action` must be specified either as properties or with the argument - to `triggerAction`, or a combination: - - ```javascript - App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { - target: Ember.computed.alias('controller'), - click: function(){ - this.triggerAction({ - action: 'save' - }); // Sends the `save` action, along with a reference to `this`, - // to the current controller - } - }); - ``` - - @method triggerAction - @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) - @return {Boolean} true if the action was sent successfully and did not return false - */ - 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; - - if (target && action) { - var ret; - - if (target.send) { - ret = target.send.apply(target, [action, actionContext]); - } else { - Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); - ret = target[action].apply(target, [actionContext]); - } - - if (ret !== false) ret = true; - - return ret; - } else { - return false; - } - } -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -/** - This mixin allows for Ember objects to subscribe to and emit events. - - ```javascript - App.Person = Ember.Object.extend(Ember.Evented, { - greet: function() { - // ... - this.trigger('greet'); - } - }); - - var person = App.Person.create(); - - person.on('greet', function() { - console.log('Our person has greeted'); - }); - - person.greet(); - - // outputs: 'Our person has greeted' - ``` - - You can also chain multiple event subscriptions: - - ```javascript - person.on('greet', function() { - console.log('Our person has greeted'); - }).one('greet', function() { - console.log('Offer one-time special'); - }).off('event', this, forgetThis); - ``` - - @class Evented - @namespace Ember - @extends Ember.Mixin - */ -Ember.Evented = Ember.Mixin.create({ - - /** - Subscribes to a named event with given function. - - ```javascript - person.on('didLoad', function() { - // fired once the person has loaded - }); - ``` - - An optional target can be passed in as the 2nd argument that will - be set as the "this" for the callback. This is a good way to give your - function access to the object triggering the event. When the target - parameter is used the callback becomes the third argument. - - @method on - @param {String} name The name of the event - @param {Object} [target] The "this" binding for the callback - @param {Function} method The callback to execute - @return this - */ - on: function(name, target, method) { - Ember.addListener(this, name, target, method); - return this; - }, - - /** - Subscribes a function to a named event and then cancels the subscription - after the first time the event is triggered. It is good to use ``one`` when - you only care about the first time an event has taken place. - - This function takes an optional 2nd argument that will become the "this" - value for the callback. If this argument is passed then the 3rd argument - becomes the function. - - @method one - @param {String} name The name of the event - @param {Object} [target] The "this" binding for the callback - @param {Function} method The callback to execute - @return this - */ - one: function(name, target, method) { - if (!method) { - method = target; - target = null; - } - - Ember.addListener(this, name, target, method, true); - return this; - }, - - /** - Triggers a named event for the object. Any additional arguments - will be passed as parameters to the functions that are subscribed to the - event. - - ```javascript - person.on('didEat', function(food) { - console.log('person ate some ' + food); - }); - - person.trigger('didEat', 'broccoli'); - - // outputs: person ate some broccoli - ``` - @method trigger - @param {String} name The name of the event - @param {Object...} args Optional arguments to pass on - */ - trigger: function(name) { - var args = [], i, l; - for (i = 1, l = arguments.length; i < l; i++) { - args.push(arguments[i]); - } - 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 give name, target, and method. - - @method off - @param {String} name The name of the event - @param {Object} target The target of the subscription - @param {Function} method The function of the subscription - @return this - */ - off: function(name, target, method) { - Ember.removeListener(this, name, target, method); - return this; - }, - - /** - Checks to see if object has any subscriptions for named event. - - @method has - @param {String} name The name of the event - @return {Boolean} does the object have a subscription for event - */ - has: function(name) { - return Ember.hasListeners(this, name); - } -}); - -})(); - - - -(function() { -var RSVP = requireModule("rsvp"); - -RSVP.configure('async', function(callback, binding) { - Ember.run.schedule('actions', binding, callback); -}); - -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get; - -/** - @class Deferred - @namespace Ember - @extends Ember.Mixin - */ -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 - */ - then: function(resolve, reject) { - var deferred, promise, entity; - - entity = this; - deferred = get(this, '_deferred'); - promise = deferred.promise; - - return promise.then(function(fulfillment) { - if (fulfillment === promise) { - return resolve(entity); - } else { - return resolve(fulfillment); - } - }, function(reason) { - return reject(reason); - }); - }, - - /** - Resolve a Deferred object and call any `doneCallbacks` with the given args. - - @method resolve - */ - resolve: function(value) { - var deferred, promise; - - deferred = get(this, '_deferred'); - promise = deferred.promise; - - if (value === this){ - deferred.resolve(promise); - } else { - deferred.resolve(value); - } - }, - - /** - Reject a Deferred object and call any `failCallbacks` with the given args. - - @method reject - */ - reject: function(value) { - get(this, '_deferred').reject(value); - }, - - _deferred: Ember.computed(function() { - return RSVP.defer(); - }) -}); - - -})(); - - - -(function() { - -})(); - - - -(function() { -Ember.Container = requireModule('container'); -Ember.Container.set = Ember.set; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime + @module ember + @submodule ember-runtime */ -// NOTE: this object should never be included directly. Instead use Ember. -// Ember.Object. We only define this separately so that Ember.Set can depend on it +// NOTE: this object should never be included directly. Instead use `Ember.Object`. +// We only define this separately so that `Ember.Set` can depend on it. var set = Ember.set, get = Ember.get, @@ -10891,6 +12209,7 @@ var set = Ember.set, get = Ember.get, meta = Ember.meta, rewatch = Ember.rewatch, finishChains = Ember.finishChains, + sendEvent = Ember.sendEvent, destroy = Ember.destroy, schedule = Ember.run.schedule, Mixin = Ember.Mixin, @@ -10921,7 +12240,7 @@ function makeCtor() { } o_defineProperty(this, GUID_KEY, undefinedDescriptor); o_defineProperty(this, '_super', undefinedDescriptor); - var m = meta(this); + var m = meta(this), proto = m.proto; m.proto = this; if (initMixins) { // capture locally so we can clear the closed over variable @@ -10941,7 +12260,16 @@ function makeCtor() { 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 (typeof properties !== 'object' && properties !== undefined) { + throw new Ember.Error("Ember.Object.create only accepts objects."); + } + + if (!properties) { 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], @@ -10961,6 +12289,9 @@ function makeCtor() { 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]; @@ -10991,9 +12322,10 @@ function makeCtor() { } } finishPartial(this, m); - delete m.proto; - finishChains(this); this.init.apply(this, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); }; Class.toString = Mixin.prototype.toString; @@ -11024,6 +12356,10 @@ function makeCtor() { } +/** + @class CoreObject + @namespace Ember +*/ var CoreObject = makeCtor(); CoreObject.toString = function() { return "Ember.CoreObject"; }; @@ -11033,8 +12369,6 @@ CoreObject.PrototypeMixin = Mixin.create({ return this; }, - isInstance: true, - /** An overridable method called when objects are instantiated. By default, does nothing unless it is overridden during class definition. @@ -11044,7 +12378,6 @@ CoreObject.PrototypeMixin = Mixin.create({ ```javascript App.Person = Ember.Object.extend({ init: function() { - this._super(); alert('Name is ' + this.get('name')); } }); @@ -11123,7 +12456,10 @@ CoreObject.PrototypeMixin = Mixin.create({ 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. + 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 @@ -11161,36 +12497,38 @@ CoreObject.PrototypeMixin = Mixin.create({ raised. Note that destruction is scheduled for the end of the run loop and does not - happen immediately. + happen immediately. It will set an isDestroying flag immediately. @method destroy @return {Ember.Object} receiver */ destroy: function() { - if (this._didCallDestroy) { return; } - + if (this.isDestroying) { return; } this.isDestroying = true; - this._didCallDestroy = 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. + @private @method _scheduledDestroy */ _scheduledDestroy: function() { - if (this.willDestroy) { this.willDestroy(); } + if (this.isDestroyed) { return; } destroy(this); this.isDestroyed = true; - if (this.didDestroy) { this.didDestroy(); } }, bind: function(to, from) { @@ -11204,27 +12542,33 @@ CoreObject.PrototypeMixin = Mixin.create({ 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() //=> "" + ```javascript + 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>" + ```javascript + 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(); //=> "" + ```javascript + App.Teacher = App.Person.extend({ + toStringExtension: function() { + return this.get('fullName'); + } + }); + teacher = App.Teacher.create() + teacher.toString(); //=> "" + ``` @method toString @return {String} string representation @@ -11260,6 +12604,86 @@ var ClassMixin = Mixin.create({ 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); @@ -11275,31 +12699,160 @@ var ClassMixin = Mixin.create({ proto = Class.prototype = o_create(this.prototype); proto.constructor = Class; - generateGuid(proto, 'ember'); + 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); @@ -11387,10 +12940,6 @@ if (Ember.config.overrideClassMixin) { CoreObject.ClassMixin = ClassMixin; ClassMixin.apply(CoreObject); -/** - @class CoreObject - @namespace Ember -*/ Ember.CoreObject = CoreObject; })(); @@ -11582,6 +13131,8 @@ function classToString() { if (this[NAME_KEY]) { ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; } else { var str = superClassString(this); if (str) { @@ -11625,344 +13176,6 @@ Ember.Mixin.prototype.toString = classToString; -(function() { -Ember.Application = Ember.Namespace.extend(); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var OUT_OF_RANGE_EXCEPTION = "Index out of range"; -var EMPTY = []; - -var get = Ember.get, set = Ember.set; - -/** - An ArrayProxy wraps any other object that implements `Ember.Array` and/or - `Ember.MutableArray,` forwarding all requests. This makes it very useful for - a number of binding use cases or other cases where being able to swap - out the underlying array is useful. - - A simple example of usage: - - ```javascript - var pets = ['dog', 'cat', 'fish']; - var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); - - ap.get('firstObject'); // 'dog' - ap.set('content', ['amoeba', 'paramecium']); - ap.get('firstObject'); // 'amoeba' - ``` - - This class can also be useful as a layer to transform the contents of - an array, as they are accessed. This can be done by overriding - `objectAtContent`: - - ```javascript - var pets = ['dog', 'cat', 'fish']; - var ap = Ember.ArrayProxy.create({ - content: Ember.A(pets), - objectAtContent: function(idx) { - return this.get('content').objectAt(idx).toUpperCase(); - } - }); - - ap.get('firstObject'); // . 'DOG' - ``` - - @class ArrayProxy - @namespace Ember - @extends Ember.Object - @uses Ember.MutableArray -*/ -Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, -/** @scope Ember.ArrayProxy.prototype */ { - - /** - The content array. Must be an object that implements `Ember.Array` and/or - `Ember.MutableArray.` - - @property content - @type Ember.Array - */ - content: null, - - /** - The array that the proxy pretends to be. In the default `ArrayProxy` - implementation, this and `content` are the same. Subclasses of `ArrayProxy` - can override this property to provide things like sorting and filtering. - - @property arrangedContent - */ - arrangedContent: Ember.computed.alias('content'), - - /** - Should actually retrieve the object at the specified index from the - content. You can override this method in subclasses to transform the - content item to something new. - - This method will only be called if content is non-`null`. - - @method objectAtContent - @param {Number} idx The index to retrieve. - @return {Object} the value or undefined if none found - */ - objectAtContent: function(idx) { - return get(this, 'arrangedContent').objectAt(idx); - }, - - /** - Should actually replace the specified objects on the content array. - You can override this method in subclasses to transform the content item - into something new. - - This method will only be called if content is non-`null`. - - @method replaceContent - @param {Number} idx The starting index - @param {Number} amt The number of items to remove from the content. - @param {Array} objects Optional array of objects to insert or null if no - objects. - @return {void} - */ - replaceContent: function(idx, amt, objects) { - get(this, 'content').replace(idx, amt, objects); - }, - - /** - @private - - Invoked when the content property is about to change. Notifies observers that the - entire array content will change. - - @method _contentWillChange - */ - _contentWillChange: Ember.beforeObserver(function() { - this._teardownContent(); - }, 'content'), - - _teardownContent: function() { - var content = get(this, 'content'); - - if (content) { - content.removeArrayObserver(this, { - willChange: 'contentArrayWillChange', - didChange: 'contentArrayDidChange' - }); - } - }, - - contentArrayWillChange: Ember.K, - contentArrayDidChange: Ember.K, - - /** - @private - - Invoked when the content property changes. Notifies observers that the - entire array content has changed. - - @method _contentDidChange - */ - _contentDidChange: Ember.observer(function() { - var content = get(this, 'content'); - - Ember.assert("Can't set ArrayProxy's content to itself", content !== this); - - this._setupContent(); - }, 'content'), - - _setupContent: function() { - var content = get(this, 'content'); - - if (content) { - content.addArrayObserver(this, { - willChange: 'contentArrayWillChange', - didChange: 'contentArrayDidChange' - }); - } - }, - - _arrangedContentWillChange: Ember.beforeObserver(function() { - var arrangedContent = get(this, 'arrangedContent'), - len = arrangedContent ? get(arrangedContent, 'length') : 0; - - this.arrangedContentArrayWillChange(this, 0, len, undefined); - this.arrangedContentWillChange(this); - - this._teardownArrangedContent(arrangedContent); - }, 'arrangedContent'), - - _arrangedContentDidChange: Ember.observer(function() { - var arrangedContent = get(this, 'arrangedContent'), - len = arrangedContent ? get(arrangedContent, 'length') : 0; - - Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); - - this._setupArrangedContent(); - - this.arrangedContentDidChange(this); - this.arrangedContentArrayDidChange(this, 0, undefined, len); - }, 'arrangedContent'), - - _setupArrangedContent: function() { - var arrangedContent = get(this, 'arrangedContent'); - - if (arrangedContent) { - arrangedContent.addArrayObserver(this, { - willChange: 'arrangedContentArrayWillChange', - didChange: 'arrangedContentArrayDidChange' - }); - } - }, - - _teardownArrangedContent: function() { - var arrangedContent = get(this, 'arrangedContent'); - - if (arrangedContent) { - arrangedContent.removeArrayObserver(this, { - willChange: 'arrangedContentArrayWillChange', - didChange: 'arrangedContentArrayDidChange' - }); - } - }, - - arrangedContentWillChange: Ember.K, - arrangedContentDidChange: Ember.K, - - objectAt: function(idx) { - return get(this, 'content') && this.objectAtContent(idx); - }, - - length: Ember.computed(function() { - var arrangedContent = get(this, 'arrangedContent'); - return arrangedContent ? get(arrangedContent, 'length') : 0; - // No dependencies since Enumerable notifies length of change - }), - - _replace: function(idx, amt, objects) { - var content = get(this, 'content'); - Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); - if (content) this.replaceContent(idx, amt, objects); - return this; - }, - - replace: function() { - if (get(this, 'arrangedContent') === get(this, 'content')) { - this._replace.apply(this, arguments); - } else { - throw new Ember.Error("Using replace on an arranged ArrayProxy is not allowed."); - } - }, - - _insertAt: function(idx, object) { - if (idx > get(this, 'content.length')) throw new Error(OUT_OF_RANGE_EXCEPTION); - this._replace(idx, 0, [object]); - return this; - }, - - insertAt: function(idx, object) { - if (get(this, 'arrangedContent') === get(this, 'content')) { - return this._insertAt(idx, object); - } else { - throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); - } - }, - - removeAt: function(start, len) { - if ('number' === typeof start) { - var content = get(this, 'content'), - arrangedContent = get(this, 'arrangedContent'), - indices = [], i; - - if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); - } - - if (len === undefined) len = 1; - - // Get a list of indices in original content to remove - for (i=start; i1) args = a_slice.call(arguments, 1); + + this.forEach(function(x, idx) { + var method = x && x[methodName]; + if ('function' === typeof method) { + ret[idx] = args ? method.apply(x, args) : method.call(x); + } + }, this); + + return ret; + }, + + /** + Simply converts the enumerable into a genuine array. The order is not + guaranteed. Corresponds to the method implemented by Prototype. + + @method toArray + @return {Array} the enumerable as an array. + */ + toArray: function() { + var ret = Ember.A(); + this.forEach(function(o, idx) { ret[idx] = o; }); + return ret ; + }, + + /** + Returns a copy of the array with all null and undefined elements removed. + + ```javascript + var arr = ["a", null, "c", undefined]; + arr.compact(); // ["a", "c"] + ``` + + @method compact + @return {Array} the array without null and undefined elements. + */ + compact: function() { + return this.filter(function(value) { return value != null; }); + }, + + /** + Returns a new enumerable that excludes the passed value. The default + implementation returns an array regardless of the receiver type unless + the receiver does not contain the value. + + ```javascript + var arr = ["a", "b", "a", "c"]; + arr.without("a"); // ["b", "c"] + ``` + + @method without + @param {Object} value + @return {Ember.Enumerable} + */ + without: function(value) { + if (!this.contains(value)) return this; // nothing to do + var ret = Ember.A(); + this.forEach(function(k) { + if (k !== value) ret[ret.length] = k; + }) ; + return ret ; + }, + + /** + Returns a new enumerable that contains only unique values. The default + implementation returns an array regardless of the receiver type. + + ```javascript + var arr = ["a", "a", "b", "b"]; + arr.uniq(); // ["a", "b"] + ``` + + @method uniq + @return {Ember.Enumerable} + */ + uniq: function() { + var ret = Ember.A(); + this.forEach(function(k) { + if (a_indexOf(ret, k)<0) ret.push(k); + }); + return ret; + }, + + /** + This property will trigger anytime the enumerable's content changes. + You can observe this property to be notified of changes to the enumerables + content. + + For plain enumerables, this property is read only. `Ember.Array` overrides + this method. + + @property [] + @type Ember.Array + @return this + */ + '[]': Ember.computed(function(key, value) { + return this; + }), + + // .......................................................... + // ENUMERABLE OBSERVERS + // + + /** + Registers an enumerable observer. Must implement `Ember.EnumerableObserver` + mixin. + + @method addEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this + */ + addEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; + + var hasObservers = get(this, 'hasEnumerableObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.addListener(this, '@enumerable:before', target, willChange); + Ember.addListener(this, '@enumerable:change', target, didChange); + if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); + return this; + }, + + /** + Removes a registered enumerable observer. + + @method removeEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this + */ + removeEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; + + var hasObservers = get(this, 'hasEnumerableObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.removeListener(this, '@enumerable:before', target, willChange); + Ember.removeListener(this, '@enumerable:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); + return this; + }, + + /** + Becomes true whenever the array currently has observers watching changes + on the array. + + @property hasEnumerableObservers + @type Boolean + */ + hasEnumerableObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); + }), + + + /** + Invoke this method just before the contents of your enumerable will + change. You can either omit the parameters completely or pass the objects + to be removed or added if available or just a count. + + @method enumerableContentWillChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to be + added or the number of items to be added. + @chainable + */ + enumerableContentWillChange: function(removing, adding) { + + var removeCnt, addCnt, hasDelta; + + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; + + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding,'length'); + else addCnt = adding = -1; + + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + Ember.propertyWillChange(this, '[]'); + if (hasDelta) Ember.propertyWillChange(this, 'length'); + Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); + + return this; + }, + + /** + Invoke this method when the contents of your enumerable has changed. + This will notify any observers watching for content changes. If your are + implementing an ordered enumerable (such as an array), also pass the + start and end values where the content changed so that it can be used to + notify range observers. + + @method enumerableContentDidChange + @param {Number} [start] optional start offset for the content change. + For unordered enumerables, you should always pass -1. + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to + be added or the number of items to be added. + @chainable + */ + enumerableContentDidChange: function(removing, adding) { + var removeCnt, addCnt, hasDelta; + + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; + + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding, 'length'); + else addCnt = adding = -1; + + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); + if (hasDelta) Ember.propertyDidChange(this, 'length'); + Ember.propertyDidChange(this, '[]'); + + return this ; + }, + + /** + Converts the enumerable into an array and sorts by the keys + specified in the argument. + + You may provide multiple arguments to sort by multiple properties. + + @method sortBy + @param {String} property name(s) to sort on + @return {Array} The sorted array. + */ + sortBy: function() { + var sortKeys = arguments; + return this.toArray().sort(function(a, b){ + for(var i = 0; i < sortKeys.length; i++) { + var key = sortKeys[i], + propA = get(a, key), + propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + var compareValue = Ember.compare(propA, propB); + if (compareValue) { return compareValue; } + } + return 0; + }); + } +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +// .......................................................... +// HELPERS +// + +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; + +// .......................................................... +// ARRAY +// +/** + This module implements Observer-friendly Array-like behavior. This mixin is + picked up by the Array class as well as other controllers, etc. that want to + appear to be arrays. + + Unlike `Ember.Enumerable,` this mixin defines methods specifically for + collections that provide index-ordered access to their contents. When you + are designing code that needs to accept any kind of Array-like object, you + should use these methods instead of Array primitives because these will + properly notify observers of changes to the array. + + Although these methods are efficient, they do add a layer of indirection to + your application so it is a good idea to use them only when you need the + flexibility of using both true JavaScript arrays and "virtual" arrays such + as controllers and collections. + + You can use the methods defined in this module to access and modify array + contents in a KVO-friendly way. You can also be notified whenever the + membership of an array changes by changing the syntax of the property to + `.observes('*myProperty.[]')`. + + To support `Ember.Array` in your own class, you must override two + primitives to use it: `replace()` and `objectAt()`. + + Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` + mixin. All `Ember.Array`-like objects are also enumerable. + + @class Array + @namespace Ember + @uses Ember.Enumerable + @since Ember 0.9.0 +*/ +Ember.Array = Ember.Mixin.create(Ember.Enumerable, { + + /** + Your array must support the `length` property. Your replace methods should + set this property whenever it changes. + + @property {Number} length + */ + length: Ember.required(), + + /** + Returns the object at the given `index`. If the given `index` is negative + or is greater or equal than the array length, returns `undefined`. + + This is one of the primitives you must implement to support `Ember.Array`. + If your object supports retrieving the value of an array item using `get()` + (i.e. `myArray.get(0)`), then you do not need to implement this method + yourself. + + ```javascript + var arr = ['a', 'b', 'c', 'd']; + arr.objectAt(0); // "a" + arr.objectAt(3); // "d" + arr.objectAt(-1); // undefined + arr.objectAt(4); // undefined + arr.objectAt(5); // undefined + ``` + + @method objectAt + @param {Number} idx The index of the item to return. + @return {*} item at index or undefined + */ + objectAt: function(idx) { + if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; + return get(this, idx); + }, + + /** + This returns the objects at the specified indexes, using `objectAt`. + + ```javascript + var arr = ['a', 'b', 'c', 'd']; + arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] + arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] + ``` + + @method objectsAt + @param {Array} indexes An array of indexes of items to return. + @return {Array} + */ + objectsAt: function(indexes) { + var self = this; + return map(indexes, function(idx) { return self.objectAt(idx); }); + }, + + // overrides Ember.Enumerable version + nextObject: function(idx) { + return this.objectAt(idx); + }, + + /** + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property it a new + array, it will replace the current content. + + This property overrides the default property defined in `Ember.Enumerable`. + + @property [] + @return this + */ + '[]': Ember.computed(function(key, value) { + if (value !== undefined) this.replace(0, get(this, 'length'), value) ; + return this ; + }), + + firstObject: Ember.computed(function() { + return this.objectAt(0); + }), + + lastObject: Ember.computed(function() { + return this.objectAt(get(this, 'length')-1); + }), + + // optimized version from Enumerable + contains: function(obj) { + return this.indexOf(obj) >= 0; + }, + + // Add any extra methods to Ember.Array that are native to the built-in Array. + /** + Returns a new array that is a slice of the receiver. This implementation + uses the observable array methods to retrieve the objects for the new + slice. + + ```javascript + var arr = ['red', 'green', 'blue']; + arr.slice(0); // ['red', 'green', 'blue'] + arr.slice(0, 2); // ['red', 'green'] + arr.slice(1, 100); // ['green', 'blue'] + ``` + + @method slice + @param {Integer} beginIndex (Optional) index to begin slicing from. + @param {Integer} endIndex (Optional) index to end the slice at. + @return {Array} New array with specified slice + */ + slice: function(beginIndex, endIndex) { + var ret = Ember.A(); + var length = get(this, 'length') ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; + + if (beginIndex < 0) beginIndex = length + beginIndex; + if (endIndex < 0) endIndex = length + endIndex; + + while(beginIndex < endIndex) { + ret[ret.length] = this.objectAt(beginIndex++) ; + } + return ret ; + }, + + /** + Returns the index of the given object's first occurrence. + If no `startAt` argument is given, the starting location to + search is 0. If it's negative, will count backward from + the end of the array. Returns -1 if no match is found. + + ```javascript + var arr = ["a", "b", "c", "d", "a"]; + arr.indexOf("a"); // 0 + arr.indexOf("z"); // -1 + arr.indexOf("a", 2); // 4 + arr.indexOf("a", -1); // 4 + arr.indexOf("b", 3); // -1 + arr.indexOf("a", 100); // -1 + ``` + + @method indexOf + @param {Object} object the item to search for + @param {Number} startAt optional starting location to search, default 0 + @return {Number} index or -1 if not found + */ + indexOf: function(object, startAt) { + var idx, len = get(this, 'length'); + + if (startAt === undefined) startAt = 0; + if (startAt < 0) startAt += len; + + for(idx=startAt;idx= len) startAt = len-1; + if (startAt < 0) startAt += len; + + for(idx=startAt;idx>=0;idx--) { + if (this.objectAt(idx) === object) return idx ; + } + return -1; + }, + + // .......................................................... + // ARRAY OBSERVERS + // + + /** + Adds an array observer to the receiving array. The array observer object + normally must implement two methods: + + * `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be + called just before the array is modified. + * `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be + called just after the array is modified. + + Both callbacks will be passed the observed object, starting index of the + change as well a a count of the items to be removed and added. You can use + these callbacks to optionally inspect the array during the change, clear + caches, or do any other bookkeeping necessary. + + In addition to passing a target, you can also include an options hash + which you can use to override the method names that will be invoked on the + target. + + @method addArrayObserver + @param {Object} target The observer object. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver + */ + addArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.addListener(this, '@array:before', target, willChange); + Ember.addListener(this, '@array:change', target, didChange); + if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); + return this; + }, + + /** + Removes an array observer from the object if the observer is current + registered. Calling this method multiple times with the same object will + have no effect. + + @method removeArrayObserver + @param {Object} target The object observing the array. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver + */ + removeArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.removeListener(this, '@array:before', target, willChange); + Ember.removeListener(this, '@array:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); + return this; + }, + + /** + Becomes true whenever the array currently has observers watching changes + on the array. + + @property Boolean + */ + hasArrayObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); + }), + + /** + If you are implementing an object that supports `Ember.Array`, call this + method just before the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. + + @method arrayContentWillChange + @param {Number} startIdx The starting index in the array that will change. + @param {Number} removeAmt The number of items that will be removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that will be added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver + */ + arrayContentWillChange: function(startIdx, removeAmt, addAmt) { + + // if no args are passed assume everything changes + if (startIdx===undefined) { + startIdx = 0; + removeAmt = addAmt = -1; + } else { + if (removeAmt === undefined) removeAmt=-1; + if (addAmt === undefined) addAmt=-1; + } + + // Make sure the @each proxy is set up if anyone is observing @each + if (Ember.isWatching(this, '@each')) { get(this, '@each'); } + + Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); + + var removing, lim; + if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { + removing = []; + lim = startIdx+removeAmt; + for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { + adding = []; + lim = startIdx+addAmt; + for(var idx=startIdx;idx 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 partiallyRecomputeFor(obj, dependentKey) { + if (arrayBracketPattern.test(dependentKey)) { + return false; + } + + var value = get(obj, dependentKey); + return Ember.Array.detect(value); +} + +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) { + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + 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) { + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + 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; } } - if (mixin) this._initMixins([mixin]); + }); + }; + ``` + + 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') + }) + ``` + + Dependent keys whose values are not arrays are treated as regular + dependencies: when they change, the computed property is completely + recalculated. It is sometimes useful to have dependent arrays with similar + semantics. Dependent keys which end in `.[]` do not use "one at a time" + semantics. When an item is added or removed from such a dependency, the + computed property is completely recomputed. + + Example + + ```javascript + Ember.Object.extend({ + // When `string` is changed, `computed` is completely recomputed. + string: 'a string', + + // When an item is added to `array`, `addedItem` is called. + array: [], + + // When an item is added to `anotherArray`, `computed` is completely + // recomputed. + anotherArray: [], + + computed: Ember.reduceComputed('string', 'array', 'anotherArray.[]', { + addedItem: addedItemCallback, + removedItem: removedItemCallback + }) + }); + ``` + + @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; +}; + +// This is a stopgap to keep the reference counts correct with lazy CPs. +ArrayComputedProperty.prototype.didChange = function (obj, keyName) { + return; +}; + +/** + 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. + + ```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; + } } - return this._super.apply(this, arguments); + }); +}; + +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + ```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. + `item` is the current item in the iteration. + + ```javascript + function(item); + ``` + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hamster = App.Hamster.create({ + chores: ['clean', 'write more unit tests'] + }); + hamster.get('excitingChores'); // ['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. + + ```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. + `item` is the current item in the iteration. + + ```javascript + function(item); + ``` + + ```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 + + ```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() { +Ember.RSVP = requireModule('rsvp'); + +Ember.RSVP.onerrorDefault = function(error) { + if (error instanceof Error) { + if (Ember.testing) { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.exception(error); + } else { + throw error; + } + } else { + Ember.Logger.error(error.stack); + Ember.assert(error, false); + } + } +}; + +Ember.RSVP.on('error', Ember.RSVP.onerrorDefault); + +})(); + + + +(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); + // ComputedProperty.prototype.property expands properties; no need for us to + // do so here. + 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 b` + + Default implementation raises an exception. + + @method compare + @param a {Object} the first object to compare + @param b {Object} the second object to compare + @return {Integer} the result of the comparison + */ + compare: Ember.required(Function) + +}); + + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + + + +var get = Ember.get, set = Ember.set; + +/** + Implements some standard methods for copying an object. Add this mixin to + any object you create that can create a copy of itself. This mixin is + added automatically to the built-in array. + + You should generally implement the `copy()` method to return a copy of the + receiver. + + Note that `frozenCopy()` will only work if you also implement + `Ember.Freezable`. + + @class Copyable + @namespace Ember + @since Ember 0.9 +*/ +Ember.Copyable = Ember.Mixin.create({ + + /** + Override to return a copy of the receiver. Default implementation raises + an exception. + + @method copy + @param {Boolean} deep if `true`, a deep copy of the object should be made + @return {Object} copy of receiver + */ + copy: Ember.required(Function), + + /** + If the object implements `Ember.Freezable`, then this will return a new + copy if the object is not frozen and the receiver if the object is frozen. + + Raises an exception if you try to call this method on a object that does + not support freezing. + + You should use this method whenever you want a copy of a freezable object + since a freezable object can simply return itself without actually + consuming more memory. + + @method frozenCopy + @return {Object} copy of receiver or receiver + */ + frozenCopy: function() { + if (Ember.Freezable && Ember.Freezable.detect(this)) { + return get(this, 'isFrozen') ? this : this.copy().freeze(); + } else { + throw new Ember.Error(Ember.String.fmt("%@ does not support freezing", [this])); + } + } +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + + +var get = Ember.get, set = Ember.set; + +/** + The `Ember.Freezable` mixin implements some basic methods for marking an + object as frozen. Once an object is frozen it should be read only. No changes + may be made the internal state of the object. + + ## Enforcement + + To fully support freezing in your subclass, you must include this mixin and + override any method that might alter any property on the object to instead + raise an exception. You can check the state of an object by checking the + `isFrozen` property. + + Although future versions of JavaScript may support language-level freezing + object objects, that is not the case today. Even if an object is freezable, + it is still technically possible to modify the object, even though it could + break other parts of your application that do not expect a frozen object to + change. It is, therefore, very important that you always respect the + `isFrozen` property on all freezable objects. + + ## Example Usage + + The example below shows a simple object that implement the `Ember.Freezable` + protocol. + + ```javascript + Contact = Ember.Object.extend(Ember.Freezable, { + firstName: null, + lastName: null, + + // swaps the names + swapNames: function() { + if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; + var tmp = this.get('firstName'); + this.set('firstName', this.get('lastName')); + this.set('lastName', tmp); + return this; + } + + }); + + c = Contact.create({ firstName: "John", lastName: "Doe" }); + c.swapNames(); // returns c + c.freeze(); + c.swapNames(); // EXCEPTION + ``` + + ## Copying + + Usually the `Ember.Freezable` protocol is implemented in cooperation with the + `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will + return a frozen object, if the object implements this method as well. + + @class Freezable + @namespace Ember + @since Ember 0.9 +*/ +Ember.Freezable = Ember.Mixin.create({ + + /** + Set to `true` when the object is frozen. Use this property to detect + whether your object is frozen or not. + + @property isFrozen + @type Boolean + */ + isFrozen: false, + + /** + Freezes the object. Once this method has been called the object should + no longer allow any properties to be edited. + + @method freeze + @return {Object} receiver + */ + freeze: function() { + if (get(this, 'isFrozen')) return this; + set(this, 'isFrozen', true); + return this; + } + +}); + +Ember.FROZEN_ERROR = "Frozen object cannot be modified."; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var forEach = Ember.EnumerableUtils.forEach; + +/** + This mixin defines the API for modifying generic enumerables. These methods + can be applied to an object regardless of whether it is ordered or + unordered. + + Note that an Enumerable can change even if it does not implement this mixin. + For example, a MappedEnumerable cannot be directly modified but if its + underlying enumerable changes, it will change also. + + ## Adding Objects + + To add an object to an enumerable, use the `addObject()` method. This + method will only add the object to the enumerable if the object is not + already present and is of a type supported by the enumerable. + + ```javascript + set.addObject(contact); + ``` + + ## Removing Objects + + To remove an object from an enumerable, use the `removeObject()` method. This + will only remove the object if it is present in the enumerable, otherwise + this method has no effect. + + ```javascript + set.removeObject(contact); + ``` + + ## Implementing In Your Own Code + + If you are implementing an object and want to support this API, just include + this mixin in your class and implement the required methods. In your unit + tests, be sure to apply the Ember.MutableEnumerableTests to your object. + + @class MutableEnumerable + @namespace Ember + @uses Ember.Enumerable +*/ +Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { + + /** + __Required.__ You must implement this method to apply this mixin. + + Attempts to add the passed object to the receiver if the object is not + already present in the collection. If the object is present, this method + has no effect. + + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. + + @method addObject + @param {Object} object The object to add to the enumerable. + @return {Object} the passed object + */ + addObject: Ember.required(Function), + + /** + Adds each object in the passed enumerable to the receiver. + + @method addObjects + @param {Ember.Enumerable} objects the objects to add. + @return {Object} receiver + */ + addObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.addObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; + }, + + /** + __Required.__ You must implement this method to apply this mixin. + + Attempts to remove the passed object from the receiver collection if the + object is present in the collection. If the object is not present, + this method has no effect. + + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. + + @method removeObject + @param {Object} object The object to remove from the enumerable. + @return {Object} the passed object + */ + removeObject: Ember.required(Function), + + + /** + Removes each object in the passed enumerable from the receiver. + + @method removeObjects + @param {Ember.Enumerable} objects the objects to remove + @return {Object} receiver + */ + removeObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.removeObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; + } + +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ +// .......................................................... +// CONSTANTS +// + +var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; +var EMPTY = []; + +// .......................................................... +// HELPERS +// + +var get = Ember.get, set = Ember.set; + +/** + This mixin defines the API for modifying array-like objects. These methods + can be applied only to a collection that keeps its items in an ordered set. + + Note that an Array can change even if it does not implement this mixin. + For example, one might implement a SparseArray that cannot be directly + modified, but if its underlying enumerable changes, it will change also. + + @class MutableArray + @namespace Ember + @uses Ember.Array + @uses Ember.MutableEnumerable +*/ +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, { + + /** + __Required.__ You must implement this method to apply this mixin. + + This is one of the primitives you must implement to support `Ember.Array`. + You should replace amt objects started at idx with the objects in the + passed array. You should also call `this.enumerableContentDidChange()` + + @method replace + @param {Number} idx Starting index in the array to replace. If + idx >= length, then append to the end of the array. + @param {Number} amt Number of elements that should be removed from + the array, starting at *idx*. + @param {Array} objects An array of zero or more objects that should be + inserted into the array at *idx* + */ + replace: Ember.required(), + + /** + Remove all elements from self. This is useful if you + want to reuse an existing array without having to recreate it. + + ```javascript + var colors = ["red", "green", "blue"]; + color.length(); // 3 + colors.clear(); // [] + colors.length(); // 0 + ``` + + @method clear + @return {Ember.Array} An empty Array. + */ + clear: function () { + var len = get(this, 'length'); + if (len === 0) return this; + this.replace(0, len, EMPTY); + return this; + }, + + /** + This will use the primitive `replace()` method to insert an object at the + specified index. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] + colors.insertAt(5, "orange"); // Error: Index out of range + ``` + + @method insertAt + @param {Number} idx index of insert the object at. + @param {Object} object object to insert + @return this + */ + insertAt: function(idx, object) { + if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; + this.replace(idx, 0, [object]) ; + return this ; + }, + + /** + Remove an object at the specified index using the `replace()` primitive + method. You can pass either a single index, or a start and a length. + + If you pass a start and length that is beyond the + length this method will throw an `OUT_OF_RANGE_EXCEPTION`. + + ```javascript + var colors = ["red", "green", "blue", "yellow", "orange"]; + colors.removeAt(0); // ["green", "blue", "yellow", "orange"] + colors.removeAt(2, 2); // ["green", "blue"] + colors.removeAt(4, 2); // Error: Index out of range + ``` + + @method removeAt + @param {Number} start index, start of range + @param {Number} len length of passing range + @return {Object} receiver + */ + removeAt: function(start, len) { + if ('number' === typeof start) { + + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); + } + + // fast case + if (len === undefined) len = 1; + this.replace(start, len, EMPTY); + } + + return this ; + }, + + /** + Push the object onto the end of the array. Works just like `push()` but it + is KVO-compliant. + + ```javascript + var colors = ["red", "green"]; + colors.pushObject("black"); // ["red", "green", "black"] + colors.pushObject(["yellow"]); // ["red", "green", ["yellow"]] + ``` + + @method pushObject + @param {*} obj object to push + @return The same obj passed as param + */ + pushObject: function(obj) { + this.insertAt(get(this, 'length'), obj) ; + return obj; + }, + + /** + Add the objects in the passed numerable to the end of the array. Defers + notifying observers of the change until all objects are added. + + ```javascript + var colors = ["red"]; + colors.pushObjects(["yellow", "orange"]); // ["red", "yellow", "orange"] + ``` + + @method pushObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + pushObjects: function(objects) { + if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { + throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); + } + this.replace(get(this, 'length'), 0, objects); + return this; + }, + + /** + Pop object from array or nil if none are left. Works just like `pop()` but + it is KVO-compliant. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.popObject(); // "blue" + console.log(colors); // ["red", "green"] + ``` + + @method popObject + @return object + */ + popObject: function() { + var len = get(this, 'length') ; + if (len === 0) return null ; + + var ret = this.objectAt(len-1) ; + this.removeAt(len-1, 1) ; + return ret ; + }, + + /** + Shift an object from start of array or nil if none are left. Works just + like `shift()` but it is KVO-compliant. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.shiftObject(); // "red" + console.log(colors); // ["green", "blue"] + ``` + + @method shiftObject + @return object + */ + shiftObject: function() { + if (get(this, 'length') === 0) return null ; + var ret = this.objectAt(0) ; + this.removeAt(0) ; + return ret ; + }, + + /** + Unshift an object to start of array. Works just like `unshift()` but it is + KVO-compliant. + + ```javascript + var colors = ["red"]; + colors.unshiftObject("yellow"); // ["yellow", "red"] + colors.unshiftObject(["black"]); // [["black"], "yellow", "red"] + ``` + + @method unshiftObject + @param {*} obj object to unshift + @return The same obj passed as param + */ + unshiftObject: function(obj) { + this.insertAt(0, obj) ; + return obj ; + }, + + /** + Adds the named objects to the beginning of the array. Defers notifying + observers until all objects have been added. + + ```javascript + var colors = ["red"]; + colors.unshiftObjects(["black", "white"]); // ["black", "white", "red"] + colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function + ``` + + @method unshiftObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + unshiftObjects: function(objects) { + this.replace(0, 0, objects); + return this; + }, + + /** + Reverse objects in the array. Works just like `reverse()` but it is + KVO-compliant. + + @method reverseObjects + @return {Ember.Array} receiver + */ + reverseObjects: function() { + var len = get(this, 'length'); + if (len === 0) return this; + var objects = this.toArray().reverse(); + this.replace(0, len, objects); + return this; + }, + + /** + Replace all the the receiver's content with content of the argument. + If argument is an empty array receiver will be cleared. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.setObjects(["black", "white"]); // ["black", "white"] + colors.setObjects([]); // [] + ``` + + @method setObjects + @param {Ember.Array} objects array whose content will be used for replacing + the content of the receiver + @return {Ember.Array} receiver with the new content + */ + setObjects: function(objects) { + if (objects.length === 0) return this.clear(); + + var len = get(this, 'length'); + this.replace(0, len, objects); + return this; + }, + + // .......................................................... + // IMPLEMENT Ember.MutableEnumerable + // + + removeObject: function(obj) { + var loc = get(this, 'length') || 0; + while(--loc >= 0) { + var curObject = this.objectAt(loc) ; + if (curObject === obj) this.removeAt(loc) ; + } + return this ; + }, + + addObject: function(obj) { + if (!this.contains(obj)) this.pushObject(obj); + return this ; + } + +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, set = Ember.set; + +/** +`Ember.TargetActionSupport` is a mixin that can be included in a class +to add a `triggerAction` method with semantics similar to the Handlebars +`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is +usually the best choice. This mixin is most often useful when you are +doing more complex event handling in View objects. + +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. + +@class TargetActionSupport +@namespace Ember +@extends Ember.Mixin +*/ +Ember.TargetActionSupport = Ember.Mixin.create({ + target: null, + action: null, + actionContext: null, + + targetObject: Ember.computed(function() { + var target = get(this, 'target'); + + if (Ember.typeOf(target) === "string") { + var value = get(this, target); + if (value === undefined) { value = get(Ember.lookup, target); } + return value; + } else { + return target; + } + }).property('target'), + + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); + + if (Ember.typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), + + /** + Send an "action" with an "actionContext" to a "target". The action, actionContext + and target will be retrieved from properties of the object. For example: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + action: 'save', + actionContext: Ember.computed.alias('context'), + click: function() { + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + click: function() { + this.triggerAction({ + action: 'save', + target: this.get('controller'), + actionContext: this.get('context'), + }); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `actionContext` defaults to the object you mixing `TargetActionSupport` into. + But `target` and `action` must be specified either as properties or with the argument + to `triggerAction`, or a combination: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + click: function() { + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with a reference to `this`, + // to the current controller + } + }); + ``` + + @method triggerAction + @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) + @return {Boolean} true if the action was sent successfully and did not return false + */ + triggerAction: function(opts) { + opts = opts || {}; + 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, args(actionContext, action)); + } else { + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, args(actionContext)); + } + + if (ret !== false) ret = true; + + return ret; + } else { + return false; + } + } +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +/** + This mixin allows for Ember objects to subscribe to and emit events. + + ```javascript + App.Person = Ember.Object.extend(Ember.Evented, { + greet: function() { + // ... + this.trigger('greet'); + } + }); + + var person = App.Person.create(); + + person.on('greet', function() { + console.log('Our person has greeted'); + }); + + person.greet(); + + // outputs: 'Our person has greeted' + ``` + + You can also chain multiple event subscriptions: + + ```javascript + person.on('greet', function() { + console.log('Our person has greeted'); + }).one('greet', function() { + console.log('Offer one-time special'); + }).off('event', this, forgetThis); + ``` + + @class Evented + @namespace Ember + */ +Ember.Evented = Ember.Mixin.create({ + + /** + Subscribes to a named event with given function. + + ```javascript + person.on('didLoad', function() { + // fired once the person has loaded + }); + ``` + + An optional target can be passed in as the 2nd argument that will + be set as the "this" for the callback. This is a good way to give your + function access to the object triggering the event. When the target + parameter is used the callback becomes the third argument. + + @method on + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + on: function(name, target, method) { + Ember.addListener(this, name, target, method); + return this; + }, + + /** + Subscribes a function to a named event and then cancels the subscription + after the first time the event is triggered. It is good to use ``one`` when + you only care about the first time an event has taken place. + + This function takes an optional 2nd argument that will become the "this" + value for the callback. If this argument is passed then the 3rd argument + becomes the function. + + @method one + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + one: function(name, target, method) { + if (!method) { + method = target; + target = null; + } + + Ember.addListener(this, name, target, method, true); + return this; + }, + + /** + Triggers a named event for the object. Any additional arguments + will be passed as parameters to the functions that are subscribed to the + event. + + ```javascript + person.on('didEat', function(food) { + console.log('person ate some ' + food); + }); + + person.trigger('didEat', 'broccoli'); + + // outputs: person ate some broccoli + ``` + @method trigger + @param {String} name The name of the event + @param {Object...} args Optional arguments to pass on + */ + trigger: function(name) { + var args = [], i, l; + for (i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + Ember.sendEvent(this, name, args); + }, + + /** + Cancels subscription for given name, target, and method. + + @method off + @param {String} name The name of the event + @param {Object} target The target of the subscription + @param {Function} method The function of the subscription + @return this + */ + off: function(name, target, method) { + Ember.removeListener(this, name, target, method); + return this; + }, + + /** + Checks to see if object has any subscriptions for named event. + + @method has + @param {String} name The name of the event + @return {Boolean} does the object have a subscription for event + */ + has: function(name) { + return Ember.hasListeners(this, name); + } +}); + +})(); + + + +(function() { +var RSVP = requireModule("rsvp"); + +RSVP.configure('async', function(callback, promise) { + Ember.run.schedule('actions', promise, callback, promise); +}); + +RSVP.Promise.prototype.fail = function(callback, label){ + Ember.deprecate('RSVP.Promise.fail has been renamed as RSVP.Promise.catch'); + return this['catch'](callback, label); +}; + +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get; + +/** + @class Deferred + @namespace Ember + */ +Ember.DeferredMixin = Ember.Mixin.create({ + /** + Add handlers to be called when the Deferred object is resolved or rejected. + + @method then + @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, label) { + var deferred, promise, entity; + + entity = this; + deferred = get(this, '_deferred'); + promise = deferred.promise; + + function fulfillmentHandler(fulfillment) { + if (fulfillment === promise) { + return resolve(entity); + } else { + return resolve(fulfillment); + } + } + + return promise.then(resolve && fulfillmentHandler, reject, label); + }, + + /** + Resolve a Deferred object and call any `doneCallbacks` with the given args. + + @method resolve + */ + resolve: function(value) { + var deferred, promise; + + deferred = get(this, '_deferred'); + promise = deferred.promise; + + if (value === this) { + deferred.resolve(promise); + } else { + deferred.resolve(value); + } + }, + + /** + Reject a Deferred object and call any `failCallbacks` with the given args. + + @method reject + */ + reject: function(value) { + get(this, '_deferred').reject(value); + }, + + _deferred: Ember.computed(function() { + return RSVP.defer('Ember: DeferredMixin - ' + this); + }) +}); + + +})(); + + + +(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'], + + /** + The collection of functions, keyed by name, available on this + `ActionHandler` as action targets. + + These functions will be invoked when a matching `{{action}}` is triggered + from within a template and the application's current route is this route. + + Actions can also be invoked from other parts of your application + via `ActionHandler#send`. + + The `actions` hash will inherit action handlers from + the `actions` hash defined on extended parent classes + or mixins rather than just replace the entire hash, e.g.: + + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + actions: { + displayBanner: function(msg) { + // ... + } + } + }); + + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + actions: { + playMusic: function() { + // ... + } + } + }); + + // `WelcomeRoute`, when active, will be able to respond + // to both actions, since the actions hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` + + Within a Controller, Route, View or Component's action handler, + the value of the `this` context is the Controller, Route, View or + Component object: + + ```js + App.SongRoute = Ember.Route.extend({ + actions: { + myAction: function() { + this.controllerFor("song"); + this.transitionTo("other.route"); + ... + } + } + }); + ``` + + It is also possible to call `this._super()` from within an + action handler if it overrides a handler defined on a parent + class or mixin: + + Take for example the following routes: + + ```js + App.DebugRoute = Ember.Mixin.create({ + actions: { + debugRouteInformation: function() { + console.debug("trololo"); + } + } + }); + + App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { + actions: { + debugRouteInformation: function() { + // also call the debugRouteInformation of mixed in App.DebugRoute + this._super(); + + // show additional annoyance + window.alert(...); + } + } + }); + ``` + + ## Bubbling + + By default, an action will stop bubbling once a handler defined + on the `actions` hash handles it. To continue bubbling the action, + you must return `true` from the handler: + + ```js + App.Router.map(function() { + this.resource("album", function() { + this.route("song"); + }); + }); + + App.AlbumRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + } + } + }); + + App.AlbumSongRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + // ... + + if (actionShouldAlsoBeTriggeredOnParentRoute) { + return true; + } + } + } + }); + ``` + + @property actions + @type Hash + @default null + */ + + /** + 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. + + @private + @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 + }, "Ember: PromiseProxy"); +} + +/** + 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({ + /** + If the proxied promise is rejected this will contain the reason + provided. + + @property reason + @default null + */ + reason: null, + + /** + Once the proxied promise has settled this will become `false`. + + @property isPending + @default true + */ + isPending: not('isSettled').readOnly(), + + /** + Once the proxied promise has settled this will become `true`. + + @property isSettled + @default false + */ + isSettled: or('isRejected', 'isFulfilled').readOnly(), + + /** + Will become `true` if the proxied promise is rejected. + + @property isRejected + @default false + */ + isRejected: false, + + /** + Will become `true` if the proxied promise is fulfilled. + + @property isFullfilled + @default false + */ + isFulfilled: false, + + /** + The promise whose fulfillment value is being proxied by this object. + + This property must be specified upon creation, and should not be + changed once created. + + Example: + + ```javascript + Ember.ObjectController.extend(Ember.PromiseProxyMixin).create({ + promise: + }); + ``` + + @property promise + */ + 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"); + } + }), + + /** + An alias to the proxied promise's `then`. + + See RSVP.Promise.then. + + @method then + @param {Function} callback + @return {RSVP.Promise} + */ + then: promiseAlias('then'), + + /** + An alias to the proxied promise's `catch`. + + See RSVP.Promise.catch. + + @method catch + @param {Function} callback + @return {RSVP.Promise} + */ + 'catch': promiseAlias('catch'), + + /** + An alias to the proxied promise's `finally`. + + See RSVP.Promise.finally. + + @method finally + @param {Function} callback + @return {RSVP.Promise} + */ + 'finally': promiseAlias('finally') + +}); + +function promiseAlias(name) { + return function () { + var promise = get(this, 'promise'); + return promise[name].apply(promise, arguments); + }; +} + +})(); + + + +(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 + @param {string} type The type of the operation. One of + `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` + @param {number} count The number of items in this operation. + @param {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 + @param {ArrayOperation} operation + @param {number} index The index of `operation` in the array of operations. + @param {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @param {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); + } +}; + +})(); + + + +(function() { +Ember.Container = requireModule('container'); +Ember.Container.set = Ember.set; + +})(); + + + +(function() { +Ember.Application = Ember.Namespace.extend(); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var OUT_OF_RANGE_EXCEPTION = "Index out of range"; +var EMPTY = []; + +var get = Ember.get, set = Ember.set; + +/** + An ArrayProxy wraps any other object that implements `Ember.Array` and/or + `Ember.MutableArray,` forwarding all requests. This makes it very useful for + a number of binding use cases or other cases where being able to swap + out the underlying array is useful. + + A simple example of usage: + + ```javascript + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); + + ap.get('firstObject'); // 'dog' + ap.set('content', ['amoeba', 'paramecium']); + ap.get('firstObject'); // 'amoeba' + ``` + + This class can also be useful as a layer to transform the contents of + an array, as they are accessed. This can be done by overriding + `objectAtContent`: + + ```javascript + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ + content: Ember.A(pets), + objectAtContent: function(idx) { + return this.get('content').objectAt(idx).toUpperCase(); + } + }); + + ap.get('firstObject'); // . 'DOG' + ``` + + @class ArrayProxy + @namespace Ember + @extends Ember.Object + @uses Ember.MutableArray +*/ +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, { + + /** + The content array. Must be an object that implements `Ember.Array` and/or + `Ember.MutableArray.` + + @property content + @type Ember.Array + */ + content: null, + + /** + The array that the proxy pretends to be. In the default `ArrayProxy` + implementation, this and `content` are the same. Subclasses of `ArrayProxy` + can override this property to provide things like sorting and filtering. + + @property arrangedContent + */ + arrangedContent: Ember.computed.alias('content'), + + /** + Should actually retrieve the object at the specified index from the + content. You can override this method in subclasses to transform the + content item to something new. + + This method will only be called if content is non-`null`. + + @method objectAtContent + @param {Number} idx The index to retrieve. + @return {Object} the value or undefined if none found + */ + objectAtContent: function(idx) { + return get(this, 'arrangedContent').objectAt(idx); + }, + + /** + Should actually replace the specified objects on the content array. + You can override this method in subclasses to transform the content item + into something new. + + This method will only be called if content is non-`null`. + + @method replaceContent + @param {Number} idx The starting index + @param {Number} amt The number of items to remove from the content. + @param {Array} objects Optional array of objects to insert or null if no + objects. + @return {void} + */ + replaceContent: function(idx, amt, objects) { + get(this, 'content').replace(idx, amt, objects); + }, + + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @private + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver('content', function() { + this._teardownContent(); + }), + + _teardownContent: function() { + var content = get(this, 'content'); + + if (content) { + content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + } + }, + + contentArrayWillChange: Ember.K, + contentArrayDidChange: Ember.K, + + /** + Invoked when the content property changes. Notifies observers that the + entire array content has changed. + + @private + @method _contentDidChange + */ + _contentDidChange: Ember.observer('content', function() { + var content = get(this, 'content'); + + Ember.assert("Can't set ArrayProxy's content to itself", content !== this); + + this._setupContent(); + }), + + _setupContent: function() { + var content = get(this, 'content'); + + if (content) { + content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + } + }, + + _arrangedContentWillChange: Ember.beforeObserver('arrangedContent', function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; + + this.arrangedContentArrayWillChange(this, 0, len, undefined); + this.arrangedContentWillChange(this); + + this._teardownArrangedContent(arrangedContent); + }), + + _arrangedContentDidChange: Ember.observer('arrangedContent', function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; + + Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); + + this._setupArrangedContent(); + + this.arrangedContentDidChange(this); + this.arrangedContentArrayDidChange(this, 0, undefined, len); + }), + + _setupArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); + + if (arrangedContent) { + arrangedContent.addArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } + }, + + _teardownArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); + + if (arrangedContent) { + arrangedContent.removeArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } + }, + + arrangedContentWillChange: Ember.K, + arrangedContentDidChange: Ember.K, + + objectAt: function(idx) { + return get(this, 'content') && this.objectAtContent(idx); + }, + + length: Ember.computed(function() { + var arrangedContent = get(this, 'arrangedContent'); + return arrangedContent ? get(arrangedContent, 'length') : 0; + // No dependencies since Enumerable notifies length of change + }), + + _replace: function(idx, amt, objects) { + var content = get(this, 'content'); + Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); + if (content) this.replaceContent(idx, amt, objects); + return this; + }, + + replace: function() { + if (get(this, 'arrangedContent') === get(this, 'content')) { + this._replace.apply(this, arguments); + } else { + throw new Ember.Error("Using replace on an arranged ArrayProxy is not allowed."); + } + }, + + _insertAt: function(idx, object) { + if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); + this._replace(idx, 0, [object]); + return this; + }, + + insertAt: function(idx, object) { + if (get(this, 'arrangedContent') === get(this, 'content')) { + return this._insertAt(idx, object); + } else { + throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); + } + }, + + removeAt: function(start, len) { + if ('number' === typeof start) { + var content = get(this, 'content'), + arrangedContent = get(this, 'arrangedContent'), + indices = [], i; + + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); + } + + if (len === undefined) len = 1; + + // Get a list of indices in original content to remove + for (i=start; i=idx) { var item = content.objectAt(loc); if (item) { + Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', Ember.typeOf(item) === 'instance' || Ember.typeOf(item) === 'object'); Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); @@ -12190,7 +19082,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; } } } @@ -12248,7 +19140,7 @@ Ember.EachProxy = Ember.Object.extend({ for(key in keys) { if (!keys.hasOwnProperty(key)) { continue; } - if (lim>0) removeObserverForContentKey(content, key, this, idx, lim); + if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } Ember.propertyWillChange(this, key); } @@ -12258,21 +19150,20 @@ Ember.EachProxy = Ember.Object.extend({ }, arrayDidChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, lim; + var keys = this._keys, lim; lim = addedCnt>0 ? idx+addedCnt : -1; - Ember.beginPropertyChanges(this); + Ember.changeProperties(function() { + for(var key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - for(key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } + if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } - if (lim>0) addObserverForContentKey(content, key, this, idx, lim); + Ember.propertyDidChange(this, key); + } - Ember.propertyDidChange(this, key); - } - - Ember.propertyDidChange(this._content, '@each'); - Ember.endPropertyChanges(this); + Ember.propertyDidChange(this._content, '@each'); + }, this); }, // .......................................................... @@ -12340,7 +19231,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 @@ -12362,7 +19253,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 @@ -12370,15 +19261,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 @@ -12421,7 +19311,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember copy: function(deep) { if (deep) { - return this.map(function(item){ return Ember.copy(item, true); }); + return this.map(function(item) { return Ember.copy(item, true); }); } return this.slice(); @@ -12447,7 +19337,6 @@ if (ignore.length>0) { @class NativeArray @namespace Ember - @extends Ember.Mixin @uses Ember.MutableArray @uses Ember.Observable @uses Ember.Copyable @@ -12456,20 +19345,49 @@ 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 @return {Ember.NativeArray} */ -Ember.A = function(arr){ +Ember.A = function(arr) { if (arr === undefined) { arr = []; } return Ember.Array.detect(arr) ? arr : Ember.NativeArray.apply(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 @@ -12564,8 +19482,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 @@ -12597,7 +19515,7 @@ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.is @since Ember 0.9 */ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, - /** @scope Ember.Set.prototype */ { + { // .......................................................... // IMPLEMENT ENUMERABLE APIS @@ -12627,7 +19545,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; } @@ -12638,7 +19556,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb Ember.propertyWillChange(this, 'firstObject'); Ember.propertyWillChange(this, 'lastObject'); - for (var i=0; i < len; i++){ + for (var i=0; i < len; i++) { guid = guidFor(this[i]); delete this[guid]; delete this[i]; @@ -12737,7 +19655,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; @@ -12854,7 +19772,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), @@ -12882,7 +19800,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), @@ -12972,18 +19890,30 @@ Ember.Deferred = Deferred; var forEach = Ember.ArrayPolyfills.forEach; /** -@module ember -@submodule ember-runtime + @module ember + @submodule ember-runtime */ var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; var loaded = {}; /** -@method onLoad -@for Ember -@param name {String} name of hook -@param callback {Function} callback to be called + 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 + @param callback {Function} callback to be called */ Ember.onLoad = function(name, callback) { var object; @@ -12997,10 +19927,13 @@ Ember.onLoad = function(name, callback) { }; /** -@method runLoadHooks -@for Ember -@param name {String} name of hook -@param object {Object} object to pass to callbacks + 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 + @param object {Object} object to pass to callbacks */ Ember.runLoadHooks = function(name, object) { loaded[name] = object; @@ -13035,40 +19968,19 @@ 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 - @extends Ember.Mixin + @uses Ember.ActionHandler */ -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`. @@ -13086,16 +19998,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; } }); @@ -13144,9 +20056,31 @@ 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 - @extends Ember.Mixin @uses Ember.MutableEnumerable */ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { @@ -13154,6 +20088,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, @@ -13165,16 +20102,39 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { */ sortAscending: true, + /** + The function used to compare two values. You can override this if you + want to do custom comparisons. Functions must be of the type expected by + Array#sort, i.e. + return 0 if the two parameters are equal, + return a negative value if the first parameter is smaller than the second or + return a positive value otherwise: + + ```javascript + function(x,y) { // These are assumed to be integers + if (x === y) + return 0; + return x < y ? -1 : 1; + } + ``` + + @property sortFunction + @type {Function} + @default Ember.compare + */ + sortFunction: Ember.compare, + orderBy: function(item1, item2) { var result = 0, sortProperties = get(this, 'sortProperties'), - sortAscending = get(this, 'sortAscending'); + sortAscending = get(this, 'sortAscending'), + sortFunction = get(this, 'sortFunction'); Ember.assert("you need to define `sortProperties`", !!sortProperties); forEach(sortProperties, function(propertyName) { if (result === 0) { - result = Ember.compare(get(item1, propertyName), get(item2, propertyName)); + result = sortFunction(get(item1, propertyName), get(item2, propertyName)); if ((result !== 0) && !sortAscending) { result = (-1) * result; } @@ -13201,6 +20161,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'), @@ -13223,7 +20190,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'); @@ -13236,18 +20203,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'); @@ -13518,28 +20485,36 @@ 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'), - subController = subControllers[idx]; + subController = subControllers[idx], + factory, fullName; - if (!subController) { - subController = container.lookup("controller:" + controllerClass, { singleton: false }); - subControllers[idx] = subController; + if (subController) { return subController; } + + fullName = "controller:" + controllerClass; + + if (!container.has(fullName)) { + throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); } - if (!subController) { - throw new Error('Could not resolve itemController: "' + controllerClass + '"'); - } + subController = container.lookupFactory(fullName).create({ + target: this, + parentController: get(this, 'parentController') || this, + content: object + }); - subController.set('target', this); - subController.set('parentController', get(this, 'parentController') || this); - subController.set('content', object); + subControllers[idx] = subController; return subController; }, @@ -13569,12 +20544,11 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, */ /** - `Ember.ObjectController` is part of Ember's Controller layer. A single shared - instance of each `Ember.ObjectController` subclass in your application's - namespace will be created at application initialization and be stored on your - application's `Ember.Router` instance. + `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 @@ -13611,8 +20585,12 @@ Ember Runtime @submodule ember-views */ -var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.8, 1.9 or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_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)); /** Alias for jQuery @@ -13652,13 +20630,13 @@ if (Ember.$) { @submodule ember-views */ -/*** BEGIN METAMORPH HELPERS ***/ +/* BEGIN METAMORPH HELPERS */ // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ -var needsShy = this.document && (function(){ +var needsShy = this.document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
    "; testEl.firstChild.innerHTML = ""; @@ -13725,7 +20703,7 @@ var setInnerHTMLWithoutFix = function(element, html) { } }; -/*** END METAMORPH HELPERS */ +/* END METAMORPH HELPERS */ var innerHTMLTags = {}; @@ -13814,14 +20792,71 @@ ClassSet.prototype = { } }; +var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z0-9\-]/; +var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z0-9\-]/g; + +function stripTagName(tagName) { + if (!tagName) { + return tagName; + } + + if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) { + return tagName; + } + + return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, ''); +} + +var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g; +var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/; + +function escapeAttribute(value) { + // Stolen shamelessly from Handlebars + + var escape = { + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + var string = value.toString(); + + if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } + 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); @@ -13832,8 +20867,7 @@ Ember._RenderBuffer = function(tagName) { this.buffer = ""; }; -Ember._RenderBuffer.prototype = -/** @scope Ember.RenderBuffer.prototype */ { +Ember._RenderBuffer.prototype = { // The root view's element _element: null, @@ -13841,12 +20875,11 @@ Ember._RenderBuffer.prototype = _hasElement: true, /** - @private - An internal set used to de-dupe class names when `addClass()` is used. After each call to `addClass()`, the `classes` property will be updated. + @private @property elementClasses @type Array @default [] @@ -14101,14 +21134,14 @@ Ember._RenderBuffer.prototype = style = this.elementStyle, attr, prop; - buffer += '<' + tagName; + buffer += '<' + stripTagName(tagName); if (id) { - buffer += ' id="' + this._escapeAttribute(id) + '"'; + buffer += ' id="' + escapeAttribute(id) + '"'; this.elementId = null; } if (classes) { - buffer += ' class="' + this._escapeAttribute(classes.join(' ')) + '"'; + buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"'; this.classes = null; } @@ -14117,7 +21150,7 @@ Ember._RenderBuffer.prototype = for (prop in style) { if (style.hasOwnProperty(prop)) { - buffer += prop + ':' + this._escapeAttribute(style[prop]) + ';'; + buffer += prop + ':' + escapeAttribute(style[prop]) + ';'; } } @@ -14129,7 +21162,7 @@ Ember._RenderBuffer.prototype = if (attrs) { for (attr in attrs) { if (attrs.hasOwnProperty(attr)) { - buffer += ' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'; + buffer += ' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"'; } } @@ -14144,7 +21177,7 @@ Ember._RenderBuffer.prototype = if (value === true) { buffer += ' ' + prop + '="' + prop + '"'; } else { - buffer += ' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'; + buffer += ' ' + prop + '="' + escapeAttribute(props[prop]) + '"'; } } } @@ -14159,7 +21192,7 @@ Ember._RenderBuffer.prototype = pushClosingTag: function() { var tagName = this.tagNames.pop(); - if (tagName) { this.buffer += ''; } + if (tagName) { this.buffer += ''; } }, currentTagName: function() { @@ -14168,14 +21201,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); @@ -14246,7 +21287,7 @@ Ember._RenderBuffer.prototype = if (this._hasElement && this._element) { // Firefox versions < 11 do not have support for element.outerHTML. var thisElement = this.element(), outerHTML = thisElement.outerHTML; - if (typeof outerHTML === 'undefined'){ + if (typeof outerHTML === 'undefined') { return Ember.$('
    ').append(thisElement).html(); } return outerHTML; @@ -14257,32 +21298,7 @@ Ember._RenderBuffer.prototype = innerString: function() { return this.buffer; - }, - - _escapeAttribute: function(value) { - // Stolen shamelessly from Handlebars - - var escape = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /&(?!\w+;)|[<>"'`]/g; - var possible = /[&<>"'`]/; - - var escapeChar = function(chr) { - return escape[chr] || "&"; - }; - - var string = value.toString(); - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); } - }; })(); @@ -14308,12 +21324,50 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @private @extends Ember.Object */ -Ember.EventDispatcher = Ember.Object.extend( -/** @scope Ember.EventDispatcher.prototype */{ +Ember.EventDispatcher = Ember.Object.extend({ /** - @private + The set of events names (and associated handler function names) to be setup + and dispatched by the `EventDispatcher`. Custom events can added to this list at setup + time, generally via the `Ember.Application.customEvents` hash. Only override this + default set to prevent the EventDispatcher from listening on some events all together. + This set will be modified by `setup` to also include any events added at that time. + + @property events + @type Object + */ + events: { + touchstart : 'touchStart', + touchmove : 'touchMove', + touchend : 'touchEnd', + touchcancel : 'touchCancel', + keydown : 'keyDown', + keyup : 'keyUp', + keypress : 'keyPress', + mousedown : 'mouseDown', + mouseup : 'mouseUp', + contextmenu : 'contextMenu', + click : 'click', + dblclick : 'doubleClick', + mousemove : 'mouseMove', + focusin : 'focusIn', + focusout : 'focusOut', + mouseenter : 'mouseEnter', + mouseleave : 'mouseLeave', + submit : 'submit', + input : 'input', + change : 'change', + dragstart : 'dragStart', + drag : 'drag', + dragenter : 'dragEnter', + dragleave : 'dragLeave', + dragover : 'dragOver', + drop : 'drop', + dragend : 'dragEnd' + }, + + /** The root DOM element to which event listeners should be attached. Event listeners will be attached to the document unless this is overridden. @@ -14322,6 +21376,7 @@ Ember.EventDispatcher = Ember.Object.extend( The default body is a string since this may be evaluated before document.body exists in the DOM. + @private @property rootElement @type DOMElement @default 'body' @@ -14329,8 +21384,6 @@ Ember.EventDispatcher = Ember.Object.extend( rootElement: 'body', /** - @private - Sets up event listeners for standard browser events. This will be called after the browser sends a `DOMContentReady` event. By @@ -14338,39 +21391,12 @@ Ember.EventDispatcher = Ember.Object.extend( would like to register the listeners on a different element, set the event dispatcher's `root` property. + @private @method setup @param addedEvents {Hash} */ setup: function(addedEvents, rootElement) { - var event, events = { - touchstart : 'touchStart', - touchmove : 'touchMove', - touchend : 'touchEnd', - touchcancel : 'touchCancel', - keydown : 'keyDown', - keyup : 'keyUp', - keypress : 'keyPress', - mousedown : 'mouseDown', - mouseup : 'mouseUp', - contextmenu : 'contextMenu', - click : 'click', - dblclick : 'doubleClick', - mousemove : 'mouseMove', - focusin : 'focusIn', - focusout : 'focusOut', - mouseenter : 'mouseEnter', - mouseleave : 'mouseLeave', - submit : 'submit', - input : 'input', - change : 'change', - dragstart : 'dragStart', - drag : 'drag', - dragenter : 'dragEnter', - dragleave : 'dragLeave', - dragover : 'dragOver', - drop : 'drop', - dragend : 'dragEnd' - }; + var event, events = get(this, 'events'); Ember.$.extend(events, addedEvents || {}); @@ -14397,8 +21423,6 @@ Ember.EventDispatcher = Ember.Object.extend( }, /** - @private - Registers an event listener on the document. If the given event is triggered, the provided event handler will be triggered on the target view. @@ -14413,6 +21437,7 @@ Ember.EventDispatcher = Ember.Object.extend( setupHandler('mousedown', 'mouseDown'); ``` + @private @method setupHandler @param {Element} rootElement @param {String} event the browser-originated event to listen to @@ -14422,7 +21447,7 @@ Ember.EventDispatcher = Ember.Object.extend( var self = this; rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { - return Ember.handleErrors(function() { + return Ember.handleErrors(function handleViewEvent() { var view = Ember.View.views[this.id], result = true, manager = null; @@ -14441,7 +21466,7 @@ Ember.EventDispatcher = Ember.Object.extend( }); rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { - return Ember.handleErrors(function() { + return Ember.handleErrors(function handleActionEvent() { var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; @@ -14473,7 +21498,9 @@ Ember.EventDispatcher = Ember.Object.extend( var handler = object[eventName]; if (Ember.typeOf(handler) === 'function') { - result = handler.call(object, evt, view); + result = Ember.run(function() { + return handler.call(object, evt, view); + }); // Do not preventDefault in eventManagers. evt.stopPropagation(); } @@ -14485,7 +21512,7 @@ Ember.EventDispatcher = Ember.Object.extend( }, _bubbleEvent: function(view, evt, eventName) { - return Ember.run(function() { + return Ember.run(function bubbleEvent() { return view.handleEvent(eventName, evt); }); }, @@ -14547,7 +21574,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) { @@ -14556,7 +21583,7 @@ Ember.ControllerMixin.reopen({ } set(this, '_childContainers', {}); - }, 'model') + }) }); })(); @@ -14581,13 +21608,17 @@ 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; a_forEach(childViews, function(view) { + var currentChildViews; if (view.isVirtual) { - ret.pushObjects(get(view, 'childViews')); + if (currentChildViews = get(view, 'childViews')) { + ret.pushObjects(currentChildViews); + } } else { ret.push(view); } @@ -14598,7 +21629,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; @@ -14617,7 +21648,23 @@ Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionali */ Ember.TEMPLATES = {}; -Ember.CoreView = Ember.Object.extend(Ember.Evented, { +/** + `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 + @extends Ember.Object + @uses Ember.Evented + @uses Ember.ActionHandler +*/ + +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { isView: true, states: states, @@ -14662,8 +21709,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { }, /** - @private - Invoked by the view system when this view needs to produce an HTML representation. This method will create a new render buffer, if needed, then apply any default attributes, such as class names and visibility. @@ -14678,6 +21723,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { @param {Ember.RenderBuffer} buffer the render buffer. If no buffer is passed, a default buffer, using the current view's `tagName`, will be used. + @private */ renderToBuffer: function(parentBuffer, bufferOperation) { var name = 'render.' + this.instrumentName, @@ -14685,7 +21731,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { this.instrumentDetails(details); - return Ember.instrument(name, details, function() { + return Ember.instrument(name, details, function instrumentRenderToBuffer() { return this._renderToBuffer(parentBuffer, bufferOperation); }, this); }, @@ -14712,13 +21758,12 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { }, /** - @private - Override the default event firing from `Ember.Evented` to also call methods with the given name. @method trigger @param name {String} + @private */ trigger: function(name) { this._super.apply(this, arguments); @@ -14732,6 +21777,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); }, @@ -14833,7 +21890,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({ @@ -14873,8 +21930,8 @@ class: MyView = Ember.View.extend({ classNameBindings: ['propertyA', 'propertyB'], propertyA: 'from-a', - propertyB: function(){ - if(someLogic){ return 'from-b'; } + propertyB: function() { + if (someLogic) { return 'from-b'; } }.property() }); ``` @@ -14959,7 +22016,7 @@ class: ```javascript // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isEnabled:enabled:disabled'] isEnabled: true }); @@ -14982,7 +22039,7 @@ class: ```javascript // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isEnabled::disabled'] isEnabled: true }); @@ -15007,8 +22064,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 @@ -15055,7 +22112,7 @@ class: MyTextInput = Ember.View.extend({ tagName: 'input', attributeBindings: ['disabled'], - disabled: function(){ + disabled: function() { if (someLogic) { return true; } else { @@ -15068,7 +22125,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 @@ -15108,12 +22165,25 @@ class: }); ``` + If you have nested resources, your Handlebars template will look like this: + + ```html + + ``` + + And `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'posts/new' + }); + ``` + 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 @@ -15164,7 +22234,7 @@ class: aController = Ember.Object.create({ firstName: 'Barry', - excitedGreeting: function(){ + excitedGreeting: function() { return this.get("content.firstName") + "!!!" }.property() }); @@ -15219,7 +22289,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 @@ -15235,7 +22306,7 @@ class: ```javascript AView = Ember.View.extend({ - click: function(event){ + click: function(event) { // will be called when when an instance's // rendered element is clicked } @@ -15256,7 +22327,7 @@ class: ```javascript AView = Ember.View.extend({ eventManager: Ember.Object.create({ - doubleClick: function(event, view){ + doubleClick: function(event, view) { // will be called when when an instance's // rendered element or any rendering // of this views's descendent @@ -15271,12 +22342,12 @@ class: ```javascript AView = Ember.View.extend({ - mouseEnter: function(event){ + mouseEnter: function(event) { // will never trigger. }, eventManager: Ember.Object.create({ - mouseEnter: function(event, view){ - // takes presedence over AView#mouseEnter + mouseEnter: function(event, view) { + // takes precedence over AView#mouseEnter } }) }); @@ -15293,21 +22364,21 @@ class: OuterView = Ember.View.extend({ template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), eventManager: Ember.Object.create({ - mouseEnter: function(event, view){ + mouseEnter: function(event, view) { // view might be instance of either - // OutsideView or InnerView depending on + // OuterView or InnerView depending on // where on the page the user interaction occured } }) }); InnerView = Ember.View.extend({ - click: function(event){ + click: function(event) { // will be called if rendered inside // an OuterView because OuterView's // eventManager doesn't handle click events }, - mouseEnter: function(event){ + mouseEnter: function(event) { // will never be called if rendered inside // an OuterView. } @@ -15316,12 +22387,14 @@ class: ### Handlebars `{{action}}` Helper - See `Handlebars.helpers.action`. + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). ### Event Names - Possible events names for any of the responding approaches described above - are: + All of the event handling approaches described above respond to the same set + of events. The names of the built-in events are listed below. (The hash of + built-in events exists in `Ember.EventDispatcher`.) Additional, custom events + can be registered by using `Ember.Application.customEvents`. Touch events: @@ -15369,16 +22442,14 @@ 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 - @extends Ember.Object - @uses Ember.Evented + @extends Ember.CoreView */ -Ember.View = Ember.CoreView.extend( -/** @scope Ember.View.prototype */ { +Ember.View = Ember.CoreView.extend({ concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], @@ -15386,7 +22457,7 @@ Ember.View = Ember.CoreView.extend( @property isView @type Boolean @default true - @final + @static */ isView: true, @@ -15397,9 +22468,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 @@ -15410,9 +22480,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 @@ -15420,15 +22489,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 @@ -15486,6 +22546,11 @@ Ember.View = Ember.CoreView.extend( return layout || get(this, 'defaultLayout'); }).property('layoutName'), + _yield: function(context, options) { + var template = get(this, 'template'); + if (template) { template(context, options); } + }, + templateForName: function(name, type) { if (!name) { return; } Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); @@ -15517,8 +22582,6 @@ Ember.View = Ember.CoreView.extend( }).volatile(), /** - @private - Private copy of the view's template context. This can be set directly by Handlebars without triggering the observer that causes the view to be re-rendered. @@ -15534,6 +22597,7 @@ Ember.View = Ember.CoreView.extend( something of a hack and should be revisited. @property _context + @private */ _context: Ember.computed(function(key) { var parentView, controller; @@ -15551,16 +22615,15 @@ Ember.View = Ember.CoreView.extend( }), /** - @private - If a value that affects template rendering changes, the view should be re-rendered to reflect the new value. - @method _displayPropertyDidChange + @method _contextDidChange + @private */ - _contextDidChange: Ember.observer(function() { + _contextDidChange: Ember.observer('context', function() { this.rerender(); - }, 'context'), + }), /** If `false`, the view will appear hidden in DOM. @@ -15572,14 +22635,13 @@ Ember.View = Ember.CoreView.extend( isVisible: true, /** - @private - Array of child views. You should never edit this array directly. Instead, use `appendChild` and `removeFromParent`. @property childViews @type Array @default [] + @private */ childViews: childViewsProperty, @@ -15587,21 +22649,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 @@ -15617,7 +22679,7 @@ Ember.View = Ember.CoreView.extend( var view = get(this, 'parentView'); while (view) { - if(view instanceof klass) { return view; } + if (view instanceof klass) { return view; } view = get(view, 'parentView'); } }, @@ -15638,7 +22700,7 @@ Ember.View = Ember.CoreView.extend( function(view) { return klass.detect(view.constructor); }; while (view) { - if( isOfType(view) ) { return view; } + if (isOfType(view)) { return view; } view = get(view, 'parentView'); } }, @@ -15646,7 +22708,7 @@ Ember.View = Ember.CoreView.extend( /** Return the nearest ancestor that has a given property. - @property nearestWithProperty + @function nearestWithProperty @param {String} property A property name @return Ember.View */ @@ -15663,7 +22725,7 @@ Ember.View = Ember.CoreView.extend( Return the nearest ancestor whose parent is an instance of `klass`. - @property nearestChildOf + @method nearestChildOf @param {Class} klass Subclass of Ember.View (or Ember.View itself) @return Ember.View */ @@ -15671,27 +22733,28 @@ Ember.View = Ember.CoreView.extend( var view = get(this, 'parentView'); while (view) { - if(get(view, 'parentView') instanceof klass) { return view; } + if (get(view, 'parentView') instanceof klass) { return view; } view = get(view, 'parentView'); } }, /** - @private - When the parent view changes, recursively invalidate `controller` @method _parentViewDidChange + @private */ - _parentViewDidChange: Ember.observer(function() { + _parentViewDidChange: Ember.observer('_parentView', function() { if (this.isDestroying) { return; } + this.trigger('parentViewDidChange'); + 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(); @@ -15699,7 +22762,7 @@ Ember.View = Ember.CoreView.extend( this.forEachChildView(function(view) { view.propertyDidChange('controller'); }); - }, 'controller'), + }), cloneKeywords: function() { var templateData = get(this, 'templateData'); @@ -15794,14 +22857,13 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Iterates over the view's `classNameBindings` array, inserts the value of the specified property into the `classNames` array, then creates an observer to update the view's element if the bound property ever changes in the future. @method _applyClassNameBindings + @private */ _applyClassNameBindings: function(classBindings) { var classNames = this.classNames, @@ -15872,13 +22934,12 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Iterates through the view's attribute bindings, sets up observers for each, then applies the current value of the attributes to the passed render buffer. @method _applyAttributeBindings @param {Ember.RenderBuffer} buffer + @private */ _applyAttributeBindings: function(buffer, attributeBindings) { var attributeValue, elem; @@ -15908,8 +22969,6 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Given a property name, returns a dasherized version of that property name if the property evaluates to a non-falsy value. @@ -15918,6 +22977,7 @@ Ember.View = Ember.CoreView.extend( @method _classStringForProperty @param property + @private */ _classStringForProperty: function(property) { var parsedPath = Ember.View._parsePropertyPath(property); @@ -15957,9 +23017,9 @@ Ember.View = Ember.CoreView.extend( For example, calling `view.$('li')` will return a jQuery object containing all of the `li` elements inside the DOM element of this view. - @property $ + @method $ @param {String} [selector] a jQuery-compatible selector string - @return {jQuery} the CoreQuery object for the DOM node + @return {jQuery} the jQuery object for the DOM node */ $: function(sel) { return this.currentState.$(this, sel); @@ -16018,6 +23078,7 @@ Ember.View = Ember.CoreView.extend( // Schedule the DOM element to be created and appended to the given // element after bindings have synchronized. this._insertElementLater(function() { + Ember.assert("You tried to append to (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); this.$().appendTo(target); }); @@ -16035,10 +23096,11 @@ Ember.View = Ember.CoreView.extend( finished synchronizing @method replaceIn - @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object + @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object @return {Ember.View} received */ replaceIn: function(target) { + Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); this._insertElementLater(function() { @@ -16050,8 +23112,6 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Schedules a DOM operation to occur during the next render phase. This ensures that all bindings have finished synchronizing before the view is rendered. @@ -16071,6 +23131,7 @@ Ember.View = Ember.CoreView.extend( @method _insertElementLater @param {Function} fn the function that inserts the element into the DOM + @private */ _insertElementLater: function(fn) { this._scheduledInsert = Ember.run.scheduleOnce('render', this, '_insertElement', fn); @@ -16182,17 +23243,16 @@ Ember.View = Ember.CoreView.extend( willClearRender: Ember.K, /** - @private - Run this callback on the current view (unless includeSelf is false) and recursively on child views. @method invokeRecursively @param fn {Function} - @param includeSelf (optional, default true) + @param includeSelf {Boolean} Includes itself if true. + @private */ invokeRecursively: function(fn, includeSelf) { var childViews = (includeSelf === false) ? this._childViews : [this]; - var currentViews, view; + var currentViews, view, currentChildViews; while (childViews.length) { currentViews = childViews.slice(); @@ -16200,16 +23260,17 @@ Ember.View = Ember.CoreView.extend( for (var i=0, l=currentViews.length; i - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['type'], type: 'button' }); @@ -16480,7 +23535,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Renders something like
    - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['enabled'], enabled: true }); @@ -16495,14 +23550,14 @@ Ember.View = Ember.CoreView.extend( // /** - @private - Setup a view, but do not finish waking it up. - - configure `childViews` - - register the view with the global views hash, which is used for event + + * configure `childViews` + * register the view with the global views hash, which is used for event dispatch @method init + @private */ init: function() { this.elementId = this.elementId || guidFor(this); @@ -16517,14 +23572,6 @@ Ember.View = Ember.CoreView.extend( Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); this.classNames = Ember.A(this.classNames.slice()); - - var viewController = get(this, 'viewController'); - if (viewController) { - viewController = get(viewController); - if (viewController) { - set(viewController, 'view', this); - } - } }, appendChild: function(view, options) { @@ -16635,36 +23682,51 @@ Ember.View = Ember.CoreView.extend( act as a child of the parent. @method createChildView - @param {Class} viewClass + @param {Class|String} viewClass @param {Hash} [attrs] Attributes to add @return {Ember.View} new instance */ createChildView: function(view, attrs) { - if (view.isView && view._parentView === this) { return view; } + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + + if (view.isView && view._parentView === this && view.container === this.container) { + return view; + } + + attrs = attrs || {}; + attrs._parentView = this; if (Ember.CoreView.detect(view)) { - attrs = attrs || {}; - attrs._parentView = this; - attrs.container = this.container; attrs.templateData = attrs.templateData || get(this, 'templateData'); + attrs.container = this.container; view = view.create(attrs); // don't set the property on a virtual view, as they are invisible to // consumers of the view API - if (view.viewName) { set(get(this, 'concreteView'), view.viewName, view); } + if (view.viewName) { + set(get(this, 'concreteView'), view.viewName, view); + } + } else if ('string' === typeof view) { + var fullName = 'view:' + view; + var View = this.container.lookupFactory(fullName); + + Ember.assert("Could not find view: '" + fullName + "'", !!View); + + attrs.templateData = get(this, 'templateData'); + view = View.create(attrs); } else { Ember.assert('You must pass instance or subclass of View', view.isView); - - if (attrs) { - view.setProperties(attrs); - } + attrs.container = this.container; if (!get(view, 'templateData')) { - set(view, 'templateData', get(this, 'templateData')); + attrs.templateData = get(this, 'templateData'); } - set(view, '_parentView', this); + Ember.setProperties(view, attrs); + } return view; @@ -16674,14 +23736,13 @@ Ember.View = Ember.CoreView.extend( becameHidden: Ember.K, /** - @private - When the view's `isVisible` property changes, toggle the visibility element of the actual DOM element. @method _isVisibleDidChange + @private */ - _isVisibleDidChange: Ember.observer(function() { + _isVisibleDidChange: Ember.observer('isVisible', function() { var $el = this.$(); if (!$el) { return; } @@ -16696,7 +23757,7 @@ Ember.View = Ember.CoreView.extend( } else { this._notifyBecameHidden(); } - }, 'isVisible'), + }), _notifyBecameVisible: function() { this.trigger('becameVisible'); @@ -16746,6 +23807,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) { @@ -16759,13 +23821,12 @@ Ember.View = Ember.CoreView.extend( // /** - @private - Handle events from `Ember.EventDispatcher` @method handleEvent @param eventName {String} @param evt {Event} + @private */ handleEvent: function(eventName, evt) { return this.currentState.handleEvent(this, eventName, evt); @@ -16777,8 +23838,12 @@ Ember.View = Ember.CoreView.extend( target = null; } + if (!root || typeof root !== 'object') { + return; + } + var view = this, - stateCheckedObserver = function(){ + stateCheckedObserver = function() { view.currentState.invokeObserver(this, observer); }, scheduledObserver = function() { @@ -16868,11 +23933,9 @@ Ember.View.reopen({ Ember.View.reopenClass({ /** - @private - 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 @@ -16886,6 +23949,7 @@ Ember.View.reopenClass({ @method _parsePropertyPath @static + @private */ _parsePropertyPath: function(path) { var split = path.split(':'), @@ -16912,8 +23976,6 @@ Ember.View.reopenClass({ }, /** - @private - Get the class name for a given value, based on the path, optional `className` and optional `falsyClassName`. @@ -16935,6 +23997,7 @@ Ember.View.reopenClass({ @param className @param falsyClassName @static + @private */ _classStringForValue: function(path, val, className, falsyClassName) { // When using the colon syntax, evaluate the truthiness or falsiness @@ -16961,7 +24024,7 @@ Ember.View.reopenClass({ // If the value is not false, undefined, or null, return the current // value of the property. - } else if (val !== false && val !== undefined && val !== null) { + } else if (val !== false && val != null) { return val; // Nothing to display. Return null so that the old class is removed @@ -17011,8 +24074,12 @@ Ember.View.applyAttributeBindings = function(elem, name, value) { elem.attr(name, value); } } else if (name === 'value' || type === 'boolean') { - // We can't set properties to undefined - if (value === undefined) { value = null; } + // We can't set properties to undefined or null + if (Ember.isNone(value)) { value = ''; } + + if (!value) { + elem.removeAttr(name); + } if (value !== elem.prop(name)) { // value and booleans should always be properties @@ -17093,10 +24160,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) { @@ -17173,7 +24248,10 @@ Ember.merge(inBuffer, { }, empty: function() { - Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications."); + Ember.assert("Emptying a view in the inBuffer state is not allowed and " + + "should not happen under normal circumstances. Most likely " + + "there is a bug in your application. This may be due to " + + "excessive property change notifications."); }, renderToBufferIfNeeded: function (view, buffer) { @@ -17305,7 +24383,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"); }); }, @@ -17401,7 +24479,7 @@ var ViewCollection = Ember._ViewCollection; /** A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` - allowing programatic management of its child views. + allowing programmatic management of its child views. ## Setting Initial Child Views @@ -17441,7 +24519,7 @@ var ViewCollection = Ember._ViewCollection; ## Adding and Removing Child Views - The container view implements `Ember.MutableArray` allowing programatic management of its child views. + The container view implements `Ember.MutableArray` allowing programmatic management of its child views. To remove a view, pass that view into a `removeObject` call on the container view. @@ -17545,30 +24623,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 @@ -17609,6 +24663,8 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { replace: function(idx, removedCount, addedViews) { var addedCount = addedViews ? get(addedViews, 'length') : 0; + var self = this; + Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); this.arrayContentWillChange(idx, removedCount, addedCount); this.childViewsWillChange(this._childViews, idx, removedCount); @@ -17633,13 +24689,12 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { length: Ember.computed(function () { return this._childViews.length; - }), + }).volatile(), /** - @private - Instructs each child view to render to the passed render buffer. + @private @method render @param {Ember.RenderBuffer} buffer the buffer to render to */ @@ -17652,14 +24707,13 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { instrumentName: 'container', /** - @private - When a child view is removed, destroy its element so that it is removed from the DOM. The array observer that triggers this action is set up in the `renderToBuffer` method. + @private @method childViewsWillChange @param {Ember.Array} views the child views array before mutation @param {Number} start the start position of the mutation @@ -17682,8 +24736,6 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { }, /** - @private - When a child view is added, make sure the DOM gets updated appropriately. If the view has already rendered an element, we tell the child view to @@ -17692,6 +24744,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { into an element, we insert the string representation of the child into the appropriate place in the buffer. + @private @method childViewsDidChange @param {Ember.Array} views the array of child views afte the mutation has occurred @param {Number} start the start position of the mutation @@ -17711,6 +24764,10 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { forEach(views, function(view) { set(view, '_parentView', parentView); + if (!view.container && parentView) { + set(view, 'container', parentView.container); + } + if (!get(view, 'templateData')) { set(view, 'templateData', templateData); } @@ -17719,19 +24776,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); @@ -17746,7 +24804,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'); } }); @@ -17818,7 +24876,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; /** `Ember.CollectionView` is an `Ember.View` descendent responsible for managing - a collection (an array or array-like object) by maintaing a child view object + a collection (an array or array-like object) by maintaining a child view object and associated DOM representation for each item in the array and ensuring that child views and their associated rendered HTML are updated when items in the array are added, removed, or replaced. @@ -17904,7 +24962,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Ember.CollectionView.CONTAINER_MAP['article'] = 'section' ``` - ## Programatic creation of child views + ## Programmatic creation of child views For cases where additional customization beyond the use of a single `itemViewClass` or `tagName` matching is required CollectionView's @@ -17958,19 +25016,13 @@ 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 @extends Ember.ContainerView @since Ember 0.9 */ -Ember.CollectionView = Ember.ContainerView.extend( -/** @scope Ember.CollectionView.prototype */ { +Ember.CollectionView = Ember.ContainerView.extend({ /** A list of items to be displayed by the `Ember.CollectionView`. @@ -17982,12 +25034,11 @@ Ember.CollectionView = Ember.ContainerView.extend( content: null, /** - @private - This provides metadata about what kind of empty view class this collection would like if it is being instantiated from another system (like Handlebars) + @private @property emptyViewClass */ emptyViewClass: Ember.View, @@ -18008,42 +25059,68 @@ Ember.CollectionView = Ember.ContainerView.extend( */ itemViewClass: Ember.View, + /** + Setup a CollectionView + + @method init + */ init: function() { var ret = this._super(); this._contentDidChange(); return ret; }, - _contentWillChange: Ember.beforeObserver(function() { + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @private + @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 - Check to make sure that the content has changed, and if so, update the children directly. This is always scheduled asynchronously, to allow the element to be created before bindings have synchronized and vice versa. + @private @method _contentDidChange */ - _contentDidChange: Ember.observer(function() { + _contentDidChange: Ember.observer('content', function() { var content = get(this, 'content'); if (content) { - Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); + this._assertArrayLike(content); content.addArrayObserver(this); } var len = content ? get(content, 'length') : 0; this.arrayDidChange(content, 0, null, len); - }, 'content'), + }), + /** + Ensure that the content implements Ember.Array + + @private + @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; } @@ -18057,6 +25134,19 @@ Ember.CollectionView = Ember.ContainerView.extend( 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. @@ -18102,17 +25192,20 @@ Ember.CollectionView = Ember.ContainerView.extend( @param {Number} added number of object added to content */ arrayDidChange: function(content, start, removed, added) { - var itemViewClass = get(this, 'itemViewClass'), - addedViews = [], view, item, idx, len; - - if ('string' === typeof itemViewClass) { - itemViewClass = get(itemViewClass); - } - - Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass)); + var addedViews = [], view, item, idx, len, itemViewClass, + emptyView; len = content ? get(content, 'length') : 0; + if (len) { + itemViewClass = get(this, 'itemViewClass'); + + if ('string' === typeof itemViewClass) { + itemViewClass = get(itemViewClass) || itemViewClass; + } + + Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), 'string' === typeof itemViewClass || Ember.View.detect(itemViewClass)); + for (idx = start; idx < start+added; idx++) { item = content.objectAt(idx); @@ -18124,27 +25217,50 @@ Ember.CollectionView = Ember.ContainerView.extend( addedViews.push(view); } } else { - var emptyView = get(this, 'emptyView'); + emptyView = get(this, 'emptyView'); + if (!emptyView) { return; } - var isClass = Ember.CoreView.detect(emptyView); + if ('string' === typeof emptyView) { + emptyView = get(emptyView) || emptyView; + } emptyView = this.createChildView(emptyView); addedViews.push(emptyView); set(this, 'emptyView', emptyView); - if (isClass) { this._createdEmptyView = emptyView; } + if (Ember.CoreView.detect(emptyView)) { + this._createdEmptyView = emptyView; + } } + 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); var itemTagName = get(view, 'tagName'); - var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName; - set(view, 'tagName', tagName); + if (itemTagName === null || itemTagName === undefined) { + itemTagName = Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')]; + set(view, 'tagName', itemTagName); + } return view; } @@ -18175,6 +25291,266 @@ Ember.CollectionView.CONTAINER_MAP = { +(function() { +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, + a_slice = Array.prototype.slice; + + +/** +@module ember +@submodule ember-views +*/ + +/** + An `Ember.Component` is a view that is completely + isolated. Property access in its templates go + to the view object and actions are targeted at + the view object. There is no access to the + surrounding context or outer controller; all + contextual information must be passed in. + + The easiest way to create an `Ember.Component` is via + a template. If you name a template + `components/my-foo`, you will be able to use + `{{my-foo}}` in other templates, which will make + an instance of the isolated component. + + ```html + {{app-profile person=currentUser}} + ``` + + ```html + +

    {{person.title}}

    + +

    {{person.signature}}

    + ``` + + You can use `yield` inside a template to + include the **contents** of any block attached to + the component. The block will be executed in the + context of the surrounding context or outer controller: + + ```handlebars + {{#app-profile person=currentUser}} +

    Admin mode

    + {{! Executed in the controllers context. }} + {{/app-profile}} + ``` + + ```handlebars + +

    {{person.title}}

    + {{! Executed in the components context. }} + {{yield}} {{! block contents }} + ``` + + 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. 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: + + ```javascript + App.AppProfileComponent = Ember.Component.extend({ + actions: { + hello: function(name) { + console.log("Hello", name); + } + } + }); + ``` + + And then use it in the component's template: + + ```html + + +

    {{person.title}}

    + {{yield}} + + + ``` + + Components must have a `-` in their name to avoid + conflicts with built-in controls that wrap HTML + elements. This is consistent with the same + requirement in web components. + + @class Component + @namespace Ember + @extends Ember.View +*/ +Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { + init: function() { + this._super(); + set(this, 'context', this); + set(this, 'controller', this); + }, + + defaultLayout: function(options){ + options.data = {view: options._context}; + Ember.Handlebars.helpers['yield'].apply(this, [options]); + }, + + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _yield: function(context, options) { + var view = options.data.view, + parentView = this._parentView, + template = get(this, 'template'); + + if (template) { + Ember.assert("A Component must have a parent view in order to yield.", parentView); + + view.appendChild(Ember.View, { + isVirtual: true, + tagName: '', + _contextView: parentView, + template: template, + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + /** + 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'), + + /** + Triggers a named action on the controller context where the component is used if + this controller has registered for notifications of the action. + + 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: + + + ```javascript + App.PlayButtonComponent = Ember.Component.extend({ + click: function(){ + if (this.get('isPlaying')) { + this.triggerAction('play'); + } else { + this.triggerAction('stop'); + } + } + }); + ``` + + 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 + {{! 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) { + var actionName, + contexts = a_slice.call(arguments, 1); + + // Send the default action + if (action === undefined) { + actionName = get(this, 'action'); + Ember.assert("The default action was triggered on the component " + this.toString() + + ", but the action name (" + actionName + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); + } else { + actionName = get(this, action); + Ember.assert("The " + action + " action was triggered on the component " + + this.toString() + ", but the action name (" + actionName + + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); + } + + // If no action name for that action could be found, just abort. + if (actionName === undefined) { return; } + + this.triggerAction({ + action: actionName, + actionContext: contexts + }); + } +}); + +})(); + + + (function() { })(); @@ -18198,7 +25574,7 @@ For example: ```javascript App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { action: 'save', - click: function(){ + click: function() { this.triggerAction(); // Sends the `save` action, along with the current context // to the current controller } @@ -18210,7 +25586,7 @@ to `triggerAction` as well. ```javascript App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { - click: function(){ + click: function() { this.triggerAction({ action: 'save' }); // Sends the `save` action, along with the current context @@ -18245,7 +25621,6 @@ Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { (function() { -/*globals jQuery*/ /** Ember Views @@ -18264,20 +25639,28 @@ define("metamorph", "use strict"; // ========================================================================== // Project: metamorph - // Copyright: ©2011 My Company Inc. All rights reserved. + // Copyright: ©2014 Tilde, Inc. All rights reserved. // ========================================================================== - var K = function(){}, + var K = function() {}, guid = 0, - document = this.document, + disableRange = (function(){ + if ('undefined' !== typeof MetamorphENV) { + return MetamorphENV.DISABLE_RANGE_API; + } else if ('undefined' !== ENV) { + return ENV.DISABLE_RANGE_API; + } else { + return false; + } + })(), // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + supportsRange = (!disableRange) && document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = document && (function(){ + needsShy = document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
    "; testEl.firstChild.innerHTML = ""; @@ -18379,6 +25762,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. @@ -18419,7 +25810,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. * @@ -18473,12 +25864,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]; @@ -18511,7 +25902,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: * @@ -18521,7 +25912,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; @@ -18530,7 +25921,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. @@ -18557,7 +25948,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); @@ -18608,6 +26000,10 @@ define("metamorph", // swallow some of the content. node = firstNodeFor(start.parentNode, html); + if (outerToo) { + start.parentNode.removeChild(start); + } + // copy the nodes for the HTML between the starting and ending // placeholder. while (node) { @@ -18732,13 +26128,19 @@ var objectCreate = Object.create || function(parent) { return new F(); }; -var Handlebars = this.Handlebars || (Ember.imports && Ember.imports.Handlebars); -if(!Handlebars && typeof require === 'function') { +var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); +if (!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.3. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars) -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.3, COMPILER_REVISION 2. Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 2); +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 @@ -18756,31 +26158,86 @@ Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.3, COMPILER_ */ Ember.Handlebars = objectCreate(Handlebars); -function makeBindings(options) { - var hash = options.hash, - hashType = options.hashTypes; +/** + Register a bound helper or custom view helper. - for (var prop in hash) { - if (hashType[prop] === 'ID') { - hash[prop + 'Binding'] = hash[prop]; - hashType[prop + 'Binding'] = 'STRING'; - delete hash[prop]; - delete hashType[prop]; - } - } -} + ## Simple bound helper example + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{capitalize name}} + ``` + + In this case, when the `name` property of the template's context changes, + the rendered value of the helper will update to reflect this change. + + For more examples of bound helpers, see documentation for + `Ember.Handlebars.registerBoundHelper`. + + ## Custom view helper example + + Assuming a view subclass named `App.CalendarView` were defined, a helper + for rendering instances of this view could be registered as follows: + + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{calendar}} + ``` + + Which is functionally equivalent to: + + ```handlebars + {{view App.CalendarView}} + ``` + + Options in the helper will be passed to the view in exactly the same + manner as with the `view` helper. + + @method helper + @for Ember.Handlebars + @param {String} name + @param {Function|Ember.View} function or view class constructor + @param {String} dependentKeys* +*/ Ember.Handlebars.helper = function(name, value) { + 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 as parameters to a application-defined helper", arguments.length < 3); - 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); } -} +}; + +/** + Returns a helper function that renders the provided ViewClass. + + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. + + @private + @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 found in '" + ViewClass.toString() + "'", arguments.length < 2); + return Ember.Handlebars.helpers.view.call(this, ViewClass, options); + }; +}; /** @class helpers @@ -18822,18 +26279,16 @@ if (Handlebars.JavaScriptCompiler) { Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { return "''"; }; /** - @private - Override the default buffer for Ember Handlebars. By default, Handlebars creates an empty String at the beginning of each invocation and appends to it. Ember's Handlebars overrides this to append to a single shared buffer. + @private @method appendToBuffer @param string {String} */ @@ -18841,15 +26296,51 @@ 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/634 + +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; /** - @private - Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that all simple mustaches in Ember's Handlebars will also set up an observer to keep the DOM up to date when the underlying property changes. + @private @method mustache @for Ember.Handlebars.Compiler @param mustache @@ -18861,12 +26352,12 @@ Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { } else if (mustache.params.length || mustache.hash) { // no changes required } else { - var id = new Handlebars.AST.IdNode(['_triageMustache']); + var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); // Update the mustache node to include a hash value indicating whether the original node // was escaped. This will allow us to properly escape values when the underlying value // changes and we need to re-render the value. - if(!mustache.escaped) { + if (!mustache.escaped) { mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); } @@ -18892,7 +26383,7 @@ Ember.Handlebars.precompile = function(string) { knownHelpers: { action: true, unbound: true, - bindAttr: true, + 'bind-attr': true, template: true, view: true, _triageMustache: true @@ -18924,7 +26415,10 @@ if (Handlebars.compile) { var environment = new Ember.Handlebars.Compiler().compile(ast, options); var templateSpec = new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); - return Ember.Handlebars.template(templateSpec); + var template = Ember.Handlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super + + return template; }; } @@ -18932,14 +26426,14 @@ if (Handlebars.compile) { })(); (function() { -var slice = Array.prototype.slice; +var slice = Array.prototype.slice, + originalTemplate = Ember.Handlebars.template; /** - @private - If a path starts with a reserved keyword, returns the root that should be used. + @private @method normalizePath @for Ember @param root {Object} @@ -18993,22 +26487,19 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { normalizedPath = normalizePath(root, path, data), value; - // In cases where the path begins with a keyword, change the - // root to the value represented by that keyword, and ensure - // the path is relative to it. - root = normalizedPath.root; - path = normalizedPath.path; + + root = normalizedPath.root; + path = normalizedPath.path; - value = Ember.get(root, path); + value = Ember.get(root, path); + + if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { + value = Ember.get(Ember.lookup, path); + } + - // If the path starts with a capital letter, look it up on Ember.lookup, - // which defaults to the `window` object in browsers. - if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { - value = Ember.get(Ember.lookup, path); - } 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; @@ -19046,8 +26537,6 @@ Ember.Handlebars.resolveHash = function(context, hash, options) { }; /** - @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. @@ -19055,21 +26544,65 @@ Ember.Handlebars.resolveHash = function(context, hash, options) { This throws an exception with a more helpful error message so the user can track down where the problem is happening. + @private @method helperMissing @for Ember.Handlebars.helpers @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){ + if (options.data) { view = options.data.view; } throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); }); +/** + 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. + + @private + @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)); + } else { + return Handlebars.helpers.helperMissing.call(this, path); + } + + 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 @@ -19101,7 +26634,7 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { Ember.Handlebars.registerBoundHelper('repeat', function(value, options) { var count = options.hash.count; var a = []; - while(a.length < count){ + while(a.length < count) { a.push(value); } return a.join(''); @@ -19145,20 +26678,20 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { ```javascript Ember.Handlebars.registerBoundHelper('concatenate', function() { - var values = arguments[arguments.length - 1]; + var values = Array.prototype.slice.call(arguments, 0, -1); return values.join('||'); }); ``` - Which allows for template syntax such as {{concatenate prop1 prop2}} or - {{concatenate prop1 prop2 prop3}}. If any of the properties change, + Which allows for template syntax such as `{{concatenate prop1 prop2}}` or + `{{concatenate prop1 prop2 prop3}}`. If any of the properties change, the helpr will re-render. Note that dependency keys cannot be using in conjunction with multi-property helpers, since it is ambiguous which property the dependent keys would belong to. ## Use with unbound helper - The {{unbound}} helper can be used with bound helper invocations + The `{{unbound}}` helper can be used with bound helper invocations to render them in their unbound form, e.g. ```handlebars @@ -19168,6 +26701,10 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { In this example, if the name property changes, the helper will not re-render. + ## Use with blocks not supported + + Bound helpers do not support use with Handlebars blocks or + the addition of child views of any kind. @method registerBoundHelper @for Ember.Handlebars @@ -19176,7 +26713,38 @@ 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); +}; + +/** + 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. + + @private + @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), @@ -19184,135 +26752,122 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { options = arguments[arguments.length - 1], normalizedProperties = [], data = options.data, + types = data.isUnbound ? slice.call(options.types, 1) : options.types, hash = options.hash, view = data.view, - currentContext = (options.contexts && options.contexts[0]) || this, - normalized, - pathRoot, path, - 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 { + if(data.isUnbound) { + normalizedProperties.push({path: properties[loc]}); + }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 + '.'; + } for (var i=0, l=dependentKeys.length; isomeString
    ') + ``` + @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); @@ -19371,10 +26946,17 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/htmlSafe"}}{{/crossLink}} + 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); @@ -19445,7 +27027,7 @@ var DOMManager = { view.transitionTo('preRender'); - Ember.run.schedule('render', this, function() { + Ember.run.schedule('render', this, function renderMetamorphView() { if (view.isDestroying) { return; } view.clearRenderedChildren(); @@ -19480,7 +27062,6 @@ var DOMManager = { /** @class _Metamorph @namespace Ember - @extends Ember.Mixin @private */ Ember._Metamorph = Ember.Mixin.create({ @@ -19526,7 +27107,7 @@ Ember._MetamorphView = Ember.View.extend(Ember._Metamorph); /** @class _SimpleMetamorphView @namespace Ember - @extends Ember.View + @extends Ember.CoreView @uses Ember._Metamorph @private */ @@ -19556,6 +27137,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; @@ -19569,7 +27152,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, @@ -19624,7 +27211,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."); @@ -19872,13 +27459,28 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; var forEach = Ember.ArrayPolyfills.forEach; +var o_create = Ember.create; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; -function exists(value){ +function exists(value) { return !Ember.isNone(value); } +function sanitizedHandlebarsGet(currentContext, property, options) { + var result = handlebarsGet(currentContext, property, options); + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } + + return result; +} + // Binds a property into the DOM. This will create a hook in DOM that the // KVO system will look for and update if the property changes. function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { @@ -19900,7 +27502,7 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer var template, context, result = handlebarsGet(currentContext, property, options); - result = valueNormalizer(result); + result = valueNormalizer ? valueNormalizer(result) : result; context = preserveContext ? currentContext : result; if (shouldDisplay(result)) { @@ -19953,24 +27555,26 @@ 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'); }; - var result = handlebarsGet(currentContext, property, options); - if (result === null || result === undefined) { result = ""; } - data.buffer.push(result); + output = sanitizedHandlebarsGet(currentContext, property, options); + + data.buffer.push(output); } else { var bindView = new Ember._SimpleHandlebarsView( property, currentContext, !options.hash.unescaped, options.data @@ -19994,39 +27598,63 @@ 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 = sanitizedHandlebarsGet(currentContext, property, options); + + data.buffer.push(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. This would not be typically invoked by directly. + @private @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); + return helpers[property].call(this, options); } - else { - return helpers.bind.apply(this, arguments); + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, property); + if (helper) { + return helper.call(this, options); } + + return helpers.bind.call(this, property, options); }); -/** - @private +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; +}; + +/** `bind` can be used to display a value, then update that value if it changes. For example, if you wanted to print the `title` property of `content`: @@ -20042,27 +27670,26 @@ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { relies on Ember's KVO system. For all other browsers this will be handled for you automatically. + @private @method bind @for Ember.Handlebars.helpers @param {String} property Property to bind @param {Function} fn Context to provide for rendering @return {String} HTML string */ -EmberHandlebars.registerHelper('bind', function(property, options) { +EmberHandlebars.registerHelper('bind', function bindHelper(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); }); /** - @private - Use the `boundIf` helper to create a conditional that re-evaluates whenever the truthiness of the bound value changes. @@ -20072,14 +27699,15 @@ EmberHandlebars.registerHelper('bind', function(property, options) { {{/boundIf}} ``` + @private @method boundIf @for Ember.Handlebars.helpers @param {String} property Property to bind @param {Function} fn Context to provide for rendering @return {String} HTML string */ -EmberHandlebars.registerHelper('boundIf', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; +EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { + 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; } @@ -20095,15 +27723,65 @@ EmberHandlebars.registerHelper('boundIf', function(property, fn) { }); /** + Use the `{{with}}` helper when you want to scope context. Take the following code as an example: + + ```handlebars +
    {{user.name}}
    + +
    +
    {{user.role.label}}
    + {{user.role.id}} + +

    {{user.role.description}}

    +
    + ``` + + `{{with}}` can be our best friend in these cases, + instead of writing `user.role.*` over and over, we use `{{#with user.role}}`. + Now the context within the `{{#with}} .. {{/with}}` block is `user.role` so you can do the following: + + ```handlebars +
    {{user.name}}
    + +
    + {{#with user.role}} +
    {{label}}
    + {{id}} + +

    {{description}}

    + {{/with}} +
    + ``` + + ### `as` operator + + This operator aliases the scope to a new name. It's helpful for semantic clarity and to retain + default scope or to reference from another `{{with}}` block. + + ```handlebars + // posts might not be + {{#with user.posts as blogPosts}} +
    + There are {{blogPosts.length}} blog posts written by {{user.name}}. +
    + + {{#each post in blogPosts}} +
  1. {{post.title}}
  2. + {{/each}} + {{/with}} + ``` + + Without the `as` operator, it would be impossible to reference `user.name` in the example above. + @method with @for Ember.Handlebars.helpers @param {Function} context @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('with', function(context, options) { +EmberHandlebars.registerHelper('with', function withHelper(context, options) { if (arguments.length === 4) { - var keywordName, path, rootPath, normalized; + var keywordName, path, rootPath, normalized, contextPath; Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); options = arguments[3]; @@ -20112,8 +27790,12 @@ EmberHandlebars.registerHelper('with', function(context, options) { Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); + var localizedOptions = o_create(options); + localizedOptions.data = o_create(options.data); + localizedOptions.data.keywords = o_create(options.data.keywords || {}); + if (Ember.isGlobalPath(path)) { - Ember.bind(options.data.keywords, keywordName, path); + contextPath = path; } else { normalized = normalizePath(this, path, options.data); path = normalized.path; @@ -20122,14 +27804,14 @@ EmberHandlebars.registerHelper('with', function(context, options) { // This is a workaround for the fact that you cannot bind separate objects // together. When we implement that functionality, we should use it here. var contextKey = Ember.$.expando + Ember.guidFor(rootPath); - options.data.keywords[contextKey] = rootPath; - + localizedOptions.data.keywords[contextKey] = rootPath; // if the path is '' ("this"), just bind directly to the current context - var contextPath = path ? contextKey + '.' + path : contextKey; - Ember.bind(options.data.keywords, keywordName, contextPath); + contextPath = path ? contextKey + '.' + path : contextKey; } - return bind.call(this, path, options, true, exists); + Ember.bind(localizedOptions.data.keywords, keywordName, contextPath); + + return bind.call(this, path, localizedOptions, true, exists); } else { Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -20139,7 +27821,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 @@ -20147,7 +27829,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('if', function(context, options) { +EmberHandlebars.registerHelper('if', function ifHelper(context, options) { Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -20161,7 +27843,7 @@ EmberHandlebars.registerHelper('if', function(context, options) { @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('unless', function(context, options) { +EmberHandlebars.registerHelper('unless', function unlessHelper(context, options) { Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -20174,11 +27856,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 @@ -20200,39 +27882,39 @@ 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: ```javascript AView = Ember.View.extend({ - someProperty: function(){ + someProperty: function() { return "aValue"; }.property() }) ``` ```handlebars - ``` Result in the following rendered output: @@ -20254,7 +27936,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - + ``` Result in the following rendered output: @@ -20268,14 +27950,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: @@ -20288,19 +27970,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 bindAttrHelper(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 = []; @@ -20313,7 +27995,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // Handle classes differently, as we can bind multiple classes var classBindings = attrs['class']; - if (classBindings !== null && classBindings !== undefined) { + if (classBindings != null) { var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options); ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); @@ -20328,7 +28010,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { var path = attrs[attr], normalized; - Ember.assert(fmt("You must provide a String for a bound attribute, not %@", [path]), typeof path === 'string'); + Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); normalized = normalizePath(ctx, path, options.data); @@ -20342,7 +28024,9 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { observer = function observer() { var result = handlebarsGet(ctx, path, options); - Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), + result === null || result === undefined || typeof result === 'number' || + typeof result === 'string' || typeof result === 'boolean'); var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); @@ -20362,7 +28046,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); } @@ -20383,8 +28067,21 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { }); /** - @private + See `bind-attr` + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', function bindAttrHelper() { + Ember.warn("The 'bindAttr' view helper is deprecated in favor of 'bind-attr'"); + return EmberHandlebars.helpers['bind-attr'].apply(this, arguments); +}); + +/** Helper that, given a space-separated string of property paths and a context, returns an array of class names. Calling this method also has the side effect of setting up observers at those property paths, such that if they @@ -20396,6 +28093,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { "fooBar"). If the value is a string, it will add that string as the class. Otherwise, it will not add any new class name. + @private @method bindClasses @for Ember.Handlebars @param {Ember.Object} context The context from which to lookup properties @@ -20514,10 +28212,41 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, var get = Ember.get, set = Ember.set; var EmberHandlebars = Ember.Handlebars; +var LOWERCASE_A_Z = /^[a-z]/; +var VIEW_PREFIX = /^view\./; + +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'], @@ -20612,7 +28341,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ return 'templateData.keywords.' + path; } else if (Ember.isGlobalPath(path)) { return null; - } else if (path === 'this') { + } else if (path === 'this' || path === '') { return '_parentView.context'; } else { return '_parentView.context.' + path; @@ -20624,8 +28353,21 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ fn = options.fn, newView; + makeBindings(thisContext, options); + if ('string' === typeof path) { - newView = EmberHandlebars.get(thisContext, path, options); + + // TODO: this is a lame conditional, this should likely change + // but something along these lines will likely need to be added + // as deprecation warnings + // + if (options.types[0] === 'STRING' && LOWERCASE_A_Z.test(path) && !VIEW_PREFIX.test(path)) { + Ember.assert("View requires a container", !!data.view.container); + newView = data.view.container.lookupFactory('view:' + path); + } else { + newView = EmberHandlebars.get(thisContext, path, options); + } + Ember.assert("Unable to find view at path '" + path + "'", !!newView); } else { newView = path; @@ -20818,7 +28560,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('view', function(path, options) { +EmberHandlebars.registerHelper('view', function viewHelper(path, options) { Ember.assert("The view helper only takes a single argument", arguments.length <= 2); // If no path is provided, treat path param as options. @@ -20836,8 +28578,6 @@ EmberHandlebars.registerHelper('view', function(path, options) { (function() { -/*globals Handlebars */ - // TODO: Don't require all of this module /** @module ember @@ -20848,8 +28588,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` @@ -20968,7 +28708,7 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm @return {String} HTML string @deprecated Use `{{each}}` helper instead. */ -Ember.Handlebars.registerHelper('collection', function(path, options) { +Ember.Handlebars.registerHelper('collection', function collectionHelper(path, options) { Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); // If no path is provided, treat path param as options. @@ -20994,11 +28734,32 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { var hash = options.hash, itemHash = {}, match; // Extract item view class if provided else default to the standard class - var itemViewClass, itemViewPath = hash.itemViewClass; - var collectionPrototype = collectionClass.proto(); + var collectionPrototype = collectionClass.proto(), + itemViewClass; + + if (hash.itemView) { + var controller = data.keywords.controller; + Ember.assert('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:' + 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); + } else { + itemViewClass = collectionPrototype.itemViewClass; + } + + Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); + delete hash.itemViewClass; - itemViewClass = itemViewPath ? handlebarsGet(collectionPrototype, itemViewPath, options) : collectionPrototype.itemViewClass; - Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewPath]), !!itemViewClass); + delete hash.itemView; // Go through options passed to the {{collection}} helper and extract options // that configure item views instead of the collection itself. @@ -21006,7 +28767,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { if (hash.hasOwnProperty(prop)) { match = prop.match(/^item(.)(.*)$/); - if(match && prop !== 'itemController') { + if (match && prop !== 'itemController') { // Convert itemShouldFoo -> shouldFoo itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; // Delete from hash as this will end up getting passed to the @@ -21022,7 +28783,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } var emptyViewClass; - if (inverse && inverse !== Handlebars.VM.noop) { + if (inverse && inverse !== Ember.Handlebars.VM.noop) { emptyViewClass = get(collectionPrototype, 'emptyViewClass'); emptyViewClass = emptyViewClass.extend({ template: inverse, @@ -21033,7 +28794,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } if (emptyViewClass) { hash.emptyView = emptyViewClass; } - if(!hash.keyword){ + if (!hash.keyword) { itemHash._context = Ember.computed.alias('content'); } @@ -21077,19 +28838,19 @@ var handlebarsGet = Ember.Handlebars.get; @param {String} property @return {String} HTML string */ -Ember.Handlebars.registerHelper('unbound', function(property, fn) { +Ember.Handlebars.registerHelper('unbound', function unboundHelper(property, fn) { var options = arguments[arguments.length - 1], helper, context, out; - if(arguments.length > 2) { + if (arguments.length > 2) { // Unbound helper call. options.data.isUnbound = true; - helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helperMissing; + helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helpers.helperMissing; out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); delete options.data.isUnbound; return out; } - context = (fn.contexts && fn.contexts[0]) || this; + context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; return handlebarsGet(context, property, fn); }); @@ -21104,10 +28865,10 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { @submodule ember-handlebars */ -var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var get = Ember.get, handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; /** - `log` allows you to output the value of a value in the current rendering + `log` allows you to output the value of a variable in the current rendering context. ```handlebars @@ -21118,8 +28879,8 @@ var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.norma @for Ember.Handlebars.helpers @param {String} property */ -Ember.Handlebars.registerHelper('log', function(property, options) { - var context = (options.contexts && options.contexts[0]) || this, +Ember.Handlebars.registerHelper('log', function logHelper(property, options) { + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this, normalized = normalizePath(context, property, options.data), pathRoot = normalized.root, path = normalized.path, @@ -21134,14 +28895,44 @@ Ember.Handlebars.registerHelper('log', function(property, options) { {{debugger}} ``` + Before invoking the `debugger` statement, there + are a few helpful variables defined in the + body of this helper that you can inspect while + debugging that describe how and where this + helper was invoked: + + - templateContext: this is most likely a controller + from which this template looks up / displays properties + - typeOfTemplateContext: a string description of + what the templateContext is + + For example, if you're wondering why a value `{{foo}}` + isn't rendering as expected within a template, you + could place a `{{debugger}}` statement, and when + the `debugger;` breakpoint is hit, you can inspect + `templateContext`, determine if it's the object you + expect, and/or evaluate expressions in the console + to perform property lookups on the `templateContext`: + + ``` + > templateContext.get('foo') // -> "" + ``` + @method debugger @for Ember.Handlebars.helpers @param {String} property */ -Ember.Handlebars.registerHelper('debugger', function() { +Ember.Handlebars.registerHelper('debugger', function debuggerHelper(options) { + + // These are helpful values you can inspect while debugging. + var templateContext = this; + var typeOfTemplateContext = Ember.inspect(templateContext); + debugger; }); + + })(); @@ -21160,12 +28951,12 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { var binding; if (itemController) { - var controller = Ember.ArrayController.create(); - set(controller, 'itemController', itemController); - set(controller, 'container', get(this, 'controller.container')); - set(controller, '_eachView', this); - set(controller, 'target', get(this, 'controller')); - set(controller, 'parentController', get(this, 'controller')); + var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + parentController: get(this, 'controller'), + itemController: itemController, + target: get(this, 'controller'), + _eachView: this + }); this.disableContentObservers(function() { set(this, 'content', controller); @@ -21184,6 +28975,11 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { return this._super(); }, + _assertArrayLike: function(content) { + Ember.assert("The value that #each loops over must be an Array. You passed " + content.constructor + ", but it should have been an ArrayController", !Ember.ControllerMixin.detect(content) || (content && content.isGenerated) || content instanceof Ember.ArrayController); + Ember.assert("The value that #each loops over must be an Array. You passed " + ((Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? ("" + content.get('model') + " (wrapped in " + content + ")") : ("" + content)), Ember.Array.detect(content)); + }, + disableContentObservers: function(callback) { Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange'); Ember.removeObserver(this, 'content', null, '_contentDidChange'); @@ -21283,6 +29079,8 @@ GroupedEach.prototype = { }, addArrayObservers: function() { + if (!this.content) { return; } + this.content.addArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -21290,6 +29088,8 @@ GroupedEach.prototype = { }, removeArrayObservers: function() { + if (!this.content) { return; } + this.content.removeArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -21307,6 +29107,8 @@ GroupedEach.prototype = { }, render: function() { + if (!this.content) { return; } + var content = this.content, contentLength = get(content, 'length'), data = this.options.data, @@ -21319,12 +29121,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; } }; @@ -21414,6 +29225,12 @@ GroupedEach.prototype = { ``` + If an `itemViewClass` is defined on the helper, and therefore the helper is not + being used as a block, an `emptyViewClass` can also be provided optionally. + The `emptyViewClass` will match the behavior of the `{{else}}` condition + described above. That is, the `emptyViewClass` will render if the collection + is empty. + ### Representing each item with a Controller. By default the controller lookup within an `{{#each}}` block will be the controller of the template where the `{{#each}}` was used. If each @@ -21427,7 +29244,7 @@ GroupedEach.prototype = { ```javascript App.DeveloperController = Ember.ObjectController.extend({ - isAvailableForHire: function(){ + isAvailableForHire: function() { return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); }.property('isEmployed', 'isSeekingWork') }) @@ -21442,6 +29259,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`) @@ -21449,8 +29309,9 @@ 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) { +Ember.Handlebars.registerHelper('each', function eachHelper(path, options) { if (arguments.length === 4) { Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); @@ -21463,6 +29324,11 @@ Ember.Handlebars.registerHelper('each', function(path, options) { options.hash.keyword = keywordName; } + if (arguments.length === 1) { + options = path; + path = 'this'; + } + options.hash.dataSourceBinding = path; // Set up emptyView as a metamorph with no tag //options.hash.emptyViewClass = Ember._MetamorphView; @@ -21522,18 +29388,15 @@ Ember.Handlebars.registerHelper('each', function(path, options) { Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('{{user}}'); ``` + @deprecated @method template @for Ember.Handlebars.helpers @param {String} templateName the template to render */ Ember.Handlebars.registerHelper('template', function(name, options) { - var view = options.data.view, - template = view.templateForName(name); - - Ember.assert("Unable to find template with name '"+name+"'.", !!template); - - template(this, { data: options.data }); + Ember.deprecate("The `template` helper has been deprecated in favor of the `partial` helper. Please use `partial` instead, which will work the same way."); + return Ember.Handlebars.helpers.partial.apply(this, arguments); }); })(); @@ -21547,24 +29410,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 @@ -21572,7 +29459,31 @@ Ember.Handlebars.registerHelper('template', function(name, options) { @param {String} partialName the name of the template to render minus the leading underscore */ -Ember.Handlebars.registerHelper('partial', function(name, options) { +Ember.Handlebars.registerHelper('partial', function partialHelper(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]; @@ -21581,15 +29492,14 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var view = options.data.view, underscoredName = nameParts.join("/"), template = view.templateForName(underscoredName), - deprecatedTemplate = view.templateForName(name); + deprecatedTemplate = !template && view.templateForName(name); - Ember.deprecate("You tried to render the partial " + name + ", which should be at '" + underscoredName + "', but Ember found '" + name + "'. Please use a leading underscore in your partials", template); Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); template = template || deprecatedTemplate; - template(this, { data: options.data }); -}); + template(context, { data: options.data }); +} })(); @@ -21604,6 +29514,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. @@ -21646,7 +29560,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 +