├── Gemfile ├── .gitignore ├── lib ├── emberjs-couchapp │ ├── templates │ │ └── app │ │ │ ├── language │ │ │ ├── _id.tt │ │ │ ├── app │ │ │ ├── modules │ │ │ │ └── .empty_directory │ │ │ ├── lib │ │ │ │ ├── models │ │ │ │ │ └── .empty_directory │ │ │ │ ├── views │ │ │ │ │ └── .empty_directory │ │ │ │ ├── controllers │ │ │ │ │ └── .empty_directory │ │ │ │ ├── store.js.tt │ │ │ │ ├── main.js.tt │ │ │ │ ├── states │ │ │ │ │ └── start.js.tt │ │ │ │ ├── state_manager.js.tt │ │ │ │ ├── core.js.tt │ │ │ │ └── ext.js.tt │ │ │ ├── templates │ │ │ │ └── main_page.handlebars.tt │ │ │ ├── css │ │ │ │ └── main.css │ │ │ ├── static │ │ │ │ └── img │ │ │ │ │ ├── glyphicons-halflings.png │ │ │ │ │ └── glyphicons-halflings-white.png │ │ │ ├── tests │ │ │ │ └── %name%_tests.js.tt │ │ │ ├── index.html.tt │ │ │ ├── plugins │ │ │ │ └── loader.js │ │ │ └── vendor │ │ │ │ └── ember-data.js │ │ │ ├── views │ │ │ └── my_view │ │ │ │ ├── map.js │ │ │ │ └── reduce.js │ │ │ ├── filters │ │ │ └── my_filter.js │ │ │ ├── lists │ │ │ └── my_list.js │ │ │ ├── shows │ │ │ └── my_show.js │ │ │ ├── .gitignore │ │ │ ├── Guardfile │ │ │ ├── couchapp.json.tt │ │ │ ├── .couchapprc.tt │ │ │ ├── .couchappignore.tt │ │ │ ├── Gemfile │ │ │ ├── config.ru.tt │ │ │ ├── LICENSE │ │ │ ├── Rakefile.tt │ │ │ ├── tests │ │ │ ├── index.html │ │ │ ├── run-tests.js │ │ │ └── qunit │ │ │ │ ├── qunit.css │ │ │ │ └── qunit.js │ │ │ ├── README.md │ │ │ └── Assetfile.tt │ ├── version.rb │ ├── cli.rb │ └── app_generator.rb └── emberjs-couchapp.rb ├── bin └── emberjs-couchapp ├── Rakefile ├── LICENSE ├── Gemfile.lock ├── emberjs-couchapp.gemspec ├── spec ├── spec_helper.rb └── ember │ └── cli_spec.rb └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tmp/ 3 | pkg/ 4 | 5 | *.gem -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/language: -------------------------------------------------------------------------------- 1 | javascript -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/_id.tt: -------------------------------------------------------------------------------- 1 | _design/<%= name %> -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/modules/.empty_directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/models/.empty_directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/views/.empty_directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/controllers/.empty_directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/views/my_view/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | 3 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/filters/my_filter.js: -------------------------------------------------------------------------------- 1 | function(doc, req) { 2 | 3 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/lists/my_list.js: -------------------------------------------------------------------------------- 1 | function(head, req) { 2 | 3 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/shows/my_show.js: -------------------------------------------------------------------------------- 1 | function(doc, req) { 2 | 3 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/version.rb: -------------------------------------------------------------------------------- 1 | module EmberjsCouchapp 2 | VERSION = "0.0.7" 3 | end 4 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tmp/ 3 | _attachments/ 4 | test_assets/ 5 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/views/my_view/reduce.js: -------------------------------------------------------------------------------- 1 | function(keys, values, rereduce) { 2 | 3 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/Guardfile: -------------------------------------------------------------------------------- 1 | guard :rake, :task => :test do 2 | watch(%r{^app/.+\.js$}) 3 | end 4 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/templates/main_page.handlebars.tt: -------------------------------------------------------------------------------- 1 |

<%= name %> v{{App.VERSION}}

2 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/couchapp.json.tt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "description": "" 4 | } -------------------------------------------------------------------------------- /bin/emberjs-couchapp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'emberjs-couchapp/cli' 4 | 5 | EmberjsCouchapp::CLI.start 6 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp.rb: -------------------------------------------------------------------------------- 1 | require "emberjs-couchapp/version" 2 | 3 | module EmberjsCouchapp 4 | # Your code goes here... 5 | end 6 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/store.js.tt: -------------------------------------------------------------------------------- 1 | require('<%= name %>/core'); 2 | 3 | App.store = DS.Store.create({ 4 | revision: 4 5 | }); 6 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/main.js.tt: -------------------------------------------------------------------------------- 1 | require('<%= name %>/core'); 2 | require('<%= name %>/store'); 3 | require('<%= name %>/state_manager'); 4 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | } 4 | 5 | a { 6 | cursor: pointer; 7 | } 8 | 9 | .navbar .brand { 10 | color: #fff; 11 | } 12 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/emberjs-couchapp-gem/master/lib/emberjs-couchapp/templates/app/app/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/emberjs-couchapp-gem/master/lib/emberjs-couchapp/templates/app/app/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/tests/%name%_tests.js.tt: -------------------------------------------------------------------------------- 1 | require('<%= name %>/core'); 2 | 3 | module("<%= title %>"); 4 | 5 | test("App is defined", function () { 6 | ok(typeof App !== 'undefined', "App is undefined"); 7 | }); 8 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/.couchapprc.tt: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "default": { 4 | "db": "http://localhost:5984/<%= name %>_dev" 5 | }, 6 | "prod": { 7 | "db": "http://localhost:5984/<%= name %>" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/states/start.js.tt: -------------------------------------------------------------------------------- 1 | require('<%= name %>/core'); 2 | 3 | App.StartState = Ember.ViewState.extend({ 4 | 5 | view: Ember.View.extend({ 6 | templateName: '<%= name %>/~templates/main_page' 7 | }) 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/state_manager.js.tt: -------------------------------------------------------------------------------- 1 | require('<%= name %>/core'); 2 | require('<%= name %>/states/start'); 3 | 4 | App.stateManager = Ember.StateManager.create({ 5 | 6 | rootElement: '#main', 7 | initialState: 'start', 8 | 9 | start: App.StartState 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/.couchappignore.tt: -------------------------------------------------------------------------------- 1 | [ 2 | // filenames matching these regexps will not be pushed to the database 3 | // uncomment to activate; separate entries with "," 4 | // ".*~$" 5 | // ".*\\.swp$" 6 | // ".*\\.bak$" 7 | "app/", 8 | "test_assets/", 9 | "tests/" 10 | ] -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/core.js.tt: -------------------------------------------------------------------------------- 1 | require('jquery'); 2 | require('ember'); 3 | require('ember-data'); 4 | require('<%= name %>/ext'); 5 | 6 | Ember.ENV.CP_DEFAULT_CACHEABLE = true; 7 | Ember.ENV.VIEW_PRESERVES_CONTEXT = true; 8 | 9 | App = Ember.Application.create({ 10 | VERSION: '0.1' 11 | }); 12 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'emberjs-couchapp/app_generator' 3 | 4 | module EmberjsCouchapp 5 | class CLI < Thor 6 | include Thor::Actions 7 | 8 | desc 'new PATH', 'Create a new CouchApp using Ember.js' 9 | def new(path) 10 | EmberjsCouchapp::AppGenerator.start([path]) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require File.expand_path('../lib/emberjs-couchapp/version', __FILE__) 3 | 4 | desc "run the specs" 5 | task :spec do 6 | sh "rspec -cfs spec" 7 | end 8 | 9 | desc "build gem" 10 | task :build do 11 | sh "gem build emberjs-couchapp.gemspec" 12 | end 13 | 14 | desc "install gem" 15 | task :install => :build do 16 | sh "gem install pkg/emberjs-couchapp-#{EmberjsCouchapp::VERSION}.gem" 17 | end 18 | 19 | task :default => :spec -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'colored' 4 | 5 | gem 'guard' 6 | gem 'guard-rake' 7 | 8 | gem 'rack' 9 | gem 'rack-rewrite' 10 | gem 'rack-streaming-proxy' 11 | 12 | gem 'sass' 13 | gem 'compass' 14 | 15 | gem 'uglifier' 16 | gem 'yui-compressor' 17 | 18 | gem 'rake-pipeline', :git => 'https://github.com/livingsocial/rake-pipeline.git' 19 | gem 'rake-pipeline-web-filters', :git => 'https://github.com/wycats/rake-pipeline-web-filters.git' 20 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/app_generator.rb: -------------------------------------------------------------------------------- 1 | require 'thor/group' 2 | 3 | module EmberjsCouchapp 4 | class AppGenerator < Thor::Group 5 | include Thor::Actions 6 | 7 | source_root File.expand_path('../templates/app', __FILE__) 8 | 9 | argument :path 10 | 11 | def name 12 | File.basename(path) 13 | end 14 | 15 | def title 16 | name.capitalize 17 | end 18 | 19 | def create 20 | self.destination_root = File.expand_path(path, destination_root) 21 | directory '.' 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/config.ru.tt: -------------------------------------------------------------------------------- 1 | require 'rake-pipeline' 2 | require 'rake-pipeline/middleware' 3 | 4 | use Rake::Pipeline::Middleware, 'Assetfile' 5 | 6 | require 'rack/streaming_proxy' 7 | use Rack::StreamingProxy do |request| 8 | if request.path.start_with?('/<%= name %>') 9 | "http://127.0.0.1:5984#{request.path}?#{request.query_string}" 10 | end 11 | end 12 | 13 | require 'rack-rewrite' 14 | use Rack::Rewrite do 15 | rewrite %r{^(.*)\/$}, '$1/_attachments/index.html' 16 | end 17 | 18 | run Rack::Directory.new('.') 19 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/lib/ext.js.tt: -------------------------------------------------------------------------------- 1 | var get = Ember.get, fmt = Ember.String.fmt; 2 | 3 | Ember.View.reopen({ 4 | templateForName: function(name, type) { 5 | if (!name) { return; } 6 | 7 | var templates = get(this, 'templates'), 8 | template = get(templates, name); 9 | 10 | if (!template) { 11 | try { 12 | template = require(name); 13 | } catch (e) { 14 | throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name])); 15 | } 16 | } 17 | 18 | return template; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/index.html.tt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | 9 |
10 | 17 |
18 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Clemens Müller 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | emberjs-couchapp (0.0.4) 5 | thor 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | coderay (1.0.6) 11 | diff-lcs (1.1.3) 12 | method_source (0.7.1) 13 | pry (0.9.9.6) 14 | coderay (~> 1.0.5) 15 | method_source (~> 0.7.1) 16 | slop (>= 2.4.4, < 3) 17 | rake (0.9.2.2) 18 | rspec (2.10.0) 19 | rspec-core (~> 2.10.0) 20 | rspec-expectations (~> 2.10.0) 21 | rspec-mocks (~> 2.10.0) 22 | rspec-core (2.10.1) 23 | rspec-expectations (2.10.0) 24 | diff-lcs (~> 1.1.3) 25 | rspec-mocks (2.10.1) 26 | slop (2.4.4) 27 | thor (0.15.2) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | emberjs-couchapp! 34 | pry 35 | rake 36 | rspec 37 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Your Name Here 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/Rakefile.tt: -------------------------------------------------------------------------------- 1 | APPNAME = '<%= name %>' 2 | 3 | require 'colored' 4 | require 'rake-pipeline' 5 | 6 | desc "Build #{APPNAME}" 7 | task :build do 8 | Rake::Pipeline::Project.new('Assetfile').invoke 9 | end 10 | 11 | desc "Clean #{APPNAME}" 12 | task :clean do 13 | Rake::Pipeline::Project.new('Assetfile').clean 14 | end 15 | 16 | desc "Run tests with PhantomJS" 17 | task :test => :build do 18 | unless system("which phantomjs > /dev/null 2>&1") 19 | abort "PhantomJS is not installed. Download from http://phantomjs.org/" 20 | end 21 | 22 | cmd = "phantomjs tests/run-tests.js \"file://#{File.dirname(__FILE__)}/tests/index.html\"" 23 | 24 | # Run the tests 25 | puts "Running #{APPNAME} tests" 26 | success = system(cmd) 27 | 28 | if success 29 | puts "Tests Passed".green 30 | else 31 | puts "Tests Failed".red 32 | exit(1) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /emberjs-couchapp.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/emberjs-couchapp/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Clemens Müller"] 6 | gem.email = ["cmueller.418@gmail.com"] 7 | gem.description = %q{Create CouchApp which uses Ember.js} 8 | gem.summary = %q{CouchApp using Ember.js} 9 | gem.homepage = "https://github.com/pangratz/emberjs-couchapp-gem" 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "emberjs-couchapp" 15 | gem.require_paths = ["lib"] 16 | gem.version = EmberjsCouchapp::VERSION 17 | 18 | gem.add_runtime_dependency 'thor' 19 | 20 | gem.add_development_dependency 'rake' 21 | gem.add_development_dependency 'rspec' 22 | gem.add_development_dependency 'pry' 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | require 'stringio' 3 | 4 | module EmberSpecHelpers 5 | def capture_io 6 | original_stdout, original_stderr = $stdout, $stderr 7 | $stdout, $stderr = StringIO.new, StringIO.new 8 | yield 9 | return $stdout.string, $stderr.string 10 | ensure 11 | $stdout, $stderr = original_stdout, original_stderr 12 | end 13 | 14 | end 15 | 16 | RSpec::Matchers.define :have_contents do |expected| 17 | match do |file| 18 | actual = file.read 19 | case expected 20 | when String; expected == actual 21 | when Regexp; expected =~ actual 22 | end 23 | end 24 | end 25 | 26 | RSpec.configure do |c| 27 | c.include EmberSpecHelpers 28 | 29 | original = Dir.pwd 30 | 31 | def tmp 32 | File.expand_path("../tmp", __FILE__) 33 | end 34 | 35 | c.before do 36 | FileUtils.rm_rf(tmp) 37 | FileUtils.mkdir_p(tmp) 38 | Dir.chdir(tmp) 39 | end 40 | 41 | c.after do 42 | Dir.chdir(original) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 |
11 |
test markup
12 | 13 | 14 | 15 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/plugins/loader.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | function requireWrapper(self) { 3 | var require = function() { 4 | return self.require.apply(self, arguments); 5 | }; 6 | require.exists = function() { 7 | return self.exists.apply(self, arguments); 8 | }; 9 | return require; 10 | } 11 | 12 | var Loader = function() { 13 | this.modules = {}; 14 | this.loaded = {}; 15 | this.exports = {}; 16 | return this; 17 | }; 18 | 19 | Loader.prototype.require = function(name) { 20 | if (!this.loaded[name]) { 21 | var module = this.modules[name]; 22 | if (module) { 23 | var require = requireWrapper(this); 24 | try { 25 | this.exports[name] = module.call(window, require); 26 | return this.exports[name]; 27 | } finally { 28 | this.loaded[name] = true; 29 | } 30 | } else { 31 | throw "The module '" + name + "' has not been registered"; 32 | } 33 | } 34 | return this.exports[name]; 35 | }; 36 | 37 | Loader.prototype.register = function(name, module) { 38 | if (this.exists(name)) { 39 | throw "The module '" + name + "' has already been registered"; 40 | } 41 | this.modules[name] = module; 42 | return true; 43 | }; 44 | 45 | Loader.prototype.unregister = function(name) { 46 | var loaded = !!this.loaded[name]; 47 | if (loaded) { 48 | delete this.exports[name]; 49 | delete this.modules[name]; 50 | delete this.loaded[name]; 51 | } 52 | return loaded; 53 | }; 54 | 55 | Loader.prototype.exists = function(name) { 56 | return name in this.modules; 57 | }; 58 | 59 | window.loader = new Loader(); 60 | })(this); 61 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/README.md: -------------------------------------------------------------------------------- 1 | Couchapp using Ember.js 2 | ======================= 3 | 4 | Couchapp using Ember.js and Rake Pipeline. 5 | 6 | Running 7 | ------- 8 | 9 | $ bundle install 10 | $ bundle exec rackup 11 | 12 | App Structure 13 | ------------- 14 | 15 | ember-skeleton 16 | ├── Assetfile - App build file 17 | ├── Gemfile - Package dependencies for rakep/rack 18 | ├── Gemfile.lock - Here be dragons: don't touch, always include 19 | ├── app - App specific code 20 | │ ├── css - App CSS or SCSS (.scss) 21 | │ ├── lib - App code, *modularized during build* 22 | │ ├── modules - Module code, *already modularized* 23 | │ ├── plugins - Plugins (e.g. jquery.jsonrpc.js) 24 | │ │ └── loader.js - JS module loader 25 | │ ├── static - Static files, never touched, copied over during build 26 | │ ├── templates - Handlebars templates, *modularized during build* 27 | │ ├── tests - QUnit application tests 28 | │ └── vendor - Vendor code, *modularized during build* 29 | ├── _attachments - Built out asset files, minified in production 30 | | ├── index.html 31 | │ ├── app.css - Built out app CSS/SCSS 32 | │ ├── app.js - Built out app JS 33 | │ └── loader.js - Built out JS module loader 34 | ├── test_assets - Built out test asset files 35 | ├── config.ru - Rack development web server configuration 36 | ├── tests - QUnit testing files 37 | │ ├── index.html - The testing entry point 38 | │ ├── qunit - Testing support files 39 | │ └── run-tests.js - The PhantomJS QUnit test runner 40 | └── tmp - Temporary build files used by rakep 41 | 42 | Testing 43 | ------- 44 | 45 | You can test the app by invoking: 46 | 47 | $ bundle exec rake test 48 | 49 | This executes the tests by using [PhantomJS](http://www.phantomjs.org/), 50 | which you need to have installed. 51 | 52 | Or you can run the tests via: 53 | 54 | $ bundle exec rackup 55 | $ open http://localhost:9292/tests/index.html 56 | 57 | If you want to continuously run the tests every time a file changes, invoke: 58 | 59 | $ bundle exec guard 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gem to create a CouchApp using Ember.js [![Build Status](https://secure.travis-ci.org/pangratz/emberjs-couchapp-gem.png?branch=master)](http://travis-ci.org/pangratz/emberjs-couchapp-gem) [![Dependency Status](https://gemnasium.com/pangratz/emberjs-couchapp-gem.png)](https://gemnasium.com/pangratz/emberjs-couchapp-gem) 2 | ======================================= 3 | 4 | Installation 5 | ------------ 6 | 7 | $ gem install emberjs-couchapp 8 | 9 | Usage 10 | ----- 11 | 12 | $ emberjs-couchapp new appname 13 | 14 | Created App Structure 15 | --------------------- 16 | 17 | appname 18 | ├── Assetfile - App build file 19 | ├── Gemfile - Package dependencies for rakep/rack 20 | ├── Gemfile.lock - Here be dragons: don't touch, always include 21 | ├── app - App specific code 22 | │ ├── index.html - The app entry point 23 | │ ├── css - App CSS or SCSS (.scss) 24 | │ ├── lib - App code, *modularized during build* 25 | │ ├── modules - Module code, *already modularized* 26 | │ ├── plugins - Plugins (e.g. jquery.jsonrpc.js) 27 | │ │ └── loader.js - JS module loader 28 | │ ├── static - Static files, never touched, copied over during build 29 | │ ├── templates - Handlebars templates, *modularized during build* 30 | │ ├── tests - App tests 31 | │ └── vendor - Vendor code, *modularized during build* 32 | ├── _attachments - Built out asset files 33 | ├── config.ru - Rack development web server configuration 34 | └── tmp - Temporary build files used by rakep 35 | 36 | Testing 37 | ------- 38 | 39 | You can test the app by invoking 40 | 41 | $ bundle exec rake test 42 | 43 | This executes the tests by using [Phantom.JS](http://www.phantomjs.org/), which you need to have installed. 44 | 45 | Or you can run the tests via 46 | 47 | $ bundle exec rackup 48 | $ open http://localhost:9292/tests/index.html 49 | 50 | You can automatically execute the tests every time a file changes via 51 | 52 | $ bundle exec guard 53 | 54 | 55 | Developing this Gem 56 | ------------------- 57 | 58 | $ gem install gem-release 59 | $ gem bump // to bump version 60 | $ gem release --tag // to release new version -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/tests/run-tests.js: -------------------------------------------------------------------------------- 1 | // PhantomJS QUnit Test Runner 2 | 3 | var args = phantom.args; 4 | if (args.length < 1 || args.length > 2) { 5 | console.log("Usage: " + phantom.scriptName + " "); 6 | phantom.exit(1); 7 | } 8 | 9 | var page = require('webpage').create(); 10 | 11 | var depRe = /^DEPRECATION:/; 12 | page.onConsoleMessage = function(msg) { 13 | if (!depRe.test(msg)) console.log(msg); 14 | }; 15 | 16 | var uri = args[0]; 17 | page.open(uri, function(status) { 18 | if (status !== 'success') { 19 | console.error("Unable to access: " + uri + " [" + status + "]"); 20 | phantom.exit(1); 21 | } else { 22 | page.evaluate(addLogging); 23 | 24 | var timeout = parseInt(args[1] || 30000, 10); 25 | var start = Date.now(); 26 | var interval = setInterval(function() { 27 | if (Date.now() > start + timeout) { 28 | console.error("Tests timed out"); 29 | phantom.exit(1); 30 | } else { 31 | var qunitDone = page.evaluate(function() { 32 | return window.qunitDone; 33 | }); 34 | 35 | if (qunitDone) { 36 | clearInterval(interval); 37 | if (qunitDone.failed > 0) { 38 | phantom.exit(1); 39 | } else { 40 | phantom.exit(); 41 | } 42 | } 43 | } 44 | }, 500); 45 | } 46 | }); 47 | 48 | function addLogging() { 49 | var testErrors = []; 50 | var assertionErrors = []; 51 | 52 | QUnit.moduleDone(function(context) { 53 | if (context.failed) { 54 | var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n"); 55 | console.error(msg); 56 | testErrors = []; 57 | } 58 | }); 59 | 60 | QUnit.testDone(function(context) { 61 | if (context.failed) { 62 | var msg = " Test Failed: " + context.name + assertionErrors.join(" "); 63 | testErrors.push(msg); 64 | assertionErrors = []; 65 | } 66 | }); 67 | 68 | QUnit.log(function(context) { 69 | if (context.result) return; 70 | 71 | var msg = "\n Assertion Failed:"; 72 | if (context.message) { 73 | msg += " " + context.message; 74 | } 75 | 76 | if (context.expected) { 77 | msg += "\n Expected: " + context.expected + ", Actual: " + context.actual; 78 | } 79 | 80 | assertionErrors.push(msg); 81 | }); 82 | 83 | QUnit.done(function(context) { 84 | var stats = [ 85 | "Time: " + context.runtime + "ms", 86 | "Total: " + context.total, 87 | "Passed: " + context.passed, 88 | "Failed: " + context.failed 89 | ]; 90 | console.log(stats.join(", ")); 91 | window.qunitDone = context; 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/Assetfile.tt: -------------------------------------------------------------------------------- 1 | APPNAME = '<%= name %>' 2 | 3 | require 'json' 4 | require 'rake-pipeline-web-filters' 5 | 6 | WebFilters = Rake::Pipeline::Web::Filters 7 | 8 | class LoaderFilter < WebFilters::MinispadeFilter 9 | def generate_output(inputs, output) 10 | inputs.each do |input| 11 | code = input.read 12 | module_id = @module_id_generator.call(input) 13 | contents = "function(require) {\n#{code}\n}" 14 | ret = "\nloader.register('#{module_id}', #{contents});\n" 15 | output.write ret 16 | end 17 | end 18 | end 19 | 20 | class EmberAssertFilter < Filter 21 | def generate_output(inputs, output) 22 | inputs.each do |input| 23 | result = input.read 24 | result.gsub!(/ember_assert\((.*)\);/, '') 25 | output.write(result) 26 | end 27 | end 28 | end 29 | 30 | class HandlebarsFilter < Filter 31 | def generate_output(inputs, output) 32 | inputs.each do |input| 33 | code = input.read.to_json 34 | name = File.basename(input.path, '.handlebars') 35 | output.write "\nreturn Ember.Handlebars.compile(#{code});\n" 36 | end 37 | end 38 | end 39 | 40 | output '_attachments' 41 | 42 | input 'app' do 43 | match 'index.html' do 44 | concat 'index.html' 45 | end 46 | 47 | match 'lib/**/*.js' do 48 | filter LoaderFilter, 49 | :module_id_generator => proc { |input| 50 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '') 51 | } 52 | 53 | if ENV['RAKEP_MODE'] == 'production' 54 | filter EmberAssertFilter 55 | uglify {|input| input} 56 | end 57 | concat 'app.js' 58 | end 59 | 60 | match 'vendor/**/*.js' do 61 | filter LoaderFilter, 62 | :module_id_generator => proc { |input| 63 | input.path.sub(/^vendor\//, '').sub(/\.js$/, '') 64 | } 65 | 66 | if ENV['RAKEP_MODE'] == 'production' 67 | filter EmberAssertFilter 68 | uglify {|input| input} 69 | end 70 | concat %w[ 71 | vendor/jquery.js 72 | vendor/ember.js 73 | vendor/ember-data.js 74 | ], 'app.js' 75 | end 76 | 77 | match 'modules/**/*.js' do 78 | if ENV['RAKEP_MODE'] == 'production' 79 | filter EmberAssertFilter 80 | uglify {|input| input} 81 | end 82 | concat 'app.js' 83 | end 84 | 85 | match 'plugins/**/*.js' do 86 | if ENV['RAKEP_MODE'] == 'production' 87 | uglify {|input| input} 88 | end 89 | concat do |input| 90 | input.sub(/plugins\//, '') 91 | end 92 | end 93 | 94 | match 'templates/**/*.handlebars' do 95 | filter HandlebarsFilter 96 | filter LoaderFilter, 97 | :module_id_generator => proc { |input| 98 | input.path.sub(/^templates\//, "#{APPNAME}/~templates/").sub(/\.handlebars$/, '') 99 | } 100 | if ENV['RAKEP_MODE'] == 'production' 101 | uglify {|input| input} 102 | end 103 | concat 'app.js' 104 | end 105 | 106 | match 'css/**/*.css' do 107 | if ENV['RAKEP_MODE'] == 'production' 108 | yui_css 109 | end 110 | concat ['bootstrap.css', 'main.css'], 'app.css' 111 | end 112 | 113 | match 'css/**/*.scss' do 114 | sass 115 | if ENV['RAKEP_MODE'] == 'production' 116 | yui_css 117 | end 118 | concat 'app.css' 119 | end 120 | 121 | match "static/**/*" do 122 | concat do |input| 123 | input.sub(/static\//, '') 124 | end 125 | end 126 | end 127 | 128 | output 'test_assets' 129 | 130 | input 'app' do 131 | match 'tests/**/*.js' do 132 | filter LoaderFilter, 133 | :module_id_generator => proc { |input| 134 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '') 135 | } 136 | concat 'app-tests.js' 137 | end 138 | end 139 | 140 | # vim: filetype=ruby 141 | -------------------------------------------------------------------------------- /spec/ember/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'emberjs-couchapp/cli' 3 | 4 | describe EmberjsCouchapp::CLI do 5 | def ember(*args) 6 | capture_io { 7 | EmberjsCouchapp::CLI.start(args) 8 | } 9 | end 10 | 11 | describe "new" do 12 | shared_examples "a new app" do 13 | before do 14 | ember('new', app_path) 15 | end 16 | 17 | def path(*segments) 18 | Pathname.new(File.join(tmp, app_path, *segments)) 19 | end 20 | 21 | let(:app_name) { File.basename(app_path) } 22 | 23 | it 'creates root files' do 24 | path.should exist 25 | path('.gitignore').should exist 26 | path('Assetfile').should exist 27 | path('config.ru').should exist 28 | path('Gemfile').should exist 29 | path('Guardfile').should exist 30 | path('LICENSE').should exist 31 | path('Rakefile').should exist 32 | path('README.md').should exist 33 | end 34 | 35 | it 'creates couchapp specific root files' do 36 | path('_id').should exist 37 | path('.couchappignore').should exist 38 | path('.couchapprc').should exist 39 | path('couchapp.json').should exist 40 | path('language').should exist 41 | end 42 | 43 | it 'creates a sample CouchDB filter, list, show and view' do 44 | path('filters/my_filter.js').should exist 45 | path('lists/my_list.js').should exist 46 | path('shows/my_show.js').should exist 47 | path('views/my_view/map.js').should exist 48 | path('views/my_view/reduce.js').should exist 49 | end 50 | 51 | it 'creates app root files' do 52 | path('app/index.html').should exist 53 | end 54 | 55 | it 'creates app/css files' do 56 | path('app/css').should exist 57 | path('app/css/bootstrap.css').should exist 58 | path('app/css/main.css').should exist 59 | end 60 | 61 | it 'creates app/lib files' do 62 | path('app/lib').should exist 63 | path('app/lib/controllers').should exist 64 | path('app/lib/core.js').should exist 65 | path('app/lib/ext.js').should exist 66 | path('app/lib/main.js').should exist 67 | path('app/lib/models').should exist 68 | path('app/lib/state_manager.js').should exist 69 | path('app/lib/states').should exist 70 | path('app/lib/states/start.js').should exist 71 | path('app/lib/store.js').should exist 72 | path('app/lib/views').should exist 73 | end 74 | 75 | it 'creates app/modules files' do 76 | path('app/modules').should exist 77 | end 78 | 79 | it 'creates app/plugins files' do 80 | path('app/plugins').should exist 81 | path('app/plugins/loader.js').should exist 82 | end 83 | 84 | it 'creates app/static files' do 85 | path('app/static').should exist 86 | path('app/static/img').should exist 87 | path('app/static/img/glyphicons-halflings.png').should exist 88 | path('app/static/img/glyphicons-halflings-white.png').should exist 89 | end 90 | 91 | it 'creates app/templates files' do 92 | path('app/templates').should exist 93 | path('app/templates/main_page.handlebars').should exist 94 | end 95 | 96 | it 'creates app/tests files' do 97 | path('app/tests').should exist 98 | path("app/tests/#{app_name}_tests.js").should exist 99 | end 100 | 101 | it 'creates app/vendor files' do 102 | path('app/vendor').should exist 103 | path('app/vendor/ember-data.js').should exist 104 | path('app/vendor/ember.js').should exist 105 | path('app/vendor/jquery.js').should exist 106 | end 107 | 108 | it 'creates tests/ files' do 109 | path('tests').should exist 110 | path('tests/qunit').should exist 111 | path('tests/qunit/qunit.css').should exist 112 | path('tests/qunit/qunit.js').should exist 113 | path('tests/index.html').should exist 114 | path('tests/run-tests.js').should exist 115 | end 116 | end 117 | 118 | context "given a name" do 119 | let(:app_path) { "inky" } 120 | include_examples "a new app" 121 | end 122 | 123 | context "given a path" do 124 | let(:app_path) { "path/to/inky" } 125 | include_examples "a new app" 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/tests/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.5.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | #qunit-testresult .module-name { 224 | font-weight: bold; 225 | } 226 | 227 | /** Fixture */ 228 | 229 | #qunit-fixture { 230 | position: absolute; 231 | top: -10000px; 232 | left: -10000px; 233 | width: 1000px; 234 | height: 1000px; 235 | } -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/tests/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.5.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | var x = "qunit-test-string"; 17 | try { 18 | sessionStorage.setItem(x, x); 19 | sessionStorage.removeItem(x); 20 | return true; 21 | } catch(e) { 22 | return false; 23 | } 24 | }()) 25 | }; 26 | 27 | var testId = 0, 28 | toString = Object.prototype.toString, 29 | hasOwn = Object.prototype.hasOwnProperty; 30 | 31 | var Test = function(name, testName, expected, async, callback) { 32 | this.name = name; 33 | this.testName = testName; 34 | this.expected = expected; 35 | this.async = async; 36 | this.callback = callback; 37 | this.assertions = []; 38 | }; 39 | Test.prototype = { 40 | init: function() { 41 | var tests = id("qunit-tests"); 42 | if (tests) { 43 | var b = document.createElement("strong"); 44 | b.innerHTML = "Running " + this.name; 45 | var li = document.createElement("li"); 46 | li.appendChild( b ); 47 | li.className = "running"; 48 | li.id = this.id = "test-output" + testId++; 49 | tests.appendChild( li ); 50 | } 51 | }, 52 | setup: function() { 53 | if (this.module != config.previousModule) { 54 | if ( config.previousModule ) { 55 | runLoggingCallbacks('moduleDone', QUnit, { 56 | name: config.previousModule, 57 | failed: config.moduleStats.bad, 58 | passed: config.moduleStats.all - config.moduleStats.bad, 59 | total: config.moduleStats.all 60 | } ); 61 | } 62 | config.previousModule = this.module; 63 | config.moduleStats = { all: 0, bad: 0 }; 64 | runLoggingCallbacks( 'moduleStart', QUnit, { 65 | name: this.module 66 | } ); 67 | } else if (config.autorun) { 68 | runLoggingCallbacks( 'moduleStart', QUnit, { 69 | name: this.module 70 | } ); 71 | } 72 | 73 | config.current = this; 74 | this.testEnvironment = extend({ 75 | setup: function() {}, 76 | teardown: function() {} 77 | }, this.moduleTestEnvironment); 78 | 79 | runLoggingCallbacks( 'testStart', QUnit, { 80 | name: this.testName, 81 | module: this.module 82 | }); 83 | 84 | // allow utility functions to access the current test environment 85 | // TODO why?? 86 | QUnit.current_testEnvironment = this.testEnvironment; 87 | 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | if ( config.notrycatch ) { 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | return; 94 | } 95 | try { 96 | this.testEnvironment.setup.call(this.testEnvironment); 97 | } catch(e) { 98 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 99 | } 100 | }, 101 | run: function() { 102 | config.current = this; 103 | 104 | var running = id("qunit-testresult"); 105 | 106 | if ( running ) { 107 | running.innerHTML = "Running:
" + this.name; 108 | } 109 | 110 | if ( this.async ) { 111 | QUnit.stop(); 112 | } 113 | 114 | if ( config.notrycatch ) { 115 | this.callback.call(this.testEnvironment); 116 | return; 117 | } 118 | try { 119 | this.callback.call(this.testEnvironment); 120 | } catch(e) { 121 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + ": " + e.message, extractStacktrace( e, 1 ) ); 122 | // else next test will carry the responsibility 123 | saveGlobal(); 124 | 125 | // Restart the tests if they're blocking 126 | if ( config.blocking ) { 127 | QUnit.start(); 128 | } 129 | } 130 | }, 131 | teardown: function() { 132 | config.current = this; 133 | if ( config.notrycatch ) { 134 | this.testEnvironment.teardown.call(this.testEnvironment); 135 | return; 136 | } else { 137 | try { 138 | this.testEnvironment.teardown.call(this.testEnvironment); 139 | } catch(e) { 140 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 141 | } 142 | } 143 | checkPollution(); 144 | }, 145 | finish: function() { 146 | config.current = this; 147 | if ( this.expected != null && this.expected != this.assertions.length ) { 148 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 149 | } else if ( this.expected == null && !this.assertions.length ) { 150 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ); 151 | } 152 | 153 | var good = 0, bad = 0, 154 | li, i, 155 | tests = id("qunit-tests"); 156 | 157 | config.stats.all += this.assertions.length; 158 | config.moduleStats.all += this.assertions.length; 159 | 160 | if ( tests ) { 161 | var ol = document.createElement("ol"); 162 | 163 | for ( i = 0; i < this.assertions.length; i++ ) { 164 | var assertion = this.assertions[i]; 165 | 166 | li = document.createElement("li"); 167 | li.className = assertion.result ? "pass" : "fail"; 168 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 169 | ol.appendChild( li ); 170 | 171 | if ( assertion.result ) { 172 | good++; 173 | } else { 174 | bad++; 175 | config.stats.bad++; 176 | config.moduleStats.bad++; 177 | } 178 | } 179 | 180 | // store result when possible 181 | if ( QUnit.config.reorder && defined.sessionStorage ) { 182 | if (bad) { 183 | sessionStorage.setItem("qunit-test-" + this.module + "-" + this.testName, bad); 184 | } else { 185 | sessionStorage.removeItem("qunit-test-" + this.module + "-" + this.testName); 186 | } 187 | } 188 | 189 | if (bad === 0) { 190 | ol.style.display = "none"; 191 | } 192 | 193 | var b = document.createElement("strong"); 194 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 195 | 196 | var a = document.createElement("a"); 197 | a.innerHTML = "Rerun"; 198 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 199 | 200 | addEvent(b, "click", function() { 201 | var next = b.nextSibling.nextSibling, 202 | display = next.style.display; 203 | next.style.display = display === "none" ? "block" : "none"; 204 | }); 205 | 206 | addEvent(b, "dblclick", function(e) { 207 | var target = e && e.target ? e.target : window.event.srcElement; 208 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 209 | target = target.parentNode; 210 | } 211 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 212 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 213 | } 214 | }); 215 | 216 | li = id(this.id); 217 | li.className = bad ? "fail" : "pass"; 218 | li.removeChild( li.firstChild ); 219 | li.appendChild( b ); 220 | li.appendChild( a ); 221 | li.appendChild( ol ); 222 | 223 | } else { 224 | for ( i = 0; i < this.assertions.length; i++ ) { 225 | if ( !this.assertions[i].result ) { 226 | bad++; 227 | config.stats.bad++; 228 | config.moduleStats.bad++; 229 | } 230 | } 231 | } 232 | 233 | QUnit.reset(); 234 | 235 | runLoggingCallbacks( 'testDone', QUnit, { 236 | name: this.testName, 237 | module: this.module, 238 | failed: bad, 239 | passed: this.assertions.length - bad, 240 | total: this.assertions.length 241 | } ); 242 | }, 243 | 244 | queue: function() { 245 | var test = this; 246 | synchronize(function() { 247 | test.init(); 248 | }); 249 | function run() { 250 | // each of these can by async 251 | synchronize(function() { 252 | test.setup(); 253 | }); 254 | synchronize(function() { 255 | test.run(); 256 | }); 257 | synchronize(function() { 258 | test.teardown(); 259 | }); 260 | synchronize(function() { 261 | test.finish(); 262 | }); 263 | } 264 | // defer when previous test run passed, if storage is available 265 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-test-" + this.module + "-" + this.testName); 266 | if (bad) { 267 | run(); 268 | } else { 269 | synchronize(run, true); 270 | } 271 | } 272 | 273 | }; 274 | 275 | var QUnit = { 276 | 277 | // call on start of module test to prepend name to all tests 278 | module: function(name, testEnvironment) { 279 | config.currentModule = name; 280 | config.currentModuleTestEnviroment = testEnvironment; 281 | }, 282 | 283 | asyncTest: function(testName, expected, callback) { 284 | if ( arguments.length === 2 ) { 285 | callback = expected; 286 | expected = null; 287 | } 288 | 289 | QUnit.test(testName, expected, callback, true); 290 | }, 291 | 292 | test: function(testName, expected, callback, async) { 293 | var name = '' + escapeInnerText(testName) + ''; 294 | 295 | if ( arguments.length === 2 ) { 296 | callback = expected; 297 | expected = null; 298 | } 299 | 300 | if ( config.currentModule ) { 301 | name = '' + config.currentModule + ": " + name; 302 | } 303 | 304 | if ( !validTest(config.currentModule + ": " + testName) ) { 305 | return; 306 | } 307 | 308 | var test = new Test(name, testName, expected, async, callback); 309 | test.module = config.currentModule; 310 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 311 | test.queue(); 312 | }, 313 | 314 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 315 | expect: function(asserts) { 316 | config.current.expected = asserts; 317 | }, 318 | 319 | // Asserts true. 320 | // @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 321 | ok: function(result, msg) { 322 | if (!config.current) { 323 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 324 | } 325 | result = !!result; 326 | var details = { 327 | result: result, 328 | message: msg 329 | }; 330 | msg = escapeInnerText(msg || (result ? "okay" : "failed")); 331 | if ( !result ) { 332 | var source = sourceFromStacktrace(2); 333 | if (source) { 334 | details.source = source; 335 | msg += '
Source:
' + escapeInnerText(source) + '
'; 336 | } 337 | } 338 | runLoggingCallbacks( 'log', QUnit, details ); 339 | config.current.assertions.push({ 340 | result: result, 341 | message: msg 342 | }); 343 | }, 344 | 345 | // Checks that the first two arguments are equal, with an optional message. Prints out both actual and expected values. 346 | // @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 347 | equal: function(actual, expected, message) { 348 | QUnit.push(expected == actual, actual, expected, message); 349 | }, 350 | 351 | notEqual: function(actual, expected, message) { 352 | QUnit.push(expected != actual, actual, expected, message); 353 | }, 354 | 355 | deepEqual: function(actual, expected, message) { 356 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 357 | }, 358 | 359 | notDeepEqual: function(actual, expected, message) { 360 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 361 | }, 362 | 363 | strictEqual: function(actual, expected, message) { 364 | QUnit.push(expected === actual, actual, expected, message); 365 | }, 366 | 367 | notStrictEqual: function(actual, expected, message) { 368 | QUnit.push(expected !== actual, actual, expected, message); 369 | }, 370 | 371 | raises: function(block, expected, message) { 372 | var actual, ok = false; 373 | 374 | if (typeof expected === 'string') { 375 | message = expected; 376 | expected = null; 377 | } 378 | 379 | try { 380 | block.call(config.current.testEnvironment); 381 | } catch (e) { 382 | actual = e; 383 | } 384 | 385 | if (actual) { 386 | // we don't want to validate thrown error 387 | if (!expected) { 388 | ok = true; 389 | // expected is a regexp 390 | } else if (QUnit.objectType(expected) === "regexp") { 391 | ok = expected.test(actual); 392 | // expected is a constructor 393 | } else if (actual instanceof expected) { 394 | ok = true; 395 | // expected is a validation function which returns true is validation passed 396 | } else if (expected.call({}, actual) === true) { 397 | ok = true; 398 | } 399 | } 400 | 401 | QUnit.ok(ok, message); 402 | }, 403 | 404 | start: function(count) { 405 | config.semaphore -= count || 1; 406 | if (config.semaphore > 0) { 407 | // don't start until equal number of stop-calls 408 | return; 409 | } 410 | if (config.semaphore < 0) { 411 | // ignore if start is called more often then stop 412 | config.semaphore = 0; 413 | } 414 | // A slight delay, to avoid any current callbacks 415 | if ( defined.setTimeout ) { 416 | window.setTimeout(function() { 417 | if (config.semaphore > 0) { 418 | return; 419 | } 420 | if ( config.timeout ) { 421 | clearTimeout(config.timeout); 422 | } 423 | 424 | config.blocking = false; 425 | process(true); 426 | }, 13); 427 | } else { 428 | config.blocking = false; 429 | process(true); 430 | } 431 | }, 432 | 433 | stop: function(count) { 434 | config.semaphore += count || 1; 435 | config.blocking = true; 436 | 437 | if ( config.testTimeout && defined.setTimeout ) { 438 | clearTimeout(config.timeout); 439 | config.timeout = window.setTimeout(function() { 440 | QUnit.ok( false, "Test timed out" ); 441 | config.semaphore = 1; 442 | QUnit.start(); 443 | }, config.testTimeout); 444 | } 445 | } 446 | }; 447 | 448 | //We want access to the constructor's prototype 449 | (function() { 450 | function F(){} 451 | F.prototype = QUnit; 452 | QUnit = new F(); 453 | //Make F QUnit's constructor so that we can add to the prototype later 454 | QUnit.constructor = F; 455 | }()); 456 | 457 | // deprecated; still export them to window to provide clear error messages 458 | // next step: remove entirely 459 | QUnit.equals = function() { 460 | QUnit.push(false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 461 | }; 462 | QUnit.same = function() { 463 | QUnit.push(false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 464 | }; 465 | 466 | // Maintain internal state 467 | var config = { 468 | // The queue of tests to run 469 | queue: [], 470 | 471 | // block until document ready 472 | blocking: true, 473 | 474 | // when enabled, show only failing tests 475 | // gets persisted through sessionStorage and can be changed in UI via checkbox 476 | hidepassed: false, 477 | 478 | // by default, run previously failed tests first 479 | // very useful in combination with "Hide passed tests" checked 480 | reorder: true, 481 | 482 | // by default, modify document.title when suite is done 483 | altertitle: true, 484 | 485 | urlConfig: ['noglobals', 'notrycatch'], 486 | 487 | //logging callback queues 488 | begin: [], 489 | done: [], 490 | log: [], 491 | testStart: [], 492 | testDone: [], 493 | moduleStart: [], 494 | moduleDone: [] 495 | }; 496 | 497 | // Load paramaters 498 | (function() { 499 | var location = window.location || { search: "", protocol: "file:" }, 500 | params = location.search.slice( 1 ).split( "&" ), 501 | length = params.length, 502 | urlParams = {}, 503 | current; 504 | 505 | if ( params[ 0 ] ) { 506 | for ( var i = 0; i < length; i++ ) { 507 | current = params[ i ].split( "=" ); 508 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 509 | // allow just a key to turn on a flag, e.g., test.html?noglobals 510 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 511 | urlParams[ current[ 0 ] ] = current[ 1 ]; 512 | } 513 | } 514 | 515 | QUnit.urlParams = urlParams; 516 | config.filter = urlParams.filter; 517 | 518 | // Figure out if we're running the tests from a server or not 519 | QUnit.isLocal = location.protocol === 'file:'; 520 | }()); 521 | 522 | // Expose the API as global variables, unless an 'exports' 523 | // object exists, in that case we assume we're in CommonJS - export everything at the end 524 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 525 | extend(window, QUnit); 526 | window.QUnit = QUnit; 527 | } 528 | 529 | // define these after exposing globals to keep them in these QUnit namespace only 530 | extend(QUnit, { 531 | config: config, 532 | 533 | // Initialize the configuration options 534 | init: function() { 535 | extend(config, { 536 | stats: { all: 0, bad: 0 }, 537 | moduleStats: { all: 0, bad: 0 }, 538 | started: +new Date(), 539 | updateRate: 1000, 540 | blocking: false, 541 | autostart: true, 542 | autorun: false, 543 | filter: "", 544 | queue: [], 545 | semaphore: 0 546 | }); 547 | 548 | var qunit = id( "qunit" ); 549 | if ( qunit ) { 550 | qunit.innerHTML = 551 | '

' + escapeInnerText( document.title ) + '

' + 552 | '

' + 553 | '
' + 554 | '

' + 555 | '
    '; 556 | } 557 | 558 | var tests = id( "qunit-tests" ), 559 | banner = id( "qunit-banner" ), 560 | result = id( "qunit-testresult" ); 561 | 562 | if ( tests ) { 563 | tests.innerHTML = ""; 564 | } 565 | 566 | if ( banner ) { 567 | banner.className = ""; 568 | } 569 | 570 | if ( result ) { 571 | result.parentNode.removeChild( result ); 572 | } 573 | 574 | if ( tests ) { 575 | result = document.createElement( "p" ); 576 | result.id = "qunit-testresult"; 577 | result.className = "result"; 578 | tests.parentNode.insertBefore( result, tests ); 579 | result.innerHTML = 'Running...
     '; 580 | } 581 | }, 582 | 583 | // Resets the test setup. Useful for tests that modify the DOM. 584 | // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 585 | reset: function() { 586 | if ( window.jQuery ) { 587 | jQuery( "#qunit-fixture" ).html( config.fixture ); 588 | } else { 589 | var main = id( 'qunit-fixture' ); 590 | if ( main ) { 591 | main.innerHTML = config.fixture; 592 | } 593 | } 594 | }, 595 | 596 | // Trigger an event on an element. 597 | // @example triggerEvent( document.body, "click" ); 598 | triggerEvent: function( elem, type, event ) { 599 | if ( document.createEvent ) { 600 | event = document.createEvent("MouseEvents"); 601 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 602 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 603 | elem.dispatchEvent( event ); 604 | 605 | } else if ( elem.fireEvent ) { 606 | elem.fireEvent("on"+type); 607 | } 608 | }, 609 | 610 | // Safe object type checking 611 | is: function( type, obj ) { 612 | return QUnit.objectType( obj ) == type; 613 | }, 614 | 615 | objectType: function( obj ) { 616 | if (typeof obj === "undefined") { 617 | return "undefined"; 618 | 619 | // consider: typeof null === object 620 | } 621 | if (obj === null) { 622 | return "null"; 623 | } 624 | 625 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 626 | 627 | switch (type) { 628 | case 'Number': 629 | if (isNaN(obj)) { 630 | return "nan"; 631 | } 632 | return "number"; 633 | case 'String': 634 | case 'Boolean': 635 | case 'Array': 636 | case 'Date': 637 | case 'RegExp': 638 | case 'Function': 639 | return type.toLowerCase(); 640 | } 641 | if (typeof obj === "object") { 642 | return "object"; 643 | } 644 | return undefined; 645 | }, 646 | 647 | push: function(result, actual, expected, message) { 648 | if (!config.current) { 649 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 650 | } 651 | var details = { 652 | result: result, 653 | message: message, 654 | actual: actual, 655 | expected: expected 656 | }; 657 | 658 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 659 | message = '' + message + ""; 660 | var output = message; 661 | if (!result) { 662 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 663 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 664 | output += ''; 665 | if (actual != expected) { 666 | output += ''; 667 | output += ''; 668 | } 669 | var source = sourceFromStacktrace(); 670 | if (source) { 671 | details.source = source; 672 | output += ''; 673 | } 674 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 675 | } 676 | 677 | runLoggingCallbacks( 'log', QUnit, details ); 678 | 679 | config.current.assertions.push({ 680 | result: !!result, 681 | message: output 682 | }); 683 | }, 684 | 685 | pushFailure: function(message, source) { 686 | var details = { 687 | result: false, 688 | message: message 689 | }; 690 | var output = escapeInnerText(message); 691 | if (source) { 692 | details.source = source; 693 | output += '
    Source:
    ' + escapeInnerText(source) + '
    '; 694 | } 695 | runLoggingCallbacks( 'log', QUnit, details ); 696 | config.current.assertions.push({ 697 | result: false, 698 | message: output 699 | }); 700 | }, 701 | 702 | url: function( params ) { 703 | params = extend( extend( {}, QUnit.urlParams ), params ); 704 | var querystring = "?", 705 | key; 706 | for ( key in params ) { 707 | if ( !hasOwn.call( params, key ) ) { 708 | continue; 709 | } 710 | querystring += encodeURIComponent( key ) + "=" + 711 | encodeURIComponent( params[ key ] ) + "&"; 712 | } 713 | return window.location.pathname + querystring.slice( 0, -1 ); 714 | }, 715 | 716 | extend: extend, 717 | id: id, 718 | addEvent: addEvent 719 | }); 720 | 721 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 722 | //Doing this allows us to tell if the following methods have been overwritten on the actual 723 | //QUnit object, which is a deprecated way of using the callbacks. 724 | extend(QUnit.constructor.prototype, { 725 | // Logging callbacks; all receive a single argument with the listed properties 726 | // run test/logs.html for any related changes 727 | begin: registerLoggingCallback('begin'), 728 | // done: { failed, passed, total, runtime } 729 | done: registerLoggingCallback('done'), 730 | // log: { result, actual, expected, message } 731 | log: registerLoggingCallback('log'), 732 | // testStart: { name } 733 | testStart: registerLoggingCallback('testStart'), 734 | // testDone: { name, failed, passed, total } 735 | testDone: registerLoggingCallback('testDone'), 736 | // moduleStart: { name } 737 | moduleStart: registerLoggingCallback('moduleStart'), 738 | // moduleDone: { name, failed, passed, total } 739 | moduleDone: registerLoggingCallback('moduleDone') 740 | }); 741 | 742 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 743 | config.autorun = true; 744 | } 745 | 746 | QUnit.load = function() { 747 | runLoggingCallbacks( 'begin', QUnit, {} ); 748 | 749 | // Initialize the config, saving the execution queue 750 | var oldconfig = extend({}, config); 751 | QUnit.init(); 752 | extend(config, oldconfig); 753 | 754 | config.blocking = false; 755 | 756 | var urlConfigHtml = '', len = config.urlConfig.length; 757 | for ( var i = 0, val; i < len; i++ ) { 758 | val = config.urlConfig[i]; 759 | config[val] = QUnit.urlParams[val]; 760 | urlConfigHtml += ''; 761 | } 762 | 763 | var userAgent = id("qunit-userAgent"); 764 | if ( userAgent ) { 765 | userAgent.innerHTML = navigator.userAgent; 766 | } 767 | var banner = id("qunit-header"); 768 | if ( banner ) { 769 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 770 | addEvent( banner, "change", function( event ) { 771 | var params = {}; 772 | params[ event.target.name ] = event.target.checked ? true : undefined; 773 | window.location = QUnit.url( params ); 774 | }); 775 | } 776 | 777 | var toolbar = id("qunit-testrunner-toolbar"); 778 | if ( toolbar ) { 779 | var filter = document.createElement("input"); 780 | filter.type = "checkbox"; 781 | filter.id = "qunit-filter-pass"; 782 | addEvent( filter, "click", function() { 783 | var ol = document.getElementById("qunit-tests"); 784 | if ( filter.checked ) { 785 | ol.className = ol.className + " hidepass"; 786 | } else { 787 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 788 | ol.className = tmp.replace(/ hidepass /, " "); 789 | } 790 | if ( defined.sessionStorage ) { 791 | if (filter.checked) { 792 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 793 | } else { 794 | sessionStorage.removeItem("qunit-filter-passed-tests"); 795 | } 796 | } 797 | }); 798 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 799 | filter.checked = true; 800 | var ol = document.getElementById("qunit-tests"); 801 | ol.className = ol.className + " hidepass"; 802 | } 803 | toolbar.appendChild( filter ); 804 | 805 | var label = document.createElement("label"); 806 | label.setAttribute("for", "qunit-filter-pass"); 807 | label.innerHTML = "Hide passed tests"; 808 | toolbar.appendChild( label ); 809 | } 810 | 811 | var main = id('qunit-fixture'); 812 | if ( main ) { 813 | config.fixture = main.innerHTML; 814 | } 815 | 816 | if (config.autostart) { 817 | QUnit.start(); 818 | } 819 | }; 820 | 821 | addEvent(window, "load", QUnit.load); 822 | 823 | // addEvent(window, "error") gives us a useless event object 824 | window.onerror = function( message, file, line ) { 825 | if ( QUnit.config.current ) { 826 | QUnit.pushFailure( message, file + ":" + line ); 827 | } else { 828 | QUnit.test( "global failure", function() { 829 | QUnit.pushFailure( message, file + ":" + line ); 830 | }); 831 | } 832 | }; 833 | 834 | function done() { 835 | config.autorun = true; 836 | 837 | // Log the last module results 838 | if ( config.currentModule ) { 839 | runLoggingCallbacks( 'moduleDone', QUnit, { 840 | name: config.currentModule, 841 | failed: config.moduleStats.bad, 842 | passed: config.moduleStats.all - config.moduleStats.bad, 843 | total: config.moduleStats.all 844 | } ); 845 | } 846 | 847 | var banner = id("qunit-banner"), 848 | tests = id("qunit-tests"), 849 | runtime = +new Date() - config.started, 850 | passed = config.stats.all - config.stats.bad, 851 | html = [ 852 | 'Tests completed in ', 853 | runtime, 854 | ' milliseconds.
    ', 855 | '', 856 | passed, 857 | ' tests of ', 858 | config.stats.all, 859 | ' passed, ', 860 | config.stats.bad, 861 | ' failed.' 862 | ].join(''); 863 | 864 | if ( banner ) { 865 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 866 | } 867 | 868 | if ( tests ) { 869 | id( "qunit-testresult" ).innerHTML = html; 870 | } 871 | 872 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 873 | // show ✖ for good, ✔ for bad suite result in title 874 | // use escape sequences in case file gets loaded with non-utf-8-charset 875 | document.title = [ 876 | (config.stats.bad ? "\u2716" : "\u2714"), 877 | document.title.replace(/^[\u2714\u2716] /i, "") 878 | ].join(" "); 879 | } 880 | 881 | // clear own sessionStorage items if all tests passed 882 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 883 | var key; 884 | for ( var i = 0; i < sessionStorage.length; i++ ) { 885 | key = sessionStorage.key( i++ ); 886 | if ( key.indexOf("qunit-test-") === 0 ) { 887 | sessionStorage.removeItem( key ); 888 | } 889 | } 890 | } 891 | 892 | runLoggingCallbacks( 'done', QUnit, { 893 | failed: config.stats.bad, 894 | passed: passed, 895 | total: config.stats.all, 896 | runtime: runtime 897 | } ); 898 | } 899 | 900 | function validTest( name ) { 901 | var filter = config.filter, 902 | run = false; 903 | 904 | if ( !filter ) { 905 | return true; 906 | } 907 | 908 | var not = filter.charAt( 0 ) === "!"; 909 | if ( not ) { 910 | filter = filter.slice( 1 ); 911 | } 912 | 913 | if ( name.indexOf( filter ) !== -1 ) { 914 | return !not; 915 | } 916 | 917 | if ( not ) { 918 | run = true; 919 | } 920 | 921 | return run; 922 | } 923 | 924 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 925 | // Later Safari and IE10 are supposed to support error.stack as well 926 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 927 | function extractStacktrace( e, offset ) { 928 | offset = offset || 3; 929 | if (e.stacktrace) { 930 | // Opera 931 | return e.stacktrace.split("\n")[offset + 3]; 932 | } else if (e.stack) { 933 | // Firefox, Chrome 934 | var stack = e.stack.split("\n"); 935 | if (/^error$/i.test(stack[0])) { 936 | stack.shift(); 937 | } 938 | return stack[offset]; 939 | } else if (e.sourceURL) { 940 | // Safari, PhantomJS 941 | // hopefully one day Safari provides actual stacktraces 942 | // exclude useless self-reference for generated Error objects 943 | if ( /qunit.js$/.test( e.sourceURL ) ) { 944 | return; 945 | } 946 | // for actual exceptions, this is useful 947 | return e.sourceURL + ":" + e.line; 948 | } 949 | } 950 | function sourceFromStacktrace(offset) { 951 | try { 952 | throw new Error(); 953 | } catch ( e ) { 954 | return extractStacktrace( e, offset ); 955 | } 956 | } 957 | 958 | function escapeInnerText(s) { 959 | if (!s) { 960 | return ""; 961 | } 962 | s = s + ""; 963 | return s.replace(/[\&<>]/g, function(s) { 964 | switch(s) { 965 | case "&": return "&"; 966 | case "<": return "<"; 967 | case ">": return ">"; 968 | default: return s; 969 | } 970 | }); 971 | } 972 | 973 | function synchronize( callback, last ) { 974 | config.queue.push( callback ); 975 | 976 | if ( config.autorun && !config.blocking ) { 977 | process(last); 978 | } 979 | } 980 | 981 | function process( last ) { 982 | function next() { 983 | process( last ); 984 | } 985 | var start = new Date().getTime(); 986 | config.depth = config.depth ? config.depth + 1 : 1; 987 | 988 | while ( config.queue.length && !config.blocking ) { 989 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 990 | config.queue.shift()(); 991 | } else { 992 | window.setTimeout( next, 13 ); 993 | break; 994 | } 995 | } 996 | config.depth--; 997 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 998 | done(); 999 | } 1000 | } 1001 | 1002 | function saveGlobal() { 1003 | config.pollution = []; 1004 | 1005 | if ( config.noglobals ) { 1006 | for ( var key in window ) { 1007 | if ( !hasOwn.call( window, key ) ) { 1008 | continue; 1009 | } 1010 | config.pollution.push( key ); 1011 | } 1012 | } 1013 | } 1014 | 1015 | function checkPollution( name ) { 1016 | var old = config.pollution; 1017 | saveGlobal(); 1018 | 1019 | var newGlobals = diff( config.pollution, old ); 1020 | if ( newGlobals.length > 0 ) { 1021 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1022 | } 1023 | 1024 | var deletedGlobals = diff( old, config.pollution ); 1025 | if ( deletedGlobals.length > 0 ) { 1026 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1027 | } 1028 | } 1029 | 1030 | // returns a new Array with the elements that are in a but not in b 1031 | function diff( a, b ) { 1032 | var result = a.slice(); 1033 | for ( var i = 0; i < result.length; i++ ) { 1034 | for ( var j = 0; j < b.length; j++ ) { 1035 | if ( result[i] === b[j] ) { 1036 | result.splice(i, 1); 1037 | i--; 1038 | break; 1039 | } 1040 | } 1041 | } 1042 | return result; 1043 | } 1044 | 1045 | function extend(a, b) { 1046 | for ( var prop in b ) { 1047 | if ( b[prop] === undefined ) { 1048 | delete a[prop]; 1049 | 1050 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1051 | } else if ( prop !== "constructor" || a !== window ) { 1052 | a[prop] = b[prop]; 1053 | } 1054 | } 1055 | 1056 | return a; 1057 | } 1058 | 1059 | function addEvent(elem, type, fn) { 1060 | if ( elem.addEventListener ) { 1061 | elem.addEventListener( type, fn, false ); 1062 | } else if ( elem.attachEvent ) { 1063 | elem.attachEvent( "on" + type, fn ); 1064 | } else { 1065 | fn(); 1066 | } 1067 | } 1068 | 1069 | function id(name) { 1070 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1071 | document.getElementById( name ); 1072 | } 1073 | 1074 | function registerLoggingCallback(key){ 1075 | return function(callback){ 1076 | config[key].push( callback ); 1077 | }; 1078 | } 1079 | 1080 | // Supports deprecated method of completely overwriting logging callbacks 1081 | function runLoggingCallbacks(key, scope, args) { 1082 | //debugger; 1083 | var callbacks; 1084 | if ( QUnit.hasOwnProperty(key) ) { 1085 | QUnit[key].call(scope, args); 1086 | } else { 1087 | callbacks = config[key]; 1088 | for( var i = 0; i < callbacks.length; i++ ) { 1089 | callbacks[i].call( scope, args ); 1090 | } 1091 | } 1092 | } 1093 | 1094 | // Test for equality any JavaScript type. 1095 | // Author: Philippe Rathé 1096 | QUnit.equiv = (function() { 1097 | 1098 | var innerEquiv; // the real equiv function 1099 | var callers = []; // stack to decide between skip/abort functions 1100 | var parents = []; // stack to avoiding loops from circular referencing 1101 | 1102 | // Call the o related callback with the given arguments. 1103 | function bindCallbacks(o, callbacks, args) { 1104 | var prop = QUnit.objectType(o); 1105 | if (prop) { 1106 | if (QUnit.objectType(callbacks[prop]) === "function") { 1107 | return callbacks[prop].apply(callbacks, args); 1108 | } else { 1109 | return callbacks[prop]; // or undefined 1110 | } 1111 | } 1112 | } 1113 | 1114 | var getProto = Object.getPrototypeOf || function (obj) { 1115 | return obj.__proto__; 1116 | }; 1117 | 1118 | var callbacks = (function () { 1119 | 1120 | // for string, boolean, number and null 1121 | function useStrictEquality(b, a) { 1122 | if (b instanceof a.constructor || a instanceof b.constructor) { 1123 | // to catch short annotaion VS 'new' annotation of a 1124 | // declaration 1125 | // e.g. var i = 1; 1126 | // var j = new Number(1); 1127 | return a == b; 1128 | } else { 1129 | return a === b; 1130 | } 1131 | } 1132 | 1133 | return { 1134 | "string" : useStrictEquality, 1135 | "boolean" : useStrictEquality, 1136 | "number" : useStrictEquality, 1137 | "null" : useStrictEquality, 1138 | "undefined" : useStrictEquality, 1139 | 1140 | "nan" : function(b) { 1141 | return isNaN(b); 1142 | }, 1143 | 1144 | "date" : function(b, a) { 1145 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1146 | }, 1147 | 1148 | "regexp" : function(b, a) { 1149 | return QUnit.objectType(b) === "regexp" && 1150 | // the regex itself 1151 | a.source === b.source && 1152 | // and its modifers 1153 | a.global === b.global && 1154 | // (gmi) ... 1155 | a.ignoreCase === b.ignoreCase && 1156 | a.multiline === b.multiline; 1157 | }, 1158 | 1159 | // - skip when the property is a method of an instance (OOP) 1160 | // - abort otherwise, 1161 | // initial === would have catch identical references anyway 1162 | "function" : function() { 1163 | var caller = callers[callers.length - 1]; 1164 | return caller !== Object && typeof caller !== "undefined"; 1165 | }, 1166 | 1167 | "array" : function(b, a) { 1168 | var i, j, loop; 1169 | var len; 1170 | 1171 | // b could be an object literal here 1172 | if (QUnit.objectType(b) !== "array") { 1173 | return false; 1174 | } 1175 | 1176 | len = a.length; 1177 | if (len !== b.length) { // safe and faster 1178 | return false; 1179 | } 1180 | 1181 | // track reference to avoid circular references 1182 | parents.push(a); 1183 | for (i = 0; i < len; i++) { 1184 | loop = false; 1185 | for (j = 0; j < parents.length; j++) { 1186 | if (parents[j] === a[i]) { 1187 | loop = true;// dont rewalk array 1188 | } 1189 | } 1190 | if (!loop && !innerEquiv(a[i], b[i])) { 1191 | parents.pop(); 1192 | return false; 1193 | } 1194 | } 1195 | parents.pop(); 1196 | return true; 1197 | }, 1198 | 1199 | "object" : function(b, a) { 1200 | var i, j, loop; 1201 | var eq = true; // unless we can proove it 1202 | var aProperties = [], bProperties = []; // collection of 1203 | // strings 1204 | 1205 | // comparing constructors is more strict than using 1206 | // instanceof 1207 | if (a.constructor !== b.constructor) { 1208 | // Allow objects with no prototype to be equivalent to 1209 | // objects with Object as their constructor. 1210 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1211 | (getProto(b) === null && getProto(a) === Object.prototype))) 1212 | { 1213 | return false; 1214 | } 1215 | } 1216 | 1217 | // stack constructor before traversing properties 1218 | callers.push(a.constructor); 1219 | // track reference to avoid circular references 1220 | parents.push(a); 1221 | 1222 | for (i in a) { // be strict: don't ensures hasOwnProperty 1223 | // and go deep 1224 | loop = false; 1225 | for (j = 0; j < parents.length; j++) { 1226 | if (parents[j] === a[i]) { 1227 | // don't go down the same path twice 1228 | loop = true; 1229 | } 1230 | } 1231 | aProperties.push(i); // collect a's properties 1232 | 1233 | if (!loop && !innerEquiv(a[i], b[i])) { 1234 | eq = false; 1235 | break; 1236 | } 1237 | } 1238 | 1239 | callers.pop(); // unstack, we are done 1240 | parents.pop(); 1241 | 1242 | for (i in b) { 1243 | bProperties.push(i); // collect b's properties 1244 | } 1245 | 1246 | // Ensures identical properties name 1247 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1248 | } 1249 | }; 1250 | }()); 1251 | 1252 | innerEquiv = function() { // can take multiple arguments 1253 | var args = Array.prototype.slice.apply(arguments); 1254 | if (args.length < 2) { 1255 | return true; // end transition 1256 | } 1257 | 1258 | return (function(a, b) { 1259 | if (a === b) { 1260 | return true; // catch the most you can 1261 | } else if (a === null || b === null || typeof a === "undefined" || 1262 | typeof b === "undefined" || 1263 | QUnit.objectType(a) !== QUnit.objectType(b)) { 1264 | return false; // don't lose time with error prone cases 1265 | } else { 1266 | return bindCallbacks(a, callbacks, [ b, a ]); 1267 | } 1268 | 1269 | // apply transition with (1..n) arguments 1270 | }(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1))); 1271 | }; 1272 | 1273 | return innerEquiv; 1274 | 1275 | }()); 1276 | 1277 | /** 1278 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1279 | * http://flesler.blogspot.com Licensed under BSD 1280 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1281 | * 1282 | * @projectDescription Advanced and extensible data dumping for Javascript. 1283 | * @version 1.0.0 1284 | * @author Ariel Flesler 1285 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1286 | */ 1287 | QUnit.jsDump = (function() { 1288 | function quote( str ) { 1289 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1290 | } 1291 | function literal( o ) { 1292 | return o + ''; 1293 | } 1294 | function join( pre, arr, post ) { 1295 | var s = jsDump.separator(), 1296 | base = jsDump.indent(), 1297 | inner = jsDump.indent(1); 1298 | if ( arr.join ) { 1299 | arr = arr.join( ',' + s + inner ); 1300 | } 1301 | if ( !arr ) { 1302 | return pre + post; 1303 | } 1304 | return [ pre, inner + arr, base + post ].join(s); 1305 | } 1306 | function array( arr, stack ) { 1307 | var i = arr.length, ret = new Array(i); 1308 | this.up(); 1309 | while ( i-- ) { 1310 | ret[i] = this.parse( arr[i] , undefined , stack); 1311 | } 1312 | this.down(); 1313 | return join( '[', ret, ']' ); 1314 | } 1315 | 1316 | var reName = /^function (\w+)/; 1317 | 1318 | var jsDump = { 1319 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1320 | stack = stack || [ ]; 1321 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1322 | type = typeof parser; 1323 | var inStack = inArray(obj, stack); 1324 | if (inStack != -1) { 1325 | return 'recursion('+(inStack - stack.length)+')'; 1326 | } 1327 | //else 1328 | if (type == 'function') { 1329 | stack.push(obj); 1330 | var res = parser.call( this, obj, stack ); 1331 | stack.pop(); 1332 | return res; 1333 | } 1334 | // else 1335 | return (type == 'string') ? parser : this.parsers.error; 1336 | }, 1337 | typeOf: function( obj ) { 1338 | var type; 1339 | if ( obj === null ) { 1340 | type = "null"; 1341 | } else if (typeof obj === "undefined") { 1342 | type = "undefined"; 1343 | } else if (QUnit.is("RegExp", obj)) { 1344 | type = "regexp"; 1345 | } else if (QUnit.is("Date", obj)) { 1346 | type = "date"; 1347 | } else if (QUnit.is("Function", obj)) { 1348 | type = "function"; 1349 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1350 | type = "window"; 1351 | } else if (obj.nodeType === 9) { 1352 | type = "document"; 1353 | } else if (obj.nodeType) { 1354 | type = "node"; 1355 | } else if ( 1356 | // native arrays 1357 | toString.call( obj ) === "[object Array]" || 1358 | // NodeList objects 1359 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1360 | ) { 1361 | type = "array"; 1362 | } else { 1363 | type = typeof obj; 1364 | } 1365 | return type; 1366 | }, 1367 | separator: function() { 1368 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1369 | }, 1370 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1371 | if ( !this.multiline ) { 1372 | return ''; 1373 | } 1374 | var chr = this.indentChar; 1375 | if ( this.HTML ) { 1376 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1377 | } 1378 | return new Array( this._depth_ + (extra||0) ).join(chr); 1379 | }, 1380 | up: function( a ) { 1381 | this._depth_ += a || 1; 1382 | }, 1383 | down: function( a ) { 1384 | this._depth_ -= a || 1; 1385 | }, 1386 | setParser: function( name, parser ) { 1387 | this.parsers[name] = parser; 1388 | }, 1389 | // The next 3 are exposed so you can use them 1390 | quote: quote, 1391 | literal: literal, 1392 | join: join, 1393 | // 1394 | _depth_: 1, 1395 | // This is the list of parsers, to modify them, use jsDump.setParser 1396 | parsers: { 1397 | window: '[Window]', 1398 | document: '[Document]', 1399 | error: '[ERROR]', //when no parser is found, shouldn't happen 1400 | unknown: '[Unknown]', 1401 | 'null': 'null', 1402 | 'undefined': 'undefined', 1403 | 'function': function( fn ) { 1404 | var ret = 'function', 1405 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1406 | if ( name ) { 1407 | ret += ' ' + name; 1408 | } 1409 | ret += '('; 1410 | 1411 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1412 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1413 | }, 1414 | array: array, 1415 | nodelist: array, 1416 | 'arguments': array, 1417 | object: function( map, stack ) { 1418 | var ret = [ ], keys, key, val, i; 1419 | QUnit.jsDump.up(); 1420 | if (Object.keys) { 1421 | keys = Object.keys( map ); 1422 | } else { 1423 | keys = []; 1424 | for (key in map) { keys.push( key ); } 1425 | } 1426 | keys.sort(); 1427 | for (i = 0; i < keys.length; i++) { 1428 | key = keys[ i ]; 1429 | val = map[ key ]; 1430 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) ); 1431 | } 1432 | QUnit.jsDump.down(); 1433 | return join( '{', ret, '}' ); 1434 | }, 1435 | node: function( node ) { 1436 | var open = QUnit.jsDump.HTML ? '<' : '<', 1437 | close = QUnit.jsDump.HTML ? '>' : '>'; 1438 | 1439 | var tag = node.nodeName.toLowerCase(), 1440 | ret = open + tag; 1441 | 1442 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1443 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1444 | if ( val ) { 1445 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1446 | } 1447 | } 1448 | return ret + close + open + '/' + tag + close; 1449 | }, 1450 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1451 | var l = fn.length; 1452 | if ( !l ) { 1453 | return ''; 1454 | } 1455 | 1456 | var args = new Array(l); 1457 | while ( l-- ) { 1458 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1459 | } 1460 | return ' ' + args.join(', ') + ' '; 1461 | }, 1462 | key: quote, //object calls it internally, the key part of an item in a map 1463 | functionCode: '[code]', //function calls it internally, it's the content of the function 1464 | attribute: quote, //node calls it internally, it's an html attribute value 1465 | string: quote, 1466 | date: quote, 1467 | regexp: literal, //regex 1468 | number: literal, 1469 | 'boolean': literal 1470 | }, 1471 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1472 | id:'id', 1473 | name:'name', 1474 | 'class':'className' 1475 | }, 1476 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1477 | indentChar:' ',//indentation unit 1478 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1479 | }; 1480 | 1481 | return jsDump; 1482 | }()); 1483 | 1484 | // from Sizzle.js 1485 | function getText( elems ) { 1486 | var ret = "", elem; 1487 | 1488 | for ( var i = 0; elems[i]; i++ ) { 1489 | elem = elems[i]; 1490 | 1491 | // Get the text from text nodes and CDATA nodes 1492 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1493 | ret += elem.nodeValue; 1494 | 1495 | // Traverse everything else, except comment nodes 1496 | } else if ( elem.nodeType !== 8 ) { 1497 | ret += getText( elem.childNodes ); 1498 | } 1499 | } 1500 | 1501 | return ret; 1502 | } 1503 | 1504 | //from jquery.js 1505 | function inArray( elem, array ) { 1506 | if ( array.indexOf ) { 1507 | return array.indexOf( elem ); 1508 | } 1509 | 1510 | for ( var i = 0, length = array.length; i < length; i++ ) { 1511 | if ( array[ i ] === elem ) { 1512 | return i; 1513 | } 1514 | } 1515 | 1516 | return -1; 1517 | } 1518 | 1519 | /* 1520 | * Javascript Diff Algorithm 1521 | * By John Resig (http://ejohn.org/) 1522 | * Modified by Chu Alan "sprite" 1523 | * 1524 | * Released under the MIT license. 1525 | * 1526 | * More Info: 1527 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1528 | * 1529 | * Usage: QUnit.diff(expected, actual) 1530 | * 1531 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1532 | */ 1533 | QUnit.diff = (function() { 1534 | function diff(o, n) { 1535 | var ns = {}; 1536 | var os = {}; 1537 | var i; 1538 | 1539 | for (i = 0; i < n.length; i++) { 1540 | if (ns[n[i]] == null) { 1541 | ns[n[i]] = { 1542 | rows: [], 1543 | o: null 1544 | }; 1545 | } 1546 | ns[n[i]].rows.push(i); 1547 | } 1548 | 1549 | for (i = 0; i < o.length; i++) { 1550 | if (os[o[i]] == null) { 1551 | os[o[i]] = { 1552 | rows: [], 1553 | n: null 1554 | }; 1555 | } 1556 | os[o[i]].rows.push(i); 1557 | } 1558 | 1559 | for (i in ns) { 1560 | if ( !hasOwn.call( ns, i ) ) { 1561 | continue; 1562 | } 1563 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1564 | n[ns[i].rows[0]] = { 1565 | text: n[ns[i].rows[0]], 1566 | row: os[i].rows[0] 1567 | }; 1568 | o[os[i].rows[0]] = { 1569 | text: o[os[i].rows[0]], 1570 | row: ns[i].rows[0] 1571 | }; 1572 | } 1573 | } 1574 | 1575 | for (i = 0; i < n.length - 1; i++) { 1576 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1577 | n[i + 1] == o[n[i].row + 1]) { 1578 | n[i + 1] = { 1579 | text: n[i + 1], 1580 | row: n[i].row + 1 1581 | }; 1582 | o[n[i].row + 1] = { 1583 | text: o[n[i].row + 1], 1584 | row: i + 1 1585 | }; 1586 | } 1587 | } 1588 | 1589 | for (i = n.length - 1; i > 0; i--) { 1590 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1591 | n[i - 1] == o[n[i].row - 1]) { 1592 | n[i - 1] = { 1593 | text: n[i - 1], 1594 | row: n[i].row - 1 1595 | }; 1596 | o[n[i].row - 1] = { 1597 | text: o[n[i].row - 1], 1598 | row: i - 1 1599 | }; 1600 | } 1601 | } 1602 | 1603 | return { 1604 | o: o, 1605 | n: n 1606 | }; 1607 | } 1608 | 1609 | return function(o, n) { 1610 | o = o.replace(/\s+$/, ''); 1611 | n = n.replace(/\s+$/, ''); 1612 | var out = diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/)); 1613 | 1614 | var str = ""; 1615 | var i; 1616 | 1617 | var oSpace = o.match(/\s+/g); 1618 | if (oSpace == null) { 1619 | oSpace = [" "]; 1620 | } 1621 | else { 1622 | oSpace.push(" "); 1623 | } 1624 | var nSpace = n.match(/\s+/g); 1625 | if (nSpace == null) { 1626 | nSpace = [" "]; 1627 | } 1628 | else { 1629 | nSpace.push(" "); 1630 | } 1631 | 1632 | if (out.n.length === 0) { 1633 | for (i = 0; i < out.o.length; i++) { 1634 | str += '' + out.o[i] + oSpace[i] + ""; 1635 | } 1636 | } 1637 | else { 1638 | if (out.n[0].text == null) { 1639 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1640 | str += '' + out.o[n] + oSpace[n] + ""; 1641 | } 1642 | } 1643 | 1644 | for (i = 0; i < out.n.length; i++) { 1645 | if (out.n[i].text == null) { 1646 | str += '' + out.n[i] + nSpace[i] + ""; 1647 | } 1648 | else { 1649 | var pre = ""; 1650 | 1651 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1652 | pre += '' + out.o[n] + oSpace[n] + ""; 1653 | } 1654 | str += " " + out.n[i].text + nSpace[i] + pre; 1655 | } 1656 | } 1657 | } 1658 | 1659 | return str; 1660 | }; 1661 | }()); 1662 | 1663 | // for CommonJS enviroments, export everything 1664 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) { 1665 | extend(exports, QUnit); 1666 | } 1667 | 1668 | // get at whatever the global object is, like window in browsers 1669 | }( (function() {return this;}.call()) )); -------------------------------------------------------------------------------- /lib/emberjs-couchapp/templates/app/app/vendor/ember-data.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | window.DS = Ember.Namespace.create({ 3 | CURRENT_API_REVISION: 4 4 | }); 5 | 6 | })(); 7 | 8 | 9 | 10 | (function() { 11 | var get = Ember.get, set = Ember.set; 12 | 13 | /** 14 | A record array is an array that contains records of a certain type. The record 15 | array materializes records as needed when they are retrieved for the first 16 | time. You should not create record arrays yourself. Instead, an instance of 17 | DS.RecordArray or its subclasses will be returned by your application's store 18 | in response to queries. 19 | */ 20 | 21 | DS.RecordArray = Ember.ArrayProxy.extend({ 22 | 23 | /** 24 | The model type contained by this record array. 25 | 26 | @type DS.Model 27 | */ 28 | type: null, 29 | 30 | // The array of client ids backing the record array. When a 31 | // record is requested from the record array, the record 32 | // for the client id at the same index is materialized, if 33 | // necessary, by the store. 34 | content: null, 35 | 36 | // The store that created this record array. 37 | store: null, 38 | 39 | init: function() { 40 | this.contentWillChange(); 41 | this._super(); 42 | }, 43 | 44 | contentWillChange: Ember.beforeObserver(function() { 45 | set(this, 'recordCache', []); 46 | }, 'content'), 47 | 48 | contentArrayDidChange: function(array, index, removed, added) { 49 | var recordCache = get(this, 'recordCache'); 50 | var args = [index, 0].concat(new Array(added)); 51 | 52 | recordCache.splice.apply(recordCache, args); 53 | }, 54 | 55 | contentArrayWillChange: function(array, index, removed, added) { 56 | var recordCache = get(this, 'recordCache'); 57 | recordCache.splice(index, removed); 58 | }, 59 | 60 | objectAtContent: function(index) { 61 | var recordCache = get(this, 'recordCache'); 62 | var record = recordCache[index]; 63 | 64 | if (!record) { 65 | var store = get(this, 'store'); 66 | var content = get(this, 'content'); 67 | 68 | var contentObject = content.objectAt(index); 69 | 70 | if (contentObject !== undefined) { 71 | record = store.findByClientId(get(this, 'type'), contentObject); 72 | recordCache[index] = record; 73 | } 74 | } 75 | 76 | return record; 77 | } 78 | }); 79 | 80 | })(); 81 | 82 | 83 | 84 | (function() { 85 | var get = Ember.get; 86 | 87 | DS.FilteredRecordArray = DS.RecordArray.extend({ 88 | filterFunction: null, 89 | 90 | replace: function() { 91 | var type = get(this, 'type').toString(); 92 | throw new Error("The result of a client-side filter (on " + type + ") is immutable."); 93 | }, 94 | 95 | updateFilter: Ember.observer(function() { 96 | var store = get(this, 'store'); 97 | store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); 98 | }, 'filterFunction') 99 | }); 100 | 101 | })(); 102 | 103 | 104 | 105 | (function() { 106 | var get = Ember.get, set = Ember.set; 107 | 108 | DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ 109 | query: null, 110 | isLoaded: false, 111 | 112 | replace: function() { 113 | var type = get(this, 'type').toString(); 114 | throw new Error("The result of a server query (on " + type + ") is immutable."); 115 | }, 116 | 117 | load: function(array) { 118 | var store = get(this, 'store'), type = get(this, 'type'); 119 | 120 | var clientIds = store.loadMany(type, array).clientIds; 121 | 122 | this.beginPropertyChanges(); 123 | set(this, 'content', Ember.A(clientIds)); 124 | set(this, 'isLoaded', true); 125 | this.endPropertyChanges(); 126 | } 127 | }); 128 | 129 | 130 | })(); 131 | 132 | 133 | 134 | (function() { 135 | var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor; 136 | 137 | var Set = function() { 138 | this.hash = {}; 139 | this.list = []; 140 | }; 141 | 142 | Set.prototype = { 143 | add: function(item) { 144 | var hash = this.hash, 145 | guid = guidFor(item); 146 | 147 | if (hash.hasOwnProperty(guid)) { return; } 148 | 149 | hash[guid] = true; 150 | this.list.push(item); 151 | }, 152 | 153 | remove: function(item) { 154 | var hash = this.hash, 155 | guid = guidFor(item); 156 | 157 | if (!hash.hasOwnProperty(guid)) { return; } 158 | 159 | delete hash[guid]; 160 | var list = this.list, 161 | index = Ember.ArrayUtils.indexOf(this, item); 162 | 163 | list.splice(index, 1); 164 | }, 165 | 166 | isEmpty: function() { 167 | return this.list.length === 0; 168 | } 169 | }; 170 | 171 | var ManyArrayState = Ember.State.extend({ 172 | recordWasAdded: function(manager, record) { 173 | var dirty = manager.dirty, observer; 174 | dirty.add(record); 175 | 176 | observer = function() { 177 | if (!get(record, 'isDirty')) { 178 | record.removeObserver('isDirty', observer); 179 | manager.send('childWasSaved', record); 180 | } 181 | }; 182 | 183 | record.addObserver('isDirty', observer); 184 | }, 185 | 186 | recordWasRemoved: function(manager, record) { 187 | var dirty = manager.dirty, observer; 188 | dirty.add(record); 189 | 190 | observer = function() { 191 | record.removeObserver('isDirty', observer); 192 | if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); } 193 | }; 194 | 195 | record.addObserver('isDirty', observer); 196 | } 197 | }); 198 | 199 | var states = { 200 | clean: ManyArrayState.create({ 201 | isDirty: false, 202 | 203 | recordWasAdded: function(manager, record) { 204 | this._super(manager, record); 205 | manager.goToState('dirty'); 206 | }, 207 | 208 | update: function(manager, clientIds) { 209 | var manyArray = manager.manyArray; 210 | set(manyArray, 'content', clientIds); 211 | } 212 | }), 213 | 214 | dirty: ManyArrayState.create({ 215 | isDirty: true, 216 | 217 | childWasSaved: function(manager, child) { 218 | var dirty = manager.dirty; 219 | dirty.remove(child); 220 | 221 | if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); } 222 | }, 223 | 224 | arrayBecameSaved: function(manager) { 225 | manager.goToState('clean'); 226 | } 227 | }) 228 | }; 229 | 230 | DS.ManyArrayStateManager = Ember.StateManager.extend({ 231 | manyArray: null, 232 | initialState: 'clean', 233 | states: states, 234 | 235 | init: function() { 236 | this._super(); 237 | this.dirty = new Set(); 238 | } 239 | }); 240 | 241 | })(); 242 | 243 | 244 | 245 | (function() { 246 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath; 247 | 248 | DS.ManyArray = DS.RecordArray.extend({ 249 | init: function() { 250 | set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this })); 251 | 252 | return this._super(); 253 | }, 254 | 255 | parentRecord: null, 256 | 257 | isDirty: Ember.computed(function() { 258 | return getPath(this, 'stateManager.currentState.isDirty'); 259 | }).property('stateManager.currentState').cacheable(), 260 | 261 | fetch: function() { 262 | var clientIds = get(this, 'content'), 263 | store = get(this, 'store'), 264 | type = get(this, 'type'); 265 | 266 | var ids = clientIds.map(function(clientId) { 267 | return store.clientIdToId[clientId]; 268 | }); 269 | 270 | store.fetchMany(type, ids); 271 | }, 272 | 273 | // Overrides Ember.Array's replace method to implement 274 | replace: function(index, removed, added) { 275 | var parentRecord = get(this, 'parentRecord'); 276 | var pendingParent = parentRecord && !get(parentRecord, 'id'); 277 | var stateManager = get(this, 'stateManager'); 278 | 279 | // Map the array of record objects into an array of client ids. 280 | added = added.map(function(record) { 281 | Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === record.constructor)); 282 | 283 | // If the record to which this many array belongs does not yet 284 | // have an id, notify the newly-added record that it must wait 285 | // for the parent to receive an id before the child can be 286 | // saved. 287 | if (pendingParent) { 288 | record.send('waitingOn', parentRecord); 289 | } 290 | 291 | this.assignInverse(record, parentRecord); 292 | 293 | stateManager.send('recordWasAdded', record); 294 | 295 | return record.get('clientId'); 296 | }, this); 297 | 298 | var store = this.store; 299 | 300 | var len = index+removed, record; 301 | for (var i = index; i < len; i++) { 302 | // TODO: null out inverse FK 303 | record = this.objectAt(i); 304 | this.assignInverse(record, parentRecord, true); 305 | 306 | // If we put the child record into a pending state because 307 | // we were waiting on the parent record to get an id, we 308 | // can tell the child it no longer needs to wait. 309 | if (pendingParent) { 310 | record.send('doneWaitingOn', parentRecord); 311 | } 312 | 313 | stateManager.send('recordWasAdded', record); 314 | } 315 | 316 | this._super(index, removed, added); 317 | }, 318 | 319 | assignInverse: function(record, parentRecord, remove) { 320 | var associationMap = get(record.constructor, 'associations'), 321 | possibleAssociations = associationMap.get(parentRecord.constructor), 322 | possible, actual; 323 | 324 | if (!possibleAssociations) { return; } 325 | 326 | for (var i = 0, l = possibleAssociations.length; i < l; i++) { 327 | possible = possibleAssociations[i]; 328 | 329 | if (possible.kind === 'belongsTo') { 330 | actual = possible; 331 | break; 332 | } 333 | } 334 | 335 | if (actual) { 336 | set(record, actual.name, remove ? null : parentRecord); 337 | } 338 | }, 339 | 340 | // Create a child record within the parentRecord 341 | createRecord: function(hash, transaction) { 342 | var parentRecord = get(this, 'parentRecord'), 343 | store = get(parentRecord, 'store'), 344 | type = get(this, 'type'), 345 | record; 346 | 347 | transaction = transaction || get(parentRecord, 'transaction'); 348 | 349 | record = store.createRecord.call(store, type, hash, transaction); 350 | this.pushObject(record); 351 | 352 | return record; 353 | } 354 | }); 355 | 356 | })(); 357 | 358 | 359 | 360 | (function() { 361 | 362 | })(); 363 | 364 | 365 | 366 | (function() { 367 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt; 368 | 369 | /** 370 | A transaction allows you to collect multiple records into a unit of work 371 | that can be committed or rolled back as a group. 372 | 373 | For example, if a record has local modifications that have not yet 374 | been saved, calling `commit()` on its transaction will cause those 375 | modifications to be sent to the adapter to be saved. Calling 376 | `rollback()` on its transaction would cause all of the modifications to 377 | be discarded and the record to return to the last known state before 378 | changes were made. 379 | 380 | If a newly created record's transaction is rolled back, it will 381 | immediately transition to the deleted state. 382 | 383 | If you do not explicitly create a transaction, a record is assigned to 384 | an implicit transaction called the default transaction. In these cases, 385 | you can treat your application's instance of `DS.Store` as a transaction 386 | and call the `commit()` and `rollback()` methods on the store itself. 387 | 388 | Once a record has been successfully committed or rolled back, it will 389 | be moved back to the implicit transaction. Because it will now be in 390 | a clean state, it can be moved to a new transaction if you wish. 391 | 392 | ### Creating a Transaction 393 | 394 | To create a new transaction, call the `transaction()` method of your 395 | application's `DS.Store` instance: 396 | 397 | var transaction = App.store.transaction(); 398 | 399 | This will return a new instance of `DS.Transaction` with no records 400 | yet assigned to it. 401 | 402 | ### Adding Existing Records 403 | 404 | Add records to a transaction using the `add()` method: 405 | 406 | record = App.store.find(Person, 1); 407 | transaction.add(record); 408 | 409 | Note that only records whose `isDirty` flag is `false` may be added 410 | to a transaction. Once modifications to a record have been made 411 | (its `isDirty` flag is `true`), it is not longer able to be added to 412 | a transaction. 413 | 414 | ### Creating New Records 415 | 416 | Because newly created records are dirty from the time they are created, 417 | and because dirty records can not be added to a transaction, you must 418 | use the `createRecord()` method to assign new records to a transaction. 419 | 420 | For example, instead of this: 421 | 422 | var transaction = store.transaction(); 423 | var person = Person.createRecord({ name: "Steve" }); 424 | 425 | // won't work because person is dirty 426 | transaction.add(person); 427 | 428 | Call `createRecord()` on the transaction directly: 429 | 430 | var transaction = store.transaction(); 431 | transaction.createRecord(Person, { name: "Steve" }); 432 | 433 | ### Asynchronous Commits 434 | 435 | Typically, all of the records in a transaction will be committed 436 | together. However, new records that have a dependency on other new 437 | records need to wait for their parent record to be saved and assigned an 438 | ID. In that case, the child record will continue to live in the 439 | transaction until its parent is saved, at which time the transaction will 440 | attempt to commit again. 441 | 442 | For this reason, you should not re-use transactions once you have committed 443 | them. Always make a new transaction and move the desired records to it before 444 | calling commit. 445 | */ 446 | 447 | DS.Transaction = Ember.Object.extend({ 448 | /** 449 | @private 450 | 451 | Creates the bucket data structure used to segregate records by 452 | type. 453 | */ 454 | init: function() { 455 | set(this, 'buckets', { 456 | clean: Ember.Map.create(), 457 | created: Ember.Map.create(), 458 | updated: Ember.Map.create(), 459 | deleted: Ember.Map.create(), 460 | inflight: Ember.Map.create() 461 | }); 462 | }, 463 | 464 | /** 465 | Creates a new record of the given type and assigns it to the transaction 466 | on which the method was called. 467 | 468 | This is useful as only clean records can be added to a transaction and 469 | new records created using other methods immediately become dirty. 470 | 471 | @param {DS.Model} type the model type to create 472 | @param {Object} hash the data hash to assign the new record 473 | */ 474 | createRecord: function(type, hash) { 475 | var store = get(this, 'store'); 476 | 477 | return store.createRecord(type, hash, this); 478 | }, 479 | 480 | /** 481 | Adds an existing record to this transaction. Only records without 482 | modficiations (i.e., records whose `isDirty` property is `false`) 483 | can be added to a transaction. 484 | 485 | @param {DS.Model} record the record to add to the transaction 486 | */ 487 | add: function(record) { 488 | // we could probably make this work if someone has a valid use case. Do you? 489 | Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty')); 490 | 491 | var recordTransaction = get(record, 'transaction'), 492 | defaultTransaction = getPath(this, 'store.defaultTransaction'); 493 | 494 | Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); 495 | 496 | this.adoptRecord(record); 497 | }, 498 | 499 | /** 500 | Commits the transaction, which causes all of the modified records that 501 | belong to the transaction to be sent to the adapter to be saved. 502 | 503 | Once you call `commit()` on a transaction, you should not re-use it. 504 | 505 | When a record is saved, it will be removed from this transaction and 506 | moved back to the store's default transaction. 507 | */ 508 | commit: function() { 509 | var self = this, 510 | iterate; 511 | 512 | iterate = function(bucketType, fn, binding) { 513 | var dirty = self.bucketForType(bucketType); 514 | 515 | dirty.forEach(function(type, records) { 516 | if (records.isEmpty()) { return; } 517 | 518 | var array = []; 519 | 520 | records.forEach(function(record) { 521 | record.send('willCommit'); 522 | 523 | if (get(record, 'isPending') === false) { 524 | array.push(record); 525 | } 526 | }); 527 | 528 | fn.call(binding, type, array); 529 | }); 530 | }; 531 | 532 | var commitDetails = { 533 | updated: { 534 | eachType: function(fn, binding) { iterate('updated', fn, binding); } 535 | }, 536 | 537 | created: { 538 | eachType: function(fn, binding) { iterate('created', fn, binding); } 539 | }, 540 | 541 | deleted: { 542 | eachType: function(fn, binding) { iterate('deleted', fn, binding); } 543 | } 544 | }; 545 | 546 | var store = get(this, 'store'); 547 | var adapter = get(store, '_adapter'); 548 | 549 | this.removeCleanRecords(); 550 | 551 | if (adapter && adapter.commit) { adapter.commit(store, commitDetails); } 552 | else { throw fmt("Adapter is either null or does not implement `commit` method", this); } 553 | }, 554 | 555 | /** 556 | Rolling back a transaction resets the records that belong to 557 | that transaction. 558 | 559 | Updated records have their properties reset to the last known 560 | value from the persistence layer. Deleted records are reverted 561 | to a clean, non-deleted state. Newly created records immediately 562 | become deleted, and are not sent to the adapter to be persisted. 563 | 564 | After the transaction is rolled back, any records that belong 565 | to it will return to the store's default transaction, and the 566 | current transaction should not be used again. 567 | */ 568 | rollback: function() { 569 | var store = get(this, 'store'), 570 | dirty; 571 | 572 | // Loop through all of the records in each of the dirty states 573 | // and initiate a rollback on them. As a side effect of telling 574 | // the record to roll back, it should also move itself out of 575 | // the dirty bucket and into the clean bucket. 576 | ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) { 577 | dirty = this.bucketForType(bucketType); 578 | 579 | dirty.forEach(function(type, records) { 580 | records.forEach(function(record) { 581 | record.send('rollback'); 582 | }); 583 | }); 584 | }, this); 585 | 586 | // Now that all records in the transaction are guaranteed to be 587 | // clean, migrate them all to the store's default transaction. 588 | this.removeCleanRecords(); 589 | }, 590 | 591 | /** 592 | @private 593 | 594 | Removes a record from this transaction and back to the store's 595 | default transaction. 596 | 597 | Note: This method is private for now, but should probably be exposed 598 | in the future once we have stricter error checking (for example, in the 599 | case of the record being dirty). 600 | 601 | @param {DS.Model} record 602 | */ 603 | remove: function(record) { 604 | var defaultTransaction = getPath(this, 'store.defaultTransaction'); 605 | defaultTransaction.adoptRecord(record); 606 | }, 607 | 608 | /** 609 | @private 610 | 611 | Removes all of the records in the transaction's clean bucket. 612 | */ 613 | removeCleanRecords: function() { 614 | var clean = this.bucketForType('clean'), 615 | self = this; 616 | 617 | clean.forEach(function(type, records) { 618 | records.forEach(function(record) { 619 | self.remove(record); 620 | }); 621 | }); 622 | }, 623 | 624 | /** 625 | @private 626 | 627 | Returns the bucket for the given bucket type. For example, you might call 628 | `this.bucketForType('updated')` to get the `Ember.Map` that contains all 629 | of the records that have changes pending. 630 | 631 | @param {String} bucketType the type of bucket 632 | @returns Ember.Map 633 | */ 634 | bucketForType: function(bucketType) { 635 | var buckets = get(this, 'buckets'); 636 | 637 | return get(buckets, bucketType); 638 | }, 639 | 640 | /** 641 | @private 642 | 643 | This method moves a record into a different transaction without the normal 644 | checks that ensure that the user is not doing something weird, like moving 645 | a dirty record into a new transaction. 646 | 647 | It is designed for internal use, such as when we are moving a clean record 648 | into a new transaction when the transaction is committed. 649 | 650 | This method must not be called unless the record is clean. 651 | 652 | @param {DS.Model} record 653 | */ 654 | adoptRecord: function(record) { 655 | var oldTransaction = get(record, 'transaction'); 656 | 657 | if (oldTransaction) { 658 | oldTransaction.removeFromBucket('clean', record); 659 | } 660 | 661 | this.addToBucket('clean', record); 662 | set(record, 'transaction', this); 663 | }, 664 | 665 | /** 666 | @private 667 | 668 | Adds a record to the named bucket. 669 | 670 | @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` 671 | */ 672 | addToBucket: function(bucketType, record) { 673 | var bucket = this.bucketForType(bucketType), 674 | type = record.constructor; 675 | 676 | var records = bucket.get(type); 677 | 678 | if (!records) { 679 | records = Ember.OrderedSet.create(); 680 | bucket.set(type, records); 681 | } 682 | 683 | records.add(record); 684 | }, 685 | 686 | /** 687 | @private 688 | 689 | Removes a record from the named bucket. 690 | 691 | @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` 692 | */ 693 | removeFromBucket: function(bucketType, record) { 694 | var bucket = this.bucketForType(bucketType), 695 | type = record.constructor; 696 | 697 | var records = bucket.get(type); 698 | records.remove(record); 699 | }, 700 | 701 | /** 702 | @private 703 | 704 | Called by a record's state manager to indicate that the record has entered 705 | a dirty state. The record will be moved from the `clean` bucket and into 706 | the appropriate dirty bucket. 707 | 708 | @param {String} bucketType one of `created`, `updated`, or `deleted` 709 | */ 710 | recordBecameDirty: function(bucketType, record) { 711 | this.removeFromBucket('clean', record); 712 | this.addToBucket(bucketType, record); 713 | }, 714 | 715 | /** 716 | @private 717 | 718 | Called by a record's state manager to indicate that the record has entered 719 | inflight state. The record will be moved from its current dirty bucket and into 720 | the `inflight` bucket. 721 | 722 | @param {String} bucketType one of `created`, `updated`, or `deleted` 723 | */ 724 | recordBecameInFlight: function(kind, record) { 725 | this.removeFromBucket(kind, record); 726 | this.addToBucket('inflight', record); 727 | }, 728 | 729 | /** 730 | @private 731 | 732 | Called by a record's state manager to indicate that the record has entered 733 | a clean state. The record will be moved from its current dirty or inflight bucket and into 734 | the `clean` bucket. 735 | 736 | @param {String} bucketType one of `created`, `updated`, or `deleted` 737 | */ 738 | recordBecameClean: function(kind, record) { 739 | this.removeFromBucket(kind, record); 740 | 741 | this.remove(record); 742 | } 743 | }); 744 | 745 | })(); 746 | 747 | 748 | 749 | (function() { 750 | /*globals Ember*/ 751 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt; 752 | 753 | var DATA_PROXY = { 754 | get: function(name) { 755 | return this.savedData[name]; 756 | } 757 | }; 758 | 759 | // These values are used in the data cache when clientIds are 760 | // needed but the underlying data has not yet been loaded by 761 | // the server. 762 | var UNLOADED = 'unloaded'; 763 | var LOADING = 'loading'; 764 | 765 | // Implementors Note: 766 | // 767 | // The variables in this file are consistently named according to the following 768 | // scheme: 769 | // 770 | // * +id+ means an identifier managed by an external source, provided inside the 771 | // data hash provided by that source. 772 | // * +clientId+ means a transient numerical identifier generated at runtime by 773 | // the data store. It is important primarily because newly created objects may 774 | // not yet have an externally generated id. 775 | // * +type+ means a subclass of DS.Model. 776 | 777 | /** 778 | The store contains all of the hashes for records loaded from the server. 779 | It is also responsible for creating instances of DS.Model when you request one 780 | of these data hashes, so that they can be bound to in your Handlebars templates. 781 | 782 | Create a new store like this: 783 | 784 | MyApp.store = DS.Store.create(); 785 | 786 | You can retrieve DS.Model instances from the store in several ways. To retrieve 787 | a record for a specific id, use the `find()` method: 788 | 789 | var record = MyApp.store.find(MyApp.Contact, 123); 790 | 791 | By default, the store will talk to your backend using a standard REST mechanism. 792 | You can customize how the store talks to your backend by specifying a custom adapter: 793 | 794 | MyApp.store = DS.Store.create({ 795 | adapter: 'MyApp.CustomAdapter' 796 | }); 797 | 798 | You can learn more about writing a custom adapter by reading the `DS.Adapter` 799 | documentation. 800 | */ 801 | DS.Store = Ember.Object.extend({ 802 | 803 | /** 804 | Many methods can be invoked without specifying which store should be used. 805 | In those cases, the first store created will be used as the default. If 806 | an application has multiple stores, it should specify which store to use 807 | when performing actions, such as finding records by id. 808 | 809 | The init method registers this store as the default if none is specified. 810 | */ 811 | init: function() { 812 | // Enforce API revisioning. See BREAKING_CHANGES.md for more. 813 | var revision = get(this, 'revision'); 814 | 815 | if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) { 816 | throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION); 817 | } 818 | 819 | if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { 820 | set(DS, 'defaultStore', this); 821 | } 822 | 823 | // internal bookkeeping; not observable 824 | this.typeMaps = {}; 825 | this.recordCache = []; 826 | this.clientIdToId = {}; 827 | this.recordArraysByClientId = {}; 828 | 829 | set(this, 'defaultTransaction', this.transaction()); 830 | 831 | return this._super(); 832 | }, 833 | 834 | /** 835 | Returns a new transaction scoped to this store. 836 | 837 | @see {DS.Transaction} 838 | @returns DS.Transaction 839 | */ 840 | transaction: function() { 841 | return DS.Transaction.create({ store: this }); 842 | }, 843 | 844 | /** 845 | @private 846 | 847 | This is used only by the record's DataProxy. Do not use this directly. 848 | */ 849 | dataForRecord: function(record) { 850 | var type = record.constructor, 851 | clientId = get(record, 'clientId'), 852 | typeMap = this.typeMapFor(type); 853 | 854 | return typeMap.cidToHash[clientId]; 855 | }, 856 | 857 | /** 858 | The adapter to use to communicate to a backend server or other persistence layer. 859 | 860 | This can be specified as an instance, a class, or a property path that specifies 861 | where the adapter can be located. 862 | 863 | @property {DS.Adapter|String} 864 | */ 865 | adapter: null, 866 | 867 | /** 868 | @private 869 | 870 | This property returns the adapter, after resolving a possible String. 871 | 872 | @returns DS.Adapter 873 | */ 874 | _adapter: Ember.computed(function() { 875 | var adapter = get(this, 'adapter'); 876 | if (typeof adapter === 'string') { 877 | return getPath(this, adapter, false) || getPath(window, adapter); 878 | } 879 | return adapter; 880 | }).property('adapter').cacheable(), 881 | 882 | // A monotonically increasing number to be used to uniquely identify 883 | // data hashes and records. 884 | clientIdCounter: 1, 885 | 886 | // ..................... 887 | // . CREATE NEW RECORD . 888 | // ..................... 889 | 890 | /** 891 | Create a new record in the current store. The properties passed 892 | to this method are set on the newly created record. 893 | 894 | @param {subclass of DS.Model} type 895 | @param {Object} properties a hash of properties to set on the 896 | newly created record. 897 | @returns DS.Model 898 | */ 899 | createRecord: function(type, properties, transaction) { 900 | properties = properties || {}; 901 | 902 | // Create a new instance of the model `type` and put it 903 | // into the specified `transaction`. If no transaction is 904 | // specified, the default transaction will be used. 905 | // 906 | // NOTE: A `transaction` is specified when the 907 | // `transaction.createRecord` API is used. 908 | var record = type._create({ 909 | store: this 910 | }); 911 | 912 | transaction = transaction || get(this, 'defaultTransaction'); 913 | transaction.adoptRecord(record); 914 | 915 | // Extract the primary key from the `properties` hash, 916 | // based on the `primaryKey` for the model type. 917 | var primaryKey = get(record, 'primaryKey'), 918 | id = properties[primaryKey] || null; 919 | 920 | // If the passed properties do not include a primary key, 921 | // give the adapter an opportunity to generate one. 922 | var adapter; 923 | if (Ember.none(id)) { 924 | adapter = get(this, 'adapter'); 925 | if (adapter && adapter.generateIdForRecord) { 926 | id = adapter.generateIdForRecord(this, record); 927 | properties.id = id; 928 | } 929 | } 930 | 931 | var hash = {}, clientId; 932 | 933 | // Push the hash into the store. If present, associate the 934 | // extracted `id` with the hash. 935 | clientId = this.pushHash(hash, id, type); 936 | 937 | record.send('didChangeData'); 938 | 939 | var recordCache = get(this, 'recordCache'); 940 | 941 | // Now that we have a clientId, attach it to the record we 942 | // just created. 943 | set(record, 'clientId', clientId); 944 | 945 | // Store the record we just created in the record cache for 946 | // this clientId. 947 | recordCache[clientId] = record; 948 | 949 | // Set the properties specified on the record. 950 | record.setProperties(properties); 951 | 952 | this.updateRecordArrays(type, clientId, get(record, 'data')); 953 | 954 | return record; 955 | }, 956 | 957 | // ................. 958 | // . DELETE RECORD . 959 | // ................. 960 | 961 | /** 962 | For symmetry, a record can be deleted via the store. 963 | 964 | @param {DS.Model} record 965 | */ 966 | deleteRecord: function(record) { 967 | record.send('deleteRecord'); 968 | }, 969 | 970 | // ................ 971 | // . FIND RECORDS . 972 | // ................ 973 | 974 | /** 975 | This is the main entry point into finding records. The first 976 | parameter to this method is always a subclass of `DS.Model`. 977 | 978 | You can use the `find` method on a subclass of `DS.Model` 979 | directly if your application only has one store. For 980 | example, instead of `store.find(App.Person, 1)`, you could 981 | say `App.Person.find(1)`. 982 | 983 | --- 984 | 985 | To find a record by ID, pass the `id` as the second parameter: 986 | 987 | store.find(App.Person, 1); 988 | App.Person.find(1); 989 | 990 | If the record with that `id` had not previously been loaded, 991 | the store will return an empty record immediately and ask 992 | the adapter to find the data by calling the adapter's `find` 993 | method. 994 | 995 | The `find` method will always return the same object for a 996 | given type and `id`. To check whether the adapter has populated 997 | a record, you can check its `isLoaded` property. 998 | 999 | --- 1000 | 1001 | To find all records for a type, call `find` with no additional 1002 | parameters: 1003 | 1004 | store.find(App.Person); 1005 | App.Person.find(); 1006 | 1007 | This will return a `RecordArray` representing all known records 1008 | for the given type and kick off a request to the adapter's 1009 | `findAll` method to load any additional records for the type. 1010 | 1011 | The `RecordArray` returned by `find()` is live. If any more 1012 | records for the type are added at a later time through any 1013 | mechanism, it will automatically update to reflect the change. 1014 | 1015 | --- 1016 | 1017 | To find a record by a query, call `find` with a hash as the 1018 | second parameter: 1019 | 1020 | store.find(App.Person, { page: 1 }); 1021 | App.Person.find({ page: 1 }); 1022 | 1023 | This will return a `RecordArray` immediately, but it will always 1024 | be an empty `RecordArray` at first. It will call the adapter's 1025 | `findQuery` method, which will populate the `RecordArray` once 1026 | the server has returned results. 1027 | 1028 | You can check whether a query results `RecordArray` has loaded 1029 | by checking its `isLoaded` property. 1030 | */ 1031 | find: function(type, id, query) { 1032 | if (id === undefined) { 1033 | return this.findAll(type); 1034 | } 1035 | 1036 | if (query !== undefined) { 1037 | return this.findMany(type, id, query); 1038 | } else if (Ember.typeOf(id) === 'object') { 1039 | return this.findQuery(type, id); 1040 | } 1041 | 1042 | if (Ember.isArray(id)) { 1043 | return this.findMany(type, id); 1044 | } 1045 | 1046 | var clientId = this.typeMapFor(type).idToCid[id]; 1047 | 1048 | return this.findByClientId(type, clientId, id); 1049 | }, 1050 | 1051 | findByClientId: function(type, clientId, id) { 1052 | var recordCache = get(this, 'recordCache'), 1053 | dataCache = this.typeMapFor(type).cidToHash, 1054 | record; 1055 | 1056 | // If there is already a clientId assigned for this 1057 | // type/id combination, try to find an existing 1058 | // record for that id and return. Otherwise, 1059 | // materialize a new record and set its data to the 1060 | // value we already have. 1061 | if (clientId !== undefined) { 1062 | record = recordCache[clientId]; 1063 | 1064 | if (!record) { 1065 | // create a new instance of the model type in the 1066 | // 'isLoading' state 1067 | record = this.materializeRecord(type, clientId); 1068 | 1069 | if (typeof dataCache[clientId] === 'object') { 1070 | record.send('didChangeData'); 1071 | } 1072 | } 1073 | } else { 1074 | clientId = this.pushHash(LOADING, id, type); 1075 | 1076 | // create a new instance of the model type in the 1077 | // 'isLoading' state 1078 | record = this.materializeRecord(type, clientId, id); 1079 | 1080 | // let the adapter set the data, possibly async 1081 | var adapter = get(this, '_adapter'); 1082 | if (adapter && adapter.find) { adapter.find(this, type, id); } 1083 | else { throw fmt("Adapter is either null or does not implement `find` method", this); } 1084 | } 1085 | 1086 | return record; 1087 | }, 1088 | 1089 | /** 1090 | @private 1091 | 1092 | Ask the adapter to fetch IDs that are not already loaded. 1093 | 1094 | This method will convert `id`s to `clientId`s, filter out 1095 | `clientId`s that already have a data hash present, and pass 1096 | the remaining `id`s to the adapter. 1097 | 1098 | @param {Class} type A model class 1099 | @param {Array} ids An array of ids 1100 | @param {Object} query 1101 | 1102 | @returns {Array} An Array of all clientIds for the 1103 | specified ids. 1104 | */ 1105 | fetchMany: function(type, ids, query) { 1106 | var typeMap = this.typeMapFor(type), 1107 | idToClientIdMap = typeMap.idToCid, 1108 | dataCache = typeMap.cidToHash, 1109 | data = typeMap.cidToHash, 1110 | needed; 1111 | 1112 | var clientIds = Ember.A([]); 1113 | 1114 | if (ids) { 1115 | needed = []; 1116 | 1117 | ids.forEach(function(id) { 1118 | // Get the clientId for the given id 1119 | var clientId = idToClientIdMap[id]; 1120 | 1121 | // If there is no `clientId` yet 1122 | if (clientId === undefined) { 1123 | // Create a new `clientId`, marking its data hash 1124 | // as loading. Once the adapter returns the data 1125 | // hash, it will be updated 1126 | clientId = this.pushHash(LOADING, id, type); 1127 | needed.push(id); 1128 | 1129 | // If there is a clientId, but its data hash is 1130 | // marked as unloaded (this happens when a 1131 | // hasMany association creates clientIds for its 1132 | // referenced ids before they were loaded) 1133 | } else if (clientId && data[clientId] === UNLOADED) { 1134 | // change the data hash marker to loading 1135 | dataCache[clientId] = LOADING; 1136 | needed.push(id); 1137 | } 1138 | 1139 | // this method is expected to return a list of 1140 | // all of the clientIds for the specified ids, 1141 | // unconditionally add it. 1142 | clientIds.push(clientId); 1143 | }, this); 1144 | } else { 1145 | needed = null; 1146 | } 1147 | 1148 | // If there are any needed ids, ask the adapter to load them 1149 | if ((needed && get(needed, 'length') > 0) || query) { 1150 | var adapter = get(this, '_adapter'); 1151 | if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); } 1152 | else { throw fmt("Adapter is either null or does not implement `findMany` method", this); } 1153 | } 1154 | 1155 | return clientIds; 1156 | }, 1157 | 1158 | /** @private 1159 | */ 1160 | findMany: function(type, ids, query) { 1161 | var clientIds = this.fetchMany(type, ids, query); 1162 | 1163 | return this.createManyArray(type, clientIds); 1164 | }, 1165 | 1166 | findQuery: function(type, query) { 1167 | var array = DS.AdapterPopulatedRecordArray.create({ type: type, content: Ember.A([]), store: this }); 1168 | var adapter = get(this, '_adapter'); 1169 | if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); } 1170 | else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); } 1171 | return array; 1172 | }, 1173 | 1174 | findAll: function(type) { 1175 | 1176 | var typeMap = this.typeMapFor(type), 1177 | findAllCache = typeMap.findAllCache; 1178 | 1179 | if (findAllCache) { return findAllCache; } 1180 | 1181 | var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this }); 1182 | this.registerRecordArray(array, type); 1183 | 1184 | var adapter = get(this, '_adapter'); 1185 | if (adapter && adapter.findAll) { adapter.findAll(this, type); } 1186 | 1187 | typeMap.findAllCache = array; 1188 | return array; 1189 | }, 1190 | 1191 | filter: function(type, query, filter) { 1192 | // allow an optional server query 1193 | if (arguments.length === 3) { 1194 | this.findQuery(type, query); 1195 | } else if (arguments.length === 2) { 1196 | filter = query; 1197 | } 1198 | 1199 | var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter }); 1200 | 1201 | this.registerRecordArray(array, type, filter); 1202 | 1203 | return array; 1204 | }, 1205 | 1206 | // ............ 1207 | // . UPDATING . 1208 | // ............ 1209 | 1210 | hashWasUpdated: function(type, clientId, record) { 1211 | // Because hash updates are invoked at the end of the run loop, 1212 | // it is possible that a record might be deleted after its hash 1213 | // has been modified and this method was scheduled to be called. 1214 | // 1215 | // If that's the case, the record would have already been removed 1216 | // from all record arrays; calling updateRecordArrays would just 1217 | // add it back. If the record is deleted, just bail. It shouldn't 1218 | // give us any more trouble after this. 1219 | 1220 | if (get(record, 'isDeleted')) { return; } 1221 | this.updateRecordArrays(type, clientId, get(record, 'data')); 1222 | }, 1223 | 1224 | // .............. 1225 | // . PERSISTING . 1226 | // .............. 1227 | 1228 | commit: function() { 1229 | var defaultTransaction = get(this, 'defaultTransaction'); 1230 | set(this, 'defaultTransaction', this.transaction()); 1231 | 1232 | defaultTransaction.commit(); 1233 | }, 1234 | 1235 | didUpdateRecords: function(array, hashes) { 1236 | if (hashes) { 1237 | array.forEach(function(record, idx) { 1238 | this.didUpdateRecord(record, hashes[idx]); 1239 | }, this); 1240 | } else { 1241 | array.forEach(function(record) { 1242 | this.didUpdateRecord(record); 1243 | }, this); 1244 | } 1245 | }, 1246 | 1247 | didUpdateRecord: function(record, hash) { 1248 | if (hash) { 1249 | var clientId = get(record, 'clientId'), 1250 | dataCache = this.typeMapFor(record.constructor).cidToHash; 1251 | 1252 | dataCache[clientId] = hash; 1253 | record.send('didChangeData'); 1254 | record.hashWasUpdated(); 1255 | } else { 1256 | record.send('didSaveData'); 1257 | } 1258 | 1259 | record.send('didCommit'); 1260 | }, 1261 | 1262 | didDeleteRecords: function(array) { 1263 | array.forEach(function(record) { 1264 | record.send('didCommit'); 1265 | }); 1266 | }, 1267 | 1268 | didDeleteRecord: function(record) { 1269 | record.send('didCommit'); 1270 | }, 1271 | 1272 | _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) { 1273 | var recordData = get(record, 'data'), id, changes; 1274 | 1275 | if (hash) { 1276 | typeMap.cidToHash[clientId] = hash; 1277 | 1278 | // If the server returns a hash, we assume that the server's version 1279 | // of the data supercedes the local changes. 1280 | record.beginPropertyChanges(); 1281 | record.send('didChangeData'); 1282 | recordData.adapterDidUpdate(); 1283 | record.hashWasUpdated(); 1284 | record.endPropertyChanges(); 1285 | 1286 | id = hash[primaryKey]; 1287 | 1288 | typeMap.idToCid[id] = clientId; 1289 | this.clientIdToId[clientId] = id; 1290 | } else { 1291 | recordData.commit(); 1292 | } 1293 | 1294 | record.send('didCommit'); 1295 | }, 1296 | 1297 | 1298 | didCreateRecords: function(type, array, hashes) { 1299 | var primaryKey = type.proto().primaryKey, 1300 | typeMap = this.typeMapFor(type), 1301 | clientId; 1302 | 1303 | for (var i=0, l=get(array, 'length'); i "created.uncommitted" 1650 | 1651 | The `DS.Model` states are themselves stateless. What we mean is that, 1652 | though each instance of a record also has a unique instance of a 1653 | `DS.StateManager`, the hierarchical states that each of *those* points 1654 | to is a shared data structure. For performance reasons, instead of each 1655 | record getting its own copy of the hierarchy of states, each state 1656 | manager points to this global, immutable shared instance. How does a 1657 | state know which record it should be acting on? We pass a reference to 1658 | the current state manager as the first parameter to every method invoked 1659 | on a state. 1660 | 1661 | The state manager passed as the first parameter is where you should stash 1662 | state about the record if needed; you should never store data on the state 1663 | object itself. If you need access to the record being acted on, you can 1664 | retrieve the state manager's `record` property. For example, if you had 1665 | an event handler `myEvent`: 1666 | 1667 | myEvent: function(manager) { 1668 | var record = manager.get('record'); 1669 | record.doSomething(); 1670 | } 1671 | 1672 | For more information about state managers in general, see the Ember.js 1673 | documentation on `Ember.StateManager`. 1674 | 1675 | ### Events, Flags, and Transitions 1676 | 1677 | A state may implement zero or more events, flags, or transitions. 1678 | 1679 | #### Events 1680 | 1681 | Events are named functions that are invoked when sent to a record. The 1682 | state manager will first look for a method with the given name on the 1683 | current state. If no method is found, it will search the current state's 1684 | parent, and then its grandparent, and so on until reaching the top of 1685 | the hierarchy. If the root is reached without an event handler being found, 1686 | an exception will be raised. This can be very helpful when debugging new 1687 | features. 1688 | 1689 | Here's an example implementation of a state with a `myEvent` event handler: 1690 | 1691 | aState: DS.State.create({ 1692 | myEvent: function(manager, param) { 1693 | console.log("Received myEvent with "+param); 1694 | } 1695 | }) 1696 | 1697 | To trigger this event: 1698 | 1699 | record.send('myEvent', 'foo'); 1700 | //=> "Received myEvent with foo" 1701 | 1702 | Note that an optional parameter can be sent to a record's `send()` method, 1703 | which will be passed as the second parameter to the event handler. 1704 | 1705 | Events should transition to a different state if appropriate. This can be 1706 | done by calling the state manager's `goToState()` method with a path to the 1707 | desired state. The state manager will attempt to resolve the state path 1708 | relative to the current state. If no state is found at that path, it will 1709 | attempt to resolve it relative to the current state's parent, and then its 1710 | parent, and so on until the root is reached. For example, imagine a hierarchy 1711 | like this: 1712 | 1713 | * created 1714 | * start <-- currentState 1715 | * inFlight 1716 | * updated 1717 | * inFlight 1718 | 1719 | If we are currently in the `start` state, calling 1720 | `goToState('inFlight')` would transition to the `created.inFlight` state, 1721 | while calling `goToState('updated.inFlight')` would transition to 1722 | the `updated.inFlight` state. 1723 | 1724 | Remember that *only events* should ever cause a state transition. You should 1725 | never call `goToState()` from outside a state's event handler. If you are 1726 | tempted to do so, create a new event and send that to the state manager. 1727 | 1728 | #### Flags 1729 | 1730 | Flags are Boolean values that can be used to introspect a record's current 1731 | state in a more user-friendly way than examining its state path. For example, 1732 | instead of doing this: 1733 | 1734 | var statePath = record.getPath('stateManager.currentState.path'); 1735 | if (statePath === 'created.inFlight') { 1736 | doSomething(); 1737 | } 1738 | 1739 | You can say: 1740 | 1741 | if (record.get('isNew') && record.get('isSaving')) { 1742 | doSomething(); 1743 | } 1744 | 1745 | If your state does not set a value for a given flag, the value will 1746 | be inherited from its parent (or the first place in the state hierarchy 1747 | where it is defined). 1748 | 1749 | The current set of flags are defined below. If you want to add a new flag, 1750 | in addition to the area below, you will also need to declare it in the 1751 | `DS.Model` class. 1752 | 1753 | #### Transitions 1754 | 1755 | Transitions are like event handlers but are called automatically upon 1756 | entering or exiting a state. To implement a transition, just call a method 1757 | either `enter` or `exit`: 1758 | 1759 | myState: DS.State.create({ 1760 | // Gets called automatically when entering 1761 | // this state. 1762 | enter: function(manager) { 1763 | console.log("Entered myState"); 1764 | } 1765 | }) 1766 | 1767 | Note that enter and exit events are called once per transition. If the 1768 | current state changes, but changes to another child state of the parent, 1769 | the transition event on the parent will not be triggered. 1770 | */ 1771 | 1772 | var stateProperty = Ember.computed(function(key) { 1773 | var parent = get(this, 'parentState'); 1774 | if (parent) { 1775 | return get(parent, key); 1776 | } 1777 | }).property(); 1778 | 1779 | var isEmptyObject = function(object) { 1780 | for (var name in object) { 1781 | if (object.hasOwnProperty(name)) { return false; } 1782 | } 1783 | 1784 | return true; 1785 | }; 1786 | 1787 | var hasDefinedProperties = function(object) { 1788 | for (var name in object) { 1789 | if (object.hasOwnProperty(name) && object[name]) { return true; } 1790 | } 1791 | 1792 | return false; 1793 | }; 1794 | 1795 | DS.State = Ember.State.extend({ 1796 | isLoaded: stateProperty, 1797 | isDirty: stateProperty, 1798 | isSaving: stateProperty, 1799 | isDeleted: stateProperty, 1800 | isError: stateProperty, 1801 | isNew: stateProperty, 1802 | isValid: stateProperty, 1803 | isPending: stateProperty, 1804 | 1805 | // For states that are substates of a 1806 | // DirtyState (updated or created), it is 1807 | // useful to be able to determine which 1808 | // type of dirty state it is. 1809 | dirtyType: stateProperty 1810 | }); 1811 | 1812 | var setProperty = function(manager, context) { 1813 | var key = context.key, value = context.value; 1814 | 1815 | var record = get(manager, 'record'), 1816 | data = get(record, 'data'); 1817 | 1818 | set(data, key, value); 1819 | }; 1820 | 1821 | var setAssociation = function(manager, context) { 1822 | var key = context.key, value = context.value; 1823 | 1824 | var record = get(manager, 'record'), 1825 | data = get(record, 'data'); 1826 | 1827 | data.setAssociation(key, value); 1828 | }; 1829 | 1830 | var didChangeData = function(manager) { 1831 | var record = get(manager, 'record'), 1832 | data = get(record, 'data'); 1833 | 1834 | data._savedData = null; 1835 | record.notifyPropertyChange('data'); 1836 | }; 1837 | 1838 | // The waitingOn event shares common functionality 1839 | // between the different dirty states, but each is 1840 | // treated slightly differently. This method is exposed 1841 | // so that each implementation can invoke the common 1842 | // behavior, and then implement the behavior specific 1843 | // to the state. 1844 | var waitingOn = function(manager, object) { 1845 | var record = get(manager, 'record'), 1846 | pendingQueue = get(record, 'pendingQueue'), 1847 | objectGuid = guidFor(object); 1848 | 1849 | var observer = function() { 1850 | if (get(object, 'id')) { 1851 | manager.send('doneWaitingOn', object); 1852 | Ember.removeObserver(object, 'id', observer); 1853 | } 1854 | }; 1855 | 1856 | pendingQueue[objectGuid] = [object, observer]; 1857 | Ember.addObserver(object, 'id', observer); 1858 | }; 1859 | 1860 | // Implementation notes: 1861 | // 1862 | // Each state has a boolean value for all of the following flags: 1863 | // 1864 | // * isLoaded: The record has a populated `data` property. When a 1865 | // record is loaded via `store.find`, `isLoaded` is false 1866 | // until the adapter sets it. When a record is created locally, 1867 | // its `isLoaded` property is always true. 1868 | // * isDirty: The record has local changes that have not yet been 1869 | // saved by the adapter. This includes records that have been 1870 | // created (but not yet saved) or deleted. 1871 | // * isSaving: The record's transaction has been committed, but 1872 | // the adapter has not yet acknowledged that the changes have 1873 | // been persisted to the backend. 1874 | // * isDeleted: The record was marked for deletion. When `isDeleted` 1875 | // is true and `isDirty` is true, the record is deleted locally 1876 | // but the deletion was not yet persisted. When `isSaving` is 1877 | // true, the change is in-flight. When both `isDirty` and 1878 | // `isSaving` are false, the change has persisted. 1879 | // * isError: The adapter reported that it was unable to save 1880 | // local changes to the backend. This may also result in the 1881 | // record having its `isValid` property become false if the 1882 | // adapter reported that server-side validations failed. 1883 | // * isNew: The record was created on the client and the adapter 1884 | // did not yet report that it was successfully saved. 1885 | // * isValid: No client-side validations have failed and the 1886 | // adapter did not report any server-side validation failures. 1887 | // * isPending: A record `isPending` when it belongs to an 1888 | // association on another record and that record has not been 1889 | // saved. A record in this state cannot be saved because it 1890 | // lacks a "foreign key" that will be supplied by its parent 1891 | // association when the parent record has been created. When 1892 | // the adapter reports that the parent has saved, the 1893 | // `isPending` property on all children will become `false` 1894 | // and the transaction will try to commit the records. 1895 | 1896 | // This mixin is mixed into various uncommitted states. Make 1897 | // sure to mix it in *after* the class definition, so its 1898 | // super points to the class definition. 1899 | var Uncommitted = Ember.Mixin.create({ 1900 | setProperty: setProperty, 1901 | setAssociation: setAssociation, 1902 | }); 1903 | 1904 | // These mixins are mixed into substates of the concrete 1905 | // subclasses of DirtyState. 1906 | 1907 | var CreatedUncommitted = Ember.Mixin.create({ 1908 | deleteRecord: function(manager) { 1909 | var record = get(manager, 'record'); 1910 | this._super(manager); 1911 | 1912 | record.withTransaction(function(t) { 1913 | t.recordBecameClean('created', record); 1914 | }); 1915 | manager.goToState('deleted.saved'); 1916 | } 1917 | }); 1918 | 1919 | var UpdatedUncommitted = Ember.Mixin.create({ 1920 | deleteRecord: function(manager) { 1921 | this._super(manager); 1922 | 1923 | var record = get(manager, 'record'); 1924 | 1925 | record.withTransaction(function(t) { 1926 | t.recordBecameClean('updated', record); 1927 | }); 1928 | 1929 | manager.goToState('deleted'); 1930 | } 1931 | }); 1932 | 1933 | // The dirty state is a abstract state whose functionality is 1934 | // shared between the `created` and `updated` states. 1935 | // 1936 | // The deleted state shares the `isDirty` flag with the 1937 | // subclasses of `DirtyState`, but with a very different 1938 | // implementation. 1939 | var DirtyState = DS.State.extend({ 1940 | initialState: 'uncommitted', 1941 | 1942 | // FLAGS 1943 | isDirty: true, 1944 | 1945 | // SUBSTATES 1946 | 1947 | // When a record first becomes dirty, it is `uncommitted`. 1948 | // This means that there are local pending changes, 1949 | // but they have not yet begun to be saved. 1950 | uncommitted: DS.State.extend({ 1951 | // TRANSITIONS 1952 | enter: function(manager) { 1953 | var dirtyType = get(this, 'dirtyType'), 1954 | record = get(manager, 'record'); 1955 | 1956 | record.withTransaction(function (t) { 1957 | t.recordBecameDirty(dirtyType, record); 1958 | }); 1959 | }, 1960 | 1961 | // EVENTS 1962 | deleteRecord: Ember.K, 1963 | 1964 | waitingOn: function(manager, object) { 1965 | waitingOn(manager, object); 1966 | manager.goToState('pending'); 1967 | }, 1968 | 1969 | willCommit: function(manager) { 1970 | manager.goToState('inFlight'); 1971 | }, 1972 | 1973 | becameInvalid: function(manager) { 1974 | var dirtyType = get(this, 'dirtyType'), 1975 | record = get(manager, 'record'); 1976 | 1977 | record.withTransaction(function (t) { 1978 | t.recordBecameInFlight(dirtyType, record); 1979 | }); 1980 | 1981 | manager.goToState('invalid'); 1982 | }, 1983 | 1984 | rollback: function(manager) { 1985 | var record = get(manager, 'record'), 1986 | dirtyType = get(this, 'dirtyType'), 1987 | data = get(record, 'data'); 1988 | 1989 | data.rollback(); 1990 | 1991 | record.withTransaction(function(t) { 1992 | t.recordBecameClean(dirtyType, record); 1993 | }); 1994 | 1995 | manager.goToState('loaded'); 1996 | } 1997 | }, Uncommitted), 1998 | 1999 | // Once a record has been handed off to the adapter to be 2000 | // saved, it is in the 'in flight' state. Changes to the 2001 | // record cannot be made during this window. 2002 | inFlight: DS.State.extend({ 2003 | // FLAGS 2004 | isSaving: true, 2005 | 2006 | // TRANSITIONS 2007 | enter: function(manager) { 2008 | var dirtyType = get(this, 'dirtyType'), 2009 | record = get(manager, 'record'); 2010 | 2011 | record.withTransaction(function (t) { 2012 | t.recordBecameInFlight(dirtyType, record); 2013 | }); 2014 | }, 2015 | 2016 | // EVENTS 2017 | didCommit: function(manager) { 2018 | var dirtyType = get(this, 'dirtyType'), 2019 | record = get(manager, 'record'); 2020 | 2021 | record.withTransaction(function(t) { 2022 | t.recordBecameClean('inflight', record); 2023 | }); 2024 | 2025 | manager.goToState('loaded'); 2026 | manager.send('invokeLifecycleCallbacks', dirtyType); 2027 | }, 2028 | 2029 | becameInvalid: function(manager, errors) { 2030 | var record = get(manager, 'record'); 2031 | 2032 | set(record, 'errors', errors); 2033 | 2034 | manager.goToState('invalid'); 2035 | manager.send('invokeLifecycleCallbacks'); 2036 | }, 2037 | 2038 | becameError: function(manager) { 2039 | manager.goToState('error'); 2040 | manager.send('invokeLifecycleCallbacks'); 2041 | }, 2042 | 2043 | didChangeData: didChangeData 2044 | }), 2045 | 2046 | // If a record becomes associated with a newly created 2047 | // parent record, it will be `pending` until the parent 2048 | // record has successfully persisted. Once this happens, 2049 | // this record can use the parent's primary key as its 2050 | // foreign key. 2051 | // 2052 | // If the record's transaction had already started to 2053 | // commit, the record will transition to the `inFlight` 2054 | // state. If it had not, the record will transition to 2055 | // the `uncommitted` state. 2056 | pending: DS.State.extend({ 2057 | initialState: 'uncommitted', 2058 | 2059 | // FLAGS 2060 | isPending: true, 2061 | 2062 | // SUBSTATES 2063 | 2064 | // A pending record whose transaction has not yet 2065 | // started to commit is in this state. 2066 | uncommitted: DS.State.extend({ 2067 | // EVENTS 2068 | deleteRecord: function(manager) { 2069 | var record = get(manager, 'record'), 2070 | pendingQueue = get(record, 'pendingQueue'), 2071 | tuple; 2072 | 2073 | // since we are leaving the pending state, remove any 2074 | // observers we have registered on other records. 2075 | for (var prop in pendingQueue) { 2076 | if (!pendingQueue.hasOwnProperty(prop)) { continue; } 2077 | 2078 | tuple = pendingQueue[prop]; 2079 | Ember.removeObserver(tuple[0], 'id', tuple[1]); 2080 | } 2081 | }, 2082 | 2083 | willCommit: function(manager) { 2084 | manager.goToState('committing'); 2085 | }, 2086 | 2087 | doneWaitingOn: function(manager, object) { 2088 | var record = get(manager, 'record'), 2089 | pendingQueue = get(record, 'pendingQueue'), 2090 | objectGuid = guidFor(object); 2091 | 2092 | delete pendingQueue[objectGuid]; 2093 | 2094 | if (isEmptyObject(pendingQueue)) { 2095 | manager.send('doneWaiting'); 2096 | } 2097 | }, 2098 | 2099 | doneWaiting: function(manager) { 2100 | var dirtyType = get(this, 'dirtyType'); 2101 | manager.goToState(dirtyType + '.uncommitted'); 2102 | } 2103 | }, Uncommitted), 2104 | 2105 | // A pending record whose transaction has started 2106 | // to commit is in this state. Since it has not yet 2107 | // been sent to the adapter, it is not `inFlight` 2108 | // until all of its dependencies have been committed. 2109 | committing: DS.State.extend({ 2110 | // FLAGS 2111 | isSaving: true, 2112 | 2113 | // EVENTS 2114 | doneWaitingOn: function(manager, object) { 2115 | var record = get(manager, 'record'), 2116 | pendingQueue = get(record, 'pendingQueue'), 2117 | objectGuid = guidFor(object); 2118 | 2119 | delete pendingQueue[objectGuid]; 2120 | 2121 | if (isEmptyObject(pendingQueue)) { 2122 | manager.send('doneWaiting'); 2123 | } 2124 | }, 2125 | 2126 | doneWaiting: function(manager) { 2127 | var record = get(manager, 'record'), 2128 | transaction = get(record, 'transaction'); 2129 | 2130 | // Now that the record is no longer pending, schedule 2131 | // the transaction to commit. 2132 | Ember.run.once(transaction, transaction.commit); 2133 | }, 2134 | 2135 | willCommit: function(manager) { 2136 | var record = get(manager, 'record'), 2137 | pendingQueue = get(record, 'pendingQueue'); 2138 | 2139 | if (isEmptyObject(pendingQueue)) { 2140 | var dirtyType = get(this, 'dirtyType'); 2141 | manager.goToState(dirtyType + '.inFlight'); 2142 | } 2143 | } 2144 | }) 2145 | }), 2146 | 2147 | // A record is in the `invalid` state when its client-side 2148 | // invalidations have failed, or if the adapter has indicated 2149 | // the the record failed server-side invalidations. 2150 | invalid: DS.State.extend({ 2151 | // FLAGS 2152 | isValid: false, 2153 | 2154 | exit: function(manager) { 2155 | var record = get(manager, 'record'); 2156 | 2157 | record.withTransaction(function (t) { 2158 | t.recordBecameClean('inflight', record); 2159 | }); 2160 | }, 2161 | 2162 | // EVENTS 2163 | deleteRecord: function(manager) { 2164 | manager.goToState('deleted'); 2165 | }, 2166 | 2167 | setAssociation: setAssociation, 2168 | 2169 | setProperty: function(manager, context) { 2170 | setProperty(manager, context); 2171 | 2172 | var record = get(manager, 'record'), 2173 | errors = get(record, 'errors'), 2174 | key = context.key; 2175 | 2176 | delete errors[key]; 2177 | 2178 | if (!hasDefinedProperties(errors)) { 2179 | manager.send('becameValid'); 2180 | } 2181 | }, 2182 | 2183 | rollback: function(manager) { 2184 | manager.send('becameValid'); 2185 | manager.send('rollback'); 2186 | }, 2187 | 2188 | becameValid: function(manager) { 2189 | manager.goToState('uncommitted'); 2190 | }, 2191 | 2192 | invokeLifecycleCallbacks: function(manager) { 2193 | var record = get(manager, 'record'); 2194 | record.fire('becameInvalid', record); 2195 | } 2196 | }) 2197 | }); 2198 | 2199 | // The created and updated states are created outside the state 2200 | // chart so we can reopen their substates and add mixins as 2201 | // necessary. 2202 | 2203 | var createdState = DirtyState.create({ 2204 | dirtyType: 'created', 2205 | 2206 | // FLAGS 2207 | isNew: true 2208 | }); 2209 | 2210 | var updatedState = DirtyState.create({ 2211 | dirtyType: 'updated' 2212 | }); 2213 | 2214 | // The created.uncommitted state and created.pending.uncommitted share 2215 | // some logic defined in CreatedUncommitted. 2216 | createdState.states.uncommitted.reopen(CreatedUncommitted); 2217 | createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted); 2218 | 2219 | // The created.uncommitted state needs to immediately transition to the 2220 | // deleted state if it is rolled back. 2221 | createdState.states.uncommitted.reopen({ 2222 | rollback: function(manager) { 2223 | this._super(manager); 2224 | manager.goToState('deleted.saved'); 2225 | } 2226 | }); 2227 | 2228 | // The updated.uncommitted state and updated.pending.uncommitted share 2229 | // some logic defined in UpdatedUncommitted. 2230 | updatedState.states.uncommitted.reopen(UpdatedUncommitted); 2231 | updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted); 2232 | updatedState.states.inFlight.reopen({ 2233 | didSaveData: function(manager) { 2234 | var record = get(manager, 'record'), 2235 | data = get(record, 'data'); 2236 | 2237 | data.saveData(); 2238 | data.adapterDidUpdate(); 2239 | } 2240 | }); 2241 | 2242 | var states = { 2243 | rootState: Ember.State.create({ 2244 | // FLAGS 2245 | isLoaded: false, 2246 | isDirty: false, 2247 | isSaving: false, 2248 | isDeleted: false, 2249 | isError: false, 2250 | isNew: false, 2251 | isValid: true, 2252 | isPending: false, 2253 | 2254 | // SUBSTATES 2255 | 2256 | // A record begins its lifecycle in the `empty` state. 2257 | // If its data will come from the adapter, it will 2258 | // transition into the `loading` state. Otherwise, if 2259 | // the record is being created on the client, it will 2260 | // transition into the `created` state. 2261 | empty: DS.State.create({ 2262 | // EVENTS 2263 | loadingData: function(manager) { 2264 | manager.goToState('loading'); 2265 | }, 2266 | 2267 | didChangeData: function(manager) { 2268 | didChangeData(manager); 2269 | 2270 | manager.goToState('loaded.created'); 2271 | } 2272 | }), 2273 | 2274 | // A record enters this state when the store askes 2275 | // the adapter for its data. It remains in this state 2276 | // until the adapter provides the requested data. 2277 | // 2278 | // Usually, this process is asynchronous, using an 2279 | // XHR to retrieve the data. 2280 | loading: DS.State.create({ 2281 | // TRANSITIONS 2282 | exit: function(manager) { 2283 | var record = get(manager, 'record'); 2284 | record.fire('didLoad'); 2285 | }, 2286 | 2287 | // EVENTS 2288 | didChangeData: function(manager, data) { 2289 | didChangeData(manager); 2290 | manager.send('loadedData'); 2291 | }, 2292 | 2293 | loadedData: function(manager) { 2294 | manager.goToState('loaded'); 2295 | } 2296 | }), 2297 | 2298 | // A record enters this state when its data is populated. 2299 | // Most of a record's lifecycle is spent inside substates 2300 | // of the `loaded` state. 2301 | loaded: DS.State.create({ 2302 | initialState: 'saved', 2303 | 2304 | // FLAGS 2305 | isLoaded: true, 2306 | 2307 | // SUBSTATES 2308 | 2309 | // If there are no local changes to a record, it remains 2310 | // in the `saved` state. 2311 | saved: DS.State.create({ 2312 | 2313 | // EVENTS 2314 | setProperty: function(manager, context) { 2315 | setProperty(manager, context); 2316 | manager.goToState('updated'); 2317 | }, 2318 | 2319 | setAssociation: function(manager, context) { 2320 | setAssociation(manager, context); 2321 | manager.goToState('updated'); 2322 | }, 2323 | 2324 | didChangeData: didChangeData, 2325 | 2326 | deleteRecord: function(manager) { 2327 | manager.goToState('deleted'); 2328 | }, 2329 | 2330 | waitingOn: function(manager, object) { 2331 | waitingOn(manager, object); 2332 | manager.goToState('updated.pending'); 2333 | }, 2334 | 2335 | invokeLifecycleCallbacks: function(manager, dirtyType) { 2336 | var record = get(manager, 'record'); 2337 | if (dirtyType === 'created') { 2338 | record.fire('didCreate', record); 2339 | } else { 2340 | record.fire('didUpdate', record); 2341 | } 2342 | } 2343 | }), 2344 | 2345 | // A record is in this state after it has been locally 2346 | // created but before the adapter has indicated that 2347 | // it has been saved. 2348 | created: createdState, 2349 | 2350 | // A record is in this state if it has already been 2351 | // saved to the server, but there are new local changes 2352 | // that have not yet been saved. 2353 | updated: updatedState 2354 | }), 2355 | 2356 | // A record is in this state if it was deleted from the store. 2357 | deleted: DS.State.create({ 2358 | // FLAGS 2359 | isDeleted: true, 2360 | isLoaded: true, 2361 | isDirty: true, 2362 | 2363 | // TRANSITIONS 2364 | enter: function(manager) { 2365 | var record = get(manager, 'record'), 2366 | store = get(record, 'store'); 2367 | 2368 | store.removeFromRecordArrays(record); 2369 | }, 2370 | 2371 | // SUBSTATES 2372 | 2373 | // When a record is deleted, it enters the `start` 2374 | // state. It will exit this state when the record's 2375 | // transaction starts to commit. 2376 | start: DS.State.create({ 2377 | // TRANSITIONS 2378 | enter: function(manager) { 2379 | var record = get(manager, 'record'); 2380 | 2381 | record.withTransaction(function(t) { 2382 | t.recordBecameDirty('deleted', record); 2383 | }); 2384 | }, 2385 | 2386 | // EVENTS 2387 | willCommit: function(manager) { 2388 | manager.goToState('inFlight'); 2389 | }, 2390 | 2391 | rollback: function(manager) { 2392 | var record = get(manager, 'record'), 2393 | data = get(record, 'data'); 2394 | 2395 | data.rollback(); 2396 | record.withTransaction(function(t) { 2397 | t.recordBecameClean('deleted', record); 2398 | }); 2399 | manager.goToState('loaded'); 2400 | } 2401 | }), 2402 | 2403 | // After a record's transaction is committing, but 2404 | // before the adapter indicates that the deletion 2405 | // has saved to the server, a record is in the 2406 | // `inFlight` substate of `deleted`. 2407 | inFlight: DS.State.create({ 2408 | // FLAGS 2409 | isSaving: true, 2410 | 2411 | // TRANSITIONS 2412 | enter: function(manager) { 2413 | var record = get(manager, 'record'); 2414 | 2415 | record.withTransaction(function (t) { 2416 | t.recordBecameInFlight('deleted', record); 2417 | }); 2418 | }, 2419 | 2420 | // EVENTS 2421 | didCommit: function(manager) { 2422 | var record = get(manager, 'record'); 2423 | 2424 | record.withTransaction(function(t) { 2425 | t.recordBecameClean('inflight', record); 2426 | }); 2427 | 2428 | manager.goToState('saved'); 2429 | 2430 | manager.send('invokeLifecycleCallbacks'); 2431 | } 2432 | }), 2433 | 2434 | // Once the adapter indicates that the deletion has 2435 | // been saved, the record enters the `saved` substate 2436 | // of `deleted`. 2437 | saved: DS.State.create({ 2438 | // FLAGS 2439 | isDirty: false, 2440 | 2441 | invokeLifecycleCallbacks: function(manager) { 2442 | var record = get(manager, 'record'); 2443 | record.fire('didDelete', record); 2444 | } 2445 | }) 2446 | }), 2447 | 2448 | // If the adapter indicates that there was an unknown 2449 | // error saving a record, the record enters the `error` 2450 | // state. 2451 | error: DS.State.create({ 2452 | isError: true, 2453 | 2454 | // EVENTS 2455 | 2456 | invokeLifecycleCallbacks: function(manager) { 2457 | var record = get(manager, 'record'); 2458 | record.fire('becameError', record); 2459 | } 2460 | }) 2461 | }) 2462 | }; 2463 | 2464 | DS.StateManager = Ember.StateManager.extend({ 2465 | record: null, 2466 | initialState: 'rootState', 2467 | states: states 2468 | }); 2469 | 2470 | })(); 2471 | 2472 | 2473 | 2474 | (function() { 2475 | var get = Ember.get, set = Ember.set; 2476 | 2477 | // When a record is changed on the client, it is considered "dirty"--there are 2478 | // pending changes that need to be saved to a persistence layer, such as a 2479 | // server. 2480 | // 2481 | // If the record is rolled back, it re-enters a clean state, any changes are 2482 | // discarded, and its attributes are reset back to the last known good copy 2483 | // of the data that came from the server. 2484 | // 2485 | // If the record is committed, the changes are sent to the server to be saved, 2486 | // and once the server confirms that they are valid, the record's "canonical" 2487 | // data becomes the original canonical data plus the changes merged in. 2488 | // 2489 | // A DataProxy is an object that encapsulates this change tracking. It 2490 | // contains three buckets: 2491 | // 2492 | // * `savedData` - the last-known copy of the data from the server 2493 | // * `unsavedData` - a hash that contains any changes that have not yet 2494 | // been committed 2495 | // * `associations` - this is similar to `savedData`, but holds the client 2496 | // ids of associated records 2497 | // 2498 | // When setting a property on the object, the value is placed into the 2499 | // `unsavedData` bucket: 2500 | // 2501 | // proxy.set('key', 'value'); 2502 | // 2503 | // // unsavedData: 2504 | // { 2505 | // key: "value" 2506 | // } 2507 | // 2508 | // When retrieving a property from the object, it first looks to see 2509 | // if that value exists in the `unsavedData` bucket, and returns it if so. 2510 | // Otherwise, it returns the value from the `savedData` bucket. 2511 | // 2512 | // When the adapter notifies a record that it has been saved, it merges the 2513 | // `unsavedData` bucket into the `savedData` bucket. If the record's 2514 | // transaction is rolled back, the `unsavedData` hash is simply discarded. 2515 | // 2516 | // This object is a regular JS object for performance. It is only 2517 | // used internally for bookkeeping purposes. 2518 | 2519 | var DataProxy = DS._DataProxy = function(record) { 2520 | this.record = record; 2521 | 2522 | this.unsavedData = {}; 2523 | 2524 | this.associations = {}; 2525 | }; 2526 | 2527 | DataProxy.prototype = { 2528 | get: function(key) { return Ember.get(this, key); }, 2529 | set: function(key, value) { return Ember.set(this, key, value); }, 2530 | 2531 | setAssociation: function(key, value) { 2532 | this.associations[key] = value; 2533 | }, 2534 | 2535 | savedData: function() { 2536 | var savedData = this._savedData; 2537 | if (savedData) { return savedData; } 2538 | 2539 | var record = this.record, 2540 | clientId = get(record, 'clientId'), 2541 | store = get(record, 'store'); 2542 | 2543 | if (store) { 2544 | savedData = store.dataForRecord(record); 2545 | this._savedData = savedData; 2546 | return savedData; 2547 | } 2548 | }, 2549 | 2550 | unknownProperty: function(key) { 2551 | var unsavedData = this.unsavedData, 2552 | associations = this.associations, 2553 | savedData = this.savedData(), 2554 | store; 2555 | 2556 | var value = unsavedData[key], association; 2557 | 2558 | // if this is a belongsTo association, this will 2559 | // be a clientId. 2560 | association = associations[key]; 2561 | 2562 | if (association !== undefined) { 2563 | store = get(this.record, 'store'); 2564 | return store.clientIdToId[association]; 2565 | } 2566 | 2567 | if (savedData && value === undefined) { 2568 | value = savedData[key]; 2569 | } 2570 | 2571 | return value; 2572 | }, 2573 | 2574 | setUnknownProperty: function(key, value) { 2575 | var record = this.record, 2576 | unsavedData = this.unsavedData; 2577 | 2578 | unsavedData[key] = value; 2579 | 2580 | record.hashWasUpdated(); 2581 | 2582 | return value; 2583 | }, 2584 | 2585 | commit: function() { 2586 | this.saveData(); 2587 | 2588 | this.record.notifyPropertyChange('data'); 2589 | }, 2590 | 2591 | rollback: function() { 2592 | this.unsavedData = {}; 2593 | 2594 | this.record.notifyPropertyChange('data'); 2595 | }, 2596 | 2597 | saveData: function() { 2598 | var record = this.record; 2599 | 2600 | var unsavedData = this.unsavedData; 2601 | var savedData = this.savedData(); 2602 | 2603 | for (var prop in unsavedData) { 2604 | if (unsavedData.hasOwnProperty(prop)) { 2605 | savedData[prop] = unsavedData[prop]; 2606 | delete unsavedData[prop]; 2607 | } 2608 | } 2609 | }, 2610 | 2611 | adapterDidUpdate: function() { 2612 | this.unsavedData = {}; 2613 | } 2614 | }; 2615 | 2616 | })(); 2617 | 2618 | 2619 | 2620 | (function() { 2621 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none; 2622 | 2623 | var retrieveFromCurrentState = Ember.computed(function(key) { 2624 | return get(getPath(this, 'stateManager.currentState'), key); 2625 | }).property('stateManager.currentState').cacheable(); 2626 | 2627 | DS.Model = Ember.Object.extend(Ember.Evented, { 2628 | isLoaded: retrieveFromCurrentState, 2629 | isDirty: retrieveFromCurrentState, 2630 | isSaving: retrieveFromCurrentState, 2631 | isDeleted: retrieveFromCurrentState, 2632 | isError: retrieveFromCurrentState, 2633 | isNew: retrieveFromCurrentState, 2634 | isPending: retrieveFromCurrentState, 2635 | isValid: retrieveFromCurrentState, 2636 | 2637 | clientId: null, 2638 | transaction: null, 2639 | stateManager: null, 2640 | pendingQueue: null, 2641 | errors: null, 2642 | 2643 | // because unknownProperty is used, any internal property 2644 | // must be initialized here. 2645 | primaryKey: 'id', 2646 | id: Ember.computed(function(key, value) { 2647 | var primaryKey = get(this, 'primaryKey'), 2648 | data = get(this, 'data'); 2649 | 2650 | if (arguments.length === 2) { 2651 | set(data, primaryKey, value); 2652 | return value; 2653 | } 2654 | 2655 | var id = get(data, primaryKey); 2656 | return id ? id : this._id; 2657 | }).property('primaryKey', 'data'), 2658 | 2659 | // The following methods are callbacks invoked by `toJSON`. You 2660 | // can override one of the callbacks to override specific behavior, 2661 | // or toJSON itself. 2662 | // 2663 | // If you override toJSON, you can invoke these callbacks manually 2664 | // to get the default behavior. 2665 | 2666 | /** 2667 | Add the record's primary key to the JSON hash. 2668 | 2669 | The default implementation uses the record's specified `primaryKey` 2670 | and the `id` computed property, which are passed in as parameters. 2671 | 2672 | @param {Object} json the JSON hash being built 2673 | @param {Number|String} id the record's id 2674 | @param {String} key the primaryKey for the record 2675 | */ 2676 | addIdToJSON: function(json, id, key) { 2677 | if (id) { json[key] = id; } 2678 | }, 2679 | 2680 | /** 2681 | Add the attributes' current values to the JSON hash. 2682 | 2683 | The default implementation gets the current value of each 2684 | attribute from the `data`, and uses a `defaultValue` if 2685 | specified in the `DS.attr` definition. 2686 | 2687 | @param {Object} json the JSON hash being build 2688 | @param {Ember.Map} attributes a Map of attributes 2689 | @param {DataProxy} data the record's data, accessed with `get` and `set`. 2690 | */ 2691 | addAttributesToJSON: function(json, attributes, data) { 2692 | attributes.forEach(function(name, meta) { 2693 | var key = meta.key(this.constructor), 2694 | value = get(data, key); 2695 | 2696 | if (value === undefined) { 2697 | value = meta.options.defaultValue; 2698 | } 2699 | 2700 | json[key] = value; 2701 | }, this); 2702 | }, 2703 | 2704 | /** 2705 | Add the value of a `hasMany` association to the JSON hash. 2706 | 2707 | The default implementation honors the `embedded` option 2708 | passed to `DS.hasMany`. If embedded, `toJSON` is recursively 2709 | called on the child records. If not, the `id` of each 2710 | record is added. 2711 | 2712 | Note that if a record is not embedded and does not 2713 | yet have an `id` (usually provided by the server), it 2714 | will not be included in the output. 2715 | 2716 | @param {Object} json the JSON hash being built 2717 | @param {DataProxy} data the record's data, accessed with `get` and `set`. 2718 | @param {Object} meta information about the association 2719 | @param {Object} options options passed to `toJSON` 2720 | */ 2721 | addHasManyToJSON: function(json, data, meta, options) { 2722 | var key = meta.key, 2723 | manyArray = get(this, key), 2724 | records = [], i, l, 2725 | clientId, id; 2726 | 2727 | if (meta.options.embedded) { 2728 | // TODO: Avoid materializing embedded hashes if possible 2729 | manyArray.forEach(function(record) { 2730 | records.push(record.toJSON(options)); 2731 | }); 2732 | } else { 2733 | var clientIds = get(manyArray, 'content'); 2734 | 2735 | for (i=0, l=clientIds.length; i