├── .npmignore ├── .gitignore ├── .travis.yml ├── src ├── sass │ ├── effects │ │ ├── _fade.scss │ │ ├── _flip.scss │ │ ├── _slideOver.scss │ │ └── _slide.scss │ └── ember-animated-outlet-mobile.scss └── js │ ├── route.js │ ├── effects │ ├── fade.js │ ├── flip.js │ ├── slideOver.js │ └── slide.js │ ├── animated-outlet-helper.js │ ├── router.js │ ├── controller-mixin.js │ ├── link-to-animated-helper.js │ └── animated-container-view.js ├── README.md ├── bower.json ├── tests ├── server.js ├── index.html ├── cli-runner.js ├── helpers.js ├── animated-container-view.js └── link-to-animated-helper.js ├── package.json ├── LICENSE ├── ember-addon.js ├── Gruntfile.js ├── vendor ├── qunit-1.11.0.css └── qunit-1.11.0.js └── dist ├── ember-animated-outlet-mobile.css ├── ember-animated-outlet-mobile.min.js └── ember-animated-outlet-mobile.js /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | src 4 | tests 5 | vendor 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.idea 3 | /.sass-cache 4 | /bower_components 5 | /node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - gem install compass --no-ri --no-rdoc 3 | - npm install -g grunt-cli 4 | - npm install 5 | - node tests/server.js & 6 | script: phantomjs tests/cli-runner.js -------------------------------------------------------------------------------- /src/sass/effects/_fade.scss: -------------------------------------------------------------------------------- 1 | .ember-animated-container-fade-old { 2 | @include transition-property(opacity); 3 | @include transition-duration(0.5s); 4 | z-index: 2; 5 | @include opacity(1); 6 | } 7 | .ember-animated-container-fade-old-fading { 8 | @include opacity(0); 9 | } 10 | 11 | .ember-animated-container-fade-new { 12 | z-index: 1; 13 | } -------------------------------------------------------------------------------- /src/js/route.js: -------------------------------------------------------------------------------- 1 | Ember.Route.reopen({ 2 | 3 | transitionToAnimated: function(name, context) { 4 | var router = this.router; 5 | return router.transitionToAnimated.apply(router, arguments); 6 | }, 7 | 8 | replaceWithAnimated: function() { 9 | var router = this.router; 10 | return router.replaceWithAnimated.apply(router, arguments); 11 | } 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /src/js/effects/fade.js: -------------------------------------------------------------------------------- 1 | Ember.AnimatedContainerView.registerEffect('fade', function(ct, newView, oldView, callback) { 2 | var newEl = newView.$(), 3 | oldEl = oldView.$(); 4 | newEl.addClass('ember-animated-container-fade-new'); 5 | oldEl.addClass('ember-animated-container-fade-old'); 6 | setTimeout(function() { 7 | oldEl.addClass('ember-animated-container-fade-old-fading'); 8 | setTimeout(function() { 9 | newEl.removeClass('ember-animated-container-fade-new'); 10 | callback(); 11 | }, 550); 12 | }, 0); 13 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Deprecation notice 3 | 4 | We will no longer be maintaining this repo as we will be using [liquid-fire](https://github.com/ef4/liquid-fire) on future projects. 5 | 6 | Fork of [Ember Animated Outlet](https://travis-ci.org/billysbilling/ember-animated-outlet) 7 | 8 | We forked this as the main repo doesn't have support for mobile so we wanted to 9 | make the animations more native looking and also have it work with touch events. 10 | 11 | If you need to respond to touch events, please include 12 | [fastclick](https://github.com/ftlabs/fastclick) in your project 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/sass/ember-animated-outlet-mobile.scss: -------------------------------------------------------------------------------- 1 | @import "compass"; 2 | 3 | .ember-animated-container { 4 | position: absolute; 5 | height: 100%; 6 | width: 100%; 7 | @include transform(translate3d(0, 0, 0)); 8 | -webkit-transform: translateZ(0); 9 | 10 | & > .ember-view { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | width: 100%; 15 | height: 100%; 16 | @include backface-visibility(hidden); 17 | } 18 | } 19 | 20 | @import "effects/fade"; 21 | @import "effects/flip"; 22 | @import "effects/slide"; 23 | @import "effects/slideOver"; 24 | -------------------------------------------------------------------------------- /src/sass/effects/_flip.scss: -------------------------------------------------------------------------------- 1 | .ember-animated-container-flip-wrap { 2 | @include perspective(1200px); 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .ember-animated-container-flip-ct { 8 | @include transition(0.6s); 9 | @include transform-style(preserve-3d); 10 | } 11 | .ember-animated-container-flip-ct-flipping { 12 | @include transform(rotateY(180deg)); 13 | } 14 | 15 | .ember-animated-container-flip-new, .ember-animated-container-flip-old { 16 | @include backface-visibility(hidden); 17 | } 18 | 19 | .ember-animated-container-flip-old { 20 | z-index: 2; 21 | } 22 | 23 | .ember-animated-container-flip-new { 24 | @include transform(rotateY(180deg)); 25 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-animated-outlet-mobile", 3 | "version": "1.0.2", 4 | "homepage": "https://github.com/poetic/ember-animated-outlet-mobile", 5 | "authors": [ 6 | "Jake Craige " 7 | ], 8 | "description": "Ember Animated Outlet is a plug'n'play module to support animated route transitions in Ember.js with a focus on mobile devices.", 9 | "main": "dist/ember-animated-outlet.js", 10 | "keywords": [ 11 | "ember", 12 | "ember.js", 13 | "animation", 14 | "animated", 15 | "outlet", 16 | "transition", 17 | "mobile" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "src", 25 | "tests", 26 | "vendor" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | childProcess = require('child_process'), 3 | express = require('express'); 4 | 5 | var app = express(); 6 | 7 | app.use('/dist', express.static(__dirname + '/../dist')); 8 | app.use('/tests', express.static(__dirname + '/../tests')); 9 | app.use('/vendor', express.static(__dirname + '/../vendor')); 10 | 11 | app.get('/', function(req, res, callback) { 12 | childProcess.exec('grunt', function(err, output) { 13 | if (err) { 14 | console.log('Grunt error:'); 15 | console.log(output); 16 | return callback(err); 17 | } 18 | fs.readFile(__dirname + '/index.html', 'utf8', function(err, html){ 19 | if (err) return callback(err); 20 | res.send(html); 21 | }); 22 | }); 23 | }); 24 | 25 | app.listen(7846); -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ember Animated Outlet Test Suite 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/js/effects/flip.js: -------------------------------------------------------------------------------- 1 | Ember.AnimatedContainerView.registerEffect('flip', function(ct, newView, oldView, callback) { 2 | var ctEl = ct.$(), 3 | newEl = newView.$(), 4 | oldEl = oldView.$(); 5 | ctEl.wrap('
') 6 | ctEl.addClass('ember-animated-container-flip-ct'); 7 | newEl.addClass('ember-animated-container-flip-new'); 8 | oldEl.addClass('ember-animated-container-flip-old'); 9 | setTimeout(function() { 10 | ctEl.addClass('ember-animated-container-flip-ct-flipping'); 11 | setTimeout(function() { 12 | ctEl.unwrap(); 13 | ctEl.removeClass('ember-animated-container-flip-ct'); 14 | ctEl.removeClass('ember-animated-container-flip-ct-flipping'); 15 | newEl.removeClass('ember-animated-container-flip-new'); 16 | callback(); 17 | }, 650); 18 | }, 0); 19 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-animated-outlet-mobile", 3 | "version": "2.0.0", 4 | "description": "Ember Animated Outlet (for mobile) is a plug'n'play module to support animated route transitions in Ember.js", 5 | "homepage": "https://github.com/poetic/ember-animated-outlet-mobile", 6 | "repository": "https://github.com/poetic/ember-animated-outlet-mobile", 7 | "devDependencies": { 8 | "grunt": "~0.4.1", 9 | "grunt-contrib-concat": "~0.1.3", 10 | "grunt-contrib-uglify": "~0.2.0", 11 | "grunt-contrib-compass": "~0.1.3", 12 | "grunt-contrib-watch": "~0.3.1", 13 | "express": "~3.1.0" 14 | }, 15 | "keywords": [ 16 | "ember-addon", 17 | "ember", 18 | "ember.js", 19 | "animation", 20 | "animated", 21 | "outlet", 22 | "transition", 23 | "mobile" 24 | ], 25 | "ember-addon": { 26 | "main": "ember-addon" 27 | }, 28 | "license": "MIT", 29 | "author": "Jake Craige ", 30 | "dependencies": { 31 | "broccoli-static-compiler": "^0.1.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Billy's Billing Corp. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ember-addon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pickFiles = require('broccoli-static-compiler'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | function AnimatedOutletMobile(project) { 8 | this.project = project; 9 | this.name = "Ember CLI Ember Animated Outlet Mobile"; 10 | } 11 | 12 | function unwatchedTree(dir) { 13 | return { 14 | read: function() { return dir; }, 15 | cleanup: function() { } 16 | }; 17 | } 18 | 19 | AnimatedOutletMobile.prototype.treeFor = function(name) { 20 | if (name !== 'vendor') { 21 | return; 22 | } 23 | 24 | var libPath = unwatchedTree(path.join(__dirname, 'dist')); 25 | 26 | var lib = pickFiles(libPath, { 27 | srcDir: '/', 28 | files: ['*.js', '*.css'], 29 | destDir: '/ember-animated-outlet-mobile/dist' 30 | }); 31 | 32 | return lib; 33 | }; 34 | 35 | AnimatedOutletMobile.prototype.included = function(app) { 36 | app.import('vendor/ember-animated-outlet-mobile/dist/ember-animated-outlet-mobile.css'); 37 | app.import('vendor/ember-animated-outlet-mobile/dist/ember-animated-outlet-mobile.js'); 38 | }; 39 | 40 | module.exports = AnimatedOutletMobile; 41 | -------------------------------------------------------------------------------- /tests/cli-runner.js: -------------------------------------------------------------------------------- 1 | var page = require('webpage').create(); 2 | 3 | page.onError = function(message, trace) { 4 | console.log('Page error: '+message, 'ERROR'); 5 | phantom.exit(1); 6 | }; 7 | page.onInitialized = function() { 8 | page.evaluate(function() { 9 | window.isCli = true; 10 | }); 11 | }; 12 | 13 | function checkDone() { 14 | var result = page.evaluate(function() { 15 | return window.cliResults; 16 | }); 17 | if (!result || !result.isDone) { 18 | setTimeout(checkDone, 10); 19 | return; 20 | } 21 | var details = result.details; 22 | console.log("Total: " + details.total+ ", Failed: " + details.failed + ", Passed: " + details.passed + ", Runtime: " + details.runtime); 23 | if (details.failed) { 24 | console.log('TESTS FAILED'); 25 | result.failures.forEach(function(details) { 26 | console.log(details.module + ': ' + details.name); 27 | }, this); 28 | } else { 29 | console.log('SUCCESS'); 30 | } 31 | phantom.exit(details.failed ? 1 : 0); 32 | } 33 | 34 | var url = 'http://localhost:7846/'; 35 | page.open(url, function(status) { 36 | if (status !== 'success') { 37 | console.log('Could not open '+url); 38 | phantom.exit(1); 39 | } 40 | checkDone(); 41 | }); -------------------------------------------------------------------------------- /src/js/animated-outlet-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | Write me... 3 | 4 | Straight-up stolen from `Handlebars.registerHelper('outlet', ...);` 5 | 6 | @method outlet 7 | @for Ember.Handlebars.helpers 8 | @param {String} property the property on the controller that holds the view for this outlet 9 | */ 10 | Handlebars.registerHelper('animated-outlet', function(property, options) { 11 | var outletSource; 12 | 13 | if (property && property.data && property.data.isRenderData) { 14 | options = property; 15 | property = 'main'; 16 | } 17 | 18 | outletSource = options.data.view; 19 | while (!(outletSource.get('template.isTop'))){ 20 | outletSource = outletSource.get('_parentView'); 21 | } 22 | 23 | options.data.view.set('outletSource', outletSource); 24 | options.hash.currentViewBinding = '_view.outletSource._outlets.' + property; 25 | 26 | //Only this line has been changed 27 | return Ember.Handlebars.helpers.view.call(this, Ember.AnimatedContainerView, options); 28 | }); 29 | 30 | /** 31 | See animated-outlet 32 | */ 33 | Handlebars.registerHelper('animatedOutlet', function(property, options) { 34 | Ember.warn("The 'animatedOutlet' view helper is deprecated in favor of 'animated-outlet'"); 35 | return Ember.Handlebars.helpers['animated-outlet'].apply(this, arguments); 36 | }); -------------------------------------------------------------------------------- /src/js/router.js: -------------------------------------------------------------------------------- 1 | Ember.Router.reopen({ 2 | 3 | /** 4 | Works as {@link Ember.Router.transitionTo}} except that it takes a third parameter, `animations`, 5 | which will enqueue animations. 6 | 7 | `animations` should be an object with outlet names as keys and effect names as value. 8 | 9 | @param name 10 | @param animations {Object} Animations to enqueue 11 | @param model 12 | */ 13 | transitionToAnimated: function(name, animations, model) { 14 | Ember.AnimatedContainerView.enqueueAnimations(animations); 15 | Array.prototype.splice.call(arguments, 1, 1); 16 | return this.transitionTo.apply(this, arguments); 17 | }, 18 | 19 | /** 20 | Works as {@link Ember.Router.replaceWith}} except that it takes a third parameter, `animations`, 21 | which will enqueue animations. 22 | 23 | `animations` should be an object with outlet names as keys and effect names as value. 24 | 25 | @param name 26 | @param animations {Object} Animations to enqueue 27 | @param model 28 | */ 29 | replaceWithAnimated: function(name, animations, model) { 30 | Ember.AnimatedContainerView.enqueueAnimations(animations); 31 | Array.prototype.splice.call(arguments, 1, 1); 32 | return this.replaceWith.apply(this, arguments); 33 | } 34 | 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /src/js/controller-mixin.js: -------------------------------------------------------------------------------- 1 | Ember.ControllerMixin.reopen({ 2 | 3 | /** 4 | Works as {@link Ember.ControllerMixin.transitionToRoute}} except that it takes a third parameter, `animations`, 5 | which will enqueue animations. 6 | 7 | `animations` should be an object with outlet names as keys and effect names as value. 8 | 9 | @param name 10 | @param animations {Object} Animations to enqueue 11 | @param model 12 | */ 13 | transitionToRouteAnimated: function(name, animations, model) { 14 | Ember.AnimatedContainerView.enqueueAnimations(animations); 15 | Array.prototype.splice.call(arguments, 1, 1); 16 | return this.transitionToRoute.apply(this, arguments); 17 | }, 18 | 19 | /** 20 | Works as {@link Ember.ControllerMixin.replaceRoute}} except that it takes a third parameter, `animations`, 21 | which will enqueue animations. 22 | 23 | `animations` should be an object with outlet names as keys and effect names as value. 24 | 25 | @param name 26 | @param animations {Object} Animations to enqueue 27 | @param model 28 | */ 29 | replaceRouteAnimated: function(name, animations, model) { 30 | Ember.AnimatedContainerView.enqueueAnimations(animations); 31 | Array.prototype.splice.call(arguments, 1, 1); 32 | return this.replaceRoute.apply(this, arguments); 33 | } 34 | 35 | }); -------------------------------------------------------------------------------- /src/sass/effects/_slideOver.scss: -------------------------------------------------------------------------------- 1 | // iOS Timing 2 | $timing-function: cubic-beizer(0.1, 0.7, 0.1, 1); 3 | 4 | .ember-animated-container-slideOver-old { 5 | z-index: 1; 6 | } 7 | .ember-animated-container-slideOver-left-new, 8 | .ember-animated-container-slideOver-right-new, 9 | .ember-animated-container-slideOver-up-new, 10 | .ember-animated-container-slideOver-down-new { 11 | @include transition-property(transform); 12 | @include transition-duration(0.4s); 13 | @include transition-timing-function($timing-function); 14 | @include transform(translateX(0%)); 15 | z-index: 2; 16 | 17 | &.ember-animated-container-slideOver-left-new-sliding { 18 | @include transform(translateX(-100%)); 19 | } 20 | 21 | &.ember-animated-container-slideOver-right-new-sliding { 22 | @include transform(translateX(100%)); 23 | } 24 | 25 | &.ember-animated-container-slideOver-up-new-sliding { 26 | @include transform(translateY(-100%)); 27 | } 28 | 29 | &.ember-animated-container-slideOver-down-new-sliding { 30 | @include transform(translateY(100%)); 31 | } 32 | 33 | } 34 | 35 | .ember-animated-container-slideOver-left-new { 36 | left: 100% !important; 37 | } 38 | 39 | .ember-animated-container-slideOver-right-new { 40 | left: -100% !important; 41 | } 42 | 43 | .ember-animated-container-slideOver-up-new { 44 | top: 100% !important; 45 | } 46 | 47 | .ember-animated-container-slideOver-down-new { 48 | top: -100% !important; 49 | } 50 | -------------------------------------------------------------------------------- /src/sass/effects/_slide.scss: -------------------------------------------------------------------------------- 1 | // iOS Timing 2 | $timing-function: cubic-beizer(0.1, 0.7, 0.1, 1); 3 | 4 | .ember-animated-container-slide-left-ct, 5 | .ember-animated-container-slide-right-ct, 6 | .ember-animated-container-slide-up-ct, 7 | .ember-animated-container-slide-down-ct { 8 | @include transition-property(transform); 9 | @include transition-duration(0.4s); 10 | @include transition-timing-function($timing-function); 11 | 12 | &.ember-animated-container-slide-slow-ct { 13 | @include transition-duration(2s); 14 | } 15 | } 16 | .ember-animated-container-slide-right-ct, 17 | .ember-animated-container-slide-down-ct { 18 | @include transition-duration(0.3s); 19 | } 20 | 21 | .ember-animated-container-slide-left-ct-sliding { 22 | @include transform(translate3d(-100%, 0, 0)); 23 | } 24 | 25 | .ember-animated-container-slide-right-ct-sliding { 26 | @include transform(translate3d(100%, 0, 0)); 27 | } 28 | 29 | .ember-animated-container-slide-up-ct-sliding { 30 | @include transform(translate3d(0, -100%, 0)); 31 | } 32 | 33 | .ember-animated-container-slide-down-ct-sliding { 34 | @include transform(translate3d(0, 100%, 0)); 35 | } 36 | 37 | .ember-animated-container-slide-left-new { 38 | left: 100% !important; 39 | } 40 | 41 | .ember-animated-container-slide-right-new { 42 | left: -100% !important; 43 | } 44 | 45 | .ember-animated-container-slide-up-new { 46 | top: 100% !important; 47 | } 48 | 49 | .ember-animated-container-slide-down-new { 50 | top: -100% !important; 51 | } 52 | -------------------------------------------------------------------------------- /src/js/effects/slideOver.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var slideOver = function(ct, newView, oldView, callback, direction) { 4 | var ctEl = ct.$(), 5 | newEl = newView.$(), 6 | duration = 450; 7 | ctEl.addClass('ember-animated-container-slideOver-old'); 8 | newEl.addClass('ember-animated-container-slideOver-'+direction+'-new'); 9 | setTimeout(function() { 10 | newEl.addClass('ember-animated-container-slideOver-'+direction+'-new-sliding'); 11 | setTimeout(function() { 12 | newEl.removeClass('ember-animated-container-slideOver-'+direction+'-new'); 13 | newEl.removeClass('ember-animated-container-slideOver-'+direction+'-new-sliding'); 14 | ctEl.removeClass('ember-animated-container-slideOver-old'); 15 | callback(); 16 | }, duration); 17 | }, 0); 18 | }; 19 | 20 | Ember.AnimatedContainerView.registerEffect('slideOverLeft', function(ct, newView, oldView, callback) { 21 | slideOver(ct, newView, oldView, callback, 'left'); 22 | }); 23 | 24 | Ember.AnimatedContainerView.registerEffect('slideOverRight', function(ct, newView, oldView, callback) { 25 | slideOver(ct, newView, oldView, callback, 'right'); 26 | }); 27 | 28 | Ember.AnimatedContainerView.registerEffect('slideOverUp', function(ct, newView, oldView, callback) { 29 | slideOver(ct, newView, oldView, callback, 'up'); 30 | }); 31 | 32 | Ember.AnimatedContainerView.registerEffect('slideOverDown', function(ct, newView, oldView, callback) { 33 | slideOver(ct, newView, oldView, callback, 'down'); 34 | }); 35 | 36 | })(); 37 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = function(grunt) { 4 | 5 | grunt.initConfig({ 6 | meta: { 7 | name: 'Ember Animated Outlet' 8 | }, 9 | concat: { 10 | dist: { 11 | separator: '\n\n', 12 | src: [ 13 | 'src/js/animated-container-view.js', 14 | 'src/js/animated-outlet-helper.js', 15 | 'src/js/link-to-animated-helper.js', 16 | 'src/js/router.js', 17 | 'src/js/route.js', 18 | 'src/js/controller-mixin.js', 19 | 'src/js/effects/*.js', 20 | ], 21 | dest: 'dist/ember-animated-outlet-mobile.js' 22 | } 23 | }, 24 | uglify: { 25 | dist: { 26 | src: ['dist/ember-animated-outlet-mobile.js'], 27 | dest: 'dist/ember-animated-outlet-mobile.min.js' 28 | } 29 | }, 30 | compass: { 31 | dist: { 32 | options: { 33 | sassDir: 'src/sass', 34 | cssDir: 'dist', 35 | environment: 'production' 36 | } 37 | } 38 | }, 39 | watch: { 40 | files: [ 41 | 'src/**/*', 42 | 'vendor/**/*' 43 | ], 44 | tasks: ['default'] 45 | } 46 | }); 47 | 48 | grunt.loadNpmTasks('grunt-contrib-concat'); 49 | grunt.loadNpmTasks('grunt-contrib-uglify'); 50 | grunt.loadNpmTasks('grunt-contrib-compass'); 51 | grunt.loadNpmTasks('grunt-contrib-watch'); 52 | 53 | grunt.registerTask('default', ['concat', 'uglify', 'compass']); 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | if (window.isCli) { 2 | window.cliResults = { 3 | failures: [], 4 | isDone: false 5 | }; 6 | QUnit.testDone(function(details) { 7 | if (details.failed) { 8 | window.cliResults.failures.push(details); 9 | } 10 | }); 11 | QUnit.done(function(details) { 12 | window.cliResults.details = details; 13 | window.cliResults.isDone = true; 14 | }); 15 | } 16 | 17 | // stolen from the ember-dev package 18 | // https://github.com/emberjs/ember-dev/blob/master/support/tests/qunit_configuration.js 19 | (function() { 20 | // Add `expectAssertion` which replaces 21 | // `raises` to detect uncatchable assertions 22 | function expectAssertion(fn, expectedMessage) { 23 | var originalAssert = Ember.assert, 24 | actualMessage, actualTest, 25 | arity, sawAssertion; 26 | 27 | var AssertionFailedError = new Error('AssertionFailed'); 28 | 29 | try { 30 | Ember.assert = function(message, test) { 31 | arity = arguments.length; 32 | actualMessage = message; 33 | actualTest = test; 34 | 35 | if (!test) { 36 | throw AssertionFailedError; 37 | } 38 | }; 39 | 40 | try { 41 | fn(); 42 | } catch(error) { 43 | if (error === AssertionFailedError) { 44 | sawAssertion = true; 45 | } else { 46 | throw error; 47 | } 48 | } 49 | 50 | if (!sawAssertion) { 51 | ok(false, "Expected Ember.assert: '" + expectedMessage + "', but no assertions where run"); 52 | } else if (arity === 2) { 53 | 54 | if (expectedMessage) { 55 | if (expectedMessage instanceof RegExp) { 56 | ok(expectedMessage.test(actualMessage), "Expected Ember.assert: '" + expectedMessage + "', but got '" + actualMessage + "'"); 57 | }else{ 58 | equal(actualMessage, expectedMessage, "Expected Ember.assert: '" + expectedMessage + "', but got '" + actualMessage + "'"); 59 | } 60 | } else { 61 | ok(!actualTest); 62 | } 63 | } else if (arity === 1) { 64 | ok(!actualTest); 65 | } else { 66 | ok(false, 'Ember.assert was called without the assertion'); 67 | } 68 | 69 | } finally { 70 | Ember.assert = originalAssert; 71 | } 72 | } 73 | 74 | window.expectAssertion = expectAssertion; 75 | })(); 76 | -------------------------------------------------------------------------------- /src/js/effects/slide.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var slide = function(ct, newView, oldView, callback, direction, slow) { 4 | var ctEl = ct.$(), 5 | newEl = newView.$(), 6 | duration = slow ? 2050 : 450; 7 | ctEl.addClass('ember-animated-container-slide-'+direction+'-ct') 8 | if (slow) { 9 | ctEl.addClass('ember-animated-container-slide-slow-ct') 10 | } 11 | newEl.addClass('ember-animated-container-slide-'+direction+'-new'); 12 | setTimeout(function() { 13 | ctEl.addClass('ember-animated-container-slide-'+direction+'-ct-sliding'); 14 | setTimeout(function() { 15 | ctEl.removeClass('ember-animated-container-slide-'+direction+'-ct'); 16 | if (slow) { 17 | ctEl.removeClass('ember-animated-container-slide-slow-ct') 18 | } 19 | ctEl.removeClass('ember-animated-container-slide-'+direction+'-ct-sliding'); 20 | newEl.removeClass('ember-animated-container-slide-'+direction+'-new'); 21 | callback(); 22 | }, duration); 23 | }, 0); 24 | }; 25 | 26 | Ember.AnimatedContainerView.registerEffect('slideLeft', function(ct, newView, oldView, callback) { 27 | slide(ct, newView, oldView, callback, 'left', false); 28 | }); 29 | 30 | Ember.AnimatedContainerView.registerEffect('slideRight', function(ct, newView, oldView, callback) { 31 | slide(ct, newView, oldView, callback, 'right', false); 32 | }); 33 | 34 | Ember.AnimatedContainerView.registerEffect('slideUp', function(ct, newView, oldView, callback) { 35 | slide(ct, newView, oldView, callback, 'up', false); 36 | }); 37 | 38 | Ember.AnimatedContainerView.registerEffect('slideDown', function(ct, newView, oldView, callback) { 39 | slide(ct, newView, oldView, callback, 'down', false); 40 | }); 41 | 42 | Ember.AnimatedContainerView.registerEffect('slowSlideLeft', function(ct, newView, oldView, callback) { 43 | slide(ct, newView, oldView, callback, 'left', true); 44 | }); 45 | 46 | Ember.AnimatedContainerView.registerEffect('slowSlideRight', function(ct, newView, oldView, callback) { 47 | slide(ct, newView, oldView, callback, 'right', true); 48 | }); 49 | 50 | Ember.AnimatedContainerView.registerEffect('slowSlideUp', function(ct, newView, oldView, callback) { 51 | slide(ct, newView, oldView, callback, 'up', false); 52 | }); 53 | 54 | Ember.AnimatedContainerView.registerEffect('slowSlideDown', function(ct, newView, oldView, callback) { 55 | slide(ct, newView, oldView, callback, 'down', false); 56 | }); 57 | 58 | })(); 59 | -------------------------------------------------------------------------------- /src/js/link-to-animated-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module ember 3 | @submodule ember-routing 4 | */ 5 | 6 | var get = Ember.get, set = Ember.set; 7 | 8 | Ember.onLoad('Ember.Handlebars', function(Handlebars) { 9 | var resolveParams = Ember.Router.resolveParams, 10 | isSimpleClick = Ember.ViewUtils.isSimpleClick; 11 | 12 | function fullRouteName(router, name) { 13 | if (!router.hasRoute(name)) { 14 | name = name + '.index'; 15 | } 16 | 17 | return name; 18 | } 19 | 20 | function resolvedPaths(options) { 21 | var types = options.options.types.slice(1), 22 | data = options.options.data; 23 | 24 | return resolveParams(options.context, options.params, { types: types, data: data }); 25 | } 26 | 27 | function args(linkView, router, route) { 28 | var ret = get(linkView,'parameters.params').slice(), 29 | animations = linkView.parameters.animations; 30 | ret.splice(1,0,animations); 31 | return ret; 32 | } 33 | 34 | /** 35 | Renders a link to the supplied route using animation. 36 | 37 | @class AnimatedLinkView 38 | @namespace Ember 39 | @extends Ember.LinkView 40 | **/ 41 | var AnimatedLinkView = Ember.AnimatedLinkView = Ember.LinkView.extend({ 42 | classNames: ['animated-link-view'], 43 | _invoke: function(event) { 44 | if (!isSimpleClick(event)) { return true; } 45 | 46 | event.preventDefault(); 47 | if (this.bubbles === false) { event.stopPropagation(); } 48 | 49 | if (get(this, '_isDisabled')) { return false; } 50 | 51 | if (get(this, 'loading')) { 52 | Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid."); 53 | return false; 54 | } 55 | 56 | var router = this.get('router'), 57 | routeArgs = args(this, router); 58 | 59 | if (get(this, ('replace'))) { 60 | router.replaceWithAnimated.apply(router, routeArgs); 61 | } else { 62 | router.transitionToAnimated.apply(router, routeArgs); 63 | } 64 | } 65 | }); 66 | 67 | AnimatedLinkView.toString = function() { return "AnimatedLinkView"; }; 68 | 69 | /** 70 | @method linkToAnimated 71 | @for Ember.Handlebars.helpers 72 | @param {String} routeName 73 | @param {Object} [context]* 74 | @return {String} HTML string 75 | */ 76 | Ember.Handlebars.registerHelper('link-to-animated', function(name) { 77 | var options = [].slice.call(arguments, -1)[0], 78 | params = [].slice.call(arguments, 0, -1), 79 | hash = options.hash; 80 | 81 | Ember.assert("link-to-animated must contain animations", typeof(hash.animations) == 'string') 82 | var re = /\s*([a-z]+)\s*:\s*([a-z]+)/gi; 83 | var animations = {}; 84 | while (match = re.exec(hash.animations)) { 85 | animations[match[1]] = match[2]; 86 | } 87 | delete(hash.animations) 88 | hash.namedRoute = name; 89 | hash.currentWhen = hash.currentWhen || name; 90 | hash.disabledBinding = hash.disabledWhen; 91 | 92 | hash.parameters = { 93 | context: this, 94 | options: options, 95 | animations: animations, 96 | params: params 97 | }; 98 | 99 | return Ember.Handlebars.helpers.view.call(this, AnimatedLinkView, options); 100 | }); 101 | 102 | /** 103 | See link-to-animated 104 | 105 | @method linkTo 106 | @for Ember.Handlebars.helpers 107 | @deprecated 108 | @param {String} routeName 109 | @param {Object} [context]* 110 | @return {String} HTML string 111 | */ 112 | Ember.Handlebars.registerHelper('linkToAnimated', function() { 113 | Ember.warn("The 'linkToAnimated' view helper is deprecated in favor of 'link-to-animated'"); 114 | return Ember.Handlebars.helpers['link-to-animated'].apply(this, arguments); 115 | }); 116 | 117 | }); 118 | 119 | -------------------------------------------------------------------------------- /vendor/qunit-1.11.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 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-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 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: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 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-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /src/js/animated-container-view.js: -------------------------------------------------------------------------------- 1 | /** 2 | Write me... 3 | 4 | @class AnimatedContainerView 5 | @namespace Ember 6 | @extends Ember.ContainerView 7 | */ 8 | Ember.AnimatedContainerView = Ember.ContainerView.extend({ 9 | 10 | classNames: ['ember-animated-container'], 11 | 12 | init: function() { 13 | this._super(); 14 | //Register this view, so queued effects can be related with this view by name 15 | Ember.AnimatedContainerView._views[this.get('name')] = this; 16 | this._isAnimating = false; 17 | }, 18 | 19 | willDestroy: function() { 20 | this._super(); 21 | //Clean up 22 | var name = this.get('name'); 23 | delete Ember.AnimatedContainerView._views[name]; 24 | delete Ember.AnimatedContainerView._animationQueue[name]; 25 | }, 26 | 27 | //Override parent method 28 | _currentViewWillChange: Ember.beforeObserver(function() { 29 | var currentView = Ember.get(this, 'currentView'); 30 | if (currentView) { 31 | //Store the old `currentView` (and don't destroy it yet) so we can use it for animation later 32 | this.set('oldView', currentView); 33 | } 34 | }, 'currentView'), 35 | 36 | _currentViewDidChange: Ember.observer(function() { 37 | var newView = Ember.get(this, 'currentView'), 38 | oldView = Ember.get(this, 'oldView'), 39 | name = this.get('name'), 40 | effect = null; 41 | if (newView) { 42 | if (oldView) { 43 | Ember.assert('Ember.AnimatedContainerView can only animate non-virtual views. You need to explicitly define your view class.', !oldView.isVirtual); 44 | Ember.assert('Ember.AnimatedContainerView can only animate non-virtual views. You need to explicitly define your view class.', !newView.isVirtual); 45 | //Get and validate a potentially queued effect 46 | effect = Ember.AnimatedContainerView._animationQueue[name]; 47 | delete Ember.AnimatedContainerView._animationQueue[name]; 48 | if (effect && !Ember.AnimatedContainerView._effects[effect]) { 49 | Ember.warn('Unknown animation effect: '+effect); 50 | effect = null; 51 | } 52 | //Forget about the old view 53 | this.set('oldView', null); 54 | } 55 | //If there is already an animation queued, we should cancel it 56 | if (this._queuedAnimation) { 57 | oldView.destroy(); //the oldView has never been visible, and never will be, so we can just destroy it now 58 | oldView = this._queuedAnimation.oldView; //instead, use the oldView from the queued animation, which is our real currentView 59 | } 60 | //Queue this animation and check the queue 61 | this._queuedAnimation = { 62 | newView: newView, 63 | oldView: oldView, 64 | effect: effect 65 | }; 66 | this._handleAnimationQueue(); 67 | } 68 | }, 'currentView'), 69 | 70 | _handleAnimationQueue: function() { 71 | //If animation is in progress, just stop here. Once the animation has finished, this method will be called again. 72 | if (this._isAnimating) { 73 | return; 74 | } 75 | var self = this, 76 | q = this._queuedAnimation; 77 | if (q) { 78 | var newView = q.newView, 79 | oldView = q.oldView, 80 | effect = q.effect; 81 | this._queuedAnimation = null; 82 | //Push the newView to this view, which will append it to the DOM 83 | this.pushObject(newView); 84 | if (oldView && effect) { 85 | //If an effect is queued, then start the effect when the new view has been inserted in the DOM 86 | this._isAnimating = true; 87 | newView.on('didInsertElement', function() { 88 | Ember.AnimatedContainerView._effects[effect](self, newView, oldView, function() { 89 | Em.run(function() { 90 | self.removeObject(oldView); 91 | oldView.destroy(); 92 | //Check to see if there are any queued animations 93 | self._isAnimating = false; 94 | self._handleAnimationQueue(); 95 | }); 96 | }); 97 | }); 98 | } else { 99 | if (oldView) { 100 | //If there is no effect queued, then just remove the old view (as would normally happen in a ContainerView) 101 | this.removeObject(oldView); 102 | oldView.destroy(); 103 | } 104 | } 105 | } 106 | }, 107 | 108 | enqueueAnimation: function(effect) { 109 | Ember.AnimatedContainerView._animationQueue[this.get('name')] = effect; 110 | }, 111 | 112 | setCurrentViewAnimated: function(currentView, effect) { 113 | this.enqueueAnimation(effect); 114 | this.set('currentView', currentView); 115 | } 116 | 117 | }); 118 | 119 | Ember.AnimatedContainerView.reopenClass({ 120 | 121 | /** 122 | All animated outlets registers itself in this hash 123 | 124 | @private 125 | @property {Object} _views 126 | */ 127 | _views: {}, 128 | 129 | /** 130 | Whenever an animated route transition is set in motion, it will be stored here, so the animated outlet view can pick it up 131 | 132 | @private 133 | @property {Object} _animationQueue 134 | */ 135 | _animationQueue: {}, 136 | 137 | /** 138 | Enqueue effects to be executed by the given outlets when the next route transition happens. 139 | 140 | @param {Object} animations A hash with keys corresponding to outlet views and values with the desired animation effect. 141 | */ 142 | enqueueAnimations: function(animations) { 143 | // Animations cause race conditions in testing so we just disable them 144 | if (Ember.testing) { 145 | return; 146 | } 147 | 148 | for (var name in animations) { 149 | if (!animations.hasOwnProperty(name)) continue; 150 | this._animationQueue[name] = animations[name]; 151 | } 152 | }, 153 | 154 | /** 155 | All animation effects are stored on this object and can be referred to by its key 156 | 157 | @private 158 | @property {Object} effects 159 | */ 160 | _effects: {}, 161 | 162 | 163 | /** 164 | Register a new effect. 165 | 166 | The `callback` function will be passed the following parameters: 167 | 168 | - The `Ember.AnimatedContainerView` instance. 169 | - The new view. 170 | - The old view. 171 | 172 | @param {String} effect The name of the effect, e.g. 'slideLeft' 173 | @param {Function} callback The function to call when effect has to be executed 174 | */ 175 | registerEffect: function(effect, callback) { 176 | this._effects[effect] = callback; 177 | } 178 | 179 | }); 180 | -------------------------------------------------------------------------------- /tests/animated-container-view.js: -------------------------------------------------------------------------------- 1 | var ct; 2 | 3 | module('AnimatedContainerView', { 4 | teardown: function() { 5 | Ember.run(function () { 6 | if (ct) { 7 | ct.destroy(); 8 | ct = null; 9 | } 10 | }); 11 | } 12 | }); 13 | 14 | test('Should register and unregister itself', function() { 15 | ct = Ember.AnimatedContainerView.create({ 16 | name: 'foo' 17 | }); 18 | equal(Ember.AnimatedContainerView._views['foo'], ct, 'View was registered'); 19 | Ember.run(function(){ 20 | ct.destroy(); 21 | ct = null; 22 | }); 23 | equal(Ember.AnimatedContainerView._views['foo'], null, 'View was unregistered'); 24 | }); 25 | 26 | test('Should work with an initial currentView', function() { 27 | ct = Ember.AnimatedContainerView.create({ 28 | name: 'foo', 29 | currentView: Ember.View.create({ 30 | template: function() { 31 | return 'Slide, and jump!'; 32 | } 33 | }) 34 | }); 35 | Ember.run(function() { 36 | ct.appendTo('#qunit-fixture'); 37 | }); 38 | equal(Ember.$.trim(ct.$().text()), 'Slide, and jump!', 'Text is correct'); 39 | }); 40 | 41 | test('Should insert first currentView without animation even if an animation is queued', function() { 42 | ct = Ember.AnimatedContainerView.create({ 43 | name: 'foo' 44 | }); 45 | ct.enqueueAnimation('slideLeft'); 46 | Ember.run(function() { 47 | ct.appendTo('#qunit-fixture'); 48 | }); 49 | var view = Ember.View.create({ 50 | template: function() { 51 | return 'Slide, and jump!'; 52 | } 53 | }); 54 | Ember.run(function() { 55 | ct.set('currentView', view); 56 | }); 57 | equal(Ember.$.trim(ct.$().text()), 'Slide, and jump!', 'Text is correct'); 58 | }); 59 | 60 | test('Should change currentView without animation if no animation is queued', function() { 61 | var oldView = Ember.View.create({ 62 | template: function() { 63 | return 'Slide, and jump!'; 64 | } 65 | }); 66 | ct = Ember.AnimatedContainerView.create({ 67 | name: 'foo', 68 | currentView: oldView 69 | }); 70 | Ember.run(function() { 71 | ct.appendTo('#qunit-fixture'); 72 | }); 73 | var newView = Ember.View.create({ 74 | template: function() { 75 | return 'Now chase the rat!'; 76 | } 77 | }); 78 | Ember.run(function() { 79 | ct.set('currentView', newView); 80 | }); 81 | equal(Ember.$.trim(ct.$().text()), 'Now chase the rat!', 'Only new view\'s text is present'); 82 | equal(oldView.get('isDestroyed'), true, 'Old view was destroyed'); 83 | }); 84 | 85 | test('Should animate through Ember.AnimatedContainerView.enqueueAnimations', function() { 86 | var oldView = Ember.View.create({ 87 | template: function() { 88 | return 'Slide, and jump!'; 89 | } 90 | }); 91 | ct = Ember.AnimatedContainerView.create({ 92 | name: 'foo', 93 | currentView: oldView 94 | }); 95 | Ember.run(function() { 96 | ct.appendTo('#qunit-fixture'); 97 | }); 98 | var newView = Ember.View.create({ 99 | template: function() { 100 | return 'Now chase the rat!'; 101 | } 102 | }); 103 | Ember.run(function() { 104 | ct.setCurrentViewAnimated(newView, 'fade'); 105 | }); 106 | equal(Ember.$.trim(ct.$().text()), 'Slide, and jump!Now chase the rat!', 'Container has both views\'s texts.'); 107 | }); 108 | 109 | var effects = [ 110 | 'fade', 111 | 'flip', 112 | 'slideLeft', 113 | 'slideRight', 114 | 'slideUp', 115 | 'slideDown', 116 | 'slideOverLeft', 117 | 'slideOverRight', 118 | 'slideOverUp', 119 | 'slideOverDown' 120 | ]; 121 | effects.forEach(function(effect) { 122 | asyncTest(effect + ' effect', function() { 123 | var oldView = Ember.View.create({ 124 | template: function() { 125 | return 'Slide, and jump!'; 126 | } 127 | }); 128 | ct = Ember.AnimatedContainerView.create({ 129 | name: 'foo', 130 | currentView: oldView 131 | }); 132 | Ember.run(function() { 133 | ct.appendTo('#qunit-fixture'); 134 | }); 135 | var newView = Ember.View.create({ 136 | template: function() { 137 | return 'Now chase the rat!'; 138 | } 139 | }); 140 | Ember.run(function() { 141 | ct.setCurrentViewAnimated(newView, effect); 142 | }); 143 | equal(Ember.$.trim(ct.$().text()), 'Slide, and jump!Now chase the rat!', 'Container has both views\'s texts.'); 144 | //Sleep 1 second to allow the animation to finish 145 | setTimeout(function() { 146 | equal(Ember.$.trim(ct.$().text()), 'Now chase the rat!', 'Only the new view\'s text is left.'); 147 | equal(oldView.get('isDestroyed'), true, 'Old view was destroyed'); 148 | start(); 149 | }, 1000); 150 | }); 151 | }); 152 | 153 | 154 | asyncTest('Queuing multiple animations', function() { 155 | var view1 = Ember.View.create({ 156 | template: function() { 157 | return 'Slide, and jump!'; 158 | } 159 | }); 160 | ct = Ember.AnimatedContainerView.create({ 161 | name: 'foo', 162 | currentView: view1 163 | }); 164 | Ember.run(function() { 165 | ct.appendTo('#qunit-fixture'); 166 | }); 167 | var view2 = Ember.View.create({ 168 | template: function() { 169 | return 'Now chase the rat!'; 170 | } 171 | }); 172 | Ember.run(function() { 173 | ct.setCurrentViewAnimated(view2, 'slideLeft'); 174 | }); 175 | setTimeout(function() { 176 | var view3 = Ember.View.create({ 177 | template: function() { 178 | return 'Go go sugar Pops!'; 179 | } 180 | }); 181 | Ember.run(function() { 182 | ct.setCurrentViewAnimated(view3, 'slideLeft'); 183 | }); 184 | var view4 = Ember.View.create({ 185 | template: function() { 186 | return 'Stop!'; 187 | } 188 | }); 189 | Ember.run(function() { 190 | ct.setCurrentViewAnimated(view4, 'slideLeft'); 191 | }); 192 | equal(Ember.$.trim(ct.$().text()), 'Slide, and jump!Now chase the rat!', 'Container still has only the two first views\'s texts.'); 193 | //Sleep 450ms to allow the first animation to finish 194 | setTimeout(function() { 195 | equal(Ember.$.trim(ct.$().text()), 'Now chase the rat!Stop!', 'Container has view2 and view 4\'s texts.'); 196 | equal(view1.get('isDestroyed'), true, 'view1 was destroyed'); 197 | equal(view3.get('isDestroyed'), true, 'view3 view was destroyed (it was never really visible)'); 198 | setTimeout(function() { 199 | equal(Ember.$.trim(ct.$().text()), 'Stop!', 'Container only has view4\'s text.'); 200 | equal(view2.get('isDestroyed'), true, 'view2 was destroyed'); 201 | start(); 202 | }, 1000); 203 | }, 450); 204 | }, 100) 205 | }); -------------------------------------------------------------------------------- /dist/ember-animated-outlet-mobile.css: -------------------------------------------------------------------------------- 1 | .ember-animated-container{position:absolute;height:100%;width:100%;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);-ms-transform:translate3d(0, 0, 0);-o-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateZ(0)}.ember-animated-container>.ember-view{position:absolute;top:0;left:0;width:100%;height:100%;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;backface-visibility:hidden}.ember-animated-container-fade-old{-webkit-transition-property:opacity;-moz-transition-property:opacity;-o-transition-property:opacity;transition-property:opacity;-webkit-transition-duration:0.5s;-moz-transition-duration:0.5s;-o-transition-duration:0.5s;transition-duration:0.5s;z-index:2;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);opacity:1}.ember-animated-container-fade-old-fading{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=0);opacity:0}.ember-animated-container-fade-new{z-index:1}.ember-animated-container-flip-wrap{-webkit-perspective:1200px;-moz-perspective:1200px;-ms-perspective:1200px;-o-perspective:1200px;perspective:1200px;width:100%;height:100%}.ember-animated-container-flip-ct{-webkit-transition:0.6s;-moz-transition:0.6s;-o-transition:0.6s;transition:0.6s;-webkit-transform-style:preserve-3d;-moz-transform-style:preserve-3d;-ms-transform-style:preserve-3d;-o-transform-style:preserve-3d;transform-style:preserve-3d}.ember-animated-container-flip-ct-flipping{-webkit-transform:rotateY(180deg);-moz-transform:rotateY(180deg);-ms-transform:rotateY(180deg);-o-transform:rotateY(180deg);transform:rotateY(180deg)}.ember-animated-container-flip-new,.ember-animated-container-flip-old{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;backface-visibility:hidden}.ember-animated-container-flip-old{z-index:2}.ember-animated-container-flip-new{-webkit-transform:rotateY(180deg);-moz-transform:rotateY(180deg);-ms-transform:rotateY(180deg);-o-transform:rotateY(180deg);transform:rotateY(180deg)}.ember-animated-container-slide-left-ct,.ember-animated-container-slide-right-ct,.ember-animated-container-slide-up-ct,.ember-animated-container-slide-down-ct{-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;transition-property:transform;-webkit-transition-duration:0.4s;-moz-transition-duration:0.4s;-o-transition-duration:0.4s;transition-duration:0.4s;-webkit-transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);-moz-transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);-o-transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1)}.ember-animated-container-slide-left-ct.ember-animated-container-slide-slow-ct,.ember-animated-container-slide-right-ct.ember-animated-container-slide-slow-ct,.ember-animated-container-slide-up-ct.ember-animated-container-slide-slow-ct,.ember-animated-container-slide-down-ct.ember-animated-container-slide-slow-ct{-webkit-transition-duration:2s;-moz-transition-duration:2s;-o-transition-duration:2s;transition-duration:2s}.ember-animated-container-slide-right-ct,.ember-animated-container-slide-down-ct{-webkit-transition-duration:0.3s;-moz-transition-duration:0.3s;-o-transition-duration:0.3s;transition-duration:0.3s}.ember-animated-container-slide-left-ct-sliding{-webkit-transform:translate3d(-100%, 0, 0);-moz-transform:translate3d(-100%, 0, 0);-ms-transform:translate3d(-100%, 0, 0);-o-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0)}.ember-animated-container-slide-right-ct-sliding{-webkit-transform:translate3d(100%, 0, 0);-moz-transform:translate3d(100%, 0, 0);-ms-transform:translate3d(100%, 0, 0);-o-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0)}.ember-animated-container-slide-up-ct-sliding{-webkit-transform:translate3d(0, -100%, 0);-moz-transform:translate3d(0, -100%, 0);-ms-transform:translate3d(0, -100%, 0);-o-transform:translate3d(0, -100%, 0);transform:translate3d(0, -100%, 0)}.ember-animated-container-slide-down-ct-sliding{-webkit-transform:translate3d(0, 100%, 0);-moz-transform:translate3d(0, 100%, 0);-ms-transform:translate3d(0, 100%, 0);-o-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}.ember-animated-container-slide-left-new{left:100% !important}.ember-animated-container-slide-right-new{left:-100% !important}.ember-animated-container-slide-up-new{top:100% !important}.ember-animated-container-slide-down-new{top:-100% !important}.ember-animated-container-slideOver-old{z-index:1}.ember-animated-container-slideOver-left-new,.ember-animated-container-slideOver-right-new,.ember-animated-container-slideOver-up-new,.ember-animated-container-slideOver-down-new{-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;transition-property:transform;-webkit-transition-duration:0.4s;-moz-transition-duration:0.4s;-o-transition-duration:0.4s;transition-duration:0.4s;-webkit-transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);-moz-transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);-o-transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);transition-timing-function:cubic-beizer(0.1, 0.7, 0.1, 1);-webkit-transform:translateX(0%);-moz-transform:translateX(0%);-ms-transform:translateX(0%);-o-transform:translateX(0%);transform:translateX(0%);z-index:2}.ember-animated-container-slideOver-left-new.ember-animated-container-slideOver-left-new-sliding,.ember-animated-container-slideOver-right-new.ember-animated-container-slideOver-left-new-sliding,.ember-animated-container-slideOver-up-new.ember-animated-container-slideOver-left-new-sliding,.ember-animated-container-slideOver-down-new.ember-animated-container-slideOver-left-new-sliding{-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);-ms-transform:translateX(-100%);-o-transform:translateX(-100%);transform:translateX(-100%)}.ember-animated-container-slideOver-left-new.ember-animated-container-slideOver-right-new-sliding,.ember-animated-container-slideOver-right-new.ember-animated-container-slideOver-right-new-sliding,.ember-animated-container-slideOver-up-new.ember-animated-container-slideOver-right-new-sliding,.ember-animated-container-slideOver-down-new.ember-animated-container-slideOver-right-new-sliding{-webkit-transform:translateX(100%);-moz-transform:translateX(100%);-ms-transform:translateX(100%);-o-transform:translateX(100%);transform:translateX(100%)}.ember-animated-container-slideOver-left-new.ember-animated-container-slideOver-up-new-sliding,.ember-animated-container-slideOver-right-new.ember-animated-container-slideOver-up-new-sliding,.ember-animated-container-slideOver-up-new.ember-animated-container-slideOver-up-new-sliding,.ember-animated-container-slideOver-down-new.ember-animated-container-slideOver-up-new-sliding{-webkit-transform:translateY(-100%);-moz-transform:translateY(-100%);-ms-transform:translateY(-100%);-o-transform:translateY(-100%);transform:translateY(-100%)}.ember-animated-container-slideOver-left-new.ember-animated-container-slideOver-down-new-sliding,.ember-animated-container-slideOver-right-new.ember-animated-container-slideOver-down-new-sliding,.ember-animated-container-slideOver-up-new.ember-animated-container-slideOver-down-new-sliding,.ember-animated-container-slideOver-down-new.ember-animated-container-slideOver-down-new-sliding{-webkit-transform:translateY(100%);-moz-transform:translateY(100%);-ms-transform:translateY(100%);-o-transform:translateY(100%);transform:translateY(100%)}.ember-animated-container-slideOver-left-new{left:100% !important}.ember-animated-container-slideOver-right-new{left:-100% !important}.ember-animated-container-slideOver-up-new{top:100% !important}.ember-animated-container-slideOver-down-new{top:-100% !important} 2 | -------------------------------------------------------------------------------- /dist/ember-animated-outlet-mobile.min.js: -------------------------------------------------------------------------------- 1 | Ember.AnimatedContainerView=Ember.ContainerView.extend({classNames:["ember-animated-container"],init:function(){this._super(),Ember.AnimatedContainerView._views[this.get("name")]=this,this._isAnimating=!1},willDestroy:function(){this._super();var a=this.get("name");delete Ember.AnimatedContainerView._views[a],delete Ember.AnimatedContainerView._animationQueue[a]},_currentViewWillChange:Ember.beforeObserver(function(){var a=Ember.get(this,"currentView");a&&this.set("oldView",a)},"currentView"),_currentViewDidChange:Ember.observer(function(){var a=Ember.get(this,"currentView"),b=Ember.get(this,"oldView"),c=this.get("name"),d=null;a&&(b&&(Ember.assert("Ember.AnimatedContainerView can only animate non-virtual views. You need to explicitly define your view class.",!b.isVirtual),Ember.assert("Ember.AnimatedContainerView can only animate non-virtual views. You need to explicitly define your view class.",!a.isVirtual),d=Ember.AnimatedContainerView._animationQueue[c],delete Ember.AnimatedContainerView._animationQueue[c],d&&!Ember.AnimatedContainerView._effects[d]&&(Ember.warn("Unknown animation effect: "+d),d=null),this.set("oldView",null)),this._queuedAnimation&&(b.destroy(),b=this._queuedAnimation.oldView),this._queuedAnimation={newView:a,oldView:b,effect:d},this._handleAnimationQueue())},"currentView"),_handleAnimationQueue:function(){if(!this._isAnimating){var a=this,b=this._queuedAnimation;if(b){var c=b.newView,d=b.oldView,e=b.effect;this._queuedAnimation=null,this.pushObject(c),d&&e?(this._isAnimating=!0,c.on("didInsertElement",function(){Ember.AnimatedContainerView._effects[e](a,c,d,function(){Em.run(function(){a.removeObject(d),d.destroy(),a._isAnimating=!1,a._handleAnimationQueue()})})})):d&&(this.removeObject(d),d.destroy())}}},enqueueAnimation:function(a){Ember.AnimatedContainerView._animationQueue[this.get("name")]=a},setCurrentViewAnimated:function(a,b){this.enqueueAnimation(b),this.set("currentView",a)}}),Ember.AnimatedContainerView.reopenClass({_views:{},_animationQueue:{},enqueueAnimations:function(a){if(!Ember.testing)for(var b in a)a.hasOwnProperty(b)&&(this._animationQueue[b]=a[b])},_effects:{},registerEffect:function(a,b){this._effects[a]=b}}),Handlebars.registerHelper("animated-outlet",function(a,b){var c;for(a&&a.data&&a.data.isRenderData&&(b=a,a="main"),c=b.data.view;!c.get("template.isTop");)c=c.get("_parentView");return b.data.view.set("outletSource",c),b.hash.currentViewBinding="_view.outletSource._outlets."+a,Ember.Handlebars.helpers.view.call(this,Ember.AnimatedContainerView,b)}),Handlebars.registerHelper("animatedOutlet",function(){return Ember.warn("The 'animatedOutlet' view helper is deprecated in favor of 'animated-outlet'"),Ember.Handlebars.helpers["animated-outlet"].apply(this,arguments)});var get=Ember.get,set=Ember.set;Ember.onLoad("Ember.Handlebars",function(){function a(a){var b=get(a,"parameters.params").slice(),c=a.parameters.animations;return b.splice(1,0,c),b}var b=(Ember.Router.resolveParams,Ember.ViewUtils.isSimpleClick),c=Ember.AnimatedLinkView=Ember.LinkView.extend({classNames:["animated-link-view"],_invoke:function(c){if(!b(c))return!0;if(c.preventDefault(),this.bubbles===!1&&c.stopPropagation(),get(this,"_isDisabled"))return!1;if(get(this,"loading"))return Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid."),!1;var d=this.get("router"),e=a(this,d);get(this,"replace")?d.replaceWithAnimated.apply(d,e):d.transitionToAnimated.apply(d,e)}});c.toString=function(){return"AnimatedLinkView"},Ember.Handlebars.registerHelper("link-to-animated",function(a){var b=[].slice.call(arguments,-1)[0],d=[].slice.call(arguments,0,-1),e=b.hash;Ember.assert("link-to-animated must contain animations","string"==typeof e.animations);for(var f=/\s*([a-z]+)\s*:\s*([a-z]+)/gi,g={};match=f.exec(e.animations);)g[match[1]]=match[2];return delete e.animations,e.namedRoute=a,e.currentWhen=e.currentWhen||a,e.disabledBinding=e.disabledWhen,e.parameters={context:this,options:b,animations:g,params:d},Ember.Handlebars.helpers.view.call(this,c,b)}),Ember.Handlebars.registerHelper("linkToAnimated",function(){return Ember.warn("The 'linkToAnimated' view helper is deprecated in favor of 'link-to-animated'"),Ember.Handlebars.helpers["link-to-animated"].apply(this,arguments)})}),Ember.Router.reopen({transitionToAnimated:function(a,b){return Ember.AnimatedContainerView.enqueueAnimations(b),Array.prototype.splice.call(arguments,1,1),this.transitionTo.apply(this,arguments)},replaceWithAnimated:function(a,b){return Ember.AnimatedContainerView.enqueueAnimations(b),Array.prototype.splice.call(arguments,1,1),this.replaceWith.apply(this,arguments)}}),Ember.Route.reopen({transitionToAnimated:function(){var a=this.router;return a.transitionToAnimated.apply(a,arguments)},replaceWithAnimated:function(){var a=this.router;return a.replaceWithAnimated.apply(a,arguments)}}),Ember.ControllerMixin.reopen({transitionToRouteAnimated:function(a,b){return Ember.AnimatedContainerView.enqueueAnimations(b),Array.prototype.splice.call(arguments,1,1),this.transitionToRoute.apply(this,arguments)},replaceRouteAnimated:function(a,b){return Ember.AnimatedContainerView.enqueueAnimations(b),Array.prototype.splice.call(arguments,1,1),this.replaceRoute.apply(this,arguments)}}),Ember.AnimatedContainerView.registerEffect("fade",function(a,b,c,d){var e=b.$(),f=c.$();e.addClass("ember-animated-container-fade-new"),f.addClass("ember-animated-container-fade-old"),setTimeout(function(){f.addClass("ember-animated-container-fade-old-fading"),setTimeout(function(){e.removeClass("ember-animated-container-fade-new"),d()},550)},0)}),Ember.AnimatedContainerView.registerEffect("flip",function(a,b,c,d){var e=a.$(),f=b.$(),g=c.$();e.wrap('
'),e.addClass("ember-animated-container-flip-ct"),f.addClass("ember-animated-container-flip-new"),g.addClass("ember-animated-container-flip-old"),setTimeout(function(){e.addClass("ember-animated-container-flip-ct-flipping"),setTimeout(function(){e.unwrap(),e.removeClass("ember-animated-container-flip-ct"),e.removeClass("ember-animated-container-flip-ct-flipping"),f.removeClass("ember-animated-container-flip-new"),d()},650)},0)}),function(){var a=function(a,b,c,d,e,f){var g=a.$(),h=b.$(),i=f?2050:450;g.addClass("ember-animated-container-slide-"+e+"-ct"),f&&g.addClass("ember-animated-container-slide-slow-ct"),h.addClass("ember-animated-container-slide-"+e+"-new"),setTimeout(function(){g.addClass("ember-animated-container-slide-"+e+"-ct-sliding"),setTimeout(function(){g.removeClass("ember-animated-container-slide-"+e+"-ct"),f&&g.removeClass("ember-animated-container-slide-slow-ct"),g.removeClass("ember-animated-container-slide-"+e+"-ct-sliding"),h.removeClass("ember-animated-container-slide-"+e+"-new"),d()},i)},0)};Ember.AnimatedContainerView.registerEffect("slideLeft",function(b,c,d,e){a(b,c,d,e,"left",!1)}),Ember.AnimatedContainerView.registerEffect("slideRight",function(b,c,d,e){a(b,c,d,e,"right",!1)}),Ember.AnimatedContainerView.registerEffect("slideUp",function(b,c,d,e){a(b,c,d,e,"up",!1)}),Ember.AnimatedContainerView.registerEffect("slideDown",function(b,c,d,e){a(b,c,d,e,"down",!1)}),Ember.AnimatedContainerView.registerEffect("slowSlideLeft",function(b,c,d,e){a(b,c,d,e,"left",!0)}),Ember.AnimatedContainerView.registerEffect("slowSlideRight",function(b,c,d,e){a(b,c,d,e,"right",!0)}),Ember.AnimatedContainerView.registerEffect("slowSlideUp",function(b,c,d,e){a(b,c,d,e,"up",!1)}),Ember.AnimatedContainerView.registerEffect("slowSlideDown",function(b,c,d,e){a(b,c,d,e,"down",!1)})}(),function(){var a=function(a,b,c,d,e){var f=a.$(),g=b.$(),h=450;f.addClass("ember-animated-container-slideOver-old"),g.addClass("ember-animated-container-slideOver-"+e+"-new"),setTimeout(function(){g.addClass("ember-animated-container-slideOver-"+e+"-new-sliding"),setTimeout(function(){g.removeClass("ember-animated-container-slideOver-"+e+"-new"),g.removeClass("ember-animated-container-slideOver-"+e+"-new-sliding"),f.removeClass("ember-animated-container-slideOver-old"),d()},h)},0)};Ember.AnimatedContainerView.registerEffect("slideOverLeft",function(b,c,d,e){a(b,c,d,e,"left")}),Ember.AnimatedContainerView.registerEffect("slideOverRight",function(b,c,d,e){a(b,c,d,e,"right")}),Ember.AnimatedContainerView.registerEffect("slideOverUp",function(b,c,d,e){a(b,c,d,e,"up")}),Ember.AnimatedContainerView.registerEffect("slideOverDown",function(b,c,d,e){a(b,c,d,e,"down")})}(); -------------------------------------------------------------------------------- /tests/link-to-animated-helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | These tests are a shameless copy of the tests for linkTo helper of Ember.js 4 | 5 | */ 6 | var Router, App, AppView, templates, router, eventDispatcher, container; 7 | var get = Ember.get, set = Ember.set; 8 | 9 | function bootApplication() { 10 | router = container.lookup('router:main'); 11 | Ember.run(App, 'advanceReadiness'); 12 | } 13 | 14 | // IE includes the host name 15 | function normalizeUrl(url) { 16 | return url.replace(/https?:\/\/[^\/]+/,''); 17 | } 18 | 19 | function compile(template) { 20 | return Ember.Handlebars.compile(template); 21 | } 22 | 23 | module("The {{link-to-animated}} helper", { 24 | setup: function() { 25 | Ember.run(function() { 26 | App = Ember.Application.create({ 27 | name: "App", 28 | rootElement: '#qunit-fixture' 29 | }); 30 | 31 | App.deferReadiness(); 32 | 33 | App.Router.reopen({ 34 | location: 'none' 35 | }); 36 | 37 | Router = App.Router; 38 | 39 | Ember.TEMPLATES.app = Ember.Handlebars.compile("{{outlet}}"); 40 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{#link-to-animated 'about' id='about-link' animations='main:fade'}}About{{/link-to-animated}}{{#link-to-animated 'index' id='self-link' animations='main:fade'}}Self{{/link-to-animated}}"); 41 | Ember.TEMPLATES.about = Ember.Handlebars.compile("

About

{{#link-to-animated 'index' id='home-link' animations='main:fade'}}Home{{/link-to-animated}}{{#link-to-animated 'about' id='self-link' animations='main:fade'}}Self{{/link-to-animated}}"); 42 | Ember.TEMPLATES.item = Ember.Handlebars.compile("

Item

{{name}}

{{#link-to-animated 'index' id='home-link' animations='main:fade'}}Home{{/link-to-animated}}"); 43 | 44 | AppView = Ember.View.extend({ 45 | templateName: 'app' 46 | }); 47 | 48 | container = App.__container__; 49 | 50 | container.register('view:app'); 51 | container.register('router:main', Router); 52 | }); 53 | }, 54 | 55 | teardown: function() { 56 | Ember.run(function() { App.destroy(); }); 57 | } 58 | }); 59 | 60 | test("moves into the named route", function() { 61 | Router.map(function(match) { 62 | this.route("about"); 63 | }); 64 | 65 | bootApplication(); 66 | 67 | Ember.run(function() { 68 | router.handleURL("/"); 69 | }); 70 | 71 | equal(Ember.$('h3:contains(Home)', '#qunit-fixture').length, 1, "The home template was rendered"); 72 | equal(Ember.$('#self-link.active', '#qunit-fixture').length, 1, "The self-link was rendered with active class"); 73 | equal(Ember.$('#about-link:not(.active)', '#qunit-fixture').length, 1, "The other link was rendered without active class"); 74 | 75 | Ember.run(function() { 76 | Ember.$('#about-link', '#qunit-fixture').click(); 77 | }); 78 | 79 | equal(Ember.$('h3:contains(About)', '#qunit-fixture').length, 1, "The about template was rendered"); 80 | equal(Ember.$('#self-link.active', '#qunit-fixture').length, 1, "The self-link was rendered with active class"); 81 | equal(Ember.$('#home-link:not(.active)', '#qunit-fixture').length, 1, "The other link was rendered without active class"); 82 | }); 83 | 84 | test("supports URL replacement", function() { 85 | var setCount = 0, 86 | replaceCount = 0; 87 | 88 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{#link-to-animated 'about' id='about-link' animations='main:fade' replace=true}}About{{/link-to-animated}}"); 89 | 90 | Router.reopen({ 91 | location: Ember.NoneLocation.createWithMixins({ 92 | setURL: function(path) { 93 | setCount++; 94 | set(this, 'path', path); 95 | }, 96 | 97 | replaceURL: function(path) { 98 | replaceCount++; 99 | set(this, 'path', path); 100 | } 101 | }) 102 | }); 103 | 104 | Router.map(function() { 105 | this.route("about"); 106 | }); 107 | 108 | bootApplication(); 109 | 110 | Ember.run(function() { 111 | router.handleURL("/"); 112 | }); 113 | 114 | equal(setCount, 0, 'precond: setURL has not been called'); 115 | equal(replaceCount, 0, 'precond: replaceURL has not been called'); 116 | 117 | Ember.run(function() { 118 | Ember.$('#about-link', '#qunit-fixture').click(); 119 | }); 120 | 121 | equal(setCount, 0, 'setURL should not be called'); 122 | equal(replaceCount, 1, 'replaceURL should be called once'); 123 | }); 124 | 125 | test("asserts when no animations given", function() { 126 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{#link-to-animated 'about'}}About{{/link-to-animated}}{{#link-to-animated 'index' id='self-link' animations='main:fade' activeClass='zomg-active'}}Self{{/link-to-animated}}"); 127 | 128 | Router.map(function() { 129 | this.route("about"); 130 | }); 131 | 132 | expectAssertion(function() { 133 | bootApplication(); 134 | }, /must contain animations/, "link-to-animated must contain animations"); 135 | }); 136 | 137 | 138 | test("passes when empty animations given", function() { 139 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{#link-to-animated 'about' animations=''}}About{{/link-to-animated}}{{#link-to-animated 'index' id='self-link' animations='main:fade' activeClass='zomg-active'}}Self{{/link-to-animated}}"); 140 | 141 | Router.map(function() { 142 | this.route("about"); 143 | }); 144 | bootApplication(); 145 | ok(true) ; // Nothing to test, just beeïng here is enough 146 | 147 | }); 148 | 149 | 150 | test("supports a custom activeClass", function() { 151 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{#link-to-animated 'about' id='about-link' animations='main:fade'}}About{{/link-to-animated}}{{#link-to-animated 'index' id='self-link' animations='main:fade' activeClass='zomg-active'}}Self{{/link-to-animated}}"); 152 | 153 | Router.map(function() { 154 | this.route("about"); 155 | }); 156 | 157 | bootApplication(); 158 | 159 | Ember.run(function() { 160 | router.handleURL("/"); 161 | }); 162 | 163 | equal(Ember.$('h3:contains(Home)', '#qunit-fixture').length, 1, "The home template was rendered"); 164 | equal(Ember.$('#self-link.zomg-active', '#qunit-fixture').length, 1, "The self-link was rendered with active class"); 165 | equal(Ember.$('#about-link:not(.active)', '#qunit-fixture').length, 1, "The other link was rendered without active class"); 166 | }); 167 | 168 | test("supports leaving off .index for nested routes", function() { 169 | Router.map(function() { 170 | this.resource("about", function() { 171 | this.route("item"); 172 | }); 173 | }); 174 | 175 | Ember.TEMPLATES.about = compile("

About

{{outlet}}"); 176 | Ember.TEMPLATES['about/index'] = compile("
Index
"); 177 | Ember.TEMPLATES['about/item'] = compile("
{{#link-to-animated 'about' animations='main:fade'}}About{{/link-to-animated}}
"); 178 | 179 | bootApplication(); 180 | 181 | Ember.run(function() { 182 | router.handleURL("/about/item"); 183 | }); 184 | 185 | equal(Ember.$('#item a', '#qunit-fixture').attr('href'), '/about'); 186 | }); 187 | 188 | test("supports custom, nested, currentWhen", function() { 189 | Router.map(function(match) { 190 | this.resource("index", { path: "/" }, function() { 191 | this.route("about"); 192 | }); 193 | 194 | this.route("item"); 195 | }); 196 | 197 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{outlet}}"); 198 | Ember.TEMPLATES['index/about'] = Ember.Handlebars.compile("{{#link-to-animated 'item' id='other-link' animations='main:fade' currentWhen='index'}}ITEM{{/link-to-animated}}"); 199 | 200 | bootApplication(); 201 | 202 | Ember.run(function() { 203 | router.handleURL("/about"); 204 | }); 205 | 206 | equal(Ember.$('#other-link.active', '#qunit-fixture').length, 1, "The link is active since currentWhen is a parent route"); 207 | }); 208 | 209 | test("defaults to bubbling", function() { 210 | Ember.TEMPLATES.about = Ember.Handlebars.compile("
{{#link-to-animated 'about.contact' animations='main:fade' id='about-contact'}}About{{/link-to-animated}}
{{outlet}}"); 211 | Ember.TEMPLATES['about/contact'] = Ember.Handlebars.compile("

Contact

"); 212 | 213 | Router.map(function() { 214 | this.resource("about", function() { 215 | this.route("contact"); 216 | }); 217 | }); 218 | 219 | var hidden = 0; 220 | 221 | App.AboutRoute = Ember.Route.extend({ 222 | actions: { 223 | hide: function() { 224 | hidden++; 225 | } 226 | } 227 | }); 228 | 229 | bootApplication(); 230 | 231 | Ember.run(function() { 232 | router.handleURL("/about"); 233 | }); 234 | 235 | Ember.run(function() { 236 | Ember.$('#about-contact', '#qunit-fixture').click(); 237 | }); 238 | 239 | equal(Ember.$("#contact", "#qunit-fixture").text(), "Contact", "precond - the link worked"); 240 | 241 | equal(hidden, 1, "The link bubbles"); 242 | }); 243 | 244 | test("supports bubbles=false", function() { 245 | Ember.TEMPLATES.about = Ember.Handlebars.compile("
{{#link-to-animated 'about.contact' animations='main:fade' id='about-contact' bubbles=false}}About{{/link-to-animated}}
{{outlet}}"); 246 | Ember.TEMPLATES['about/contact'] = Ember.Handlebars.compile("

Contact

"); 247 | 248 | Router.map(function() { 249 | this.resource("about", function() { 250 | this.route("contact"); 251 | }); 252 | }); 253 | 254 | var hidden = 0; 255 | 256 | App.AboutRoute = Ember.Route.extend({ 257 | actions: { 258 | hide: function() { 259 | hidden++; 260 | } 261 | } 262 | }); 263 | 264 | bootApplication(); 265 | 266 | Ember.run(function() { 267 | router.handleURL("/about"); 268 | }); 269 | 270 | Ember.run(function() { 271 | Ember.$('#about-contact', '#qunit-fixture').click(); 272 | }); 273 | 274 | equal(Ember.$("#contact", "#qunit-fixture").text(), "Contact", "precond - the link worked"); 275 | 276 | equal(hidden, 0, "The link didn't bubble"); 277 | }); 278 | 279 | test("moves into the named route with context", function() { 280 | Router.map(function(match) { 281 | this.route("about"); 282 | this.resource("item", { path: "/item/:id" }); 283 | }); 284 | 285 | Ember.TEMPLATES.about = Ember.Handlebars.compile("

List

{{#link-to-animated 'index' animations='main:fade' id='home-link'}}Home{{/link-to-animated}}"); 286 | 287 | var people = { 288 | yehuda: "Yehuda Katz", 289 | tom: "Tom Dale", 290 | erik: "Erik Brynroflsson" 291 | }; 292 | 293 | App.AboutRoute = Ember.Route.extend({ 294 | model: function() { 295 | return Ember.A([ 296 | { id: "yehuda", name: "Yehuda Katz" }, 297 | { id: "tom", name: "Tom Dale" }, 298 | { id: "erik", name: "Erik Brynroflsson" } 299 | ]); 300 | } 301 | }); 302 | 303 | App.ItemRoute = Ember.Route.extend({ 304 | serialize: function(object) { 305 | return { id: object.id }; 306 | }, 307 | 308 | deserialize: function(params) { 309 | return { id: params.id, name: people[params.id] }; 310 | } 311 | }); 312 | 313 | bootApplication(); 314 | 315 | Ember.run(function() { 316 | router.handleURL("/about"); 317 | }); 318 | 319 | equal(Ember.$('h3:contains(List)', '#qunit-fixture').length, 1, "The home template was rendered"); 320 | equal(normalizeUrl(Ember.$('#home-link').attr('href')), '/', "The home link points back at /"); 321 | 322 | Ember.run(function() { 323 | Ember.$('li a:contains(Yehuda)', '#qunit-fixture').click(); 324 | }); 325 | 326 | equal(Ember.$('h3:contains(Item)', '#qunit-fixture').length, 1, "The item template was rendered"); 327 | equal(Ember.$('p', '#qunit-fixture').text(), "Yehuda Katz", "The name is correct"); 328 | 329 | Ember.run(function() { Ember.$('#home-link').click(); }); 330 | Ember.run(function() { Ember.$('#about-link').click(); }); 331 | 332 | equal(normalizeUrl(Ember.$('li a:contains(Yehuda)').attr('href')), "/item/yehuda"); 333 | equal(normalizeUrl(Ember.$('li a:contains(Tom)').attr('href')), "/item/tom"); 334 | equal(normalizeUrl(Ember.$('li a:contains(Erik)').attr('href')), "/item/erik"); 335 | 336 | Ember.run(function() { 337 | Ember.$('li a:contains(Erik)', '#qunit-fixture').click(); 338 | }); 339 | 340 | equal(Ember.$('h3:contains(Item)', '#qunit-fixture').length, 1, "The item template was rendered"); 341 | equal(Ember.$('p', '#qunit-fixture').text(), "Erik Brynroflsson", "The name is correct"); 342 | }); 343 | 344 | test("binds some anchor html tag common attributes", function() { 345 | Ember.TEMPLATES.index = Ember.Handlebars.compile("

Home

{{#link-to-animated 'index' id='self-link' animations='main:fade' title='title-attr'}}Self{{/link-to-animated}}"); 346 | bootApplication(); 347 | 348 | Ember.run(function() { 349 | router.handleURL("/"); 350 | }); 351 | 352 | equal(Ember.$('#self-link', '#qunit-fixture').attr('title'), 'title-attr', "The self-link contains title attribute"); 353 | }); 354 | 355 | test("accepts string arguments", function() { 356 | Router.map(function() { 357 | this.route('filter', { path: '/filters/:filter' }); 358 | }); 359 | 360 | Ember.TEMPLATES.filter = compile('

{{filter}}

{{#link-to-animated "filter" "unpopular" animations="main:fade" id="link"}}Unpopular{{/link-to-animated}}'); 361 | Ember.TEMPLATES.index = compile(''); 362 | 363 | bootApplication(); 364 | 365 | Ember.run(function() { router.handleURL("/filters/popular"); }); 366 | 367 | equal(Ember.$('#link', '#qunit-fixture').attr('href'), "/filters/unpopular"); 368 | }); 369 | 370 | test("unwraps controllers", function() { 371 | // The serialize hook is called twice: once to generate the href for the 372 | // link and once to generate the URL when the link is clicked. 373 | expect(2); 374 | 375 | Router.map(function() { 376 | this.route('filter', { path: '/filters/:filter' }); 377 | }); 378 | 379 | var indexObject = { filter: 'popular' }; 380 | 381 | App.FilterRoute = Ember.Route.extend({ 382 | model: function(params) { 383 | return indexObject; 384 | }, 385 | 386 | serialize: function(passedObject) { 387 | equal(passedObject, indexObject, "The unwrapped object is passed"); 388 | return { filter: 'popular' }; 389 | } 390 | }); 391 | 392 | App.IndexRoute = Ember.Route.extend({ 393 | model: function() { 394 | return indexObject; 395 | } 396 | }); 397 | 398 | Ember.TEMPLATES.filter = compile('

{{filter}}

'); 399 | Ember.TEMPLATES.index = compile('{{#link-to-animated "filter" this id="link" animations="main:fade"}}Filter{{/link-to-animated}}'); 400 | 401 | bootApplication(); 402 | 403 | Ember.run(function() { router.handleURL("/"); }); 404 | 405 | Ember.$('#link', '#qunit-fixture').trigger('click'); 406 | }); 407 | 408 | test("doesn't change view context", function() { 409 | App.IndexView = Ember.View.extend({ 410 | elementId: 'index', 411 | name: 'test' 412 | }); 413 | 414 | Ember.TEMPLATES.index = Ember.Handlebars.compile("{{view.name}}-{{#link-to-animated 'index' id='self-link' animations='main:fade'}}Link: {{view.name}}{{/link-to-animated}}"); 415 | 416 | bootApplication(); 417 | 418 | Ember.run(function() { 419 | router.handleURL("/"); 420 | }); 421 | 422 | equal(Ember.$('#index', '#qunit-fixture').text(), 'test-Link: test', "accesses correct view"); 423 | }); 424 | 425 | test("{{linkToAnimated}} is aliased", function() { 426 | var originalLinkToAnimated = Ember.Handlebars.helpers['link-to-animated'], 427 | originalWarn = Ember.warn; 428 | 429 | Ember.warn = function(msg) { 430 | equal(msg, "The 'linkToAnimated' view helper is deprecated in favor of 'link-to-animated'", 'Warning called'); 431 | }; 432 | 433 | Ember.Handlebars.helpers['link-to-animated'] = function() { 434 | equal(arguments[0], 'foo', 'First arg match'); 435 | equal(arguments[1], 'bar', 'Second arg match'); 436 | return 'result'; 437 | }; 438 | var result = Ember.Handlebars.helpers.linkToAnimated('foo', 'bar'); 439 | equal(result, 'result', 'Result match'); 440 | 441 | Ember.Handlebars.helpers['link-to-animated'] = originalLinkToAnimated; 442 | Ember.warn = originalWarn; 443 | }); -------------------------------------------------------------------------------- /dist/ember-animated-outlet-mobile.js: -------------------------------------------------------------------------------- 1 | /** 2 | Write me... 3 | 4 | @class AnimatedContainerView 5 | @namespace Ember 6 | @extends Ember.ContainerView 7 | */ 8 | Ember.AnimatedContainerView = Ember.ContainerView.extend({ 9 | 10 | classNames: ['ember-animated-container'], 11 | 12 | init: function() { 13 | this._super(); 14 | //Register this view, so queued effects can be related with this view by name 15 | Ember.AnimatedContainerView._views[this.get('name')] = this; 16 | this._isAnimating = false; 17 | }, 18 | 19 | willDestroy: function() { 20 | this._super(); 21 | //Clean up 22 | var name = this.get('name'); 23 | delete Ember.AnimatedContainerView._views[name]; 24 | delete Ember.AnimatedContainerView._animationQueue[name]; 25 | }, 26 | 27 | //Override parent method 28 | _currentViewWillChange: Ember.beforeObserver(function() { 29 | var currentView = Ember.get(this, 'currentView'); 30 | if (currentView) { 31 | //Store the old `currentView` (and don't destroy it yet) so we can use it for animation later 32 | this.set('oldView', currentView); 33 | } 34 | }, 'currentView'), 35 | 36 | _currentViewDidChange: Ember.observer(function() { 37 | var newView = Ember.get(this, 'currentView'), 38 | oldView = Ember.get(this, 'oldView'), 39 | name = this.get('name'), 40 | effect = null; 41 | if (newView) { 42 | if (oldView) { 43 | Ember.assert('Ember.AnimatedContainerView can only animate non-virtual views. You need to explicitly define your view class.', !oldView.isVirtual); 44 | Ember.assert('Ember.AnimatedContainerView can only animate non-virtual views. You need to explicitly define your view class.', !newView.isVirtual); 45 | //Get and validate a potentially queued effect 46 | effect = Ember.AnimatedContainerView._animationQueue[name]; 47 | delete Ember.AnimatedContainerView._animationQueue[name]; 48 | if (effect && !Ember.AnimatedContainerView._effects[effect]) { 49 | Ember.warn('Unknown animation effect: '+effect); 50 | effect = null; 51 | } 52 | //Forget about the old view 53 | this.set('oldView', null); 54 | } 55 | //If there is already an animation queued, we should cancel it 56 | if (this._queuedAnimation) { 57 | oldView.destroy(); //the oldView has never been visible, and never will be, so we can just destroy it now 58 | oldView = this._queuedAnimation.oldView; //instead, use the oldView from the queued animation, which is our real currentView 59 | } 60 | //Queue this animation and check the queue 61 | this._queuedAnimation = { 62 | newView: newView, 63 | oldView: oldView, 64 | effect: effect 65 | }; 66 | this._handleAnimationQueue(); 67 | } 68 | }, 'currentView'), 69 | 70 | _handleAnimationQueue: function() { 71 | //If animation is in progress, just stop here. Once the animation has finished, this method will be called again. 72 | if (this._isAnimating) { 73 | return; 74 | } 75 | var self = this, 76 | q = this._queuedAnimation; 77 | if (q) { 78 | var newView = q.newView, 79 | oldView = q.oldView, 80 | effect = q.effect; 81 | this._queuedAnimation = null; 82 | //Push the newView to this view, which will append it to the DOM 83 | this.pushObject(newView); 84 | if (oldView && effect) { 85 | //If an effect is queued, then start the effect when the new view has been inserted in the DOM 86 | this._isAnimating = true; 87 | newView.on('didInsertElement', function() { 88 | Ember.AnimatedContainerView._effects[effect](self, newView, oldView, function() { 89 | Em.run(function() { 90 | self.removeObject(oldView); 91 | oldView.destroy(); 92 | //Check to see if there are any queued animations 93 | self._isAnimating = false; 94 | self._handleAnimationQueue(); 95 | }); 96 | }); 97 | }); 98 | } else { 99 | if (oldView) { 100 | //If there is no effect queued, then just remove the old view (as would normally happen in a ContainerView) 101 | this.removeObject(oldView); 102 | oldView.destroy(); 103 | } 104 | } 105 | } 106 | }, 107 | 108 | enqueueAnimation: function(effect) { 109 | Ember.AnimatedContainerView._animationQueue[this.get('name')] = effect; 110 | }, 111 | 112 | setCurrentViewAnimated: function(currentView, effect) { 113 | this.enqueueAnimation(effect); 114 | this.set('currentView', currentView); 115 | } 116 | 117 | }); 118 | 119 | Ember.AnimatedContainerView.reopenClass({ 120 | 121 | /** 122 | All animated outlets registers itself in this hash 123 | 124 | @private 125 | @property {Object} _views 126 | */ 127 | _views: {}, 128 | 129 | /** 130 | Whenever an animated route transition is set in motion, it will be stored here, so the animated outlet view can pick it up 131 | 132 | @private 133 | @property {Object} _animationQueue 134 | */ 135 | _animationQueue: {}, 136 | 137 | /** 138 | Enqueue effects to be executed by the given outlets when the next route transition happens. 139 | 140 | @param {Object} animations A hash with keys corresponding to outlet views and values with the desired animation effect. 141 | */ 142 | enqueueAnimations: function(animations) { 143 | // Animations cause race conditions in testing so we just disable them 144 | if (Ember.testing) { 145 | return; 146 | } 147 | 148 | for (var name in animations) { 149 | if (!animations.hasOwnProperty(name)) continue; 150 | this._animationQueue[name] = animations[name]; 151 | } 152 | }, 153 | 154 | /** 155 | All animation effects are stored on this object and can be referred to by its key 156 | 157 | @private 158 | @property {Object} effects 159 | */ 160 | _effects: {}, 161 | 162 | 163 | /** 164 | Register a new effect. 165 | 166 | The `callback` function will be passed the following parameters: 167 | 168 | - The `Ember.AnimatedContainerView` instance. 169 | - The new view. 170 | - The old view. 171 | 172 | @param {String} effect The name of the effect, e.g. 'slideLeft' 173 | @param {Function} callback The function to call when effect has to be executed 174 | */ 175 | registerEffect: function(effect, callback) { 176 | this._effects[effect] = callback; 177 | } 178 | 179 | }); 180 | 181 | /** 182 | Write me... 183 | 184 | Straight-up stolen from `Handlebars.registerHelper('outlet', ...);` 185 | 186 | @method outlet 187 | @for Ember.Handlebars.helpers 188 | @param {String} property the property on the controller that holds the view for this outlet 189 | */ 190 | Handlebars.registerHelper('animated-outlet', function(property, options) { 191 | var outletSource; 192 | 193 | if (property && property.data && property.data.isRenderData) { 194 | options = property; 195 | property = 'main'; 196 | } 197 | 198 | outletSource = options.data.view; 199 | while (!(outletSource.get('template.isTop'))){ 200 | outletSource = outletSource.get('_parentView'); 201 | } 202 | 203 | options.data.view.set('outletSource', outletSource); 204 | options.hash.currentViewBinding = '_view.outletSource._outlets.' + property; 205 | 206 | //Only this line has been changed 207 | return Ember.Handlebars.helpers.view.call(this, Ember.AnimatedContainerView, options); 208 | }); 209 | 210 | /** 211 | See animated-outlet 212 | */ 213 | Handlebars.registerHelper('animatedOutlet', function(property, options) { 214 | Ember.warn("The 'animatedOutlet' view helper is deprecated in favor of 'animated-outlet'"); 215 | return Ember.Handlebars.helpers['animated-outlet'].apply(this, arguments); 216 | }); 217 | /** 218 | @module ember 219 | @submodule ember-routing 220 | */ 221 | 222 | var get = Ember.get, set = Ember.set; 223 | 224 | Ember.onLoad('Ember.Handlebars', function(Handlebars) { 225 | var resolveParams = Ember.Router.resolveParams, 226 | isSimpleClick = Ember.ViewUtils.isSimpleClick; 227 | 228 | function fullRouteName(router, name) { 229 | if (!router.hasRoute(name)) { 230 | name = name + '.index'; 231 | } 232 | 233 | return name; 234 | } 235 | 236 | function resolvedPaths(options) { 237 | var types = options.options.types.slice(1), 238 | data = options.options.data; 239 | 240 | return resolveParams(options.context, options.params, { types: types, data: data }); 241 | } 242 | 243 | function args(linkView, router, route) { 244 | var ret = get(linkView,'parameters.params').slice(), 245 | animations = linkView.parameters.animations; 246 | ret.splice(1,0,animations); 247 | return ret; 248 | } 249 | 250 | /** 251 | Renders a link to the supplied route using animation. 252 | 253 | @class AnimatedLinkView 254 | @namespace Ember 255 | @extends Ember.LinkView 256 | **/ 257 | var AnimatedLinkView = Ember.AnimatedLinkView = Ember.LinkView.extend({ 258 | classNames: ['animated-link-view'], 259 | _invoke: function(event) { 260 | if (!isSimpleClick(event)) { return true; } 261 | 262 | event.preventDefault(); 263 | if (this.bubbles === false) { event.stopPropagation(); } 264 | 265 | if (get(this, '_isDisabled')) { return false; } 266 | 267 | if (get(this, 'loading')) { 268 | Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid."); 269 | return false; 270 | } 271 | 272 | var router = this.get('router'), 273 | routeArgs = args(this, router); 274 | 275 | if (get(this, ('replace'))) { 276 | router.replaceWithAnimated.apply(router, routeArgs); 277 | } else { 278 | router.transitionToAnimated.apply(router, routeArgs); 279 | } 280 | } 281 | }); 282 | 283 | AnimatedLinkView.toString = function() { return "AnimatedLinkView"; }; 284 | 285 | /** 286 | @method linkToAnimated 287 | @for Ember.Handlebars.helpers 288 | @param {String} routeName 289 | @param {Object} [context]* 290 | @return {String} HTML string 291 | */ 292 | Ember.Handlebars.registerHelper('link-to-animated', function(name) { 293 | var options = [].slice.call(arguments, -1)[0], 294 | params = [].slice.call(arguments, 0, -1), 295 | hash = options.hash; 296 | 297 | Ember.assert("link-to-animated must contain animations", typeof(hash.animations) == 'string') 298 | var re = /\s*([a-z]+)\s*:\s*([a-z]+)/gi; 299 | var animations = {}; 300 | while (match = re.exec(hash.animations)) { 301 | animations[match[1]] = match[2]; 302 | } 303 | delete(hash.animations) 304 | hash.namedRoute = name; 305 | hash.currentWhen = hash.currentWhen || name; 306 | hash.disabledBinding = hash.disabledWhen; 307 | 308 | hash.parameters = { 309 | context: this, 310 | options: options, 311 | animations: animations, 312 | params: params 313 | }; 314 | 315 | return Ember.Handlebars.helpers.view.call(this, AnimatedLinkView, options); 316 | }); 317 | 318 | /** 319 | See link-to-animated 320 | 321 | @method linkTo 322 | @for Ember.Handlebars.helpers 323 | @deprecated 324 | @param {String} routeName 325 | @param {Object} [context]* 326 | @return {String} HTML string 327 | */ 328 | Ember.Handlebars.registerHelper('linkToAnimated', function() { 329 | Ember.warn("The 'linkToAnimated' view helper is deprecated in favor of 'link-to-animated'"); 330 | return Ember.Handlebars.helpers['link-to-animated'].apply(this, arguments); 331 | }); 332 | 333 | }); 334 | 335 | 336 | Ember.Router.reopen({ 337 | 338 | /** 339 | Works as {@link Ember.Router.transitionTo}} except that it takes a third parameter, `animations`, 340 | which will enqueue animations. 341 | 342 | `animations` should be an object with outlet names as keys and effect names as value. 343 | 344 | @param name 345 | @param animations {Object} Animations to enqueue 346 | @param model 347 | */ 348 | transitionToAnimated: function(name, animations, model) { 349 | Ember.AnimatedContainerView.enqueueAnimations(animations); 350 | Array.prototype.splice.call(arguments, 1, 1); 351 | return this.transitionTo.apply(this, arguments); 352 | }, 353 | 354 | /** 355 | Works as {@link Ember.Router.replaceWith}} except that it takes a third parameter, `animations`, 356 | which will enqueue animations. 357 | 358 | `animations` should be an object with outlet names as keys and effect names as value. 359 | 360 | @param name 361 | @param animations {Object} Animations to enqueue 362 | @param model 363 | */ 364 | replaceWithAnimated: function(name, animations, model) { 365 | Ember.AnimatedContainerView.enqueueAnimations(animations); 366 | Array.prototype.splice.call(arguments, 1, 1); 367 | return this.replaceWith.apply(this, arguments); 368 | } 369 | 370 | }); 371 | 372 | 373 | Ember.Route.reopen({ 374 | 375 | transitionToAnimated: function(name, context) { 376 | var router = this.router; 377 | return router.transitionToAnimated.apply(router, arguments); 378 | }, 379 | 380 | replaceWithAnimated: function() { 381 | var router = this.router; 382 | return router.replaceWithAnimated.apply(router, arguments); 383 | } 384 | 385 | }); 386 | 387 | Ember.ControllerMixin.reopen({ 388 | 389 | /** 390 | Works as {@link Ember.ControllerMixin.transitionToRoute}} except that it takes a third parameter, `animations`, 391 | which will enqueue animations. 392 | 393 | `animations` should be an object with outlet names as keys and effect names as value. 394 | 395 | @param name 396 | @param animations {Object} Animations to enqueue 397 | @param model 398 | */ 399 | transitionToRouteAnimated: function(name, animations, model) { 400 | Ember.AnimatedContainerView.enqueueAnimations(animations); 401 | Array.prototype.splice.call(arguments, 1, 1); 402 | return this.transitionToRoute.apply(this, arguments); 403 | }, 404 | 405 | /** 406 | Works as {@link Ember.ControllerMixin.replaceRoute}} except that it takes a third parameter, `animations`, 407 | which will enqueue animations. 408 | 409 | `animations` should be an object with outlet names as keys and effect names as value. 410 | 411 | @param name 412 | @param animations {Object} Animations to enqueue 413 | @param model 414 | */ 415 | replaceRouteAnimated: function(name, animations, model) { 416 | Ember.AnimatedContainerView.enqueueAnimations(animations); 417 | Array.prototype.splice.call(arguments, 1, 1); 418 | return this.replaceRoute.apply(this, arguments); 419 | } 420 | 421 | }); 422 | Ember.AnimatedContainerView.registerEffect('fade', function(ct, newView, oldView, callback) { 423 | var newEl = newView.$(), 424 | oldEl = oldView.$(); 425 | newEl.addClass('ember-animated-container-fade-new'); 426 | oldEl.addClass('ember-animated-container-fade-old'); 427 | setTimeout(function() { 428 | oldEl.addClass('ember-animated-container-fade-old-fading'); 429 | setTimeout(function() { 430 | newEl.removeClass('ember-animated-container-fade-new'); 431 | callback(); 432 | }, 550); 433 | }, 0); 434 | }); 435 | Ember.AnimatedContainerView.registerEffect('flip', function(ct, newView, oldView, callback) { 436 | var ctEl = ct.$(), 437 | newEl = newView.$(), 438 | oldEl = oldView.$(); 439 | ctEl.wrap('
') 440 | ctEl.addClass('ember-animated-container-flip-ct'); 441 | newEl.addClass('ember-animated-container-flip-new'); 442 | oldEl.addClass('ember-animated-container-flip-old'); 443 | setTimeout(function() { 444 | ctEl.addClass('ember-animated-container-flip-ct-flipping'); 445 | setTimeout(function() { 446 | ctEl.unwrap(); 447 | ctEl.removeClass('ember-animated-container-flip-ct'); 448 | ctEl.removeClass('ember-animated-container-flip-ct-flipping'); 449 | newEl.removeClass('ember-animated-container-flip-new'); 450 | callback(); 451 | }, 650); 452 | }, 0); 453 | }); 454 | (function() { 455 | 456 | var slide = function(ct, newView, oldView, callback, direction, slow) { 457 | var ctEl = ct.$(), 458 | newEl = newView.$(), 459 | duration = slow ? 2050 : 450; 460 | ctEl.addClass('ember-animated-container-slide-'+direction+'-ct') 461 | if (slow) { 462 | ctEl.addClass('ember-animated-container-slide-slow-ct') 463 | } 464 | newEl.addClass('ember-animated-container-slide-'+direction+'-new'); 465 | setTimeout(function() { 466 | ctEl.addClass('ember-animated-container-slide-'+direction+'-ct-sliding'); 467 | setTimeout(function() { 468 | ctEl.removeClass('ember-animated-container-slide-'+direction+'-ct'); 469 | if (slow) { 470 | ctEl.removeClass('ember-animated-container-slide-slow-ct') 471 | } 472 | ctEl.removeClass('ember-animated-container-slide-'+direction+'-ct-sliding'); 473 | newEl.removeClass('ember-animated-container-slide-'+direction+'-new'); 474 | callback(); 475 | }, duration); 476 | }, 0); 477 | }; 478 | 479 | Ember.AnimatedContainerView.registerEffect('slideLeft', function(ct, newView, oldView, callback) { 480 | slide(ct, newView, oldView, callback, 'left', false); 481 | }); 482 | 483 | Ember.AnimatedContainerView.registerEffect('slideRight', function(ct, newView, oldView, callback) { 484 | slide(ct, newView, oldView, callback, 'right', false); 485 | }); 486 | 487 | Ember.AnimatedContainerView.registerEffect('slideUp', function(ct, newView, oldView, callback) { 488 | slide(ct, newView, oldView, callback, 'up', false); 489 | }); 490 | 491 | Ember.AnimatedContainerView.registerEffect('slideDown', function(ct, newView, oldView, callback) { 492 | slide(ct, newView, oldView, callback, 'down', false); 493 | }); 494 | 495 | Ember.AnimatedContainerView.registerEffect('slowSlideLeft', function(ct, newView, oldView, callback) { 496 | slide(ct, newView, oldView, callback, 'left', true); 497 | }); 498 | 499 | Ember.AnimatedContainerView.registerEffect('slowSlideRight', function(ct, newView, oldView, callback) { 500 | slide(ct, newView, oldView, callback, 'right', true); 501 | }); 502 | 503 | Ember.AnimatedContainerView.registerEffect('slowSlideUp', function(ct, newView, oldView, callback) { 504 | slide(ct, newView, oldView, callback, 'up', false); 505 | }); 506 | 507 | Ember.AnimatedContainerView.registerEffect('slowSlideDown', function(ct, newView, oldView, callback) { 508 | slide(ct, newView, oldView, callback, 'down', false); 509 | }); 510 | 511 | })(); 512 | 513 | (function() { 514 | 515 | var slideOver = function(ct, newView, oldView, callback, direction) { 516 | var ctEl = ct.$(), 517 | newEl = newView.$(), 518 | duration = 450; 519 | ctEl.addClass('ember-animated-container-slideOver-old'); 520 | newEl.addClass('ember-animated-container-slideOver-'+direction+'-new'); 521 | setTimeout(function() { 522 | newEl.addClass('ember-animated-container-slideOver-'+direction+'-new-sliding'); 523 | setTimeout(function() { 524 | newEl.removeClass('ember-animated-container-slideOver-'+direction+'-new'); 525 | newEl.removeClass('ember-animated-container-slideOver-'+direction+'-new-sliding'); 526 | ctEl.removeClass('ember-animated-container-slideOver-old'); 527 | callback(); 528 | }, duration); 529 | }, 0); 530 | }; 531 | 532 | Ember.AnimatedContainerView.registerEffect('slideOverLeft', function(ct, newView, oldView, callback) { 533 | slideOver(ct, newView, oldView, callback, 'left'); 534 | }); 535 | 536 | Ember.AnimatedContainerView.registerEffect('slideOverRight', function(ct, newView, oldView, callback) { 537 | slideOver(ct, newView, oldView, callback, 'right'); 538 | }); 539 | 540 | Ember.AnimatedContainerView.registerEffect('slideOverUp', function(ct, newView, oldView, callback) { 541 | slideOver(ct, newView, oldView, callback, 'up'); 542 | }); 543 | 544 | Ember.AnimatedContainerView.registerEffect('slideOverDown', function(ct, newView, oldView, callback) { 545 | slideOver(ct, newView, oldView, callback, 'down'); 546 | }); 547 | 548 | })(); 549 | -------------------------------------------------------------------------------- /vendor/qunit-1.11.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 18 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | defined = { 24 | setTimeout: typeof window.setTimeout !== "undefined", 25 | sessionStorage: (function() { 26 | var x = "qunit-test-string"; 27 | try { 28 | sessionStorage.setItem( x, x ); 29 | sessionStorage.removeItem( x ); 30 | return true; 31 | } catch( e ) { 32 | return false; 33 | } 34 | }()) 35 | }, 36 | /** 37 | * Provides a normalized error string, correcting an issue 38 | * with IE 7 (and prior) where Error.prototype.toString is 39 | * not properly implemented 40 | * 41 | * Based on http://es5.github.com/#x15.11.4.4 42 | * 43 | * @param {String|Error} error 44 | * @return {String} error message 45 | */ 46 | errorString = function( error ) { 47 | var name, message, 48 | errorString = error.toString(); 49 | if ( errorString.substring( 0, 7 ) === "[object" ) { 50 | name = error.name ? error.name.toString() : "Error"; 51 | message = error.message ? error.message.toString() : ""; 52 | if ( name && message ) { 53 | return name + ": " + message; 54 | } else if ( name ) { 55 | return name; 56 | } else if ( message ) { 57 | return message; 58 | } else { 59 | return "Error"; 60 | } 61 | } else { 62 | return errorString; 63 | } 64 | }, 65 | /** 66 | * Makes a clone of an object using only Array or Object as base, 67 | * and copies over the own enumerable properties. 68 | * 69 | * @param {Object} obj 70 | * @return {Object} New object with only the own properties (recursively). 71 | */ 72 | objectValues = function( obj ) { 73 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 74 | /*jshint newcap: false */ 75 | var key, val, 76 | vals = QUnit.is( "array", obj ) ? [] : {}; 77 | for ( key in obj ) { 78 | if ( hasOwn.call( obj, key ) ) { 79 | val = obj[key]; 80 | vals[key] = val === Object(val) ? objectValues(val) : val; 81 | } 82 | } 83 | return vals; 84 | }; 85 | 86 | function Test( settings ) { 87 | extend( this, settings ); 88 | this.assertions = []; 89 | this.testNumber = ++Test.count; 90 | } 91 | 92 | Test.count = 0; 93 | 94 | Test.prototype = { 95 | init: function() { 96 | var a, b, li, 97 | tests = id( "qunit-tests" ); 98 | 99 | if ( tests ) { 100 | b = document.createElement( "strong" ); 101 | b.innerHTML = this.nameHtml; 102 | 103 | // `a` initialized at top of scope 104 | a = document.createElement( "a" ); 105 | a.innerHTML = "Rerun"; 106 | a.href = QUnit.url({ testNumber: this.testNumber }); 107 | 108 | li = document.createElement( "li" ); 109 | li.appendChild( b ); 110 | li.appendChild( a ); 111 | li.className = "running"; 112 | li.id = this.id = "qunit-test-output" + testId++; 113 | 114 | tests.appendChild( li ); 115 | } 116 | }, 117 | setup: function() { 118 | if ( this.module !== config.previousModule ) { 119 | if ( config.previousModule ) { 120 | runLoggingCallbacks( "moduleDone", QUnit, { 121 | name: config.previousModule, 122 | failed: config.moduleStats.bad, 123 | passed: config.moduleStats.all - config.moduleStats.bad, 124 | total: config.moduleStats.all 125 | }); 126 | } 127 | config.previousModule = this.module; 128 | config.moduleStats = { all: 0, bad: 0 }; 129 | runLoggingCallbacks( "moduleStart", QUnit, { 130 | name: this.module 131 | }); 132 | } else if ( config.autorun ) { 133 | runLoggingCallbacks( "moduleStart", QUnit, { 134 | name: this.module 135 | }); 136 | } 137 | 138 | config.current = this; 139 | 140 | this.testEnvironment = extend({ 141 | setup: function() {}, 142 | teardown: function() {} 143 | }, this.moduleTestEnvironment ); 144 | 145 | this.started = +new Date(); 146 | runLoggingCallbacks( "testStart", QUnit, { 147 | name: this.testName, 148 | module: this.module 149 | }); 150 | 151 | // allow utility functions to access the current test environment 152 | // TODO why?? 153 | QUnit.current_testEnvironment = this.testEnvironment; 154 | 155 | if ( !config.pollution ) { 156 | saveGlobal(); 157 | } 158 | if ( config.notrycatch ) { 159 | this.testEnvironment.setup.call( this.testEnvironment ); 160 | return; 161 | } 162 | try { 163 | this.testEnvironment.setup.call( this.testEnvironment ); 164 | } catch( e ) { 165 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 166 | } 167 | }, 168 | run: function() { 169 | config.current = this; 170 | 171 | var running = id( "qunit-testresult" ); 172 | 173 | if ( running ) { 174 | running.innerHTML = "Running:
" + this.nameHtml; 175 | } 176 | 177 | if ( this.async ) { 178 | QUnit.stop(); 179 | } 180 | 181 | this.callbackStarted = +new Date(); 182 | 183 | if ( config.notrycatch ) { 184 | this.callback.call( this.testEnvironment, QUnit.assert ); 185 | this.callbackRuntime = +new Date() - this.callbackStarted; 186 | return; 187 | } 188 | 189 | try { 190 | this.callback.call( this.testEnvironment, QUnit.assert ); 191 | this.callbackRuntime = +new Date() - this.callbackStarted; 192 | } catch( e ) { 193 | this.callbackRuntime = +new Date() - this.callbackStarted; 194 | 195 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 196 | // else next test will carry the responsibility 197 | saveGlobal(); 198 | 199 | // Restart the tests if they're blocking 200 | if ( config.blocking ) { 201 | QUnit.start(); 202 | } 203 | } 204 | }, 205 | teardown: function() { 206 | config.current = this; 207 | if ( config.notrycatch ) { 208 | if ( typeof this.callbackRuntime === "undefined" ) { 209 | this.callbackRuntime = +new Date() - this.callbackStarted; 210 | } 211 | this.testEnvironment.teardown.call( this.testEnvironment ); 212 | return; 213 | } else { 214 | try { 215 | this.testEnvironment.teardown.call( this.testEnvironment ); 216 | } catch( e ) { 217 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 218 | } 219 | } 220 | checkPollution(); 221 | }, 222 | finish: function() { 223 | config.current = this; 224 | if ( config.requireExpects && this.expected === null ) { 225 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 226 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 227 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 228 | } else if ( this.expected === null && !this.assertions.length ) { 229 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 230 | } 231 | 232 | var i, assertion, a, b, time, li, ol, 233 | test = this, 234 | good = 0, 235 | bad = 0, 236 | tests = id( "qunit-tests" ); 237 | 238 | this.runtime = +new Date() - this.started; 239 | config.stats.all += this.assertions.length; 240 | config.moduleStats.all += this.assertions.length; 241 | 242 | if ( tests ) { 243 | ol = document.createElement( "ol" ); 244 | ol.className = "qunit-assert-list"; 245 | 246 | for ( i = 0; i < this.assertions.length; i++ ) { 247 | assertion = this.assertions[i]; 248 | 249 | li = document.createElement( "li" ); 250 | li.className = assertion.result ? "pass" : "fail"; 251 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 252 | ol.appendChild( li ); 253 | 254 | if ( assertion.result ) { 255 | good++; 256 | } else { 257 | bad++; 258 | config.stats.bad++; 259 | config.moduleStats.bad++; 260 | } 261 | } 262 | 263 | // store result when possible 264 | if ( QUnit.config.reorder && defined.sessionStorage ) { 265 | if ( bad ) { 266 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 267 | } else { 268 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 269 | } 270 | } 271 | 272 | if ( bad === 0 ) { 273 | addClass( ol, "qunit-collapsed" ); 274 | } 275 | 276 | // `b` initialized at top of scope 277 | b = document.createElement( "strong" ); 278 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 279 | 280 | addEvent(b, "click", function() { 281 | var next = b.parentNode.lastChild, 282 | collapsed = hasClass( next, "qunit-collapsed" ); 283 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 284 | }); 285 | 286 | addEvent(b, "dblclick", function( e ) { 287 | var target = e && e.target ? e.target : window.event.srcElement; 288 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 289 | target = target.parentNode; 290 | } 291 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 292 | window.location = QUnit.url({ testNumber: test.testNumber }); 293 | } 294 | }); 295 | 296 | // `time` initialized at top of scope 297 | time = document.createElement( "span" ); 298 | time.className = "runtime"; 299 | time.innerHTML = this.runtime + " ms"; 300 | 301 | // `li` initialized at top of scope 302 | li = id( this.id ); 303 | li.className = bad ? "fail" : "pass"; 304 | li.removeChild( li.firstChild ); 305 | a = li.firstChild; 306 | li.appendChild( b ); 307 | li.appendChild( a ); 308 | li.appendChild( time ); 309 | li.appendChild( ol ); 310 | 311 | } else { 312 | for ( i = 0; i < this.assertions.length; i++ ) { 313 | if ( !this.assertions[i].result ) { 314 | bad++; 315 | config.stats.bad++; 316 | config.moduleStats.bad++; 317 | } 318 | } 319 | } 320 | 321 | runLoggingCallbacks( "testDone", QUnit, { 322 | name: this.testName, 323 | module: this.module, 324 | failed: bad, 325 | passed: this.assertions.length - bad, 326 | total: this.assertions.length, 327 | duration: this.runtime 328 | }); 329 | 330 | QUnit.reset(); 331 | 332 | config.current = undefined; 333 | }, 334 | 335 | queue: function() { 336 | var bad, 337 | test = this; 338 | 339 | synchronize(function() { 340 | test.init(); 341 | }); 342 | function run() { 343 | // each of these can by async 344 | synchronize(function() { 345 | test.setup(); 346 | }); 347 | synchronize(function() { 348 | test.run(); 349 | }); 350 | synchronize(function() { 351 | test.teardown(); 352 | }); 353 | synchronize(function() { 354 | test.finish(); 355 | }); 356 | } 357 | 358 | // `bad` initialized at top of scope 359 | // defer when previous test run passed, if storage is available 360 | bad = QUnit.config.reorder && defined.sessionStorage && 361 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 362 | 363 | if ( bad ) { 364 | run(); 365 | } else { 366 | synchronize( run, true ); 367 | } 368 | } 369 | }; 370 | 371 | // Root QUnit object. 372 | // `QUnit` initialized at top of scope 373 | QUnit = { 374 | 375 | // call on start of module test to prepend name to all tests 376 | module: function( name, testEnvironment ) { 377 | config.currentModule = name; 378 | config.currentModuleTestEnvironment = testEnvironment; 379 | config.modules[name] = true; 380 | }, 381 | 382 | asyncTest: function( testName, expected, callback ) { 383 | if ( arguments.length === 2 ) { 384 | callback = expected; 385 | expected = null; 386 | } 387 | 388 | QUnit.test( testName, expected, callback, true ); 389 | }, 390 | 391 | test: function( testName, expected, callback, async ) { 392 | var test, 393 | nameHtml = "" + escapeText( testName ) + ""; 394 | 395 | if ( arguments.length === 2 ) { 396 | callback = expected; 397 | expected = null; 398 | } 399 | 400 | if ( config.currentModule ) { 401 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 402 | } 403 | 404 | test = new Test({ 405 | nameHtml: nameHtml, 406 | testName: testName, 407 | expected: expected, 408 | async: async, 409 | callback: callback, 410 | module: config.currentModule, 411 | moduleTestEnvironment: config.currentModuleTestEnvironment, 412 | stack: sourceFromStacktrace( 2 ) 413 | }); 414 | 415 | if ( !validTest( test ) ) { 416 | return; 417 | } 418 | 419 | test.queue(); 420 | }, 421 | 422 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 423 | expect: function( asserts ) { 424 | if (arguments.length === 1) { 425 | config.current.expected = asserts; 426 | } else { 427 | return config.current.expected; 428 | } 429 | }, 430 | 431 | start: function( count ) { 432 | // QUnit hasn't been initialized yet. 433 | // Note: RequireJS (et al) may delay onLoad 434 | if ( config.semaphore === undefined ) { 435 | QUnit.begin(function() { 436 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 437 | setTimeout(function() { 438 | QUnit.start( count ); 439 | }); 440 | }); 441 | return; 442 | } 443 | 444 | config.semaphore -= count || 1; 445 | // don't start until equal number of stop-calls 446 | if ( config.semaphore > 0 ) { 447 | return; 448 | } 449 | // ignore if start is called more often then stop 450 | if ( config.semaphore < 0 ) { 451 | config.semaphore = 0; 452 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 453 | return; 454 | } 455 | // A slight delay, to avoid any current callbacks 456 | if ( defined.setTimeout ) { 457 | window.setTimeout(function() { 458 | if ( config.semaphore > 0 ) { 459 | return; 460 | } 461 | if ( config.timeout ) { 462 | clearTimeout( config.timeout ); 463 | } 464 | 465 | config.blocking = false; 466 | process( true ); 467 | }, 13); 468 | } else { 469 | config.blocking = false; 470 | process( true ); 471 | } 472 | }, 473 | 474 | stop: function( count ) { 475 | config.semaphore += count || 1; 476 | config.blocking = true; 477 | 478 | if ( config.testTimeout && defined.setTimeout ) { 479 | clearTimeout( config.timeout ); 480 | config.timeout = window.setTimeout(function() { 481 | QUnit.ok( false, "Test timed out" ); 482 | config.semaphore = 1; 483 | QUnit.start(); 484 | }, config.testTimeout ); 485 | } 486 | } 487 | }; 488 | 489 | // `assert` initialized at top of scope 490 | // Asssert helpers 491 | // All of these must either call QUnit.push() or manually do: 492 | // - runLoggingCallbacks( "log", .. ); 493 | // - config.current.assertions.push({ .. }); 494 | // We attach it to the QUnit object *after* we expose the public API, 495 | // otherwise `assert` will become a global variable in browsers (#341). 496 | assert = { 497 | /** 498 | * Asserts rough true-ish result. 499 | * @name ok 500 | * @function 501 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 502 | */ 503 | ok: function( result, msg ) { 504 | if ( !config.current ) { 505 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 506 | } 507 | result = !!result; 508 | 509 | var source, 510 | details = { 511 | module: config.current.module, 512 | name: config.current.testName, 513 | result: result, 514 | message: msg 515 | }; 516 | 517 | msg = escapeText( msg || (result ? "okay" : "failed" ) ); 518 | msg = "" + msg + ""; 519 | 520 | if ( !result ) { 521 | source = sourceFromStacktrace( 2 ); 522 | if ( source ) { 523 | details.source = source; 524 | msg += "
Source:
" + escapeText( source ) + "
"; 525 | } 526 | } 527 | runLoggingCallbacks( "log", QUnit, details ); 528 | config.current.assertions.push({ 529 | result: result, 530 | message: msg 531 | }); 532 | }, 533 | 534 | /** 535 | * Assert that the first two arguments are equal, with an optional message. 536 | * Prints out both actual and expected values. 537 | * @name equal 538 | * @function 539 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 540 | */ 541 | equal: function( actual, expected, message ) { 542 | /*jshint eqeqeq:false */ 543 | QUnit.push( expected == actual, actual, expected, message ); 544 | }, 545 | 546 | /** 547 | * @name notEqual 548 | * @function 549 | */ 550 | notEqual: function( actual, expected, message ) { 551 | /*jshint eqeqeq:false */ 552 | QUnit.push( expected != actual, actual, expected, message ); 553 | }, 554 | 555 | /** 556 | * @name propEqual 557 | * @function 558 | */ 559 | propEqual: function( actual, expected, message ) { 560 | actual = objectValues(actual); 561 | expected = objectValues(expected); 562 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 563 | }, 564 | 565 | /** 566 | * @name notPropEqual 567 | * @function 568 | */ 569 | notPropEqual: function( actual, expected, message ) { 570 | actual = objectValues(actual); 571 | expected = objectValues(expected); 572 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 573 | }, 574 | 575 | /** 576 | * @name deepEqual 577 | * @function 578 | */ 579 | deepEqual: function( actual, expected, message ) { 580 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 581 | }, 582 | 583 | /** 584 | * @name notDeepEqual 585 | * @function 586 | */ 587 | notDeepEqual: function( actual, expected, message ) { 588 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 589 | }, 590 | 591 | /** 592 | * @name strictEqual 593 | * @function 594 | */ 595 | strictEqual: function( actual, expected, message ) { 596 | QUnit.push( expected === actual, actual, expected, message ); 597 | }, 598 | 599 | /** 600 | * @name notStrictEqual 601 | * @function 602 | */ 603 | notStrictEqual: function( actual, expected, message ) { 604 | QUnit.push( expected !== actual, actual, expected, message ); 605 | }, 606 | 607 | "throws": function( block, expected, message ) { 608 | var actual, 609 | expectedOutput = expected, 610 | ok = false; 611 | 612 | // 'expected' is optional 613 | if ( typeof expected === "string" ) { 614 | message = expected; 615 | expected = null; 616 | } 617 | 618 | config.current.ignoreGlobalErrors = true; 619 | try { 620 | block.call( config.current.testEnvironment ); 621 | } catch (e) { 622 | actual = e; 623 | } 624 | config.current.ignoreGlobalErrors = false; 625 | 626 | if ( actual ) { 627 | // we don't want to validate thrown error 628 | if ( !expected ) { 629 | ok = true; 630 | expectedOutput = null; 631 | // expected is a regexp 632 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 633 | ok = expected.test( errorString( actual ) ); 634 | // expected is a constructor 635 | } else if ( actual instanceof expected ) { 636 | ok = true; 637 | // expected is a validation function which returns true is validation passed 638 | } else if ( expected.call( {}, actual ) === true ) { 639 | expectedOutput = null; 640 | ok = true; 641 | } 642 | 643 | QUnit.push( ok, actual, expectedOutput, message ); 644 | } else { 645 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 646 | } 647 | } 648 | }; 649 | 650 | /** 651 | * @deprecate since 1.8.0 652 | * Kept assertion helpers in root for backwards compatibility. 653 | */ 654 | extend( QUnit, assert ); 655 | 656 | /** 657 | * @deprecated since 1.9.0 658 | * Kept root "raises()" for backwards compatibility. 659 | * (Note that we don't introduce assert.raises). 660 | */ 661 | QUnit.raises = assert[ "throws" ]; 662 | 663 | /** 664 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 665 | * Kept to avoid TypeErrors for undefined methods. 666 | */ 667 | QUnit.equals = function() { 668 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 669 | }; 670 | QUnit.same = function() { 671 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 672 | }; 673 | 674 | // We want access to the constructor's prototype 675 | (function() { 676 | function F() {} 677 | F.prototype = QUnit; 678 | QUnit = new F(); 679 | // Make F QUnit's constructor so that we can add to the prototype later 680 | QUnit.constructor = F; 681 | }()); 682 | 683 | /** 684 | * Config object: Maintain internal state 685 | * Later exposed as QUnit.config 686 | * `config` initialized at top of scope 687 | */ 688 | config = { 689 | // The queue of tests to run 690 | queue: [], 691 | 692 | // block until document ready 693 | blocking: true, 694 | 695 | // when enabled, show only failing tests 696 | // gets persisted through sessionStorage and can be changed in UI via checkbox 697 | hidepassed: false, 698 | 699 | // by default, run previously failed tests first 700 | // very useful in combination with "Hide passed tests" checked 701 | reorder: true, 702 | 703 | // by default, modify document.title when suite is done 704 | altertitle: true, 705 | 706 | // when enabled, all tests must call expect() 707 | requireExpects: false, 708 | 709 | // add checkboxes that are persisted in the query-string 710 | // when enabled, the id is set to `true` as a `QUnit.config` property 711 | urlConfig: [ 712 | { 713 | id: "noglobals", 714 | label: "Check for Globals", 715 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 716 | }, 717 | { 718 | id: "notrycatch", 719 | label: "No try-catch", 720 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 721 | } 722 | ], 723 | 724 | // Set of all modules. 725 | modules: {}, 726 | 727 | // logging callback queues 728 | begin: [], 729 | done: [], 730 | log: [], 731 | testStart: [], 732 | testDone: [], 733 | moduleStart: [], 734 | moduleDone: [] 735 | }; 736 | 737 | // Export global variables, unless an 'exports' object exists, 738 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 739 | if ( typeof exports === "undefined" ) { 740 | extend( window, QUnit ); 741 | 742 | // Expose QUnit object 743 | window.QUnit = QUnit; 744 | } 745 | 746 | // Initialize more QUnit.config and QUnit.urlParams 747 | (function() { 748 | var i, 749 | location = window.location || { search: "", protocol: "file:" }, 750 | params = location.search.slice( 1 ).split( "&" ), 751 | length = params.length, 752 | urlParams = {}, 753 | current; 754 | 755 | if ( params[ 0 ] ) { 756 | for ( i = 0; i < length; i++ ) { 757 | current = params[ i ].split( "=" ); 758 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 759 | // allow just a key to turn on a flag, e.g., test.html?noglobals 760 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 761 | urlParams[ current[ 0 ] ] = current[ 1 ]; 762 | } 763 | } 764 | 765 | QUnit.urlParams = urlParams; 766 | 767 | // String search anywhere in moduleName+testName 768 | config.filter = urlParams.filter; 769 | 770 | // Exact match of the module name 771 | config.module = urlParams.module; 772 | 773 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 774 | 775 | // Figure out if we're running the tests from a server or not 776 | QUnit.isLocal = location.protocol === "file:"; 777 | }()); 778 | 779 | // Extend QUnit object, 780 | // these after set here because they should not be exposed as global functions 781 | extend( QUnit, { 782 | assert: assert, 783 | 784 | config: config, 785 | 786 | // Initialize the configuration options 787 | init: function() { 788 | extend( config, { 789 | stats: { all: 0, bad: 0 }, 790 | moduleStats: { all: 0, bad: 0 }, 791 | started: +new Date(), 792 | updateRate: 1000, 793 | blocking: false, 794 | autostart: true, 795 | autorun: false, 796 | filter: "", 797 | queue: [], 798 | semaphore: 1 799 | }); 800 | 801 | var tests, banner, result, 802 | qunit = id( "qunit" ); 803 | 804 | if ( qunit ) { 805 | qunit.innerHTML = 806 | "

" + escapeText( document.title ) + "

" + 807 | "

" + 808 | "
" + 809 | "

" + 810 | "
    "; 811 | } 812 | 813 | tests = id( "qunit-tests" ); 814 | banner = id( "qunit-banner" ); 815 | result = id( "qunit-testresult" ); 816 | 817 | if ( tests ) { 818 | tests.innerHTML = ""; 819 | } 820 | 821 | if ( banner ) { 822 | banner.className = ""; 823 | } 824 | 825 | if ( result ) { 826 | result.parentNode.removeChild( result ); 827 | } 828 | 829 | if ( tests ) { 830 | result = document.createElement( "p" ); 831 | result.id = "qunit-testresult"; 832 | result.className = "result"; 833 | tests.parentNode.insertBefore( result, tests ); 834 | result.innerHTML = "Running...
     "; 835 | } 836 | }, 837 | 838 | // Resets the test setup. Useful for tests that modify the DOM. 839 | reset: function() { 840 | var fixture = id( "qunit-fixture" ); 841 | if ( fixture ) { 842 | fixture.innerHTML = config.fixture; 843 | } 844 | }, 845 | 846 | // Trigger an event on an element. 847 | // @example triggerEvent( document.body, "click" ); 848 | triggerEvent: function( elem, type, event ) { 849 | if ( document.createEvent ) { 850 | event = document.createEvent( "MouseEvents" ); 851 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 852 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 853 | 854 | elem.dispatchEvent( event ); 855 | } else if ( elem.fireEvent ) { 856 | elem.fireEvent( "on" + type ); 857 | } 858 | }, 859 | 860 | // Safe object type checking 861 | is: function( type, obj ) { 862 | return QUnit.objectType( obj ) === type; 863 | }, 864 | 865 | objectType: function( obj ) { 866 | if ( typeof obj === "undefined" ) { 867 | return "undefined"; 868 | // consider: typeof null === object 869 | } 870 | if ( obj === null ) { 871 | return "null"; 872 | } 873 | 874 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 875 | type = match && match[1] || ""; 876 | 877 | switch ( type ) { 878 | case "Number": 879 | if ( isNaN(obj) ) { 880 | return "nan"; 881 | } 882 | return "number"; 883 | case "String": 884 | case "Boolean": 885 | case "Array": 886 | case "Date": 887 | case "RegExp": 888 | case "Function": 889 | return type.toLowerCase(); 890 | } 891 | if ( typeof obj === "object" ) { 892 | return "object"; 893 | } 894 | return undefined; 895 | }, 896 | 897 | push: function( result, actual, expected, message ) { 898 | if ( !config.current ) { 899 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 900 | } 901 | 902 | var output, source, 903 | details = { 904 | module: config.current.module, 905 | name: config.current.testName, 906 | result: result, 907 | message: message, 908 | actual: actual, 909 | expected: expected 910 | }; 911 | 912 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 913 | message = "" + message + ""; 914 | output = message; 915 | 916 | if ( !result ) { 917 | expected = escapeText( QUnit.jsDump.parse(expected) ); 918 | actual = escapeText( QUnit.jsDump.parse(actual) ); 919 | output += ""; 920 | 921 | if ( actual !== expected ) { 922 | output += ""; 923 | output += ""; 924 | } 925 | 926 | source = sourceFromStacktrace(); 927 | 928 | if ( source ) { 929 | details.source = source; 930 | output += ""; 931 | } 932 | 933 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 934 | } 935 | 936 | runLoggingCallbacks( "log", QUnit, details ); 937 | 938 | config.current.assertions.push({ 939 | result: !!result, 940 | message: output 941 | }); 942 | }, 943 | 944 | pushFailure: function( message, source, actual ) { 945 | if ( !config.current ) { 946 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 947 | } 948 | 949 | var output, 950 | details = { 951 | module: config.current.module, 952 | name: config.current.testName, 953 | result: false, 954 | message: message 955 | }; 956 | 957 | message = escapeText( message ) || "error"; 958 | message = "" + message + ""; 959 | output = message; 960 | 961 | output += ""; 962 | 963 | if ( actual ) { 964 | output += ""; 965 | } 966 | 967 | if ( source ) { 968 | details.source = source; 969 | output += ""; 970 | } 971 | 972 | output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 973 | 974 | runLoggingCallbacks( "log", QUnit, details ); 975 | 976 | config.current.assertions.push({ 977 | result: false, 978 | message: output 979 | }); 980 | }, 981 | 982 | url: function( params ) { 983 | params = extend( extend( {}, QUnit.urlParams ), params ); 984 | var key, 985 | querystring = "?"; 986 | 987 | for ( key in params ) { 988 | if ( !hasOwn.call( params, key ) ) { 989 | continue; 990 | } 991 | querystring += encodeURIComponent( key ) + "=" + 992 | encodeURIComponent( params[ key ] ) + "&"; 993 | } 994 | return window.location.protocol + "//" + window.location.host + 995 | window.location.pathname + querystring.slice( 0, -1 ); 996 | }, 997 | 998 | extend: extend, 999 | id: id, 1000 | addEvent: addEvent 1001 | // load, equiv, jsDump, diff: Attached later 1002 | }); 1003 | 1004 | /** 1005 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 1006 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 1007 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 1008 | * Doing this allows us to tell if the following methods have been overwritten on the actual 1009 | * QUnit object. 1010 | */ 1011 | extend( QUnit.constructor.prototype, { 1012 | 1013 | // Logging callbacks; all receive a single argument with the listed properties 1014 | // run test/logs.html for any related changes 1015 | begin: registerLoggingCallback( "begin" ), 1016 | 1017 | // done: { failed, passed, total, runtime } 1018 | done: registerLoggingCallback( "done" ), 1019 | 1020 | // log: { result, actual, expected, message } 1021 | log: registerLoggingCallback( "log" ), 1022 | 1023 | // testStart: { name } 1024 | testStart: registerLoggingCallback( "testStart" ), 1025 | 1026 | // testDone: { name, failed, passed, total, duration } 1027 | testDone: registerLoggingCallback( "testDone" ), 1028 | 1029 | // moduleStart: { name } 1030 | moduleStart: registerLoggingCallback( "moduleStart" ), 1031 | 1032 | // moduleDone: { name, failed, passed, total } 1033 | moduleDone: registerLoggingCallback( "moduleDone" ) 1034 | }); 1035 | 1036 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 1037 | config.autorun = true; 1038 | } 1039 | 1040 | QUnit.load = function() { 1041 | runLoggingCallbacks( "begin", QUnit, {} ); 1042 | 1043 | // Initialize the config, saving the execution queue 1044 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, 1045 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, 1046 | numModules = 0, 1047 | moduleFilterHtml = "", 1048 | urlConfigHtml = "", 1049 | oldconfig = extend( {}, config ); 1050 | 1051 | QUnit.init(); 1052 | extend(config, oldconfig); 1053 | 1054 | config.blocking = false; 1055 | 1056 | len = config.urlConfig.length; 1057 | 1058 | for ( i = 0; i < len; i++ ) { 1059 | val = config.urlConfig[i]; 1060 | if ( typeof val === "string" ) { 1061 | val = { 1062 | id: val, 1063 | label: val, 1064 | tooltip: "[no tooltip available]" 1065 | }; 1066 | } 1067 | config[ val.id ] = QUnit.urlParams[ val.id ]; 1068 | urlConfigHtml += ""; 1074 | } 1075 | 1076 | moduleFilterHtml += ""; 1089 | 1090 | // `userAgent` initialized at top of scope 1091 | userAgent = id( "qunit-userAgent" ); 1092 | if ( userAgent ) { 1093 | userAgent.innerHTML = navigator.userAgent; 1094 | } 1095 | 1096 | // `banner` initialized at top of scope 1097 | banner = id( "qunit-header" ); 1098 | if ( banner ) { 1099 | banner.innerHTML = "" + banner.innerHTML + " "; 1100 | } 1101 | 1102 | // `toolbar` initialized at top of scope 1103 | toolbar = id( "qunit-testrunner-toolbar" ); 1104 | if ( toolbar ) { 1105 | // `filter` initialized at top of scope 1106 | filter = document.createElement( "input" ); 1107 | filter.type = "checkbox"; 1108 | filter.id = "qunit-filter-pass"; 1109 | 1110 | addEvent( filter, "click", function() { 1111 | var tmp, 1112 | ol = document.getElementById( "qunit-tests" ); 1113 | 1114 | if ( filter.checked ) { 1115 | ol.className = ol.className + " hidepass"; 1116 | } else { 1117 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 1118 | ol.className = tmp.replace( / hidepass /, " " ); 1119 | } 1120 | if ( defined.sessionStorage ) { 1121 | if (filter.checked) { 1122 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 1123 | } else { 1124 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 1125 | } 1126 | } 1127 | }); 1128 | 1129 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1130 | filter.checked = true; 1131 | // `ol` initialized at top of scope 1132 | ol = document.getElementById( "qunit-tests" ); 1133 | ol.className = ol.className + " hidepass"; 1134 | } 1135 | toolbar.appendChild( filter ); 1136 | 1137 | // `label` initialized at top of scope 1138 | label = document.createElement( "label" ); 1139 | label.setAttribute( "for", "qunit-filter-pass" ); 1140 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); 1141 | label.innerHTML = "Hide passed tests"; 1142 | toolbar.appendChild( label ); 1143 | 1144 | urlConfigCheckboxesContainer = document.createElement("span"); 1145 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; 1146 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); 1147 | // For oldIE support: 1148 | // * Add handlers to the individual elements instead of the container 1149 | // * Use "click" instead of "change" 1150 | // * Fallback from event.target to event.srcElement 1151 | addEvents( urlConfigCheckboxes, "click", function( event ) { 1152 | var params = {}, 1153 | target = event.target || event.srcElement; 1154 | params[ target.name ] = target.checked ? true : undefined; 1155 | window.location = QUnit.url( params ); 1156 | }); 1157 | toolbar.appendChild( urlConfigCheckboxesContainer ); 1158 | 1159 | if (numModules > 1) { 1160 | moduleFilter = document.createElement( 'span' ); 1161 | moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); 1162 | moduleFilter.innerHTML = moduleFilterHtml; 1163 | addEvent( moduleFilter.lastChild, "change", function() { 1164 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1165 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1166 | 1167 | window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); 1168 | }); 1169 | toolbar.appendChild(moduleFilter); 1170 | } 1171 | } 1172 | 1173 | // `main` initialized at top of scope 1174 | main = id( "qunit-fixture" ); 1175 | if ( main ) { 1176 | config.fixture = main.innerHTML; 1177 | } 1178 | 1179 | if ( config.autostart ) { 1180 | QUnit.start(); 1181 | } 1182 | }; 1183 | 1184 | addEvent( window, "load", QUnit.load ); 1185 | 1186 | // `onErrorFnPrev` initialized at top of scope 1187 | // Preserve other handlers 1188 | onErrorFnPrev = window.onerror; 1189 | 1190 | // Cover uncaught exceptions 1191 | // Returning true will surpress the default browser handler, 1192 | // returning false will let it run. 1193 | window.onerror = function ( error, filePath, linerNr ) { 1194 | var ret = false; 1195 | if ( onErrorFnPrev ) { 1196 | ret = onErrorFnPrev( error, filePath, linerNr ); 1197 | } 1198 | 1199 | // Treat return value as window.onerror itself does, 1200 | // Only do our handling if not surpressed. 1201 | if ( ret !== true ) { 1202 | if ( QUnit.config.current ) { 1203 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1204 | return true; 1205 | } 1206 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1207 | } else { 1208 | QUnit.test( "global failure", extend( function() { 1209 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1210 | }, { validTest: validTest } ) ); 1211 | } 1212 | return false; 1213 | } 1214 | 1215 | return ret; 1216 | }; 1217 | 1218 | function done() { 1219 | config.autorun = true; 1220 | 1221 | // Log the last module results 1222 | if ( config.currentModule ) { 1223 | runLoggingCallbacks( "moduleDone", QUnit, { 1224 | name: config.currentModule, 1225 | failed: config.moduleStats.bad, 1226 | passed: config.moduleStats.all - config.moduleStats.bad, 1227 | total: config.moduleStats.all 1228 | }); 1229 | } 1230 | 1231 | var i, key, 1232 | banner = id( "qunit-banner" ), 1233 | tests = id( "qunit-tests" ), 1234 | runtime = +new Date() - config.started, 1235 | passed = config.stats.all - config.stats.bad, 1236 | html = [ 1237 | "Tests completed in ", 1238 | runtime, 1239 | " milliseconds.
    ", 1240 | "", 1241 | passed, 1242 | " assertions of ", 1243 | config.stats.all, 1244 | " passed, ", 1245 | config.stats.bad, 1246 | " failed." 1247 | ].join( "" ); 1248 | 1249 | if ( banner ) { 1250 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1251 | } 1252 | 1253 | if ( tests ) { 1254 | id( "qunit-testresult" ).innerHTML = html; 1255 | } 1256 | 1257 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1258 | // show ✖ for good, ✔ for bad suite result in title 1259 | // use escape sequences in case file gets loaded with non-utf-8-charset 1260 | document.title = [ 1261 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1262 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1263 | ].join( " " ); 1264 | } 1265 | 1266 | // clear own sessionStorage items if all tests passed 1267 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1268 | // `key` & `i` initialized at top of scope 1269 | for ( i = 0; i < sessionStorage.length; i++ ) { 1270 | key = sessionStorage.key( i++ ); 1271 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1272 | sessionStorage.removeItem( key ); 1273 | } 1274 | } 1275 | } 1276 | 1277 | // scroll back to top to show results 1278 | if ( window.scrollTo ) { 1279 | window.scrollTo(0, 0); 1280 | } 1281 | 1282 | runLoggingCallbacks( "done", QUnit, { 1283 | failed: config.stats.bad, 1284 | passed: passed, 1285 | total: config.stats.all, 1286 | runtime: runtime 1287 | }); 1288 | } 1289 | 1290 | /** @return Boolean: true if this test should be ran */ 1291 | function validTest( test ) { 1292 | var include, 1293 | filter = config.filter && config.filter.toLowerCase(), 1294 | module = config.module && config.module.toLowerCase(), 1295 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1296 | 1297 | // Internally-generated tests are always valid 1298 | if ( test.callback && test.callback.validTest === validTest ) { 1299 | delete test.callback.validTest; 1300 | return true; 1301 | } 1302 | 1303 | if ( config.testNumber ) { 1304 | return test.testNumber === config.testNumber; 1305 | } 1306 | 1307 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1308 | return false; 1309 | } 1310 | 1311 | if ( !filter ) { 1312 | return true; 1313 | } 1314 | 1315 | include = filter.charAt( 0 ) !== "!"; 1316 | if ( !include ) { 1317 | filter = filter.slice( 1 ); 1318 | } 1319 | 1320 | // If the filter matches, we need to honour include 1321 | if ( fullName.indexOf( filter ) !== -1 ) { 1322 | return include; 1323 | } 1324 | 1325 | // Otherwise, do the opposite 1326 | return !include; 1327 | } 1328 | 1329 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1330 | // Later Safari and IE10 are supposed to support error.stack as well 1331 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1332 | function extractStacktrace( e, offset ) { 1333 | offset = offset === undefined ? 3 : offset; 1334 | 1335 | var stack, include, i; 1336 | 1337 | if ( e.stacktrace ) { 1338 | // Opera 1339 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1340 | } else if ( e.stack ) { 1341 | // Firefox, Chrome 1342 | stack = e.stack.split( "\n" ); 1343 | if (/^error$/i.test( stack[0] ) ) { 1344 | stack.shift(); 1345 | } 1346 | if ( fileName ) { 1347 | include = []; 1348 | for ( i = offset; i < stack.length; i++ ) { 1349 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 1350 | break; 1351 | } 1352 | include.push( stack[ i ] ); 1353 | } 1354 | if ( include.length ) { 1355 | return include.join( "\n" ); 1356 | } 1357 | } 1358 | return stack[ offset ]; 1359 | } else if ( e.sourceURL ) { 1360 | // Safari, PhantomJS 1361 | // hopefully one day Safari provides actual stacktraces 1362 | // exclude useless self-reference for generated Error objects 1363 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1364 | return; 1365 | } 1366 | // for actual exceptions, this is useful 1367 | return e.sourceURL + ":" + e.line; 1368 | } 1369 | } 1370 | function sourceFromStacktrace( offset ) { 1371 | try { 1372 | throw new Error(); 1373 | } catch ( e ) { 1374 | return extractStacktrace( e, offset ); 1375 | } 1376 | } 1377 | 1378 | /** 1379 | * Escape text for attribute or text content. 1380 | */ 1381 | function escapeText( s ) { 1382 | if ( !s ) { 1383 | return ""; 1384 | } 1385 | s = s + ""; 1386 | // Both single quotes and double quotes (for attributes) 1387 | return s.replace( /['"<>&]/g, function( s ) { 1388 | switch( s ) { 1389 | case '\'': 1390 | return '''; 1391 | case '"': 1392 | return '"'; 1393 | case '<': 1394 | return '<'; 1395 | case '>': 1396 | return '>'; 1397 | case '&': 1398 | return '&'; 1399 | } 1400 | }); 1401 | } 1402 | 1403 | function synchronize( callback, last ) { 1404 | config.queue.push( callback ); 1405 | 1406 | if ( config.autorun && !config.blocking ) { 1407 | process( last ); 1408 | } 1409 | } 1410 | 1411 | function process( last ) { 1412 | function next() { 1413 | process( last ); 1414 | } 1415 | var start = new Date().getTime(); 1416 | config.depth = config.depth ? config.depth + 1 : 1; 1417 | 1418 | while ( config.queue.length && !config.blocking ) { 1419 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1420 | config.queue.shift()(); 1421 | } else { 1422 | window.setTimeout( next, 13 ); 1423 | break; 1424 | } 1425 | } 1426 | config.depth--; 1427 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1428 | done(); 1429 | } 1430 | } 1431 | 1432 | function saveGlobal() { 1433 | config.pollution = []; 1434 | 1435 | if ( config.noglobals ) { 1436 | for ( var key in window ) { 1437 | // in Opera sometimes DOM element ids show up here, ignore them 1438 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { 1439 | continue; 1440 | } 1441 | config.pollution.push( key ); 1442 | } 1443 | } 1444 | } 1445 | 1446 | function checkPollution() { 1447 | var newGlobals, 1448 | deletedGlobals, 1449 | old = config.pollution; 1450 | 1451 | saveGlobal(); 1452 | 1453 | newGlobals = diff( config.pollution, old ); 1454 | if ( newGlobals.length > 0 ) { 1455 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1456 | } 1457 | 1458 | deletedGlobals = diff( old, config.pollution ); 1459 | if ( deletedGlobals.length > 0 ) { 1460 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1461 | } 1462 | } 1463 | 1464 | // returns a new Array with the elements that are in a but not in b 1465 | function diff( a, b ) { 1466 | var i, j, 1467 | result = a.slice(); 1468 | 1469 | for ( i = 0; i < result.length; i++ ) { 1470 | for ( j = 0; j < b.length; j++ ) { 1471 | if ( result[i] === b[j] ) { 1472 | result.splice( i, 1 ); 1473 | i--; 1474 | break; 1475 | } 1476 | } 1477 | } 1478 | return result; 1479 | } 1480 | 1481 | function extend( a, b ) { 1482 | for ( var prop in b ) { 1483 | if ( b[ prop ] === undefined ) { 1484 | delete a[ prop ]; 1485 | 1486 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1487 | } else if ( prop !== "constructor" || a !== window ) { 1488 | a[ prop ] = b[ prop ]; 1489 | } 1490 | } 1491 | 1492 | return a; 1493 | } 1494 | 1495 | /** 1496 | * @param {HTMLElement} elem 1497 | * @param {string} type 1498 | * @param {Function} fn 1499 | */ 1500 | function addEvent( elem, type, fn ) { 1501 | // Standards-based browsers 1502 | if ( elem.addEventListener ) { 1503 | elem.addEventListener( type, fn, false ); 1504 | // IE 1505 | } else { 1506 | elem.attachEvent( "on" + type, fn ); 1507 | } 1508 | } 1509 | 1510 | /** 1511 | * @param {Array|NodeList} elems 1512 | * @param {string} type 1513 | * @param {Function} fn 1514 | */ 1515 | function addEvents( elems, type, fn ) { 1516 | var i = elems.length; 1517 | while ( i-- ) { 1518 | addEvent( elems[i], type, fn ); 1519 | } 1520 | } 1521 | 1522 | function hasClass( elem, name ) { 1523 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1524 | } 1525 | 1526 | function addClass( elem, name ) { 1527 | if ( !hasClass( elem, name ) ) { 1528 | elem.className += (elem.className ? " " : "") + name; 1529 | } 1530 | } 1531 | 1532 | function removeClass( elem, name ) { 1533 | var set = " " + elem.className + " "; 1534 | // Class name may appear multiple times 1535 | while ( set.indexOf(" " + name + " ") > -1 ) { 1536 | set = set.replace(" " + name + " " , " "); 1537 | } 1538 | // If possible, trim it for prettiness, but not neccecarily 1539 | elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set ); 1540 | } 1541 | 1542 | function id( name ) { 1543 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1544 | document.getElementById( name ); 1545 | } 1546 | 1547 | function registerLoggingCallback( key ) { 1548 | return function( callback ) { 1549 | config[key].push( callback ); 1550 | }; 1551 | } 1552 | 1553 | // Supports deprecated method of completely overwriting logging callbacks 1554 | function runLoggingCallbacks( key, scope, args ) { 1555 | var i, callbacks; 1556 | if ( QUnit.hasOwnProperty( key ) ) { 1557 | QUnit[ key ].call(scope, args ); 1558 | } else { 1559 | callbacks = config[ key ]; 1560 | for ( i = 0; i < callbacks.length; i++ ) { 1561 | callbacks[ i ].call( scope, args ); 1562 | } 1563 | } 1564 | } 1565 | 1566 | // Test for equality any JavaScript type. 1567 | // Author: Philippe Rathé 1568 | QUnit.equiv = (function() { 1569 | 1570 | // Call the o related callback with the given arguments. 1571 | function bindCallbacks( o, callbacks, args ) { 1572 | var prop = QUnit.objectType( o ); 1573 | if ( prop ) { 1574 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1575 | return callbacks[ prop ].apply( callbacks, args ); 1576 | } else { 1577 | return callbacks[ prop ]; // or undefined 1578 | } 1579 | } 1580 | } 1581 | 1582 | // the real equiv function 1583 | var innerEquiv, 1584 | // stack to decide between skip/abort functions 1585 | callers = [], 1586 | // stack to avoiding loops from circular referencing 1587 | parents = [], 1588 | 1589 | getProto = Object.getPrototypeOf || function ( obj ) { 1590 | return obj.__proto__; 1591 | }, 1592 | callbacks = (function () { 1593 | 1594 | // for string, boolean, number and null 1595 | function useStrictEquality( b, a ) { 1596 | /*jshint eqeqeq:false */ 1597 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1598 | // to catch short annotaion VS 'new' annotation of a 1599 | // declaration 1600 | // e.g. var i = 1; 1601 | // var j = new Number(1); 1602 | return a == b; 1603 | } else { 1604 | return a === b; 1605 | } 1606 | } 1607 | 1608 | return { 1609 | "string": useStrictEquality, 1610 | "boolean": useStrictEquality, 1611 | "number": useStrictEquality, 1612 | "null": useStrictEquality, 1613 | "undefined": useStrictEquality, 1614 | 1615 | "nan": function( b ) { 1616 | return isNaN( b ); 1617 | }, 1618 | 1619 | "date": function( b, a ) { 1620 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1621 | }, 1622 | 1623 | "regexp": function( b, a ) { 1624 | return QUnit.objectType( b ) === "regexp" && 1625 | // the regex itself 1626 | a.source === b.source && 1627 | // and its modifers 1628 | a.global === b.global && 1629 | // (gmi) ... 1630 | a.ignoreCase === b.ignoreCase && 1631 | a.multiline === b.multiline && 1632 | a.sticky === b.sticky; 1633 | }, 1634 | 1635 | // - skip when the property is a method of an instance (OOP) 1636 | // - abort otherwise, 1637 | // initial === would have catch identical references anyway 1638 | "function": function() { 1639 | var caller = callers[callers.length - 1]; 1640 | return caller !== Object && typeof caller !== "undefined"; 1641 | }, 1642 | 1643 | "array": function( b, a ) { 1644 | var i, j, len, loop; 1645 | 1646 | // b could be an object literal here 1647 | if ( QUnit.objectType( b ) !== "array" ) { 1648 | return false; 1649 | } 1650 | 1651 | len = a.length; 1652 | if ( len !== b.length ) { 1653 | // safe and faster 1654 | return false; 1655 | } 1656 | 1657 | // track reference to avoid circular references 1658 | parents.push( a ); 1659 | for ( i = 0; i < len; i++ ) { 1660 | loop = false; 1661 | for ( j = 0; j < parents.length; j++ ) { 1662 | if ( parents[j] === a[i] ) { 1663 | loop = true;// dont rewalk array 1664 | } 1665 | } 1666 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1667 | parents.pop(); 1668 | return false; 1669 | } 1670 | } 1671 | parents.pop(); 1672 | return true; 1673 | }, 1674 | 1675 | "object": function( b, a ) { 1676 | var i, j, loop, 1677 | // Default to true 1678 | eq = true, 1679 | aProperties = [], 1680 | bProperties = []; 1681 | 1682 | // comparing constructors is more strict than using 1683 | // instanceof 1684 | if ( a.constructor !== b.constructor ) { 1685 | // Allow objects with no prototype to be equivalent to 1686 | // objects with Object as their constructor. 1687 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1688 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1689 | return false; 1690 | } 1691 | } 1692 | 1693 | // stack constructor before traversing properties 1694 | callers.push( a.constructor ); 1695 | // track reference to avoid circular references 1696 | parents.push( a ); 1697 | 1698 | for ( i in a ) { // be strict: don't ensures hasOwnProperty 1699 | // and go deep 1700 | loop = false; 1701 | for ( j = 0; j < parents.length; j++ ) { 1702 | if ( parents[j] === a[i] ) { 1703 | // don't go down the same path twice 1704 | loop = true; 1705 | } 1706 | } 1707 | aProperties.push(i); // collect a's properties 1708 | 1709 | if (!loop && !innerEquiv( a[i], b[i] ) ) { 1710 | eq = false; 1711 | break; 1712 | } 1713 | } 1714 | 1715 | callers.pop(); // unstack, we are done 1716 | parents.pop(); 1717 | 1718 | for ( i in b ) { 1719 | bProperties.push( i ); // collect b's properties 1720 | } 1721 | 1722 | // Ensures identical properties name 1723 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1724 | } 1725 | }; 1726 | }()); 1727 | 1728 | innerEquiv = function() { // can take multiple arguments 1729 | var args = [].slice.apply( arguments ); 1730 | if ( args.length < 2 ) { 1731 | return true; // end transition 1732 | } 1733 | 1734 | return (function( a, b ) { 1735 | if ( a === b ) { 1736 | return true; // catch the most you can 1737 | } else if ( a === null || b === null || typeof a === "undefined" || 1738 | typeof b === "undefined" || 1739 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1740 | return false; // don't lose time with error prone cases 1741 | } else { 1742 | return bindCallbacks(a, callbacks, [ b, a ]); 1743 | } 1744 | 1745 | // apply transition with (1..n) arguments 1746 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); 1747 | }; 1748 | 1749 | return innerEquiv; 1750 | }()); 1751 | 1752 | /** 1753 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1754 | * http://flesler.blogspot.com Licensed under BSD 1755 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1756 | * 1757 | * @projectDescription Advanced and extensible data dumping for Javascript. 1758 | * @version 1.0.0 1759 | * @author Ariel Flesler 1760 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1761 | */ 1762 | QUnit.jsDump = (function() { 1763 | function quote( str ) { 1764 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; 1765 | } 1766 | function literal( o ) { 1767 | return o + ""; 1768 | } 1769 | function join( pre, arr, post ) { 1770 | var s = jsDump.separator(), 1771 | base = jsDump.indent(), 1772 | inner = jsDump.indent(1); 1773 | if ( arr.join ) { 1774 | arr = arr.join( "," + s + inner ); 1775 | } 1776 | if ( !arr ) { 1777 | return pre + post; 1778 | } 1779 | return [ pre, inner + arr, base + post ].join(s); 1780 | } 1781 | function array( arr, stack ) { 1782 | var i = arr.length, ret = new Array(i); 1783 | this.up(); 1784 | while ( i-- ) { 1785 | ret[i] = this.parse( arr[i] , undefined , stack); 1786 | } 1787 | this.down(); 1788 | return join( "[", ret, "]" ); 1789 | } 1790 | 1791 | var reName = /^function (\w+)/, 1792 | jsDump = { 1793 | // type is used mostly internally, you can fix a (custom)type in advance 1794 | parse: function( obj, type, stack ) { 1795 | stack = stack || [ ]; 1796 | var inStack, res, 1797 | parser = this.parsers[ type || this.typeOf(obj) ]; 1798 | 1799 | type = typeof parser; 1800 | inStack = inArray( obj, stack ); 1801 | 1802 | if ( inStack !== -1 ) { 1803 | return "recursion(" + (inStack - stack.length) + ")"; 1804 | } 1805 | if ( type === "function" ) { 1806 | stack.push( obj ); 1807 | res = parser.call( this, obj, stack ); 1808 | stack.pop(); 1809 | return res; 1810 | } 1811 | return ( type === "string" ) ? parser : this.parsers.error; 1812 | }, 1813 | typeOf: function( obj ) { 1814 | var type; 1815 | if ( obj === null ) { 1816 | type = "null"; 1817 | } else if ( typeof obj === "undefined" ) { 1818 | type = "undefined"; 1819 | } else if ( QUnit.is( "regexp", obj) ) { 1820 | type = "regexp"; 1821 | } else if ( QUnit.is( "date", obj) ) { 1822 | type = "date"; 1823 | } else if ( QUnit.is( "function", obj) ) { 1824 | type = "function"; 1825 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1826 | type = "window"; 1827 | } else if ( obj.nodeType === 9 ) { 1828 | type = "document"; 1829 | } else if ( obj.nodeType ) { 1830 | type = "node"; 1831 | } else if ( 1832 | // native arrays 1833 | toString.call( obj ) === "[object Array]" || 1834 | // NodeList objects 1835 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1836 | ) { 1837 | type = "array"; 1838 | } else if ( obj.constructor === Error.prototype.constructor ) { 1839 | type = "error"; 1840 | } else { 1841 | type = typeof obj; 1842 | } 1843 | return type; 1844 | }, 1845 | separator: function() { 1846 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1847 | }, 1848 | // extra can be a number, shortcut for increasing-calling-decreasing 1849 | indent: function( extra ) { 1850 | if ( !this.multiline ) { 1851 | return ""; 1852 | } 1853 | var chr = this.indentChar; 1854 | if ( this.HTML ) { 1855 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1856 | } 1857 | return new Array( this._depth_ + (extra||0) ).join(chr); 1858 | }, 1859 | up: function( a ) { 1860 | this._depth_ += a || 1; 1861 | }, 1862 | down: function( a ) { 1863 | this._depth_ -= a || 1; 1864 | }, 1865 | setParser: function( name, parser ) { 1866 | this.parsers[name] = parser; 1867 | }, 1868 | // The next 3 are exposed so you can use them 1869 | quote: quote, 1870 | literal: literal, 1871 | join: join, 1872 | // 1873 | _depth_: 1, 1874 | // This is the list of parsers, to modify them, use jsDump.setParser 1875 | parsers: { 1876 | window: "[Window]", 1877 | document: "[Document]", 1878 | error: function(error) { 1879 | return "Error(\"" + error.message + "\")"; 1880 | }, 1881 | unknown: "[Unknown]", 1882 | "null": "null", 1883 | "undefined": "undefined", 1884 | "function": function( fn ) { 1885 | var ret = "function", 1886 | // functions never have name in IE 1887 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1888 | 1889 | if ( name ) { 1890 | ret += " " + name; 1891 | } 1892 | ret += "( "; 1893 | 1894 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1895 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1896 | }, 1897 | array: array, 1898 | nodelist: array, 1899 | "arguments": array, 1900 | object: function( map, stack ) { 1901 | var ret = [ ], keys, key, val, i; 1902 | QUnit.jsDump.up(); 1903 | keys = []; 1904 | for ( key in map ) { 1905 | keys.push( key ); 1906 | } 1907 | keys.sort(); 1908 | for ( i = 0; i < keys.length; i++ ) { 1909 | key = keys[ i ]; 1910 | val = map[ key ]; 1911 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1912 | } 1913 | QUnit.jsDump.down(); 1914 | return join( "{", ret, "}" ); 1915 | }, 1916 | node: function( node ) { 1917 | var len, i, val, 1918 | open = QUnit.jsDump.HTML ? "<" : "<", 1919 | close = QUnit.jsDump.HTML ? ">" : ">", 1920 | tag = node.nodeName.toLowerCase(), 1921 | ret = open + tag, 1922 | attrs = node.attributes; 1923 | 1924 | if ( attrs ) { 1925 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1926 | val = attrs[i].nodeValue; 1927 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1928 | // Those have values like undefined, null, 0, false, "" or "inherit". 1929 | if ( val && val !== "inherit" ) { 1930 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1931 | } 1932 | } 1933 | } 1934 | ret += close; 1935 | 1936 | // Show content of TextNode or CDATASection 1937 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1938 | ret += node.nodeValue; 1939 | } 1940 | 1941 | return ret + open + "/" + tag + close; 1942 | }, 1943 | // function calls it internally, it's the arguments part of the function 1944 | functionArgs: function( fn ) { 1945 | var args, 1946 | l = fn.length; 1947 | 1948 | if ( !l ) { 1949 | return ""; 1950 | } 1951 | 1952 | args = new Array(l); 1953 | while ( l-- ) { 1954 | // 97 is 'a' 1955 | args[l] = String.fromCharCode(97+l); 1956 | } 1957 | return " " + args.join( ", " ) + " "; 1958 | }, 1959 | // object calls it internally, the key part of an item in a map 1960 | key: quote, 1961 | // function calls it internally, it's the content of the function 1962 | functionCode: "[code]", 1963 | // node calls it internally, it's an html attribute value 1964 | attribute: quote, 1965 | string: quote, 1966 | date: quote, 1967 | regexp: literal, 1968 | number: literal, 1969 | "boolean": literal 1970 | }, 1971 | // if true, entities are escaped ( <, >, \t, space and \n ) 1972 | HTML: false, 1973 | // indentation unit 1974 | indentChar: " ", 1975 | // if true, items in a collection, are separated by a \n, else just a space. 1976 | multiline: true 1977 | }; 1978 | 1979 | return jsDump; 1980 | }()); 1981 | 1982 | // from jquery.js 1983 | function inArray( elem, array ) { 1984 | if ( array.indexOf ) { 1985 | return array.indexOf( elem ); 1986 | } 1987 | 1988 | for ( var i = 0, length = array.length; i < length; i++ ) { 1989 | if ( array[ i ] === elem ) { 1990 | return i; 1991 | } 1992 | } 1993 | 1994 | return -1; 1995 | } 1996 | 1997 | /* 1998 | * Javascript Diff Algorithm 1999 | * By John Resig (http://ejohn.org/) 2000 | * Modified by Chu Alan "sprite" 2001 | * 2002 | * Released under the MIT license. 2003 | * 2004 | * More Info: 2005 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2006 | * 2007 | * Usage: QUnit.diff(expected, actual) 2008 | * 2009 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2010 | */ 2011 | QUnit.diff = (function() { 2012 | /*jshint eqeqeq:false, eqnull:true */ 2013 | function diff( o, n ) { 2014 | var i, 2015 | ns = {}, 2016 | os = {}; 2017 | 2018 | for ( i = 0; i < n.length; i++ ) { 2019 | if ( !hasOwn.call( ns, n[i] ) ) { 2020 | ns[ n[i] ] = { 2021 | rows: [], 2022 | o: null 2023 | }; 2024 | } 2025 | ns[ n[i] ].rows.push( i ); 2026 | } 2027 | 2028 | for ( i = 0; i < o.length; i++ ) { 2029 | if ( !hasOwn.call( os, o[i] ) ) { 2030 | os[ o[i] ] = { 2031 | rows: [], 2032 | n: null 2033 | }; 2034 | } 2035 | os[ o[i] ].rows.push( i ); 2036 | } 2037 | 2038 | for ( i in ns ) { 2039 | if ( !hasOwn.call( ns, i ) ) { 2040 | continue; 2041 | } 2042 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2043 | n[ ns[i].rows[0] ] = { 2044 | text: n[ ns[i].rows[0] ], 2045 | row: os[i].rows[0] 2046 | }; 2047 | o[ os[i].rows[0] ] = { 2048 | text: o[ os[i].rows[0] ], 2049 | row: ns[i].rows[0] 2050 | }; 2051 | } 2052 | } 2053 | 2054 | for ( i = 0; i < n.length - 1; i++ ) { 2055 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2056 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2057 | 2058 | n[ i + 1 ] = { 2059 | text: n[ i + 1 ], 2060 | row: n[i].row + 1 2061 | }; 2062 | o[ n[i].row + 1 ] = { 2063 | text: o[ n[i].row + 1 ], 2064 | row: i + 1 2065 | }; 2066 | } 2067 | } 2068 | 2069 | for ( i = n.length - 1; i > 0; i-- ) { 2070 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2071 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2072 | 2073 | n[ i - 1 ] = { 2074 | text: n[ i - 1 ], 2075 | row: n[i].row - 1 2076 | }; 2077 | o[ n[i].row - 1 ] = { 2078 | text: o[ n[i].row - 1 ], 2079 | row: i - 1 2080 | }; 2081 | } 2082 | } 2083 | 2084 | return { 2085 | o: o, 2086 | n: n 2087 | }; 2088 | } 2089 | 2090 | return function( o, n ) { 2091 | o = o.replace( /\s+$/, "" ); 2092 | n = n.replace( /\s+$/, "" ); 2093 | 2094 | var i, pre, 2095 | str = "", 2096 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2097 | oSpace = o.match(/\s+/g), 2098 | nSpace = n.match(/\s+/g); 2099 | 2100 | if ( oSpace == null ) { 2101 | oSpace = [ " " ]; 2102 | } 2103 | else { 2104 | oSpace.push( " " ); 2105 | } 2106 | 2107 | if ( nSpace == null ) { 2108 | nSpace = [ " " ]; 2109 | } 2110 | else { 2111 | nSpace.push( " " ); 2112 | } 2113 | 2114 | if ( out.n.length === 0 ) { 2115 | for ( i = 0; i < out.o.length; i++ ) { 2116 | str += "" + out.o[i] + oSpace[i] + ""; 2117 | } 2118 | } 2119 | else { 2120 | if ( out.n[0].text == null ) { 2121 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2122 | str += "" + out.o[n] + oSpace[n] + ""; 2123 | } 2124 | } 2125 | 2126 | for ( i = 0; i < out.n.length; i++ ) { 2127 | if (out.n[i].text == null) { 2128 | str += "" + out.n[i] + nSpace[i] + ""; 2129 | } 2130 | else { 2131 | // `pre` initialized at top of scope 2132 | pre = ""; 2133 | 2134 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2135 | pre += "" + out.o[n] + oSpace[n] + ""; 2136 | } 2137 | str += " " + out.n[i].text + nSpace[i] + pre; 2138 | } 2139 | } 2140 | } 2141 | 2142 | return str; 2143 | }; 2144 | }()); 2145 | 2146 | // for CommonJS enviroments, export everything 2147 | if ( typeof exports !== "undefined" ) { 2148 | extend( exports, QUnit ); 2149 | } 2150 | 2151 | // get at whatever the global object is, like window in browsers 2152 | }( (function() {return this;}.call()) )); 2153 | --------------------------------------------------------------------------------