├── .gitignore
├── .jshintrc
├── .travis.yml
├── CONTRIBUTING.md
├── Gruntfile.js
├── LICENSE-BSDv3
├── README.md
├── bower.json
├── demo
├── demo
│ ├── __init__.py
│ ├── settings.py
│ ├── todos
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── models.py
│ │ ├── urls.py
│ │ └── views.py
│ └── urls.py
├── index.html
├── manage.py
├── static
│ ├── css
│ │ └── demo.css
│ ├── fonts
│ │ ├── icons.ttf
│ │ └── icons.woff
│ └── js
│ │ ├── django-superformset.min.js
│ │ └── jquery.js
└── templates
│ └── index.html
├── dist
├── django-superformset.js
└── django-superformset.min.js
├── django-superformset.jquery.json
├── libs
├── jquery-loader.js
├── jquery
│ └── jquery.js
└── qunit
│ ├── qunit-assert-html.js
│ ├── qunit.css
│ ├── qunit.js
│ ├── sinon-qunit.js
│ └── sinon.js
├── package.json
├── src
├── .jshintrc
└── django-superformset.js
└── test
├── .jshintrc
├── django-superformset.html
└── django-superformset_test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | *~
3 | /node_modules/
4 | jscov/
5 | jscov_temp/
6 | *.sqlite3
7 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 |
4 | "bitwise": true,
5 | "curly": true,
6 | "eqeqeq": true,
7 | "forin": true,
8 | "immed": true,
9 | "indent": 2,
10 | "latedef": true,
11 | "newcap": true,
12 | "noarg": true,
13 | "noempty": true,
14 | "nonew": true,
15 | "plusplus": true,
16 | "quotmark": "single",
17 | "undef": true,
18 | "unused": true,
19 | "strict": true,
20 | "trailing": true,
21 | "maxparams": 4
22 | }
23 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "0.11"
5 | - "0.10"
6 |
7 | before_install:
8 | - npm install -g grunt-cli
9 |
10 | matrix:
11 | fast_finish: true
12 | allow_failures:
13 | - node_js: "0.11"
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Important notes
4 | Please don't edit files in the `dist` subdirectory as they are generated via Grunt. You'll find source code in the `src` subdirectory!
5 |
6 | ### Code style
7 | Regarding code style like indentation and whitespace, **follow the conventions you see used in the source already.**
8 |
9 | ### PhantomJS
10 | While Grunt can run the included unit tests via [PhantomJS](http://phantomjs.org/), this shouldn't be considered a substitute for the real thing. Please be sure to test the `test/*.html` unit test file(s) in _actual_ browsers.
11 |
12 | ## Modifying the code
13 | First, ensure that you have the latest [Node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed.
14 |
15 | Test that Grunt's CLI is installed by running `grunt --version`. If the command isn't found, run `npm install -g grunt-cli`. For more information about installing Grunt, see the [getting started guide](http://gruntjs.com/getting-started).
16 |
17 | 1. Fork and clone the repo.
18 | 1. Run `npm install` to install all dependencies (including Grunt).
19 | 1. Run `grunt` to grunt this project.
20 |
21 | Assuming that you don't see any red, you're ready to go. Just be sure to run `grunt` after making any changes, to ensure that nothing is broken.
22 |
23 | ## Submitting pull requests
24 |
25 | 1. Create a new branch, please don't work in your `master` branch directly.
26 | 1. Add failing tests for the change you want to make. Run `grunt` to see the tests fail.
27 | 1. Fix stuff.
28 | 1. Run `grunt` to see if the tests pass. Repeat steps 2-4 until done.
29 | 1. Open `test/*.html` unit test file(s) in actual browser to ensure tests pass everywhere.
30 | 1. Update the documentation to reflect any changes.
31 | 1. Push to your fork and submit a pull request.
32 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(grunt) {
4 |
5 | // Project configuration.
6 | grunt.initConfig({
7 | // Metadata.
8 | pkg: grunt.file.readJSON('django-superformset.jquery.json'),
9 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
10 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
11 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
12 | '* Based on jQuery Formset 1.1r14, by Stanislaus Madueke\n' +
13 | '* Original Portions Copyright (c) 2009 Stanislaus Madueke\n' +
14 | '* Modifications Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
15 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n',
16 | // Task configuration.
17 | clean: {
18 | files: ['dist']
19 | },
20 | concat: {
21 | options: {
22 | banner: '<%= banner %>',
23 | stripBanners: true
24 | },
25 | dist: {
26 | src: ['src/<%= pkg.name %>.js'],
27 | dest: 'dist/<%= pkg.name %>.js'
28 | },
29 | },
30 | uglify: {
31 | options: {
32 | banner: '<%= banner %>'
33 | },
34 | dist: {
35 | src: '<%= concat.dist.dest %>',
36 | dest: 'dist/<%= pkg.name %>.min.js'
37 | },
38 | demo: {
39 | src: '<%= concat.dist.dest %>',
40 | dest: 'demo/static/js/<%= pkg.name %>.min.js'
41 | },
42 | },
43 | qunit: {
44 | options: {
45 | coverage: {
46 | src: ['src/**/*.js'],
47 | instrumentedFiles: 'jscov_temp/',
48 | htmlReport: 'jscov/'
49 | }
50 | },
51 | files: ['test/**/*.html']
52 | },
53 | jshint: {
54 | gruntfile: {
55 | options: {
56 | jshintrc: '.jshintrc'
57 | },
58 | src: 'Gruntfile.js'
59 | },
60 | src: {
61 | options: {
62 | jshintrc: 'src/.jshintrc'
63 | },
64 | src: ['src/**/*.js']
65 | },
66 | test: {
67 | options: {
68 | jshintrc: 'test/.jshintrc'
69 | },
70 | src: ['test/**/*.js']
71 | },
72 | },
73 | watch: {
74 | gruntfile: {
75 | files: '<%= jshint.gruntfile.src %>',
76 | tasks: ['jshint:gruntfile']
77 | },
78 | src: {
79 | files: '<%= jshint.src.src %>',
80 | tasks: ['jshint:src', 'qunit']
81 | },
82 | test: {
83 | files: '<%= jshint.test.src %>',
84 | tasks: ['jshint:test', 'qunit']
85 | },
86 | },
87 | });
88 |
89 | // These plugins provide necessary tasks.
90 | grunt.loadNpmTasks('grunt-contrib-clean');
91 | grunt.loadNpmTasks('grunt-contrib-concat');
92 | grunt.loadNpmTasks('grunt-contrib-uglify');
93 | grunt.loadNpmTasks('grunt-qunit-istanbul');
94 | grunt.loadNpmTasks('grunt-contrib-jshint');
95 | grunt.loadNpmTasks('grunt-contrib-watch');
96 |
97 | // Default task.
98 | grunt.registerTask('default', ['jshint', 'qunit', 'clean', 'concat', 'uglify']);
99 |
100 | };
101 |
--------------------------------------------------------------------------------
/LICENSE-BSDv3:
--------------------------------------------------------------------------------
1 | Original Portions Copyright (c) 2009, Stanislaus Madueke
2 | Modifications Copyright (c) 2013, Jonny Gerig Meyer
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | * Neither the name of the organization nor the names of its contributors may
13 | be used to endorse or promote products derived from this software without
14 | specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django Superformset
2 |
3 | [](https://travis-ci.org/jgerigmeyer/jquery-django-superformset)
4 | [](http://gruntjs.com/)
5 |
6 | jQuery Django Dynamic Formset Plugin
7 |
8 | ## Getting Started
9 |
10 | Download the [production version][min] or the [development version][max].
11 |
12 | [min]: https://raw.github.com/jgerigmeyer/jquery-django-superformset/master/dist/django-superformset.min.js
13 | [max]: https://raw.github.com/jgerigmeyer/jquery-django-superformset/master/dist/django-superformset.js
14 |
15 | In your web page:
16 |
17 | ```html
18 |
36 |
37 |
38 |
39 |
40 |
45 | ```
46 |
47 | ## Documentation
48 |
49 | There are numerous options documented in the [development version][max].
50 |
51 | Available options, explictly set to their defaults:
52 |
53 | ```html
54 | $('.formlist').superformset({
55 | prefix: 'form', // The form prefix for your django formset
56 | containerSel: 'form', // Container selector (must contain rows and formTemplate)
57 | rowSel: '.dynamic-form', // Selector used to match each form (row) in a formset
58 | formTemplate: '.empty-form .dynamic-form',
59 | // Selector for empty form (row) template to be cloned to
60 | // ...generate new form instances
61 | // ...This must be outside the element on which ``formset`` is
62 | // ...called, but within the containerSel
63 | deleteTrigger: 'remove',
64 | // The HTML "remove" link added to the end of each form-row
65 | // ...(if ``canDelete: true``)
66 | deleteTriggerSel: '.remove-row',// Selector for HTML "remove" links
67 | // ...Used to target existing delete-trigger, or to target
68 | // ...``deleteTrigger``
69 | addTrigger: 'add',
70 | // The HTML "add" link added to the end of all forms if no
71 | // ...``addTriggerSel``
72 | addTriggerSel: null, // Selector for trigger to add a new row, if already in markup
73 | // ...Used to target existing trigger; if provided,
74 | // ...``addTrigger`` will be ignored
75 | addedCallback: null, // Function called each time a new form row is added
76 | removedCallback: null, // Function called each time a form row is deleted
77 | deletedRowClass: 'deleted', // Added to deleted row if ``canDelete: false``
78 | addAnimationSpeed: 'normal', // Speed (ms) to animate adding rows
79 | // ...If false, new rows will appear without animation
80 | removeAnimationSpeed: 'fast', // Speed (ms) to animate removing rows
81 | // ...If false, new rows will disappear without animation
82 | autoAdd: false, // If true, the "add" link will be removed, and a row will be
83 | // ...automatically added when text is entered in the final
84 | // ...textarea of the last row
85 | alwaysShowExtra: false, // If true, an extra (empty) row will always be displayed
86 | // ...(requires ``autoAdd: true``)
87 | deleteOnlyActive: false, // If true, extra empty rows cannot be removed until they
88 | // ...acquire focus (requires ``alwaysShowExtra: true``)
89 | canDelete: false, // If false, rows cannot be deleted (removed from the DOM).
90 | // ...``deleteTriggerSel`` will remove ``required`` attr from
91 | // ...fields within a "deleted" row
92 | // ...deleted rows should be hidden via CSS
93 | deleteOnlyNew: false, // If true, only newly-added rows can be deleted
94 | // ...(requires ``canDelete: true``)
95 | insertAbove: false, // If true, ``insertAboveTrigger`` will be added to the end of
96 | // ...each form-row
97 | insertAboveTrigger: 'insert',
98 | // The HTML "insert" link add to the end of each form-row
99 | // ...(requires ``insertAbove: true``)
100 | optionalIfEmpty: true, // If true, required fields in a row will be optional until
101 | // ...changed from their initial values
102 | optionalIfEmptySel: '[data-empty-permitted="true"]'
103 | // Selector for rows to apply optionalIfEmpty logic
104 | // ...(requires ``optionalIfEmpty: true``)
105 | });
106 | ```
107 |
108 | ## Release History
109 |
110 | * 1.0.4 - (06/13/2014) Bugfix: Allow adding row after deleting all rows with ``canDelete: true``
111 | * 1.0.3 - (04/16/2014) Make totalForms and maxForms selectors less restrictive
112 | * 1.0.2 - (02/20/2014) Add deletedRowClass option
113 | * 1.0.1 - (02/19/2014) Add bower.json
114 | * 1.0.0 - (10/14/2013) Initial release
115 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-superformset",
3 | "version": "1.0.4",
4 | "homepage": "https://github.com/jgerigmeyer/jquery-django-superformset",
5 | "authors": [
6 | "Jonny Gerig Meyer "
7 | ],
8 | "description": "jQuery Django Dynamic Formset Plugin",
9 | "main": "dist/django-superformset.js",
10 | "keywords": [
11 | "formset",
12 | "dynamic",
13 | "django"
14 | ],
15 | "license": "BSDv3",
16 | "ignore": [
17 | "**/.*",
18 | "node_modules",
19 | "bower_components",
20 | "test",
21 | "libs",
22 | "Gruntfile.js",
23 | "demo"
24 | ],
25 | "dependencies": {
26 | "jquery": "*"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/demo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgerigmeyer/jquery-django-superformset/07c2a2c018c73c973afac5313c0ec50888eeaa32/demo/demo/__init__.py
--------------------------------------------------------------------------------
/demo/demo/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for demo project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.6/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.6/ref/settings/
9 | """
10 |
11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12 | import os
13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
14 |
15 |
16 | # Quick-start development settings - unsuitable for production
17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
18 |
19 | # SECURITY WARNING: keep the secret key used in production secret!
20 | SECRET_KEY = 'h@&4y-z01hm3wksscqi#o_cmfjm^a&$m0m&jq(o=#8r3x#ldr7'
21 |
22 | # SECURITY WARNING: don't run with debug turned on in production!
23 | DEBUG = True
24 |
25 | TEMPLATE_DEBUG = True
26 |
27 | ALLOWED_HOSTS = []
28 |
29 |
30 | # Application definition
31 |
32 | INSTALLED_APPS = (
33 | 'django.contrib.admin',
34 | 'django.contrib.auth',
35 | 'django.contrib.contenttypes',
36 | 'django.contrib.sessions',
37 | 'django.contrib.messages',
38 | 'django.contrib.staticfiles',
39 | 'demo',
40 | 'demo.todos',
41 | )
42 |
43 | MIDDLEWARE_CLASSES = (
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.common.CommonMiddleware',
46 | 'django.middleware.csrf.CsrfViewMiddleware',
47 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
48 | 'django.contrib.messages.middleware.MessageMiddleware',
49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
50 | )
51 |
52 | ROOT_URLCONF = 'demo.urls'
53 |
54 | WSGI_APPLICATION = 'demo.wsgi.application'
55 |
56 | TEMPLATE_DIRS = os.path.join(BASE_DIR, 'templates')
57 |
58 |
59 | # Database
60 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
61 |
62 | DATABASES = {
63 | 'default': {
64 | 'ENGINE': 'django.db.backends.sqlite3',
65 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
66 | }
67 | }
68 |
69 | # Internationalization
70 | # https://docs.djangoproject.com/en/1.6/topics/i18n/
71 |
72 | LANGUAGE_CODE = 'en-us'
73 |
74 | TIME_ZONE = 'US/Eastern'
75 |
76 | USE_I18N = True
77 |
78 | USE_L10N = False
79 |
80 | USE_TZ = True
81 |
82 |
83 | # Static files (CSS, JavaScript, Images)
84 | # https://docs.djangoproject.com/en/1.6/howto/static-files/
85 |
86 | STATIC_URL = '/static/'
87 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
88 |
--------------------------------------------------------------------------------
/demo/demo/todos/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgerigmeyer/jquery-django-superformset/07c2a2c018c73c973afac5313c0ec50888eeaa32/demo/demo/todos/__init__.py
--------------------------------------------------------------------------------
/demo/demo/todos/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Todo
3 |
4 |
5 | admin.site.register(Todo)
6 |
--------------------------------------------------------------------------------
/demo/demo/todos/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Todo(models.Model):
5 | name = models.CharField(max_length=200)
6 |
--------------------------------------------------------------------------------
/demo/demo/todos/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import patterns, url
2 |
3 | from . import views
4 |
5 |
6 | urlpatterns = patterns('',
7 | url(r'^$', views.index, name='index')
8 | )
9 |
--------------------------------------------------------------------------------
/demo/demo/todos/views.py:
--------------------------------------------------------------------------------
1 | from django.core.context_processors import csrf
2 | from django.forms.models import modelformset_factory
3 | from django.shortcuts import render_to_response
4 | from django.http import HttpResponseRedirect
5 | from django.template import RequestContext
6 |
7 | from .models import Todo
8 |
9 |
10 | def index(request):
11 | TodoFormSet = modelformset_factory(Todo, can_delete=True)
12 | if request.method == 'POST':
13 | formset = TodoFormSet(request.POST)
14 | if formset.is_valid():
15 | formset.save()
16 | return HttpResponseRedirect("/")
17 | else:
18 | formset = TodoFormSet()
19 |
20 | return render_to_response(
21 | 'index.html',
22 | {'formset': formset},
23 | context_instance=RequestContext(request),
24 | )
25 |
--------------------------------------------------------------------------------
/demo/demo/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import patterns, include, url
2 |
3 | from django.contrib import admin
4 | admin.autodiscover()
5 |
6 |
7 | urlpatterns = patterns('',
8 | url(r'^$', include('demo.todos.urls')),
9 | url(r'^admin/', include(admin.site.urls)),
10 | )
11 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Static Demo | jQuery Django Superformset
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
58 |
67 |
68 |
69 |
70 |
71 |
JavaScript:
72 |
73 | jQuery(function($) {
74 | $('.init').one('click', function () {
75 | $('.formlist').superformset({
76 | containerSel: '.demo',
77 | deleteTriggerSel: 'input[name$="-DELETE"]'
78 | });
79 | });
80 | });
81 |
82 |
83 |
84 |
Django Template:
85 |
86 | <form method="post">
87 | <fieldset class="formlist">
88 | {{ formset.management_form }}
89 | {% for form in formset %}
90 | <div class="dynamic-form">
91 | {{ form }}
92 | </div>
93 | {% endfor %}
94 | </fieldset>
95 | </form>
96 |
97 | <div class="empty-form" style="display: none;">
98 | <div class="dynamic-form">
99 | {{ formset.empty_form }}
100 | </div>
101 | </div>
102 |
103 |
104 |
105 |
106 |
107 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/demo/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/demo/static/css/demo.css:
--------------------------------------------------------------------------------
1 | /* Superformset demo styles */
2 |
3 | @font-face {
4 | font-family: "superformset-demo";
5 | src: url('../fonts/icons.woff') format("woff"), url('../fonts/icons.ttf') format("truetype");
6 | }
7 |
8 | #css-input:checked ~ form .add-row {
9 | font-size: 0.75rem;
10 | line-height: 1.3125rem;
11 | margin-top: -0.32813rem;
12 | float: right;
13 | margin-right: 1.64063rem;
14 | }
15 | #css-input:checked ~ form .add-row:before {
16 | vertical-align: middle;
17 | content: "\e600";
18 | font-family: "superformset-demo";
19 | display: inline-block;
20 | }
21 | #css-input:checked ~ form input[type="text"] {
22 | width: 80%;
23 | }
24 | #css-input:checked ~ form .deleted {
25 | display: none;
26 | }
27 |
28 | /* Demo page styles */
29 |
30 | * {
31 | box-sizing: border-box;
32 | }
33 |
34 | html {
35 | font-family: "Franklin Gothic Medium", "Franklin Gothic", "ITC Franklin Gothic", Arial, sans-serif;
36 | }
37 |
38 | .demo {
39 | min-width: 34em;
40 | max-width: 52em;
41 | margin: 1em auto;
42 | padding: 1em;
43 | }
44 |
45 | .demo-content {
46 | text-align: right;
47 | }
48 |
49 | form {
50 | clear: both;
51 | width: 100%;
52 | margin-bottom: 1em;
53 | text-align: left;
54 | }
55 |
56 | .init {
57 | float: left;
58 | margin-bottom: 1em;
59 | padding: 1em 2em;
60 | border: none;
61 | border-radius: .25em;
62 | background: pink;
63 | font: inherit;
64 | font-size: 1.25em;
65 | text-transform: uppercase;
66 | color: white;
67 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
68 | transition: all 150ms;
69 | }
70 |
71 | .init:focus {
72 | outline: none;
73 | box-shadow: 0 0 1px 2px rgba(0, 0, 0, 0.1) inset;
74 | }
75 |
76 | .init:hover {
77 | cursor: pointer;
78 | background: #fd9297;
79 | }
80 |
81 | .init:active {
82 | background: #bc6d71;
83 | box-shadow: 0 0 1px 2px rgba(0, 0, 0, 0.25) inset;
84 | }
85 |
86 | label[for="css-input"] {
87 | display: inline-block;
88 | padding-top: 3em;
89 | }
90 |
91 | pre {
92 | overflow: auto;
93 | font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace, serif;
94 | font-size: .85em;
95 | line-height: 1.5em;
96 | color: grey;
97 | }
98 |
99 | .code-samples .js {
100 | clear: both;
101 | margin-bottom: 1em;
102 | }
103 |
104 | .code-samples .html {
105 | clear: both;
106 | border-top: 1px dashed;
107 | border-color: grey;
108 | }
109 |
--------------------------------------------------------------------------------
/demo/static/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgerigmeyer/jquery-django-superformset/07c2a2c018c73c973afac5313c0ec50888eeaa32/demo/static/fonts/icons.ttf
--------------------------------------------------------------------------------
/demo/static/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgerigmeyer/jquery-django-superformset/07c2a2c018c73c973afac5313c0ec50888eeaa32/demo/static/fonts/icons.woff
--------------------------------------------------------------------------------
/demo/static/js/django-superformset.min.js:
--------------------------------------------------------------------------------
1 | /*! Django Superformset - v1.0.4 - 2014-06-13
2 | * https://github.com/jgerigmeyer/jquery-django-superformset
3 | * Based on jQuery Formset 1.1r14, by Stanislaus Madueke
4 | * Original Portions Copyright (c) 2009 Stanislaus Madueke
5 | * Modifications Copyright (c) 2014 Jonny Gerig Meyer; Licensed BSDv3 */
6 | !function(a){"use strict";var b={init:function(c){var d={},e=d.opts=a.extend({},a.fn.superformset.defaults,c),f=d.wrapper=a(this),g=d.rows=f.find(e.rowSel),h=d.container=g.closest(e.containerSel);d.totalForms=f.find('input[id$="'+e.prefix+'-TOTAL_FORMS"]'),d.maxForms=f.find('input[id$="'+e.prefix+'-MAX_NUM_FORMS"]');var i=d.tpl=h.find(e.formTemplate).clone(!0);return h.find(e.formTemplate).find("[required], .required").removeAttr("required").removeData("required-by").addClass("required"),i.removeAttr("id").find("input, select, textarea").filter("[required]").addClass("required").removeAttr("required"),b.addDeleteTrigger(i,e.canDelete,d),b.addInsertAboveTrigger(i,d),g.each(function(){var c=a(this);b.addDeleteTrigger(c,e.canDelete&&!e.deleteOnlyNew,d),b.addInsertAboveTrigger(c,d),b.watchForChangesToOptionalIfEmptyRow(c,d)}),e.autoAdd||b.activateAddTrigger(d),e.alwaysShowExtra&&e.autoAdd&&(b.autoAddRow(d),f.closest("form").submit(function(){a(this).find(e.rowSel).filter(".extra-row").find("input, select, textarea").each(function(){a(this).removeAttr("name")})})),f},activateAddTrigger:function(c){var d,e=c.opts;d=c.addButton=c.wrapper.find(e.addTriggerSel).length?c.wrapper.find(e.addTriggerSel):a(e.addTrigger).appendTo(c.wrapper),b.showAddButton(c)||d.hide(),d.click(function(d){var f=a(this),g=parseInt(c.totalForms.val(),10),h=c.tpl.clone(!0).addClass("new-row"),i=c.wrapper.find(e.rowSel).last();h.find("input, select, textarea").filter(".required").attr("required","required"),e.addAnimationSpeed?(i.length?h.hide().insertAfter(i):h.hide().insertBefore(f),h.animate({height:"toggle",opacity:"toggle"},e.addAnimationSpeed)):i.length?h.insertAfter(i).show():h.insertBefore(f).show(),h.find("input, select, textarea, label").each(function(){b.updateElementIndex(a(this),e.prefix,g)}),b.watchForChangesToOptionalIfEmptyRow(h,c),c.totalForms.val(g+1),b.showAddButton(c)||f.hide(),e.addedCallback&&e.addedCallback(h),d.preventDefault()})},watchForChangesToOptionalIfEmptyRow:function(a,c){var d=c.opts;if(d.optionalIfEmpty&&a.is(d.optionalIfEmptySel)){var e=a.find("input, select, textarea");e.filter("[required], .required").removeAttr("required").data("required-by",d.prefix).addClass("required"),a.data("original-vals",e.serialize()),e.not(d.deleteTriggerSel).change(function(){b.updateRequiredFields(a,c)})}},updateElementIndex:function(a,b,c){var d=new RegExp("("+b+"-(\\d+|__prefix__))"),e=b+"-"+c;a.attr("for")&&a.attr("for",a.attr("for").replace(d,e)),a.attr("id")&&a.attr("id",a.attr("id").replace(d,e)),a.attr("name")&&a.attr("name",a.attr("name").replace(d,e))},showAddButton:function(a){return""===a.maxForms.val()||a.maxForms.val()-a.totalForms.val()>0},addDeleteTrigger:function(c,d,e){var f=e.opts;d?a(f.deleteTrigger).appendTo(c).click(function(c){var d,g,h=a(this).closest(f.rowSel),i=function(c,d){c.eq(d).find("input, select, textarea, label").each(function(){b.updateElementIndex(a(this),f.prefix,d)})},j=function(){for(h.remove(),d=e.wrapper.find(f.rowSel),e.totalForms.val(d.not(".extra-row").length),g=0;gremove',deleteTriggerSel:".remove-row",addTrigger:'add',addTriggerSel:null,addedCallback:null,removedCallback:null,deletedRowClass:"deleted",addAnimationSpeed:"normal",removeAnimationSpeed:"fast",autoAdd:!1,alwaysShowExtra:!1,deleteOnlyActive:!1,canDelete:!1,deleteOnlyNew:!1,insertAbove:!1,insertAboveTrigger:'insert',optionalIfEmpty:!0,optionalIfEmptySel:'[data-empty-permitted="true"]'}}(jQuery);
--------------------------------------------------------------------------------
/demo/templates/index.html:
--------------------------------------------------------------------------------
1 | {% load staticfiles %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Demo | jQuery Django Superformset
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
JavaScript:
55 |
56 | jQuery(function($) {
57 | $('.init').one('click', function () {
58 | $('.formlist').superformset({
59 | containerSel: '.demo',
60 | deleteTriggerSel: 'input[name$="-DELETE"]'
61 | });
62 | });
63 | });
64 |
65 |
66 |
67 |
Django Template:
68 |
69 | {% verbatim %}
70 | <form method="post">
71 | {% csrf_token %}
72 | <fieldset class="formlist">
73 | {{ formset.management_form }}
74 | {% for form in formset %}
75 | <div class="dynamic-form">
76 | {{ form }}
77 | </div>
78 | {% endfor %}
79 | </fieldset>
80 | <button type="submit">Save</button>
81 | </form>
82 |
83 | <div class="empty-form" style="display: none;">
84 | <div class="dynamic-form">
85 | {{ formset.empty_form }}
86 | </div>
87 | </div>
88 | {% endverbatim %}
89 |
90 |
91 |
92 |
93 |
94 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/dist/django-superformset.js:
--------------------------------------------------------------------------------
1 | /*! Django Superformset - v1.0.4 - 2014-06-13
2 | * https://github.com/jgerigmeyer/jquery-django-superformset
3 | * Based on jQuery Formset 1.1r14, by Stanislaus Madueke
4 | * Original Portions Copyright (c) 2009 Stanislaus Madueke
5 | * Modifications Copyright (c) 2014 Jonny Gerig Meyer; Licensed BSDv3 */
6 | (function ($) {
7 |
8 | 'use strict';
9 |
10 | var methods = {
11 | init: function (options) {
12 | var vars = {};
13 | var opts = vars.opts = $.extend({}, $.fn.superformset.defaults, options);
14 | var wrapper = vars.wrapper = $(this);
15 | var rows = vars.rows = wrapper.find(opts.rowSel);
16 | var container = vars.container = rows.closest(opts.containerSel);
17 | vars.totalForms = wrapper
18 | .find('input[id$="' + opts.prefix + '-TOTAL_FORMS"]');
19 | vars.maxForms = wrapper
20 | .find('input[id$="' + opts.prefix + '-MAX_NUM_FORMS"]');
21 |
22 | // Clone the form template to generate new form instances
23 | var tpl = vars.tpl = container.find(opts.formTemplate).clone(true);
24 | container.find(opts.formTemplate).find('[required], .required')
25 | .removeAttr('required').removeData('required-by').addClass('required');
26 | tpl.removeAttr('id').find('input, select, textarea').filter('[required]')
27 | .addClass('required').removeAttr('required');
28 |
29 | // Add delete-trigger and insert-above-trigger (if applicable) to template
30 | methods.addDeleteTrigger(tpl, opts.canDelete, vars);
31 | methods.addInsertAboveTrigger(tpl, vars);
32 |
33 | // Iterate over existing rows...
34 | rows.each(function () {
35 | var thisRow = $(this);
36 | // Add delete-trigger and insert-above-trigger to existing rows
37 | methods.addDeleteTrigger(
38 | thisRow,
39 | (opts.canDelete && !opts.deleteOnlyNew),
40 | vars
41 | );
42 | methods.addInsertAboveTrigger(thisRow, vars);
43 | // Attaches handlers watching for changes to inputs,
44 | // ...to add/remove ``required`` attr
45 | methods.watchForChangesToOptionalIfEmptyRow(thisRow, vars);
46 | });
47 |
48 | // Unless using auto-added rows, add and/or activate trigger to add rows
49 | if (!opts.autoAdd) {
50 | methods.activateAddTrigger(vars);
51 | }
52 |
53 | // Add extra empty row, if applicable
54 | if (opts.alwaysShowExtra && opts.autoAdd) {
55 | methods.autoAddRow(vars);
56 | wrapper.closest('form').submit(function () {
57 | $(this).find(opts.rowSel).filter('.extra-row')
58 | .find('input, select, textarea').each(function () {
59 | $(this).removeAttr('name');
60 | }
61 | );
62 | });
63 | }
64 |
65 | return wrapper;
66 | },
67 |
68 | activateAddTrigger: function (vars) {
69 | var opts = vars.opts;
70 | var addButton;
71 | if (vars.wrapper.find(opts.addTriggerSel).length) {
72 | addButton = vars.addButton = vars.wrapper.find(opts.addTriggerSel);
73 | } else {
74 | addButton = vars.addButton = $(opts.addTrigger).appendTo(vars.wrapper);
75 | }
76 | // Hide the add-trigger if we've reach the maxForms limit
77 | if (!methods.showAddButton(vars)) { addButton.hide(); }
78 | addButton.click(function (e) {
79 | var trigger = $(this);
80 | var formCount = parseInt(vars.totalForms.val(), 10);
81 | var newRow = vars.tpl.clone(true).addClass('new-row');
82 | var lastRow = vars.wrapper.find(opts.rowSel).last();
83 | newRow.find('input, select, textarea').filter('.required')
84 | .attr('required', 'required');
85 | if (opts.addAnimationSpeed) {
86 | if (lastRow.length) {
87 | newRow.hide().insertAfter(lastRow);
88 | } else {
89 | newRow.hide().insertBefore(trigger);
90 | }
91 | newRow.animate(
92 | {'height': 'toggle', 'opacity': 'toggle'},
93 | opts.addAnimationSpeed
94 | );
95 | } else {
96 | if (lastRow.length) {
97 | newRow.insertAfter(lastRow).show();
98 | } else {
99 | newRow.insertBefore(trigger).show();
100 | }
101 | }
102 | newRow.find('input, select, textarea, label').each(function () {
103 | methods.updateElementIndex($(this), opts.prefix, formCount);
104 | });
105 | // Attaches handlers watching for changes to inputs,
106 | // ...to add/remove ``required`` attr
107 | methods.watchForChangesToOptionalIfEmptyRow(newRow, vars);
108 | vars.totalForms.val(formCount + 1);
109 | // Check if we've exceeded the maximum allowed number of forms:
110 | if (!methods.showAddButton(vars)) { trigger.hide(); }
111 | // If a post-add callback was supplied, call it with the added form:
112 | if (opts.addedCallback) { opts.addedCallback(newRow); }
113 | e.preventDefault();
114 | });
115 | },
116 |
117 | // Attaches handlers watching for changes to inputs,
118 | // ...to add/remove ``required`` attr
119 | watchForChangesToOptionalIfEmptyRow: function (row, vars) {
120 | var opts = vars.opts;
121 | if (opts.optionalIfEmpty && row.is(opts.optionalIfEmptySel)) {
122 | var inputs = row.find('input, select, textarea');
123 | inputs.filter('[required], .required').removeAttr('required')
124 | .data('required-by', opts.prefix).addClass('required');
125 | row.data('original-vals', inputs.serialize());
126 | inputs.not(opts.deleteTriggerSel).change(function () {
127 | methods.updateRequiredFields(row, vars);
128 | });
129 | }
130 | },
131 |
132 | // Replace ``-__prefix__`` with correct index in for, id, name attrs
133 | updateElementIndex: function (elem, prefix, ndx) {
134 | var idRegex = new RegExp('(' + prefix + '-(\\d+|__prefix__))');
135 | var replacement = prefix + '-' + ndx;
136 | if (elem.attr('for')) {
137 | elem.attr('for', elem.attr('for').replace(idRegex, replacement));
138 | }
139 | if (elem.attr('id')) {
140 | elem.attr('id', elem.attr('id').replace(idRegex, replacement));
141 | }
142 | if (elem.attr('name')) {
143 | elem.attr('name', elem.attr('name').replace(idRegex, replacement));
144 | }
145 | },
146 |
147 | // Check whether we can add more rows
148 | showAddButton: function (vars) {
149 | return (
150 | vars.maxForms.val() === '' ||
151 | (vars.maxForms.val() - vars.totalForms.val() > 0)
152 | );
153 | },
154 |
155 | // Add delete trigger to end of a row, or activate existing delete-trigger
156 | addDeleteTrigger: function (row, canDelete, vars) {
157 | var opts = vars.opts;
158 | if (canDelete) {
159 | // Add a delete-trigger to remove the row from the DOM
160 | $(opts.deleteTrigger).appendTo(row).click(function (e) {
161 | var thisRow = $(this).closest(opts.rowSel);
162 | var rows, i;
163 | var updateSequence = function (rows, i) {
164 | rows.eq(i).find('input, select, textarea, label').each(function () {
165 | methods.updateElementIndex($(this), opts.prefix, i);
166 | });
167 | };
168 | var removeRow = function () {
169 | thisRow.remove();
170 | // Update the TOTAL_FORMS count:
171 | rows = vars.wrapper.find(opts.rowSel);
172 | vars.totalForms.val(rows.not('.extra-row').length);
173 | // Update names and IDs for all child controls,
174 | // ...so they remain in sequence.
175 | for (i = 0; i < rows.length; i = i + 1) {
176 | updateSequence(rows, i);
177 | }
178 | // If a post-delete callback was provided, call it with deleted form
179 | if (opts.removedCallback) { opts.removedCallback(thisRow); }
180 | };
181 | if (opts.removeAnimationSpeed) {
182 | $.when(
183 | thisRow.animate(
184 | {'height': 'toggle', 'opacity': 'toggle'},
185 | opts.removeAnimationSpeed
186 | )
187 | ).done(removeRow);
188 | } else {
189 | removeRow();
190 | }
191 | e.preventDefault();
192 | });
193 | } else {
194 | // If we're dealing with an inline formset,
195 | // ...just remove :required attrs when marking a row deleted
196 | row.find(opts.deleteTriggerSel).change(function () {
197 | var trigger = $(this);
198 | var thisRow = trigger.closest(opts.rowSel);
199 | if (trigger.prop('checked')) {
200 | thisRow.addClass(opts.deletedRowClass);
201 | thisRow.find('[required]').removeAttr('required')
202 | .addClass('deleted-required');
203 | // If a post-delete callback was provided, call it with deleted form
204 | if (opts.removedCallback) { opts.removedCallback(thisRow); }
205 | } else {
206 | thisRow.removeClass(opts.deletedRowClass);
207 | thisRow.find('.deleted-required').attr('required', 'required')
208 | .removeClass('deleted-required');
209 | }
210 | });
211 | }
212 | },
213 |
214 | // Add insert-above trigger before a row, if ``insertAboveTrigger: true``
215 | addInsertAboveTrigger: function (row, vars) {
216 | var opts = vars.opts;
217 | if (opts.insertAbove) {
218 | $(opts.insertAboveTrigger).prependTo(row).click(function (e) {
219 | var thisRow = $(this).closest(opts.rowSel);
220 | var formCount = parseInt(vars.totalForms.val(), 10);
221 | var newRow = vars.tpl.clone(true).addClass('new-row');
222 | var rows, i;
223 | var updateSequence = function (rows, i) {
224 | rows.eq(i).find('input, select, textarea, label').each(function () {
225 | methods.updateElementIndex($(this), opts.prefix, i);
226 | });
227 | };
228 | newRow.find('input, select, textarea').filter('.required')
229 | .attr('required', 'required');
230 | if (opts.addAnimationSpeed) {
231 | newRow.hide().insertBefore(thisRow).animate(
232 | {'height': 'toggle', 'opacity': 'toggle'}, opts.addAnimationSpeed
233 | );
234 | } else {
235 | newRow.insertBefore(thisRow).show();
236 | }
237 | // Update the TOTAL_FORMS count:
238 | rows = vars.wrapper.find(opts.rowSel);
239 | vars.totalForms.val(formCount + 1);
240 | // Update names and IDs for child controls so they remain in sequence.
241 | for (i = 0; i < rows.length; i = i + 1) {
242 | updateSequence(rows, i);
243 | }
244 | // Attaches handlers watching for changes to inputs,
245 | // ...to add/remove ``required`` attr
246 | methods.watchForChangesToOptionalIfEmptyRow(newRow, vars);
247 | // Check if we've exceeded the maximum allowed number of rows:
248 | if (!methods.showAddButton(vars)) { $(this).hide(); }
249 | // If a post-add callback was supplied, call it with the added form:
250 | if (opts.addedCallback) { opts.addedCallback(newRow); }
251 | $(this).blur();
252 | e.preventDefault();
253 | });
254 | }
255 | },
256 |
257 | // Add a row automatically
258 | autoAddRow: function (vars) {
259 | var opts = vars.opts;
260 | var formCount = parseInt(vars.totalForms.val(), 10);
261 | var newRow = vars.tpl.clone(true);
262 | var rows = vars.wrapper.find(opts.rowSel);
263 | if (opts.addAnimationSpeed) {
264 | newRow.hide().css('opacity', 0).insertAfter(rows.last())
265 | .addClass('extra-row').animate(
266 | {'height': 'toggle', 'opacity': '0.5'},
267 | opts.addAnimationSpeed
268 | );
269 | } else {
270 | newRow.css('opacity', 0.5).insertAfter(rows.last())
271 | .addClass('extra-row');
272 | }
273 | // When the extra-row receives focus...
274 | newRow.find('input, select, textarea, label').one('focus', function () {
275 | var el = $(this);
276 | var thisRow = el.closest(opts.rowSel);
277 | // fade it in
278 | thisRow.removeClass('extra-row').css('opacity', 1);
279 | // add "required" to appropriate inputs if not an "optionalIfEmpty" row
280 | if (
281 | el.hasClass('required') &&
282 | !(opts.optionalIfEmpty && newRow.is(opts.optionalIfEmptySel))
283 | ) {
284 | el.attr('required', 'required');
285 | }
286 | // update the totalForms count
287 | vars.totalForms.val(
288 | vars.wrapper.find(opts.rowSel).not('.extra-row').length
289 | );
290 | // fade in the delete-trigger
291 | if (opts.deleteOnlyActive) {
292 | thisRow.find(opts.deleteTriggerSel).fadeIn();
293 | }
294 | // and auto-add another extra-row
295 | if (
296 | methods.showAddButton(vars) &&
297 | thisRow.is(vars.wrapper.find(opts.rowSel).last())
298 | ) {
299 | methods.autoAddRow(vars);
300 | }
301 | }).each(function () {
302 | var el = $(this);
303 | methods.updateElementIndex(el, opts.prefix, formCount);
304 | el.filter('[required]').removeAttr('required').addClass('required');
305 | });
306 | // Attaches handlers watching for changes to inputs,
307 | // ...to add/remove ``required`` attr
308 | methods.watchForChangesToOptionalIfEmptyRow(newRow, vars);
309 | // Hide the delete-trigger initially, if ``deleteOnlyActive: true``
310 | if (opts.deleteOnlyActive) {
311 | newRow.find(opts.deleteTriggerSel).hide();
312 | }
313 | // If a post-add callback was supplied, call it with the added form
314 | if (opts.addedCallback) { opts.addedCallback(newRow); }
315 | },
316 |
317 | // Check if inputs have changed from original state,
318 | // ...and update ``required`` attr accordingly
319 | updateRequiredFields: function (row, vars) {
320 | var opts = vars.opts;
321 | var inputs = row.find('input, select, textarea');
322 | var relevantInputs = inputs.filter(function () {
323 | return $(this).data('required-by') === opts.prefix;
324 | });
325 | var state = inputs.serialize();
326 | var originalState = row.data('original-vals');
327 | if (state === originalState) {
328 | relevantInputs.removeAttr('required');
329 | } else {
330 | relevantInputs.filter('.required').not('.deleted-required')
331 | .attr('required', 'required');
332 | }
333 | },
334 |
335 | // Expose internal methods to allow stubbing in tests
336 | exposeMethods: function () {
337 | return methods;
338 | }
339 | };
340 |
341 | $.fn.superformset = function (method) {
342 | if (methods[method]) {
343 | return methods[method].apply(
344 | this,
345 | Array.prototype.slice.call(arguments, 1)
346 | );
347 | } else if (typeof method === 'object' || !method) {
348 | return methods.init.apply(this, arguments);
349 | } else {
350 | $.error('Method ' + method + ' does not exist on jQuery.superformset');
351 | }
352 | };
353 |
354 | /* Setup plugin defaults */
355 | $.fn.superformset.defaults = {
356 | prefix: 'form', // The form prefix for your django formset
357 | containerSel: 'form', // Container selector
358 | // ...(must contain rows and formTemplate)
359 | rowSel: '.dynamic-form', // Selector to match each row in a formset
360 | formTemplate: '.empty-form .dynamic-form',
361 | // Selector for empty form template to be
362 | // ...cloned to generate new form instances
363 | // ...Must be outside element on which formset
364 | // ...is called, but within containerSel
365 | deleteTrigger: 'remove',
366 | // The HTML "remove" link added to the end of
367 | // ...each form-row (if ``canDelete: true``)
368 | deleteTriggerSel: '.remove-row',
369 | // Selector for HTML "remove" links
370 | // ...Used to target existing delete-trigger,
371 | // ...or to target ``deleteTrigger``
372 | addTrigger: 'add',
373 | // The HTML "add" link added to the end of all
374 | // ...forms if no ``addTriggerSel``
375 | addTriggerSel: null, // Selector for trigger to add a new row
376 | // ...Used to target existing trigger
377 | // ...if provided, ``addTrigger`` is ignored
378 | addedCallback: null, // Fn called each time a new form row is added
379 | removedCallback: null, // Fn called each time a form row is deleted
380 | deletedRowClass: 'deleted', // Add to deleted row if ``canDelete: false``
381 | addAnimationSpeed: 'normal', // Speed (ms) to animate adding rows
382 | // ...If false, new rows appear w/o animation
383 | removeAnimationSpeed: 'fast', // Speed (ms) to animate removing rows
384 | // ...If false, new rows disappear w/o anim.
385 | autoAdd: false, // If true, the "add" link will be removed,
386 | // ...and a row will be automatically added
387 | // ...when text is entered in the final
388 | // ...textarea of the last row
389 | alwaysShowExtra: false, // If true, an extra (empty) row will always
390 | // ...be displayed (req. ``autoAdd: true``)
391 | deleteOnlyActive: false, // If true, extra empty rows cannot be removed
392 | // ...until they acquire focus
393 | // ...(requires ``alwaysShowExtra: true``)
394 | canDelete: false, // If false, rows cannot be removed from DOM.
395 | // ...``deleteTriggerSel`` will remove
396 | // ...``required`` attr from fields within a
397 | // ..."deleted" row.
398 | // ...deleted rows should be hidden via CSS
399 | deleteOnlyNew: false, // If true, only newly-added rows can be
400 | // ...deleted (requires ``canDelete: true``)
401 | insertAbove: false, // If true, ``insertAboveTrigger`` will be
402 | // ...added to the end of each form-row
403 | insertAboveTrigger:
404 | 'insert',
405 | // The HTML "insert" link add to the end of
406 | // ...each row (req. ``insertAbove: true``)
407 | optionalIfEmpty: true, // If true, required fields in a row will be
408 | // ...optional until changed from initial vals
409 | optionalIfEmptySel: '[data-empty-permitted="true"]'
410 | // Selector for rows to apply optionalIfEmpty
411 | // ...logic (req. ``optionalIfEmpty: true``)
412 | };
413 | }(jQuery));
414 |
--------------------------------------------------------------------------------
/dist/django-superformset.min.js:
--------------------------------------------------------------------------------
1 | /*! Django Superformset - v1.0.4 - 2014-06-13
2 | * https://github.com/jgerigmeyer/jquery-django-superformset
3 | * Based on jQuery Formset 1.1r14, by Stanislaus Madueke
4 | * Original Portions Copyright (c) 2009 Stanislaus Madueke
5 | * Modifications Copyright (c) 2014 Jonny Gerig Meyer; Licensed BSDv3 */
6 | !function(a){"use strict";var b={init:function(c){var d={},e=d.opts=a.extend({},a.fn.superformset.defaults,c),f=d.wrapper=a(this),g=d.rows=f.find(e.rowSel),h=d.container=g.closest(e.containerSel);d.totalForms=f.find('input[id$="'+e.prefix+'-TOTAL_FORMS"]'),d.maxForms=f.find('input[id$="'+e.prefix+'-MAX_NUM_FORMS"]');var i=d.tpl=h.find(e.formTemplate).clone(!0);return h.find(e.formTemplate).find("[required], .required").removeAttr("required").removeData("required-by").addClass("required"),i.removeAttr("id").find("input, select, textarea").filter("[required]").addClass("required").removeAttr("required"),b.addDeleteTrigger(i,e.canDelete,d),b.addInsertAboveTrigger(i,d),g.each(function(){var c=a(this);b.addDeleteTrigger(c,e.canDelete&&!e.deleteOnlyNew,d),b.addInsertAboveTrigger(c,d),b.watchForChangesToOptionalIfEmptyRow(c,d)}),e.autoAdd||b.activateAddTrigger(d),e.alwaysShowExtra&&e.autoAdd&&(b.autoAddRow(d),f.closest("form").submit(function(){a(this).find(e.rowSel).filter(".extra-row").find("input, select, textarea").each(function(){a(this).removeAttr("name")})})),f},activateAddTrigger:function(c){var d,e=c.opts;d=c.addButton=c.wrapper.find(e.addTriggerSel).length?c.wrapper.find(e.addTriggerSel):a(e.addTrigger).appendTo(c.wrapper),b.showAddButton(c)||d.hide(),d.click(function(d){var f=a(this),g=parseInt(c.totalForms.val(),10),h=c.tpl.clone(!0).addClass("new-row"),i=c.wrapper.find(e.rowSel).last();h.find("input, select, textarea").filter(".required").attr("required","required"),e.addAnimationSpeed?(i.length?h.hide().insertAfter(i):h.hide().insertBefore(f),h.animate({height:"toggle",opacity:"toggle"},e.addAnimationSpeed)):i.length?h.insertAfter(i).show():h.insertBefore(f).show(),h.find("input, select, textarea, label").each(function(){b.updateElementIndex(a(this),e.prefix,g)}),b.watchForChangesToOptionalIfEmptyRow(h,c),c.totalForms.val(g+1),b.showAddButton(c)||f.hide(),e.addedCallback&&e.addedCallback(h),d.preventDefault()})},watchForChangesToOptionalIfEmptyRow:function(a,c){var d=c.opts;if(d.optionalIfEmpty&&a.is(d.optionalIfEmptySel)){var e=a.find("input, select, textarea");e.filter("[required], .required").removeAttr("required").data("required-by",d.prefix).addClass("required"),a.data("original-vals",e.serialize()),e.not(d.deleteTriggerSel).change(function(){b.updateRequiredFields(a,c)})}},updateElementIndex:function(a,b,c){var d=new RegExp("("+b+"-(\\d+|__prefix__))"),e=b+"-"+c;a.attr("for")&&a.attr("for",a.attr("for").replace(d,e)),a.attr("id")&&a.attr("id",a.attr("id").replace(d,e)),a.attr("name")&&a.attr("name",a.attr("name").replace(d,e))},showAddButton:function(a){return""===a.maxForms.val()||a.maxForms.val()-a.totalForms.val()>0},addDeleteTrigger:function(c,d,e){var f=e.opts;d?a(f.deleteTrigger).appendTo(c).click(function(c){var d,g,h=a(this).closest(f.rowSel),i=function(c,d){c.eq(d).find("input, select, textarea, label").each(function(){b.updateElementIndex(a(this),f.prefix,d)})},j=function(){for(h.remove(),d=e.wrapper.find(f.rowSel),e.totalForms.val(d.not(".extra-row").length),g=0;gremove',deleteTriggerSel:".remove-row",addTrigger:'add',addTriggerSel:null,addedCallback:null,removedCallback:null,deletedRowClass:"deleted",addAnimationSpeed:"normal",removeAnimationSpeed:"fast",autoAdd:!1,alwaysShowExtra:!1,deleteOnlyActive:!1,canDelete:!1,deleteOnlyNew:!1,insertAbove:!1,insertAboveTrigger:'insert',optionalIfEmpty:!0,optionalIfEmptySel:'[data-empty-permitted="true"]'}}(jQuery);
--------------------------------------------------------------------------------
/django-superformset.jquery.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-superformset",
3 | "title": "Django Superformset",
4 | "description": "jQuery Django Dynamic Formset Plugin",
5 | "version": "1.0.4",
6 | "homepage": "https://github.com/jgerigmeyer/jquery-django-superformset",
7 | "author": {
8 | "name": "Jonny Gerig Meyer",
9 | "email": "jonny@oddbird.net",
10 | "url": "http://oddbird.net/"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git://github.com/jgerigmeyer/jquery-django-superformset.git"
15 | },
16 | "bugs": "https://github.com/jgerigmeyer/jquery-django-superformset/issues",
17 | "licenses": [
18 | {
19 | "type": "BSDv3",
20 | "url": "https://github.com/jgerigmeyer/jquery-django-superformset/blob/master/LICENSE-BSDv3"
21 | }
22 | ],
23 | "dependencies": {
24 | "jquery": "*"
25 | },
26 | "keywords": ["formset", "dynamic", "django"]
27 | }
28 |
--------------------------------------------------------------------------------
/libs/jquery-loader.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | // Default to the local version.
3 | var path = '../libs/jquery/jquery.js';
4 | // Get any jquery=___ param from the query string.
5 | var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/);
6 | // If a version was specified, use that version from code.jquery.com.
7 | if (jqversion) {
8 | path = 'http://code.jquery.com/jquery-' + jqversion[1] + '.js';
9 | }
10 | // This is the only time I'll ever use document.write, I promise!
11 | document.write('');
12 | }());
13 |
--------------------------------------------------------------------------------
/libs/qunit/qunit-assert-html.js:
--------------------------------------------------------------------------------
1 | /*! qunit-assert-html - v0.2.0 - 2013-03-04
2 | * https://github.com/JamesMGreene/qunit-assert-html
3 | * Copyright (c) 2013 James M. Greene; Licensed MIT */
4 | (function( QUnit, window, undefined ) {
5 | "use strict";
6 |
7 | var trim = function( s ) {
8 | if ( !s ) {
9 | return "";
10 | }
11 | return typeof s.trim === "function" ? s.trim() : s.replace( /^\s+|\s+$/g, "" );
12 | };
13 |
14 | var normalizeWhitespace = function( s ) {
15 | if ( !s ) {
16 | return "";
17 | }
18 | return trim( s.replace( /\s+/g, " " ) );
19 | };
20 |
21 | var dedupeFlatDict = function( dictToDedupe, parentDict ) {
22 | var key, val;
23 | if ( parentDict ) {
24 | for ( key in dictToDedupe ) {
25 | val = dictToDedupe[key];
26 | if ( val && ( val === parentDict[key] ) ) {
27 | delete dictToDedupe[key];
28 | }
29 | }
30 | }
31 | return dictToDedupe;
32 | };
33 |
34 | var objectKeys = Object.keys || (function() {
35 | var hasOwn = function( obj, propName ) {
36 | return Object.prototype.hasOwnProperty.call( obj, propName );
37 | };
38 | return function( obj ) {
39 | var keys = [],
40 | key;
41 | for ( key in obj ) {
42 | if ( hasOwn( obj, key ) ) {
43 | keys.push( key );
44 | }
45 | }
46 | return keys;
47 | };
48 | })();
49 |
50 | /**
51 | * Calculate based on `currentStyle`/`getComputedStyle` styles instead
52 | */
53 | var getElementStyles = (function() {
54 |
55 | // Memoized
56 | var camelCase = (function() {
57 | var camelCaseFn = (function() {
58 | // Matches dashed string for camelizing
59 | var rmsPrefix = /^-ms-/,
60 | msPrefixFix = "ms-",
61 | rdashAlpha = /-([\da-z])/gi,
62 | camelCaseReplacerFn = function( all, letter ) {
63 | return ( letter + "" ).toUpperCase();
64 | };
65 |
66 | return function( s ) {
67 | return s.replace(rmsPrefix, msPrefixFix).replace(rdashAlpha, camelCaseReplacerFn);
68 | };
69 | })();
70 |
71 | var camelCaseMemoizer = {};
72 |
73 | return function( s ) {
74 | var temp = camelCaseMemoizer[s];
75 | if ( temp ) {
76 | return temp;
77 | }
78 |
79 | temp = camelCaseFn( s );
80 | camelCaseMemoizer[s] = temp;
81 | return temp;
82 | };
83 | })();
84 |
85 | var styleKeySortingFn = function( a, b ) {
86 | return camelCase( a ) < camelCase( b );
87 | };
88 |
89 | return function( elem ) {
90 | var styleCount, i, key,
91 | styles = {},
92 | styleKeys = [],
93 | style = elem.ownerDocument.defaultView ?
94 | elem.ownerDocument.defaultView.getComputedStyle( elem, null ) :
95 | elem.currentStyle;
96 |
97 | // `getComputedStyle`
98 | if ( style && style.length && style[0] && style[style[0]] ) {
99 | styleCount = style.length;
100 | while ( styleCount-- ) {
101 | styleKeys.push( style[styleCount] );
102 | }
103 | styleKeys.sort( styleKeySortingFn );
104 |
105 | for ( i = 0, styleCount = styleKeys.length ; i < styleCount ; i++ ) {
106 | key = styleKeys[i];
107 | if ( key !== "cssText" && typeof style[key] === "string" && style[key] ) {
108 | styles[camelCase( key )] = style[key];
109 | }
110 | }
111 | }
112 | // `currentStyle` support: IE < 9.0, Opera < 10.6
113 | else {
114 | for ( key in style ) {
115 | styleKeys.push( key );
116 | }
117 | styleKeys.sort();
118 |
119 | for ( i = 0, styleCount = styleKeys.length ; i < styleCount ; i++ ) {
120 | key = styleKeys[i];
121 | if ( key !== "cssText" && typeof style[key] === "string" && style[key] ) {
122 | styles[key] = style[key];
123 | }
124 | }
125 | }
126 |
127 | return styles;
128 |
129 | };
130 | })();
131 |
132 | var serializeElementNode = function( elementNode, rootNodeStyles ) {
133 | var subNodes, i, len, styles, attrName,
134 | serializedNode = {
135 | NodeType: elementNode.nodeType,
136 | NodeName: elementNode.nodeName.toLowerCase(),
137 | Attributes: {},
138 | ChildNodes: []
139 | };
140 |
141 | subNodes = elementNode.attributes;
142 | for ( i = 0, len = subNodes.length ; i < len ; i++ ) {
143 | attrName = subNodes[i].name.toLowerCase();
144 | if ( attrName === "class" ) {
145 | serializedNode.Attributes[attrName] = normalizeWhitespace( subNodes[i].value );
146 | }
147 | else if ( attrName !== "style" ) {
148 | serializedNode.Attributes[attrName] = subNodes[i].value;
149 | }
150 | // Ignore the "style" attribute completely
151 | }
152 |
153 | // Only add the style attribute if there is 1+ pertinent rules
154 | styles = dedupeFlatDict( getElementStyles( elementNode ), rootNodeStyles );
155 | if ( styles && objectKeys( styles ).length ) {
156 | serializedNode.Attributes["style"] = styles;
157 | }
158 |
159 | subNodes = elementNode.childNodes;
160 | for ( i = 0, len = subNodes.length; i < len; i++ ) {
161 | serializedNode.ChildNodes.push( serializeNode( subNodes[i], rootNodeStyles ) );
162 | }
163 |
164 | return serializedNode;
165 | };
166 |
167 | var serializeNode = function( node, rootNodeStyles ) {
168 | var serializedNode;
169 |
170 | switch (node.nodeType) {
171 | case 1: // Node.ELEMENT_NODE
172 | serializedNode = serializeElementNode( node, rootNodeStyles );
173 | break;
174 | case 3: // Node.TEXT_NODE
175 | serializedNode = {
176 | NodeType: node.nodeType,
177 | NodeName: node.nodeName.toLowerCase(),
178 | NodeValue: node.nodeValue
179 | };
180 | break;
181 | case 4: // Node.CDATA_SECTION_NODE
182 | case 7: // Node.PROCESSING_INSTRUCTION_NODE
183 | case 8: // Node.COMMENT_NODE
184 | serializedNode = {
185 | NodeType: node.nodeType,
186 | NodeName: node.nodeName.toLowerCase(),
187 | NodeValue: trim( node.nodeValue )
188 | };
189 | break;
190 | case 5: // Node.ENTITY_REFERENCE_NODE
191 | case 6: // Node.ENTITY_NODE
192 | case 9: // Node.DOCUMENT_NODE
193 | case 10: // Node.DOCUMENT_TYPE_NODE
194 | case 11: // Node.DOCUMENT_FRAGMENT_NODE
195 | case 12: // Node.NOTATION_NODE
196 | serializedNode = {
197 | NodeType: node.nodeType,
198 | NodeName: node.nodeName
199 | };
200 | break;
201 | case 2: // Node.ATTRIBUTE_NODE
202 | throw new Error( "`node.nodeType` was `Node.ATTRIBUTE_NODE` (2), which is not supported by this method" );
203 | default:
204 | throw new Error( "`node.nodeType` was not recognized: " + node.nodeType );
205 | }
206 |
207 | return serializedNode;
208 | };
209 |
210 | var serializeHtml = function( html ) {
211 | var scratch = getCleanSlate(),
212 | rootNode = scratch.container(),
213 | rootNodeStyles = getElementStyles( rootNode ),
214 | serializedHtml = [],
215 | kids, i, len;
216 | rootNode.innerHTML = trim( html );
217 |
218 | kids = rootNode.childNodes;
219 | for ( i = 0, len = kids.length; i < len; i++ ) {
220 | serializedHtml.push( serializeNode( kids[i], rootNodeStyles ) );
221 | }
222 |
223 | scratch.reset();
224 |
225 | return serializedHtml;
226 | };
227 |
228 | var getCleanSlate = (function() {
229 | var containerElId = "qunit-html-addon-container",
230 | iframeReady = false,
231 | iframeLoaded = function() {
232 | iframeReady = true;
233 | },
234 | iframeReadied = function() {
235 | if (iframe.readyState === "complete" || iframe.readyState === 4) {
236 | iframeReady = true;
237 | }
238 | },
239 | iframeApi,
240 | iframe,
241 | iframeWin,
242 | iframeDoc;
243 |
244 | if ( !iframeApi ) {
245 |
246 | QUnit.begin(function() {
247 | // Initialize the background iframe!
248 | if ( !iframe || !iframeWin || !iframeDoc ) {
249 | iframe = window.document.createElement( "iframe" );
250 | QUnit.addEvent( iframe, "load", iframeLoaded );
251 | QUnit.addEvent( iframe, "readystatechange", iframeReadied );
252 | iframe.style.position = "absolute";
253 | iframe.style.top = iframe.style.left = "-1000px";
254 | iframe.height = iframe.width = 0;
255 |
256 | // `getComputedStyle` behaves inconsistently cross-browser when not attached to a live DOM
257 | window.document.body.appendChild( iframe );
258 |
259 | iframeWin = iframe.contentWindow ||
260 | iframe.window ||
261 | iframe.contentDocument && iframe.contentDocument.defaultView ||
262 | iframe.document && ( iframe.document.defaultView || iframe.document.window ) ||
263 | window.frames[( iframe.name || iframe.id )];
264 |
265 | iframeDoc = iframeWin && iframeWin.document ||
266 | iframe.contentDocument ||
267 | iframe.document;
268 |
269 | var iframeContents = [
270 | "",
271 | "",
272 | "",
273 | " QUnit HTML addon iframe",
274 | "",
275 | "",
276 | " ",
277 | " ",
280 | "",
281 | ""
282 | ].join( "\n" );
283 |
284 | iframeDoc.open();
285 | iframeDoc.write( iframeContents );
286 | iframeDoc.close();
287 |
288 | // Is ready?
289 | iframeReady = iframeReady || iframeWin.isReady;
290 | }
291 | });
292 |
293 | QUnit.done(function() {
294 | if ( iframe && iframe.ownerDocument ) {
295 | iframe.parentNode.removeChild( iframe );
296 | }
297 | iframe = iframeWin = iframeDoc = null;
298 | iframeReady = false;
299 | });
300 |
301 | var waitForIframeReady = function( maxTimeout ) {
302 | if ( !iframeReady ) {
303 | if ( !maxTimeout ) {
304 | maxTimeout = 2000; // 2 seconds MAX
305 | }
306 | var startTime = new Date();
307 | while ( !iframeReady && ( ( new Date() - startTime ) < maxTimeout ) ) {
308 | iframeReady = iframeReady || iframeWin.isReady;
309 | }
310 | }
311 | };
312 |
313 | iframeApi = {
314 | container: function() {
315 | waitForIframeReady();
316 | if ( iframeReady && iframeDoc ) {
317 | return iframeDoc.getElementById( containerElId );
318 | }
319 | return undefined;
320 | },
321 | reset: function() {
322 | var containerEl = iframeApi.container();
323 | if ( containerEl ) {
324 | containerEl.innerHTML = "";
325 | }
326 | }
327 | };
328 | }
329 |
330 | // Actual function signature for `getCleanState`
331 | return function() { return iframeApi; };
332 | })();
333 |
334 | QUnit.extend( QUnit.assert, {
335 |
336 | /**
337 | * Compare two snippets of HTML for equality after normalization.
338 | *
339 | * @example htmlEqual("Hello, QUnit! ", "Hello, QUnit!", "HTML should be equal");
340 | * @param {String} actual The actual HTML before normalization.
341 | * @param {String} expected The excepted HTML before normalization.
342 | * @param {String} [message] Optional message to display in the results.
343 | */
344 | htmlEqual: function( actual, expected, message ) {
345 | if ( !message ) {
346 | message = "HTML should be equal";
347 | }
348 |
349 | this.deepEqual( serializeHtml( actual ), serializeHtml( expected ), message );
350 | },
351 |
352 | /**
353 | * Compare two snippets of HTML for inequality after normalization.
354 | *
355 | * @example notHtmlEqual("Hello, QUnit!", "Hello, QUnit!", "HTML should not be equal");
356 | * @param {String} actual The actual HTML before normalization.
357 | * @param {String} expected The excepted HTML before normalization.
358 | * @param {String} [message] Optional message to display in the results.
359 | */
360 | notHtmlEqual: function( actual, expected, message ) {
361 | if ( !message ) {
362 | message = "HTML should not be equal";
363 | }
364 |
365 | this.notDeepEqual( serializeHtml( actual ), serializeHtml( expected ), message );
366 | },
367 |
368 | /**
369 | * @private
370 | * Normalize and serialize an HTML snippet. Primarily only exposed for unit testing purposes.
371 | *
372 | * @example _serializeHtml('Test');
373 | * @param {String} html The HTML snippet to normalize and serialize.
374 | * @returns {Object[]} The normalized and serialized form of the HTML snippet.
375 | */
376 | _serializeHtml: serializeHtml
377 |
378 | });
379 | })( QUnit, this );
380 |
--------------------------------------------------------------------------------
/libs/qunit/qunit.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * QUnit 1.14.0
3 | * http://qunitjs.com/
4 | *
5 | * Copyright 2013 jQuery Foundation and other contributors
6 | * Released under the MIT license
7 | * http://jquery.org/license
8 | *
9 | * Date: 2014-01-31T16:40Z
10 | */
11 |
12 | /** Font Family and Sizes */
13 |
14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
16 | }
17 |
18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
19 | #qunit-tests { font-size: smaller; }
20 |
21 |
22 | /** Resets */
23 |
24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
29 |
30 | /** Header */
31 |
32 | #qunit-header {
33 | padding: 0.5em 0 0.5em 1em;
34 |
35 | color: #8699A4;
36 | background-color: #0D3349;
37 |
38 | font-size: 1.5em;
39 | line-height: 1em;
40 | font-weight: 400;
41 |
42 | border-radius: 5px 5px 0 0;
43 | }
44 |
45 | #qunit-header a {
46 | text-decoration: none;
47 | color: #C2CCD1;
48 | }
49 |
50 | #qunit-header a:hover,
51 | #qunit-header a:focus {
52 | color: #FFF;
53 | }
54 |
55 | #qunit-testrunner-toolbar label {
56 | display: inline-block;
57 | padding: 0 0.5em 0 0.1em;
58 | }
59 |
60 | #qunit-banner {
61 | height: 5px;
62 | }
63 |
64 | #qunit-testrunner-toolbar {
65 | padding: 0.5em 0 0.5em 2em;
66 | color: #5E740B;
67 | background-color: #EEE;
68 | overflow: hidden;
69 | }
70 |
71 | #qunit-userAgent {
72 | padding: 0.5em 0 0.5em 2.5em;
73 | background-color: #2B81AF;
74 | color: #FFF;
75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
76 | }
77 |
78 | #qunit-modulefilter-container {
79 | float: right;
80 | }
81 |
82 | /** Tests: Pass/Fail */
83 |
84 | #qunit-tests {
85 | list-style-position: inside;
86 | }
87 |
88 | #qunit-tests li {
89 | padding: 0.4em 0.5em 0.4em 2.5em;
90 | border-bottom: 1px solid #FFF;
91 | list-style-position: inside;
92 | }
93 |
94 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
95 | display: none;
96 | }
97 |
98 | #qunit-tests li strong {
99 | cursor: pointer;
100 | }
101 |
102 | #qunit-tests li a {
103 | padding: 0.5em;
104 | color: #C2CCD1;
105 | text-decoration: none;
106 | }
107 | #qunit-tests li a:hover,
108 | #qunit-tests li a:focus {
109 | color: #000;
110 | }
111 |
112 | #qunit-tests li .runtime {
113 | float: right;
114 | font-size: smaller;
115 | }
116 |
117 | .qunit-assert-list {
118 | margin-top: 0.5em;
119 | padding: 0.5em;
120 |
121 | background-color: #FFF;
122 |
123 | border-radius: 5px;
124 | }
125 |
126 | .qunit-collapsed {
127 | display: none;
128 | }
129 |
130 | #qunit-tests table {
131 | border-collapse: collapse;
132 | margin-top: 0.2em;
133 | }
134 |
135 | #qunit-tests th {
136 | text-align: right;
137 | vertical-align: top;
138 | padding: 0 0.5em 0 0;
139 | }
140 |
141 | #qunit-tests td {
142 | vertical-align: top;
143 | }
144 |
145 | #qunit-tests pre {
146 | margin: 0;
147 | white-space: pre-wrap;
148 | word-wrap: break-word;
149 | }
150 |
151 | #qunit-tests del {
152 | background-color: #E0F2BE;
153 | color: #374E0C;
154 | text-decoration: none;
155 | }
156 |
157 | #qunit-tests ins {
158 | background-color: #FFCACA;
159 | color: #500;
160 | text-decoration: none;
161 | }
162 |
163 | /*** Test Counts */
164 |
165 | #qunit-tests b.counts { color: #000; }
166 | #qunit-tests b.passed { color: #5E740B; }
167 | #qunit-tests b.failed { color: #710909; }
168 |
169 | #qunit-tests li li {
170 | padding: 5px;
171 | background-color: #FFF;
172 | border-bottom: none;
173 | list-style-position: inside;
174 | }
175 |
176 | /*** Passing Styles */
177 |
178 | #qunit-tests li li.pass {
179 | color: #3C510C;
180 | background-color: #FFF;
181 | border-left: 10px solid #C6E746;
182 | }
183 |
184 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
185 | #qunit-tests .pass .test-name { color: #366097; }
186 |
187 | #qunit-tests .pass .test-actual,
188 | #qunit-tests .pass .test-expected { color: #999; }
189 |
190 | #qunit-banner.qunit-pass { background-color: #C6E746; }
191 |
192 | /*** Failing Styles */
193 |
194 | #qunit-tests li li.fail {
195 | color: #710909;
196 | background-color: #FFF;
197 | border-left: 10px solid #EE5757;
198 | white-space: pre;
199 | }
200 |
201 | #qunit-tests > li:last-child {
202 | border-radius: 0 0 5px 5px;
203 | }
204 |
205 | #qunit-tests .fail { color: #000; background-color: #EE5757; }
206 | #qunit-tests .fail .test-name,
207 | #qunit-tests .fail .module-name { color: #000; }
208 |
209 | #qunit-tests .fail .test-actual { color: #EE5757; }
210 | #qunit-tests .fail .test-expected { color: #008000; }
211 |
212 | #qunit-banner.qunit-fail { background-color: #EE5757; }
213 |
214 |
215 | /** Result */
216 |
217 | #qunit-testresult {
218 | padding: 0.5em 0.5em 0.5em 2.5em;
219 |
220 | color: #2B81AF;
221 | background-color: #D2E0E6;
222 |
223 | border-bottom: 1px solid #FFF;
224 | }
225 | #qunit-testresult .module-name {
226 | font-weight: 700;
227 | }
228 |
229 | /** Fixture */
230 |
231 | #qunit-fixture {
232 | position: absolute;
233 | top: -10000px;
234 | left: -10000px;
235 | width: 1000px;
236 | height: 1000px;
237 | }
238 |
--------------------------------------------------------------------------------
/libs/qunit/qunit.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * QUnit 1.14.0
3 | * http://qunitjs.com/
4 | *
5 | * Copyright 2013 jQuery Foundation and other contributors
6 | * Released under the MIT license
7 | * http://jquery.org/license
8 | *
9 | * Date: 2014-01-31T16:40Z
10 | */
11 |
12 | (function( window ) {
13 |
14 | var QUnit,
15 | assert,
16 | config,
17 | onErrorFnPrev,
18 | testId = 0,
19 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
20 | toString = Object.prototype.toString,
21 | hasOwn = Object.prototype.hasOwnProperty,
22 | // Keep a local reference to Date (GH-283)
23 | Date = window.Date,
24 | setTimeout = window.setTimeout,
25 | clearTimeout = window.clearTimeout,
26 | defined = {
27 | document: typeof window.document !== "undefined",
28 | setTimeout: typeof window.setTimeout !== "undefined",
29 | sessionStorage: (function() {
30 | var x = "qunit-test-string";
31 | try {
32 | sessionStorage.setItem( x, x );
33 | sessionStorage.removeItem( x );
34 | return true;
35 | } catch( e ) {
36 | return false;
37 | }
38 | }())
39 | },
40 | /**
41 | * Provides a normalized error string, correcting an issue
42 | * with IE 7 (and prior) where Error.prototype.toString is
43 | * not properly implemented
44 | *
45 | * Based on http://es5.github.com/#x15.11.4.4
46 | *
47 | * @param {String|Error} error
48 | * @return {String} error message
49 | */
50 | errorString = function( error ) {
51 | var name, message,
52 | errorString = error.toString();
53 | if ( errorString.substring( 0, 7 ) === "[object" ) {
54 | name = error.name ? error.name.toString() : "Error";
55 | message = error.message ? error.message.toString() : "";
56 | if ( name && message ) {
57 | return name + ": " + message;
58 | } else if ( name ) {
59 | return name;
60 | } else if ( message ) {
61 | return message;
62 | } else {
63 | return "Error";
64 | }
65 | } else {
66 | return errorString;
67 | }
68 | },
69 | /**
70 | * Makes a clone of an object using only Array or Object as base,
71 | * and copies over the own enumerable properties.
72 | *
73 | * @param {Object} obj
74 | * @return {Object} New object with only the own properties (recursively).
75 | */
76 | objectValues = function( obj ) {
77 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
78 | /*jshint newcap: false */
79 | var key, val,
80 | vals = QUnit.is( "array", obj ) ? [] : {};
81 | for ( key in obj ) {
82 | if ( hasOwn.call( obj, key ) ) {
83 | val = obj[key];
84 | vals[key] = val === Object(val) ? objectValues(val) : val;
85 | }
86 | }
87 | return vals;
88 | };
89 |
90 |
91 | // Root QUnit object.
92 | // `QUnit` initialized at top of scope
93 | QUnit = {
94 |
95 | // call on start of module test to prepend name to all tests
96 | module: function( name, testEnvironment ) {
97 | config.currentModule = name;
98 | config.currentModuleTestEnvironment = testEnvironment;
99 | config.modules[name] = true;
100 | },
101 |
102 | asyncTest: function( testName, expected, callback ) {
103 | if ( arguments.length === 2 ) {
104 | callback = expected;
105 | expected = null;
106 | }
107 |
108 | QUnit.test( testName, expected, callback, true );
109 | },
110 |
111 | test: function( testName, expected, callback, async ) {
112 | var test,
113 | nameHtml = "" + escapeText( testName ) + "";
114 |
115 | if ( arguments.length === 2 ) {
116 | callback = expected;
117 | expected = null;
118 | }
119 |
120 | if ( config.currentModule ) {
121 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml;
122 | }
123 |
124 | test = new Test({
125 | nameHtml: nameHtml,
126 | testName: testName,
127 | expected: expected,
128 | async: async,
129 | callback: callback,
130 | module: config.currentModule,
131 | moduleTestEnvironment: config.currentModuleTestEnvironment,
132 | stack: sourceFromStacktrace( 2 )
133 | });
134 |
135 | if ( !validTest( test ) ) {
136 | return;
137 | }
138 |
139 | test.queue();
140 | },
141 |
142 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through.
143 | expect: function( asserts ) {
144 | if (arguments.length === 1) {
145 | config.current.expected = asserts;
146 | } else {
147 | return config.current.expected;
148 | }
149 | },
150 |
151 | start: function( count ) {
152 | // QUnit hasn't been initialized yet.
153 | // Note: RequireJS (et al) may delay onLoad
154 | if ( config.semaphore === undefined ) {
155 | QUnit.begin(function() {
156 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
157 | setTimeout(function() {
158 | QUnit.start( count );
159 | });
160 | });
161 | return;
162 | }
163 |
164 | config.semaphore -= count || 1;
165 | // don't start until equal number of stop-calls
166 | if ( config.semaphore > 0 ) {
167 | return;
168 | }
169 | // ignore if start is called more often then stop
170 | if ( config.semaphore < 0 ) {
171 | config.semaphore = 0;
172 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
173 | return;
174 | }
175 | // A slight delay, to avoid any current callbacks
176 | if ( defined.setTimeout ) {
177 | setTimeout(function() {
178 | if ( config.semaphore > 0 ) {
179 | return;
180 | }
181 | if ( config.timeout ) {
182 | clearTimeout( config.timeout );
183 | }
184 |
185 | config.blocking = false;
186 | process( true );
187 | }, 13);
188 | } else {
189 | config.blocking = false;
190 | process( true );
191 | }
192 | },
193 |
194 | stop: function( count ) {
195 | config.semaphore += count || 1;
196 | config.blocking = true;
197 |
198 | if ( config.testTimeout && defined.setTimeout ) {
199 | clearTimeout( config.timeout );
200 | config.timeout = setTimeout(function() {
201 | QUnit.ok( false, "Test timed out" );
202 | config.semaphore = 1;
203 | QUnit.start();
204 | }, config.testTimeout );
205 | }
206 | }
207 | };
208 |
209 | // We use the prototype to distinguish between properties that should
210 | // be exposed as globals (and in exports) and those that shouldn't
211 | (function() {
212 | function F() {}
213 | F.prototype = QUnit;
214 | QUnit = new F();
215 | // Make F QUnit's constructor so that we can add to the prototype later
216 | QUnit.constructor = F;
217 | }());
218 |
219 | /**
220 | * Config object: Maintain internal state
221 | * Later exposed as QUnit.config
222 | * `config` initialized at top of scope
223 | */
224 | config = {
225 | // The queue of tests to run
226 | queue: [],
227 |
228 | // block until document ready
229 | blocking: true,
230 |
231 | // when enabled, show only failing tests
232 | // gets persisted through sessionStorage and can be changed in UI via checkbox
233 | hidepassed: false,
234 |
235 | // by default, run previously failed tests first
236 | // very useful in combination with "Hide passed tests" checked
237 | reorder: true,
238 |
239 | // by default, modify document.title when suite is done
240 | altertitle: true,
241 |
242 | // by default, scroll to top of the page when suite is done
243 | scrolltop: true,
244 |
245 | // when enabled, all tests must call expect()
246 | requireExpects: false,
247 |
248 | // add checkboxes that are persisted in the query-string
249 | // when enabled, the id is set to `true` as a `QUnit.config` property
250 | urlConfig: [
251 | {
252 | id: "noglobals",
253 | label: "Check for Globals",
254 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
255 | },
256 | {
257 | id: "notrycatch",
258 | label: "No try-catch",
259 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
260 | }
261 | ],
262 |
263 | // Set of all modules.
264 | modules: {},
265 |
266 | // logging callback queues
267 | begin: [],
268 | done: [],
269 | log: [],
270 | testStart: [],
271 | testDone: [],
272 | moduleStart: [],
273 | moduleDone: []
274 | };
275 |
276 | // Initialize more QUnit.config and QUnit.urlParams
277 | (function() {
278 | var i, current,
279 | location = window.location || { search: "", protocol: "file:" },
280 | params = location.search.slice( 1 ).split( "&" ),
281 | length = params.length,
282 | urlParams = {};
283 |
284 | if ( params[ 0 ] ) {
285 | for ( i = 0; i < length; i++ ) {
286 | current = params[ i ].split( "=" );
287 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
288 |
289 | // allow just a key to turn on a flag, e.g., test.html?noglobals
290 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
291 | if ( urlParams[ current[ 0 ] ] ) {
292 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
293 | } else {
294 | urlParams[ current[ 0 ] ] = current[ 1 ];
295 | }
296 | }
297 | }
298 |
299 | QUnit.urlParams = urlParams;
300 |
301 | // String search anywhere in moduleName+testName
302 | config.filter = urlParams.filter;
303 |
304 | // Exact match of the module name
305 | config.module = urlParams.module;
306 |
307 | config.testNumber = [];
308 | if ( urlParams.testNumber ) {
309 |
310 | // Ensure that urlParams.testNumber is an array
311 | urlParams.testNumber = [].concat( urlParams.testNumber );
312 | for ( i = 0; i < urlParams.testNumber.length; i++ ) {
313 | current = urlParams.testNumber[ i ];
314 | config.testNumber.push( parseInt( current, 10 ) );
315 | }
316 | }
317 |
318 | // Figure out if we're running the tests from a server or not
319 | QUnit.isLocal = location.protocol === "file:";
320 | }());
321 |
322 | extend( QUnit, {
323 |
324 | config: config,
325 |
326 | // Initialize the configuration options
327 | init: function() {
328 | extend( config, {
329 | stats: { all: 0, bad: 0 },
330 | moduleStats: { all: 0, bad: 0 },
331 | started: +new Date(),
332 | updateRate: 1000,
333 | blocking: false,
334 | autostart: true,
335 | autorun: false,
336 | filter: "",
337 | queue: [],
338 | semaphore: 1
339 | });
340 |
341 | var tests, banner, result,
342 | qunit = id( "qunit" );
343 |
344 | if ( qunit ) {
345 | qunit.innerHTML =
346 | "" +
347 | "" +
348 | "" +
349 | "" +
350 | "
";
351 | }
352 |
353 | tests = id( "qunit-tests" );
354 | banner = id( "qunit-banner" );
355 | result = id( "qunit-testresult" );
356 |
357 | if ( tests ) {
358 | tests.innerHTML = "";
359 | }
360 |
361 | if ( banner ) {
362 | banner.className = "";
363 | }
364 |
365 | if ( result ) {
366 | result.parentNode.removeChild( result );
367 | }
368 |
369 | if ( tests ) {
370 | result = document.createElement( "p" );
371 | result.id = "qunit-testresult";
372 | result.className = "result";
373 | tests.parentNode.insertBefore( result, tests );
374 | result.innerHTML = "Running...
";
375 | }
376 | },
377 |
378 | // Resets the test setup. Useful for tests that modify the DOM.
379 | /*
380 | DEPRECATED: Use multiple tests instead of resetting inside a test.
381 | Use testStart or testDone for custom cleanup.
382 | This method will throw an error in 2.0, and will be removed in 2.1
383 | */
384 | reset: function() {
385 | var fixture = id( "qunit-fixture" );
386 | if ( fixture ) {
387 | fixture.innerHTML = config.fixture;
388 | }
389 | },
390 |
391 | // Safe object type checking
392 | is: function( type, obj ) {
393 | return QUnit.objectType( obj ) === type;
394 | },
395 |
396 | objectType: function( obj ) {
397 | if ( typeof obj === "undefined" ) {
398 | return "undefined";
399 | }
400 |
401 | // Consider: typeof null === object
402 | if ( obj === null ) {
403 | return "null";
404 | }
405 |
406 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
407 | type = match && match[1] || "";
408 |
409 | switch ( type ) {
410 | case "Number":
411 | if ( isNaN(obj) ) {
412 | return "nan";
413 | }
414 | return "number";
415 | case "String":
416 | case "Boolean":
417 | case "Array":
418 | case "Date":
419 | case "RegExp":
420 | case "Function":
421 | return type.toLowerCase();
422 | }
423 | if ( typeof obj === "object" ) {
424 | return "object";
425 | }
426 | return undefined;
427 | },
428 |
429 | push: function( result, actual, expected, message ) {
430 | if ( !config.current ) {
431 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
432 | }
433 |
434 | var output, source,
435 | details = {
436 | module: config.current.module,
437 | name: config.current.testName,
438 | result: result,
439 | message: message,
440 | actual: actual,
441 | expected: expected
442 | };
443 |
444 | message = escapeText( message ) || ( result ? "okay" : "failed" );
445 | message = "" + message + "";
446 | output = message;
447 |
448 | if ( !result ) {
449 | expected = escapeText( QUnit.jsDump.parse(expected) );
450 | actual = escapeText( QUnit.jsDump.parse(actual) );
451 | output += "Expected: | " + expected + " |
";
452 |
453 | if ( actual !== expected ) {
454 | output += "Result: | " + actual + " |
";
455 | output += "Diff: | " + QUnit.diff( expected, actual ) + " |
";
456 | }
457 |
458 | source = sourceFromStacktrace();
459 |
460 | if ( source ) {
461 | details.source = source;
462 | output += "Source: | " + escapeText( source ) + " |
";
463 | }
464 |
465 | output += "
";
466 | }
467 |
468 | runLoggingCallbacks( "log", QUnit, details );
469 |
470 | config.current.assertions.push({
471 | result: !!result,
472 | message: output
473 | });
474 | },
475 |
476 | pushFailure: function( message, source, actual ) {
477 | if ( !config.current ) {
478 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) );
479 | }
480 |
481 | var output,
482 | details = {
483 | module: config.current.module,
484 | name: config.current.testName,
485 | result: false,
486 | message: message
487 | };
488 |
489 | message = escapeText( message ) || "error";
490 | message = "" + message + "";
491 | output = message;
492 |
493 | output += "";
494 |
495 | if ( actual ) {
496 | output += "Result: | " + escapeText( actual ) + " |
";
497 | }
498 |
499 | if ( source ) {
500 | details.source = source;
501 | output += "Source: | " + escapeText( source ) + " |
";
502 | }
503 |
504 | output += "
";
505 |
506 | runLoggingCallbacks( "log", QUnit, details );
507 |
508 | config.current.assertions.push({
509 | result: false,
510 | message: output
511 | });
512 | },
513 |
514 | url: function( params ) {
515 | params = extend( extend( {}, QUnit.urlParams ), params );
516 | var key,
517 | querystring = "?";
518 |
519 | for ( key in params ) {
520 | if ( hasOwn.call( params, key ) ) {
521 | querystring += encodeURIComponent( key ) + "=" +
522 | encodeURIComponent( params[ key ] ) + "&";
523 | }
524 | }
525 | return window.location.protocol + "//" + window.location.host +
526 | window.location.pathname + querystring.slice( 0, -1 );
527 | },
528 |
529 | extend: extend,
530 | id: id,
531 | addEvent: addEvent,
532 | addClass: addClass,
533 | hasClass: hasClass,
534 | removeClass: removeClass
535 | // load, equiv, jsDump, diff: Attached later
536 | });
537 |
538 | /**
539 | * @deprecated: Created for backwards compatibility with test runner that set the hook function
540 | * into QUnit.{hook}, instead of invoking it and passing the hook function.
541 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
542 | * Doing this allows us to tell if the following methods have been overwritten on the actual
543 | * QUnit object.
544 | */
545 | extend( QUnit.constructor.prototype, {
546 |
547 | // Logging callbacks; all receive a single argument with the listed properties
548 | // run test/logs.html for any related changes
549 | begin: registerLoggingCallback( "begin" ),
550 |
551 | // done: { failed, passed, total, runtime }
552 | done: registerLoggingCallback( "done" ),
553 |
554 | // log: { result, actual, expected, message }
555 | log: registerLoggingCallback( "log" ),
556 |
557 | // testStart: { name }
558 | testStart: registerLoggingCallback( "testStart" ),
559 |
560 | // testDone: { name, failed, passed, total, runtime }
561 | testDone: registerLoggingCallback( "testDone" ),
562 |
563 | // moduleStart: { name }
564 | moduleStart: registerLoggingCallback( "moduleStart" ),
565 |
566 | // moduleDone: { name, failed, passed, total }
567 | moduleDone: registerLoggingCallback( "moduleDone" )
568 | });
569 |
570 | if ( !defined.document || document.readyState === "complete" ) {
571 | config.autorun = true;
572 | }
573 |
574 | QUnit.load = function() {
575 | runLoggingCallbacks( "begin", QUnit, {} );
576 |
577 | // Initialize the config, saving the execution queue
578 | var banner, filter, i, j, label, len, main, ol, toolbar, val, selection,
579 | urlConfigContainer, moduleFilter, userAgent,
580 | numModules = 0,
581 | moduleNames = [],
582 | moduleFilterHtml = "",
583 | urlConfigHtml = "",
584 | oldconfig = extend( {}, config );
585 |
586 | QUnit.init();
587 | extend(config, oldconfig);
588 |
589 | config.blocking = false;
590 |
591 | len = config.urlConfig.length;
592 |
593 | for ( i = 0; i < len; i++ ) {
594 | val = config.urlConfig[i];
595 | if ( typeof val === "string" ) {
596 | val = {
597 | id: val,
598 | label: val
599 | };
600 | }
601 | config[ val.id ] = QUnit.urlParams[ val.id ];
602 | if ( !val.value || typeof val.value === "string" ) {
603 | urlConfigHtml += "";
611 | } else {
612 | urlConfigHtml += "";
646 | }
647 | }
648 | for ( i in config.modules ) {
649 | if ( config.modules.hasOwnProperty( i ) ) {
650 | moduleNames.push(i);
651 | }
652 | }
653 | numModules = moduleNames.length;
654 | moduleNames.sort( function( a, b ) {
655 | return a.localeCompare( b );
656 | });
657 | moduleFilterHtml += "";
668 |
669 | // `userAgent` initialized at top of scope
670 | userAgent = id( "qunit-userAgent" );
671 | if ( userAgent ) {
672 | userAgent.innerHTML = navigator.userAgent;
673 | }
674 |
675 | // `banner` initialized at top of scope
676 | banner = id( "qunit-header" );
677 | if ( banner ) {
678 | banner.innerHTML = "" + banner.innerHTML + " ";
679 | }
680 |
681 | // `toolbar` initialized at top of scope
682 | toolbar = id( "qunit-testrunner-toolbar" );
683 | if ( toolbar ) {
684 | // `filter` initialized at top of scope
685 | filter = document.createElement( "input" );
686 | filter.type = "checkbox";
687 | filter.id = "qunit-filter-pass";
688 |
689 | addEvent( filter, "click", function() {
690 | var tmp,
691 | ol = id( "qunit-tests" );
692 |
693 | if ( filter.checked ) {
694 | ol.className = ol.className + " hidepass";
695 | } else {
696 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
697 | ol.className = tmp.replace( / hidepass /, " " );
698 | }
699 | if ( defined.sessionStorage ) {
700 | if (filter.checked) {
701 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" );
702 | } else {
703 | sessionStorage.removeItem( "qunit-filter-passed-tests" );
704 | }
705 | }
706 | });
707 |
708 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) {
709 | filter.checked = true;
710 | // `ol` initialized at top of scope
711 | ol = id( "qunit-tests" );
712 | ol.className = ol.className + " hidepass";
713 | }
714 | toolbar.appendChild( filter );
715 |
716 | // `label` initialized at top of scope
717 | label = document.createElement( "label" );
718 | label.setAttribute( "for", "qunit-filter-pass" );
719 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." );
720 | label.innerHTML = "Hide passed tests";
721 | toolbar.appendChild( label );
722 |
723 | urlConfigContainer = document.createElement("span");
724 | urlConfigContainer.innerHTML = urlConfigHtml;
725 | // For oldIE support:
726 | // * Add handlers to the individual elements instead of the container
727 | // * Use "click" instead of "change" for checkboxes
728 | // * Fallback from event.target to event.srcElement
729 | addEvents( urlConfigContainer.getElementsByTagName("input"), "click", function( event ) {
730 | var params = {},
731 | target = event.target || event.srcElement;
732 | params[ target.name ] = target.checked ?
733 | target.defaultValue || true :
734 | undefined;
735 | window.location = QUnit.url( params );
736 | });
737 | addEvents( urlConfigContainer.getElementsByTagName("select"), "change", function( event ) {
738 | var params = {},
739 | target = event.target || event.srcElement;
740 | params[ target.name ] = target.options[ target.selectedIndex ].value || undefined;
741 | window.location = QUnit.url( params );
742 | });
743 | toolbar.appendChild( urlConfigContainer );
744 |
745 | if (numModules > 1) {
746 | moduleFilter = document.createElement( "span" );
747 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
748 | moduleFilter.innerHTML = moduleFilterHtml;
749 | addEvent( moduleFilter.lastChild, "change", function() {
750 | var selectBox = moduleFilter.getElementsByTagName("select")[0],
751 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
752 |
753 | window.location = QUnit.url({
754 | module: ( selectedModule === "" ) ? undefined : selectedModule,
755 | // Remove any existing filters
756 | filter: undefined,
757 | testNumber: undefined
758 | });
759 | });
760 | toolbar.appendChild(moduleFilter);
761 | }
762 | }
763 |
764 | // `main` initialized at top of scope
765 | main = id( "qunit-fixture" );
766 | if ( main ) {
767 | config.fixture = main.innerHTML;
768 | }
769 |
770 | if ( config.autostart ) {
771 | QUnit.start();
772 | }
773 | };
774 |
775 | if ( defined.document ) {
776 | addEvent( window, "load", QUnit.load );
777 | }
778 |
779 | // `onErrorFnPrev` initialized at top of scope
780 | // Preserve other handlers
781 | onErrorFnPrev = window.onerror;
782 |
783 | // Cover uncaught exceptions
784 | // Returning true will suppress the default browser handler,
785 | // returning false will let it run.
786 | window.onerror = function ( error, filePath, linerNr ) {
787 | var ret = false;
788 | if ( onErrorFnPrev ) {
789 | ret = onErrorFnPrev( error, filePath, linerNr );
790 | }
791 |
792 | // Treat return value as window.onerror itself does,
793 | // Only do our handling if not suppressed.
794 | if ( ret !== true ) {
795 | if ( QUnit.config.current ) {
796 | if ( QUnit.config.current.ignoreGlobalErrors ) {
797 | return true;
798 | }
799 | QUnit.pushFailure( error, filePath + ":" + linerNr );
800 | } else {
801 | QUnit.test( "global failure", extend( function() {
802 | QUnit.pushFailure( error, filePath + ":" + linerNr );
803 | }, { validTest: validTest } ) );
804 | }
805 | return false;
806 | }
807 |
808 | return ret;
809 | };
810 |
811 | function done() {
812 | config.autorun = true;
813 |
814 | // Log the last module results
815 | if ( config.previousModule ) {
816 | runLoggingCallbacks( "moduleDone", QUnit, {
817 | name: config.previousModule,
818 | failed: config.moduleStats.bad,
819 | passed: config.moduleStats.all - config.moduleStats.bad,
820 | total: config.moduleStats.all
821 | });
822 | }
823 | delete config.previousModule;
824 |
825 | var i, key,
826 | banner = id( "qunit-banner" ),
827 | tests = id( "qunit-tests" ),
828 | runtime = +new Date() - config.started,
829 | passed = config.stats.all - config.stats.bad,
830 | html = [
831 | "Tests completed in ",
832 | runtime,
833 | " milliseconds.
",
834 | "",
835 | passed,
836 | " assertions of ",
837 | config.stats.all,
838 | " passed, ",
839 | config.stats.bad,
840 | " failed."
841 | ].join( "" );
842 |
843 | if ( banner ) {
844 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" );
845 | }
846 |
847 | if ( tests ) {
848 | id( "qunit-testresult" ).innerHTML = html;
849 | }
850 |
851 | if ( config.altertitle && defined.document && document.title ) {
852 | // show ✖ for good, ✔ for bad suite result in title
853 | // use escape sequences in case file gets loaded with non-utf-8-charset
854 | document.title = [
855 | ( config.stats.bad ? "\u2716" : "\u2714" ),
856 | document.title.replace( /^[\u2714\u2716] /i, "" )
857 | ].join( " " );
858 | }
859 |
860 | // clear own sessionStorage items if all tests passed
861 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
862 | // `key` & `i` initialized at top of scope
863 | for ( i = 0; i < sessionStorage.length; i++ ) {
864 | key = sessionStorage.key( i++ );
865 | if ( key.indexOf( "qunit-test-" ) === 0 ) {
866 | sessionStorage.removeItem( key );
867 | }
868 | }
869 | }
870 |
871 | // scroll back to top to show results
872 | if ( config.scrolltop && window.scrollTo ) {
873 | window.scrollTo(0, 0);
874 | }
875 |
876 | runLoggingCallbacks( "done", QUnit, {
877 | failed: config.stats.bad,
878 | passed: passed,
879 | total: config.stats.all,
880 | runtime: runtime
881 | });
882 | }
883 |
884 | /** @return Boolean: true if this test should be ran */
885 | function validTest( test ) {
886 | var include,
887 | filter = config.filter && config.filter.toLowerCase(),
888 | module = config.module && config.module.toLowerCase(),
889 | fullName = ( test.module + ": " + test.testName ).toLowerCase();
890 |
891 | // Internally-generated tests are always valid
892 | if ( test.callback && test.callback.validTest === validTest ) {
893 | delete test.callback.validTest;
894 | return true;
895 | }
896 |
897 | if ( config.testNumber.length > 0 ) {
898 | if ( inArray( test.testNumber, config.testNumber ) < 0 ) {
899 | return false;
900 | }
901 | }
902 |
903 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) {
904 | return false;
905 | }
906 |
907 | if ( !filter ) {
908 | return true;
909 | }
910 |
911 | include = filter.charAt( 0 ) !== "!";
912 | if ( !include ) {
913 | filter = filter.slice( 1 );
914 | }
915 |
916 | // If the filter matches, we need to honour include
917 | if ( fullName.indexOf( filter ) !== -1 ) {
918 | return include;
919 | }
920 |
921 | // Otherwise, do the opposite
922 | return !include;
923 | }
924 |
925 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
926 | // Later Safari and IE10 are supposed to support error.stack as well
927 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
928 | function extractStacktrace( e, offset ) {
929 | offset = offset === undefined ? 3 : offset;
930 |
931 | var stack, include, i;
932 |
933 | if ( e.stacktrace ) {
934 | // Opera
935 | return e.stacktrace.split( "\n" )[ offset + 3 ];
936 | } else if ( e.stack ) {
937 | // Firefox, Chrome
938 | stack = e.stack.split( "\n" );
939 | if (/^error$/i.test( stack[0] ) ) {
940 | stack.shift();
941 | }
942 | if ( fileName ) {
943 | include = [];
944 | for ( i = offset; i < stack.length; i++ ) {
945 | if ( stack[ i ].indexOf( fileName ) !== -1 ) {
946 | break;
947 | }
948 | include.push( stack[ i ] );
949 | }
950 | if ( include.length ) {
951 | return include.join( "\n" );
952 | }
953 | }
954 | return stack[ offset ];
955 | } else if ( e.sourceURL ) {
956 | // Safari, PhantomJS
957 | // hopefully one day Safari provides actual stacktraces
958 | // exclude useless self-reference for generated Error objects
959 | if ( /qunit.js$/.test( e.sourceURL ) ) {
960 | return;
961 | }
962 | // for actual exceptions, this is useful
963 | return e.sourceURL + ":" + e.line;
964 | }
965 | }
966 | function sourceFromStacktrace( offset ) {
967 | try {
968 | throw new Error();
969 | } catch ( e ) {
970 | return extractStacktrace( e, offset );
971 | }
972 | }
973 |
974 | /**
975 | * Escape text for attribute or text content.
976 | */
977 | function escapeText( s ) {
978 | if ( !s ) {
979 | return "";
980 | }
981 | s = s + "";
982 | // Both single quotes and double quotes (for attributes)
983 | return s.replace( /['"<>&]/g, function( s ) {
984 | switch( s ) {
985 | case "'":
986 | return "'";
987 | case "\"":
988 | return """;
989 | case "<":
990 | return "<";
991 | case ">":
992 | return ">";
993 | case "&":
994 | return "&";
995 | }
996 | });
997 | }
998 |
999 | function synchronize( callback, last ) {
1000 | config.queue.push( callback );
1001 |
1002 | if ( config.autorun && !config.blocking ) {
1003 | process( last );
1004 | }
1005 | }
1006 |
1007 | function process( last ) {
1008 | function next() {
1009 | process( last );
1010 | }
1011 | var start = new Date().getTime();
1012 | config.depth = config.depth ? config.depth + 1 : 1;
1013 |
1014 | while ( config.queue.length && !config.blocking ) {
1015 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
1016 | config.queue.shift()();
1017 | } else {
1018 | setTimeout( next, 13 );
1019 | break;
1020 | }
1021 | }
1022 | config.depth--;
1023 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
1024 | done();
1025 | }
1026 | }
1027 |
1028 | function saveGlobal() {
1029 | config.pollution = [];
1030 |
1031 | if ( config.noglobals ) {
1032 | for ( var key in window ) {
1033 | if ( hasOwn.call( window, key ) ) {
1034 | // in Opera sometimes DOM element ids show up here, ignore them
1035 | if ( /^qunit-test-output/.test( key ) ) {
1036 | continue;
1037 | }
1038 | config.pollution.push( key );
1039 | }
1040 | }
1041 | }
1042 | }
1043 |
1044 | function checkPollution() {
1045 | var newGlobals,
1046 | deletedGlobals,
1047 | old = config.pollution;
1048 |
1049 | saveGlobal();
1050 |
1051 | newGlobals = diff( config.pollution, old );
1052 | if ( newGlobals.length > 0 ) {
1053 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
1054 | }
1055 |
1056 | deletedGlobals = diff( old, config.pollution );
1057 | if ( deletedGlobals.length > 0 ) {
1058 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
1059 | }
1060 | }
1061 |
1062 | // returns a new Array with the elements that are in a but not in b
1063 | function diff( a, b ) {
1064 | var i, j,
1065 | result = a.slice();
1066 |
1067 | for ( i = 0; i < result.length; i++ ) {
1068 | for ( j = 0; j < b.length; j++ ) {
1069 | if ( result[i] === b[j] ) {
1070 | result.splice( i, 1 );
1071 | i--;
1072 | break;
1073 | }
1074 | }
1075 | }
1076 | return result;
1077 | }
1078 |
1079 | function extend( a, b ) {
1080 | for ( var prop in b ) {
1081 | if ( hasOwn.call( b, prop ) ) {
1082 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor
1083 | if ( !( prop === "constructor" && a === window ) ) {
1084 | if ( b[ prop ] === undefined ) {
1085 | delete a[ prop ];
1086 | } else {
1087 | a[ prop ] = b[ prop ];
1088 | }
1089 | }
1090 | }
1091 | }
1092 |
1093 | return a;
1094 | }
1095 |
1096 | /**
1097 | * @param {HTMLElement} elem
1098 | * @param {string} type
1099 | * @param {Function} fn
1100 | */
1101 | function addEvent( elem, type, fn ) {
1102 | if ( elem.addEventListener ) {
1103 |
1104 | // Standards-based browsers
1105 | elem.addEventListener( type, fn, false );
1106 | } else if ( elem.attachEvent ) {
1107 |
1108 | // support: IE <9
1109 | elem.attachEvent( "on" + type, fn );
1110 | } else {
1111 |
1112 | // Caller must ensure support for event listeners is present
1113 | throw new Error( "addEvent() was called in a context without event listener support" );
1114 | }
1115 | }
1116 |
1117 | /**
1118 | * @param {Array|NodeList} elems
1119 | * @param {string} type
1120 | * @param {Function} fn
1121 | */
1122 | function addEvents( elems, type, fn ) {
1123 | var i = elems.length;
1124 | while ( i-- ) {
1125 | addEvent( elems[i], type, fn );
1126 | }
1127 | }
1128 |
1129 | function hasClass( elem, name ) {
1130 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
1131 | }
1132 |
1133 | function addClass( elem, name ) {
1134 | if ( !hasClass( elem, name ) ) {
1135 | elem.className += (elem.className ? " " : "") + name;
1136 | }
1137 | }
1138 |
1139 | function removeClass( elem, name ) {
1140 | var set = " " + elem.className + " ";
1141 | // Class name may appear multiple times
1142 | while ( set.indexOf(" " + name + " ") > -1 ) {
1143 | set = set.replace(" " + name + " " , " ");
1144 | }
1145 | // If possible, trim it for prettiness, but not necessarily
1146 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, "");
1147 | }
1148 |
1149 | function id( name ) {
1150 | return defined.document && document.getElementById && document.getElementById( name );
1151 | }
1152 |
1153 | function registerLoggingCallback( key ) {
1154 | return function( callback ) {
1155 | config[key].push( callback );
1156 | };
1157 | }
1158 |
1159 | // Supports deprecated method of completely overwriting logging callbacks
1160 | function runLoggingCallbacks( key, scope, args ) {
1161 | var i, callbacks;
1162 | if ( QUnit.hasOwnProperty( key ) ) {
1163 | QUnit[ key ].call(scope, args );
1164 | } else {
1165 | callbacks = config[ key ];
1166 | for ( i = 0; i < callbacks.length; i++ ) {
1167 | callbacks[ i ].call( scope, args );
1168 | }
1169 | }
1170 | }
1171 |
1172 | // from jquery.js
1173 | function inArray( elem, array ) {
1174 | if ( array.indexOf ) {
1175 | return array.indexOf( elem );
1176 | }
1177 |
1178 | for ( var i = 0, length = array.length; i < length; i++ ) {
1179 | if ( array[ i ] === elem ) {
1180 | return i;
1181 | }
1182 | }
1183 |
1184 | return -1;
1185 | }
1186 |
1187 | function Test( settings ) {
1188 | extend( this, settings );
1189 | this.assertions = [];
1190 | this.testNumber = ++Test.count;
1191 | }
1192 |
1193 | Test.count = 0;
1194 |
1195 | Test.prototype = {
1196 | init: function() {
1197 | var a, b, li,
1198 | tests = id( "qunit-tests" );
1199 |
1200 | if ( tests ) {
1201 | b = document.createElement( "strong" );
1202 | b.innerHTML = this.nameHtml;
1203 |
1204 | // `a` initialized at top of scope
1205 | a = document.createElement( "a" );
1206 | a.innerHTML = "Rerun";
1207 | a.href = QUnit.url({ testNumber: this.testNumber });
1208 |
1209 | li = document.createElement( "li" );
1210 | li.appendChild( b );
1211 | li.appendChild( a );
1212 | li.className = "running";
1213 | li.id = this.id = "qunit-test-output" + testId++;
1214 |
1215 | tests.appendChild( li );
1216 | }
1217 | },
1218 | setup: function() {
1219 | if (
1220 | // Emit moduleStart when we're switching from one module to another
1221 | this.module !== config.previousModule ||
1222 | // They could be equal (both undefined) but if the previousModule property doesn't
1223 | // yet exist it means this is the first test in a suite that isn't wrapped in a
1224 | // module, in which case we'll just emit a moduleStart event for 'undefined'.
1225 | // Without this, reporters can get testStart before moduleStart which is a problem.
1226 | !hasOwn.call( config, "previousModule" )
1227 | ) {
1228 | if ( hasOwn.call( config, "previousModule" ) ) {
1229 | runLoggingCallbacks( "moduleDone", QUnit, {
1230 | name: config.previousModule,
1231 | failed: config.moduleStats.bad,
1232 | passed: config.moduleStats.all - config.moduleStats.bad,
1233 | total: config.moduleStats.all
1234 | });
1235 | }
1236 | config.previousModule = this.module;
1237 | config.moduleStats = { all: 0, bad: 0 };
1238 | runLoggingCallbacks( "moduleStart", QUnit, {
1239 | name: this.module
1240 | });
1241 | }
1242 |
1243 | config.current = this;
1244 |
1245 | this.testEnvironment = extend({
1246 | setup: function() {},
1247 | teardown: function() {}
1248 | }, this.moduleTestEnvironment );
1249 |
1250 | this.started = +new Date();
1251 | runLoggingCallbacks( "testStart", QUnit, {
1252 | name: this.testName,
1253 | module: this.module
1254 | });
1255 |
1256 | /*jshint camelcase:false */
1257 |
1258 |
1259 | /**
1260 | * Expose the current test environment.
1261 | *
1262 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
1263 | */
1264 | QUnit.current_testEnvironment = this.testEnvironment;
1265 |
1266 | /*jshint camelcase:true */
1267 |
1268 | if ( !config.pollution ) {
1269 | saveGlobal();
1270 | }
1271 | if ( config.notrycatch ) {
1272 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
1273 | return;
1274 | }
1275 | try {
1276 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
1277 | } catch( e ) {
1278 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
1279 | }
1280 | },
1281 | run: function() {
1282 | config.current = this;
1283 |
1284 | var running = id( "qunit-testresult" );
1285 |
1286 | if ( running ) {
1287 | running.innerHTML = "Running:
" + this.nameHtml;
1288 | }
1289 |
1290 | if ( this.async ) {
1291 | QUnit.stop();
1292 | }
1293 |
1294 | this.callbackStarted = +new Date();
1295 |
1296 | if ( config.notrycatch ) {
1297 | this.callback.call( this.testEnvironment, QUnit.assert );
1298 | this.callbackRuntime = +new Date() - this.callbackStarted;
1299 | return;
1300 | }
1301 |
1302 | try {
1303 | this.callback.call( this.testEnvironment, QUnit.assert );
1304 | this.callbackRuntime = +new Date() - this.callbackStarted;
1305 | } catch( e ) {
1306 | this.callbackRuntime = +new Date() - this.callbackStarted;
1307 |
1308 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
1309 | // else next test will carry the responsibility
1310 | saveGlobal();
1311 |
1312 | // Restart the tests if they're blocking
1313 | if ( config.blocking ) {
1314 | QUnit.start();
1315 | }
1316 | }
1317 | },
1318 | teardown: function() {
1319 | config.current = this;
1320 | if ( config.notrycatch ) {
1321 | if ( typeof this.callbackRuntime === "undefined" ) {
1322 | this.callbackRuntime = +new Date() - this.callbackStarted;
1323 | }
1324 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
1325 | return;
1326 | } else {
1327 | try {
1328 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
1329 | } catch( e ) {
1330 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
1331 | }
1332 | }
1333 | checkPollution();
1334 | },
1335 | finish: function() {
1336 | config.current = this;
1337 | if ( config.requireExpects && this.expected === null ) {
1338 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
1339 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
1340 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
1341 | } else if ( this.expected === null && !this.assertions.length ) {
1342 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
1343 | }
1344 |
1345 | var i, assertion, a, b, time, li, ol,
1346 | test = this,
1347 | good = 0,
1348 | bad = 0,
1349 | tests = id( "qunit-tests" );
1350 |
1351 | this.runtime = +new Date() - this.started;
1352 | config.stats.all += this.assertions.length;
1353 | config.moduleStats.all += this.assertions.length;
1354 |
1355 | if ( tests ) {
1356 | ol = document.createElement( "ol" );
1357 | ol.className = "qunit-assert-list";
1358 |
1359 | for ( i = 0; i < this.assertions.length; i++ ) {
1360 | assertion = this.assertions[i];
1361 |
1362 | li = document.createElement( "li" );
1363 | li.className = assertion.result ? "pass" : "fail";
1364 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
1365 | ol.appendChild( li );
1366 |
1367 | if ( assertion.result ) {
1368 | good++;
1369 | } else {
1370 | bad++;
1371 | config.stats.bad++;
1372 | config.moduleStats.bad++;
1373 | }
1374 | }
1375 |
1376 | // store result when possible
1377 | if ( QUnit.config.reorder && defined.sessionStorage ) {
1378 | if ( bad ) {
1379 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
1380 | } else {
1381 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
1382 | }
1383 | }
1384 |
1385 | if ( bad === 0 ) {
1386 | addClass( ol, "qunit-collapsed" );
1387 | }
1388 |
1389 | // `b` initialized at top of scope
1390 | b = document.createElement( "strong" );
1391 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")";
1392 |
1393 | addEvent(b, "click", function() {
1394 | var next = b.parentNode.lastChild,
1395 | collapsed = hasClass( next, "qunit-collapsed" );
1396 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
1397 | });
1398 |
1399 | addEvent(b, "dblclick", function( e ) {
1400 | var target = e && e.target ? e.target : window.event.srcElement;
1401 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
1402 | target = target.parentNode;
1403 | }
1404 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
1405 | window.location = QUnit.url({ testNumber: test.testNumber });
1406 | }
1407 | });
1408 |
1409 | // `time` initialized at top of scope
1410 | time = document.createElement( "span" );
1411 | time.className = "runtime";
1412 | time.innerHTML = this.runtime + " ms";
1413 |
1414 | // `li` initialized at top of scope
1415 | li = id( this.id );
1416 | li.className = bad ? "fail" : "pass";
1417 | li.removeChild( li.firstChild );
1418 | a = li.firstChild;
1419 | li.appendChild( b );
1420 | li.appendChild( a );
1421 | li.appendChild( time );
1422 | li.appendChild( ol );
1423 |
1424 | } else {
1425 | for ( i = 0; i < this.assertions.length; i++ ) {
1426 | if ( !this.assertions[i].result ) {
1427 | bad++;
1428 | config.stats.bad++;
1429 | config.moduleStats.bad++;
1430 | }
1431 | }
1432 | }
1433 |
1434 | runLoggingCallbacks( "testDone", QUnit, {
1435 | name: this.testName,
1436 | module: this.module,
1437 | failed: bad,
1438 | passed: this.assertions.length - bad,
1439 | total: this.assertions.length,
1440 | runtime: this.runtime,
1441 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
1442 | duration: this.runtime
1443 | });
1444 |
1445 | QUnit.reset();
1446 |
1447 | config.current = undefined;
1448 | },
1449 |
1450 | queue: function() {
1451 | var bad,
1452 | test = this;
1453 |
1454 | synchronize(function() {
1455 | test.init();
1456 | });
1457 | function run() {
1458 | // each of these can by async
1459 | synchronize(function() {
1460 | test.setup();
1461 | });
1462 | synchronize(function() {
1463 | test.run();
1464 | });
1465 | synchronize(function() {
1466 | test.teardown();
1467 | });
1468 | synchronize(function() {
1469 | test.finish();
1470 | });
1471 | }
1472 |
1473 | // `bad` initialized at top of scope
1474 | // defer when previous test run passed, if storage is available
1475 | bad = QUnit.config.reorder && defined.sessionStorage &&
1476 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
1477 |
1478 | if ( bad ) {
1479 | run();
1480 | } else {
1481 | synchronize( run, true );
1482 | }
1483 | }
1484 | };
1485 |
1486 | // `assert` initialized at top of scope
1487 | // Assert helpers
1488 | // All of these must either call QUnit.push() or manually do:
1489 | // - runLoggingCallbacks( "log", .. );
1490 | // - config.current.assertions.push({ .. });
1491 | assert = QUnit.assert = {
1492 | /**
1493 | * Asserts rough true-ish result.
1494 | * @name ok
1495 | * @function
1496 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
1497 | */
1498 | ok: function( result, msg ) {
1499 | if ( !config.current ) {
1500 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
1501 | }
1502 | result = !!result;
1503 | msg = msg || ( result ? "okay" : "failed" );
1504 |
1505 | var source,
1506 | details = {
1507 | module: config.current.module,
1508 | name: config.current.testName,
1509 | result: result,
1510 | message: msg
1511 | };
1512 |
1513 | msg = "" + escapeText( msg ) + "";
1514 |
1515 | if ( !result ) {
1516 | source = sourceFromStacktrace( 2 );
1517 | if ( source ) {
1518 | details.source = source;
1519 | msg += "Source: | " +
1520 | escapeText( source ) +
1521 | " |
---|
";
1522 | }
1523 | }
1524 | runLoggingCallbacks( "log", QUnit, details );
1525 | config.current.assertions.push({
1526 | result: result,
1527 | message: msg
1528 | });
1529 | },
1530 |
1531 | /**
1532 | * Assert that the first two arguments are equal, with an optional message.
1533 | * Prints out both actual and expected values.
1534 | * @name equal
1535 | * @function
1536 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
1537 | */
1538 | equal: function( actual, expected, message ) {
1539 | /*jshint eqeqeq:false */
1540 | QUnit.push( expected == actual, actual, expected, message );
1541 | },
1542 |
1543 | /**
1544 | * @name notEqual
1545 | * @function
1546 | */
1547 | notEqual: function( actual, expected, message ) {
1548 | /*jshint eqeqeq:false */
1549 | QUnit.push( expected != actual, actual, expected, message );
1550 | },
1551 |
1552 | /**
1553 | * @name propEqual
1554 | * @function
1555 | */
1556 | propEqual: function( actual, expected, message ) {
1557 | actual = objectValues(actual);
1558 | expected = objectValues(expected);
1559 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
1560 | },
1561 |
1562 | /**
1563 | * @name notPropEqual
1564 | * @function
1565 | */
1566 | notPropEqual: function( actual, expected, message ) {
1567 | actual = objectValues(actual);
1568 | expected = objectValues(expected);
1569 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
1570 | },
1571 |
1572 | /**
1573 | * @name deepEqual
1574 | * @function
1575 | */
1576 | deepEqual: function( actual, expected, message ) {
1577 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
1578 | },
1579 |
1580 | /**
1581 | * @name notDeepEqual
1582 | * @function
1583 | */
1584 | notDeepEqual: function( actual, expected, message ) {
1585 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
1586 | },
1587 |
1588 | /**
1589 | * @name strictEqual
1590 | * @function
1591 | */
1592 | strictEqual: function( actual, expected, message ) {
1593 | QUnit.push( expected === actual, actual, expected, message );
1594 | },
1595 |
1596 | /**
1597 | * @name notStrictEqual
1598 | * @function
1599 | */
1600 | notStrictEqual: function( actual, expected, message ) {
1601 | QUnit.push( expected !== actual, actual, expected, message );
1602 | },
1603 |
1604 | "throws": function( block, expected, message ) {
1605 | var actual,
1606 | expectedOutput = expected,
1607 | ok = false;
1608 |
1609 | // 'expected' is optional
1610 | if ( !message && typeof expected === "string" ) {
1611 | message = expected;
1612 | expected = null;
1613 | }
1614 |
1615 | config.current.ignoreGlobalErrors = true;
1616 | try {
1617 | block.call( config.current.testEnvironment );
1618 | } catch (e) {
1619 | actual = e;
1620 | }
1621 | config.current.ignoreGlobalErrors = false;
1622 |
1623 | if ( actual ) {
1624 |
1625 | // we don't want to validate thrown error
1626 | if ( !expected ) {
1627 | ok = true;
1628 | expectedOutput = null;
1629 |
1630 | // expected is an Error object
1631 | } else if ( expected instanceof Error ) {
1632 | ok = actual instanceof Error &&
1633 | actual.name === expected.name &&
1634 | actual.message === expected.message;
1635 |
1636 | // expected is a regexp
1637 | } else if ( QUnit.objectType( expected ) === "regexp" ) {
1638 | ok = expected.test( errorString( actual ) );
1639 |
1640 | // expected is a string
1641 | } else if ( QUnit.objectType( expected ) === "string" ) {
1642 | ok = expected === errorString( actual );
1643 |
1644 | // expected is a constructor
1645 | } else if ( actual instanceof expected ) {
1646 | ok = true;
1647 |
1648 | // expected is a validation function which returns true is validation passed
1649 | } else if ( expected.call( {}, actual ) === true ) {
1650 | expectedOutput = null;
1651 | ok = true;
1652 | }
1653 |
1654 | QUnit.push( ok, actual, expectedOutput, message );
1655 | } else {
1656 | QUnit.pushFailure( message, null, "No exception was thrown." );
1657 | }
1658 | }
1659 | };
1660 |
1661 | /**
1662 | * @deprecated since 1.8.0
1663 | * Kept assertion helpers in root for backwards compatibility.
1664 | */
1665 | extend( QUnit.constructor.prototype, assert );
1666 |
1667 | /**
1668 | * @deprecated since 1.9.0
1669 | * Kept to avoid TypeErrors for undefined methods.
1670 | */
1671 | QUnit.constructor.prototype.raises = function() {
1672 | QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" );
1673 | };
1674 |
1675 | /**
1676 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
1677 | * Kept to avoid TypeErrors for undefined methods.
1678 | */
1679 | QUnit.constructor.prototype.equals = function() {
1680 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
1681 | };
1682 | QUnit.constructor.prototype.same = function() {
1683 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
1684 | };
1685 |
1686 | // Test for equality any JavaScript type.
1687 | // Author: Philippe Rathé
1688 | QUnit.equiv = (function() {
1689 |
1690 | // Call the o related callback with the given arguments.
1691 | function bindCallbacks( o, callbacks, args ) {
1692 | var prop = QUnit.objectType( o );
1693 | if ( prop ) {
1694 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
1695 | return callbacks[ prop ].apply( callbacks, args );
1696 | } else {
1697 | return callbacks[ prop ]; // or undefined
1698 | }
1699 | }
1700 | }
1701 |
1702 | // the real equiv function
1703 | var innerEquiv,
1704 | // stack to decide between skip/abort functions
1705 | callers = [],
1706 | // stack to avoiding loops from circular referencing
1707 | parents = [],
1708 | parentsB = [],
1709 |
1710 | getProto = Object.getPrototypeOf || function ( obj ) {
1711 | /*jshint camelcase:false */
1712 | return obj.__proto__;
1713 | },
1714 | callbacks = (function () {
1715 |
1716 | // for string, boolean, number and null
1717 | function useStrictEquality( b, a ) {
1718 | /*jshint eqeqeq:false */
1719 | if ( b instanceof a.constructor || a instanceof b.constructor ) {
1720 | // to catch short annotation VS 'new' annotation of a
1721 | // declaration
1722 | // e.g. var i = 1;
1723 | // var j = new Number(1);
1724 | return a == b;
1725 | } else {
1726 | return a === b;
1727 | }
1728 | }
1729 |
1730 | return {
1731 | "string": useStrictEquality,
1732 | "boolean": useStrictEquality,
1733 | "number": useStrictEquality,
1734 | "null": useStrictEquality,
1735 | "undefined": useStrictEquality,
1736 |
1737 | "nan": function( b ) {
1738 | return isNaN( b );
1739 | },
1740 |
1741 | "date": function( b, a ) {
1742 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
1743 | },
1744 |
1745 | "regexp": function( b, a ) {
1746 | return QUnit.objectType( b ) === "regexp" &&
1747 | // the regex itself
1748 | a.source === b.source &&
1749 | // and its modifiers
1750 | a.global === b.global &&
1751 | // (gmi) ...
1752 | a.ignoreCase === b.ignoreCase &&
1753 | a.multiline === b.multiline &&
1754 | a.sticky === b.sticky;
1755 | },
1756 |
1757 | // - skip when the property is a method of an instance (OOP)
1758 | // - abort otherwise,
1759 | // initial === would have catch identical references anyway
1760 | "function": function() {
1761 | var caller = callers[callers.length - 1];
1762 | return caller !== Object && typeof caller !== "undefined";
1763 | },
1764 |
1765 | "array": function( b, a ) {
1766 | var i, j, len, loop, aCircular, bCircular;
1767 |
1768 | // b could be an object literal here
1769 | if ( QUnit.objectType( b ) !== "array" ) {
1770 | return false;
1771 | }
1772 |
1773 | len = a.length;
1774 | if ( len !== b.length ) {
1775 | // safe and faster
1776 | return false;
1777 | }
1778 |
1779 | // track reference to avoid circular references
1780 | parents.push( a );
1781 | parentsB.push( b );
1782 | for ( i = 0; i < len; i++ ) {
1783 | loop = false;
1784 | for ( j = 0; j < parents.length; j++ ) {
1785 | aCircular = parents[j] === a[i];
1786 | bCircular = parentsB[j] === b[i];
1787 | if ( aCircular || bCircular ) {
1788 | if ( a[i] === b[i] || aCircular && bCircular ) {
1789 | loop = true;
1790 | } else {
1791 | parents.pop();
1792 | parentsB.pop();
1793 | return false;
1794 | }
1795 | }
1796 | }
1797 | if ( !loop && !innerEquiv(a[i], b[i]) ) {
1798 | parents.pop();
1799 | parentsB.pop();
1800 | return false;
1801 | }
1802 | }
1803 | parents.pop();
1804 | parentsB.pop();
1805 | return true;
1806 | },
1807 |
1808 | "object": function( b, a ) {
1809 | /*jshint forin:false */
1810 | var i, j, loop, aCircular, bCircular,
1811 | // Default to true
1812 | eq = true,
1813 | aProperties = [],
1814 | bProperties = [];
1815 |
1816 | // comparing constructors is more strict than using
1817 | // instanceof
1818 | if ( a.constructor !== b.constructor ) {
1819 | // Allow objects with no prototype to be equivalent to
1820 | // objects with Object as their constructor.
1821 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
1822 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
1823 | return false;
1824 | }
1825 | }
1826 |
1827 | // stack constructor before traversing properties
1828 | callers.push( a.constructor );
1829 |
1830 | // track reference to avoid circular references
1831 | parents.push( a );
1832 | parentsB.push( b );
1833 |
1834 | // be strict: don't ensure hasOwnProperty and go deep
1835 | for ( i in a ) {
1836 | loop = false;
1837 | for ( j = 0; j < parents.length; j++ ) {
1838 | aCircular = parents[j] === a[i];
1839 | bCircular = parentsB[j] === b[i];
1840 | if ( aCircular || bCircular ) {
1841 | if ( a[i] === b[i] || aCircular && bCircular ) {
1842 | loop = true;
1843 | } else {
1844 | eq = false;
1845 | break;
1846 | }
1847 | }
1848 | }
1849 | aProperties.push(i);
1850 | if ( !loop && !innerEquiv(a[i], b[i]) ) {
1851 | eq = false;
1852 | break;
1853 | }
1854 | }
1855 |
1856 | parents.pop();
1857 | parentsB.pop();
1858 | callers.pop(); // unstack, we are done
1859 |
1860 | for ( i in b ) {
1861 | bProperties.push( i ); // collect b's properties
1862 | }
1863 |
1864 | // Ensures identical properties name
1865 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
1866 | }
1867 | };
1868 | }());
1869 |
1870 | innerEquiv = function() { // can take multiple arguments
1871 | var args = [].slice.apply( arguments );
1872 | if ( args.length < 2 ) {
1873 | return true; // end transition
1874 | }
1875 |
1876 | return (function( a, b ) {
1877 | if ( a === b ) {
1878 | return true; // catch the most you can
1879 | } else if ( a === null || b === null || typeof a === "undefined" ||
1880 | typeof b === "undefined" ||
1881 | QUnit.objectType(a) !== QUnit.objectType(b) ) {
1882 | return false; // don't lose time with error prone cases
1883 | } else {
1884 | return bindCallbacks(a, callbacks, [ b, a ]);
1885 | }
1886 |
1887 | // apply transition with (1..n) arguments
1888 | }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) );
1889 | };
1890 |
1891 | return innerEquiv;
1892 | }());
1893 |
1894 | /**
1895 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
1896 | * http://flesler.blogspot.com Licensed under BSD
1897 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
1898 | *
1899 | * @projectDescription Advanced and extensible data dumping for Javascript.
1900 | * @version 1.0.0
1901 | * @author Ariel Flesler
1902 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
1903 | */
1904 | QUnit.jsDump = (function() {
1905 | function quote( str ) {
1906 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
1907 | }
1908 | function literal( o ) {
1909 | return o + "";
1910 | }
1911 | function join( pre, arr, post ) {
1912 | var s = jsDump.separator(),
1913 | base = jsDump.indent(),
1914 | inner = jsDump.indent(1);
1915 | if ( arr.join ) {
1916 | arr = arr.join( "," + s + inner );
1917 | }
1918 | if ( !arr ) {
1919 | return pre + post;
1920 | }
1921 | return [ pre, inner + arr, base + post ].join(s);
1922 | }
1923 | function array( arr, stack ) {
1924 | var i = arr.length, ret = new Array(i);
1925 | this.up();
1926 | while ( i-- ) {
1927 | ret[i] = this.parse( arr[i] , undefined , stack);
1928 | }
1929 | this.down();
1930 | return join( "[", ret, "]" );
1931 | }
1932 |
1933 | var reName = /^function (\w+)/,
1934 | jsDump = {
1935 | // type is used mostly internally, you can fix a (custom)type in advance
1936 | parse: function( obj, type, stack ) {
1937 | stack = stack || [ ];
1938 | var inStack, res,
1939 | parser = this.parsers[ type || this.typeOf(obj) ];
1940 |
1941 | type = typeof parser;
1942 | inStack = inArray( obj, stack );
1943 |
1944 | if ( inStack !== -1 ) {
1945 | return "recursion(" + (inStack - stack.length) + ")";
1946 | }
1947 | if ( type === "function" ) {
1948 | stack.push( obj );
1949 | res = parser.call( this, obj, stack );
1950 | stack.pop();
1951 | return res;
1952 | }
1953 | return ( type === "string" ) ? parser : this.parsers.error;
1954 | },
1955 | typeOf: function( obj ) {
1956 | var type;
1957 | if ( obj === null ) {
1958 | type = "null";
1959 | } else if ( typeof obj === "undefined" ) {
1960 | type = "undefined";
1961 | } else if ( QUnit.is( "regexp", obj) ) {
1962 | type = "regexp";
1963 | } else if ( QUnit.is( "date", obj) ) {
1964 | type = "date";
1965 | } else if ( QUnit.is( "function", obj) ) {
1966 | type = "function";
1967 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) {
1968 | type = "window";
1969 | } else if ( obj.nodeType === 9 ) {
1970 | type = "document";
1971 | } else if ( obj.nodeType ) {
1972 | type = "node";
1973 | } else if (
1974 | // native arrays
1975 | toString.call( obj ) === "[object Array]" ||
1976 | // NodeList objects
1977 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
1978 | ) {
1979 | type = "array";
1980 | } else if ( obj.constructor === Error.prototype.constructor ) {
1981 | type = "error";
1982 | } else {
1983 | type = typeof obj;
1984 | }
1985 | return type;
1986 | },
1987 | separator: function() {
1988 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " ";
1989 | },
1990 | // extra can be a number, shortcut for increasing-calling-decreasing
1991 | indent: function( extra ) {
1992 | if ( !this.multiline ) {
1993 | return "";
1994 | }
1995 | var chr = this.indentChar;
1996 | if ( this.HTML ) {
1997 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
1998 | }
1999 | return new Array( this.depth + ( extra || 0 ) ).join(chr);
2000 | },
2001 | up: function( a ) {
2002 | this.depth += a || 1;
2003 | },
2004 | down: function( a ) {
2005 | this.depth -= a || 1;
2006 | },
2007 | setParser: function( name, parser ) {
2008 | this.parsers[name] = parser;
2009 | },
2010 | // The next 3 are exposed so you can use them
2011 | quote: quote,
2012 | literal: literal,
2013 | join: join,
2014 | //
2015 | depth: 1,
2016 | // This is the list of parsers, to modify them, use jsDump.setParser
2017 | parsers: {
2018 | window: "[Window]",
2019 | document: "[Document]",
2020 | error: function(error) {
2021 | return "Error(\"" + error.message + "\")";
2022 | },
2023 | unknown: "[Unknown]",
2024 | "null": "null",
2025 | "undefined": "undefined",
2026 | "function": function( fn ) {
2027 | var ret = "function",
2028 | // functions never have name in IE
2029 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
2030 |
2031 | if ( name ) {
2032 | ret += " " + name;
2033 | }
2034 | ret += "( ";
2035 |
2036 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" );
2037 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" );
2038 | },
2039 | array: array,
2040 | nodelist: array,
2041 | "arguments": array,
2042 | object: function( map, stack ) {
2043 | /*jshint forin:false */
2044 | var ret = [ ], keys, key, val, i;
2045 | QUnit.jsDump.up();
2046 | keys = [];
2047 | for ( key in map ) {
2048 | keys.push( key );
2049 | }
2050 | keys.sort();
2051 | for ( i = 0; i < keys.length; i++ ) {
2052 | key = keys[ i ];
2053 | val = map[ key ];
2054 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) );
2055 | }
2056 | QUnit.jsDump.down();
2057 | return join( "{", ret, "}" );
2058 | },
2059 | node: function( node ) {
2060 | var len, i, val,
2061 | open = QUnit.jsDump.HTML ? "<" : "<",
2062 | close = QUnit.jsDump.HTML ? ">" : ">",
2063 | tag = node.nodeName.toLowerCase(),
2064 | ret = open + tag,
2065 | attrs = node.attributes;
2066 |
2067 | if ( attrs ) {
2068 | for ( i = 0, len = attrs.length; i < len; i++ ) {
2069 | val = attrs[i].nodeValue;
2070 | // IE6 includes all attributes in .attributes, even ones not explicitly set.
2071 | // Those have values like undefined, null, 0, false, "" or "inherit".
2072 | if ( val && val !== "inherit" ) {
2073 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
2074 | }
2075 | }
2076 | }
2077 | ret += close;
2078 |
2079 | // Show content of TextNode or CDATASection
2080 | if ( node.nodeType === 3 || node.nodeType === 4 ) {
2081 | ret += node.nodeValue;
2082 | }
2083 |
2084 | return ret + open + "/" + tag + close;
2085 | },
2086 | // function calls it internally, it's the arguments part of the function
2087 | functionArgs: function( fn ) {
2088 | var args,
2089 | l = fn.length;
2090 |
2091 | if ( !l ) {
2092 | return "";
2093 | }
2094 |
2095 | args = new Array(l);
2096 | while ( l-- ) {
2097 | // 97 is 'a'
2098 | args[l] = String.fromCharCode(97+l);
2099 | }
2100 | return " " + args.join( ", " ) + " ";
2101 | },
2102 | // object calls it internally, the key part of an item in a map
2103 | key: quote,
2104 | // function calls it internally, it's the content of the function
2105 | functionCode: "[code]",
2106 | // node calls it internally, it's an html attribute value
2107 | attribute: quote,
2108 | string: quote,
2109 | date: quote,
2110 | regexp: literal,
2111 | number: literal,
2112 | "boolean": literal
2113 | },
2114 | // if true, entities are escaped ( <, >, \t, space and \n )
2115 | HTML: false,
2116 | // indentation unit
2117 | indentChar: " ",
2118 | // if true, items in a collection, are separated by a \n, else just a space.
2119 | multiline: true
2120 | };
2121 |
2122 | return jsDump;
2123 | }());
2124 |
2125 | /*
2126 | * Javascript Diff Algorithm
2127 | * By John Resig (http://ejohn.org/)
2128 | * Modified by Chu Alan "sprite"
2129 | *
2130 | * Released under the MIT license.
2131 | *
2132 | * More Info:
2133 | * http://ejohn.org/projects/javascript-diff-algorithm/
2134 | *
2135 | * Usage: QUnit.diff(expected, actual)
2136 | *
2137 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over"
2138 | */
2139 | QUnit.diff = (function() {
2140 | /*jshint eqeqeq:false, eqnull:true */
2141 | function diff( o, n ) {
2142 | var i,
2143 | ns = {},
2144 | os = {};
2145 |
2146 | for ( i = 0; i < n.length; i++ ) {
2147 | if ( !hasOwn.call( ns, n[i] ) ) {
2148 | ns[ n[i] ] = {
2149 | rows: [],
2150 | o: null
2151 | };
2152 | }
2153 | ns[ n[i] ].rows.push( i );
2154 | }
2155 |
2156 | for ( i = 0; i < o.length; i++ ) {
2157 | if ( !hasOwn.call( os, o[i] ) ) {
2158 | os[ o[i] ] = {
2159 | rows: [],
2160 | n: null
2161 | };
2162 | }
2163 | os[ o[i] ].rows.push( i );
2164 | }
2165 |
2166 | for ( i in ns ) {
2167 | if ( hasOwn.call( ns, i ) ) {
2168 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
2169 | n[ ns[i].rows[0] ] = {
2170 | text: n[ ns[i].rows[0] ],
2171 | row: os[i].rows[0]
2172 | };
2173 | o[ os[i].rows[0] ] = {
2174 | text: o[ os[i].rows[0] ],
2175 | row: ns[i].rows[0]
2176 | };
2177 | }
2178 | }
2179 | }
2180 |
2181 | for ( i = 0; i < n.length - 1; i++ ) {
2182 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
2183 | n[ i + 1 ] == o[ n[i].row + 1 ] ) {
2184 |
2185 | n[ i + 1 ] = {
2186 | text: n[ i + 1 ],
2187 | row: n[i].row + 1
2188 | };
2189 | o[ n[i].row + 1 ] = {
2190 | text: o[ n[i].row + 1 ],
2191 | row: i + 1
2192 | };
2193 | }
2194 | }
2195 |
2196 | for ( i = n.length - 1; i > 0; i-- ) {
2197 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
2198 | n[ i - 1 ] == o[ n[i].row - 1 ]) {
2199 |
2200 | n[ i - 1 ] = {
2201 | text: n[ i - 1 ],
2202 | row: n[i].row - 1
2203 | };
2204 | o[ n[i].row - 1 ] = {
2205 | text: o[ n[i].row - 1 ],
2206 | row: i - 1
2207 | };
2208 | }
2209 | }
2210 |
2211 | return {
2212 | o: o,
2213 | n: n
2214 | };
2215 | }
2216 |
2217 | return function( o, n ) {
2218 | o = o.replace( /\s+$/, "" );
2219 | n = n.replace( /\s+$/, "" );
2220 |
2221 | var i, pre,
2222 | str = "",
2223 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ),
2224 | oSpace = o.match(/\s+/g),
2225 | nSpace = n.match(/\s+/g);
2226 |
2227 | if ( oSpace == null ) {
2228 | oSpace = [ " " ];
2229 | }
2230 | else {
2231 | oSpace.push( " " );
2232 | }
2233 |
2234 | if ( nSpace == null ) {
2235 | nSpace = [ " " ];
2236 | }
2237 | else {
2238 | nSpace.push( " " );
2239 | }
2240 |
2241 | if ( out.n.length === 0 ) {
2242 | for ( i = 0; i < out.o.length; i++ ) {
2243 | str += "" + out.o[i] + oSpace[i] + "";
2244 | }
2245 | }
2246 | else {
2247 | if ( out.n[0].text == null ) {
2248 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) {
2249 | str += "" + out.o[n] + oSpace[n] + "";
2250 | }
2251 | }
2252 |
2253 | for ( i = 0; i < out.n.length; i++ ) {
2254 | if (out.n[i].text == null) {
2255 | str += "" + out.n[i] + nSpace[i] + "";
2256 | }
2257 | else {
2258 | // `pre` initialized at top of scope
2259 | pre = "";
2260 |
2261 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
2262 | pre += "" + out.o[n] + oSpace[n] + "";
2263 | }
2264 | str += " " + out.n[i].text + nSpace[i] + pre;
2265 | }
2266 | }
2267 | }
2268 |
2269 | return str;
2270 | };
2271 | }());
2272 |
2273 | // For browser, export only select globals
2274 | if ( typeof window !== "undefined" ) {
2275 | extend( window, QUnit.constructor.prototype );
2276 | window.QUnit = QUnit;
2277 | }
2278 |
2279 | // For CommonJS environments, export everything
2280 | if ( typeof module !== "undefined" && module.exports ) {
2281 | module.exports = QUnit;
2282 | }
2283 |
2284 |
2285 | // Get a reference to the global object, like window in browsers
2286 | }( (function() {
2287 | return this;
2288 | })() ));
2289 |
--------------------------------------------------------------------------------
/libs/qunit/sinon-qunit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * sinon-qunit 1.0.0, 2010/12/09
3 | *
4 | * @author Christian Johansen (christian@cjohansen.no)
5 | *
6 | * (The BSD License)
7 | *
8 | * Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no
9 | * All rights reserved.
10 | *
11 | * Redistribution and use in source and binary forms, with or without modification,
12 | * are permitted provided that the following conditions are met:
13 | *
14 | * * Redistributions of source code must retain the above copyright notice,
15 | * this list of conditions and the following disclaimer.
16 | * * Redistributions in binary form must reproduce the above copyright notice,
17 | * this list of conditions and the following disclaimer in the documentation
18 | * and/or other materials provided with the distribution.
19 | * * Neither the name of Christian Johansen nor the names of his contributors
20 | * may be used to endorse or promote products derived from this software
21 | * without specific prior written permission.
22 | *
23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
32 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 | */
34 | /*global sinon, QUnit, test*/
35 | sinon.assert.fail = function (msg) {
36 | QUnit.ok(false, msg);
37 | };
38 |
39 | sinon.assert.pass = function (assertion) {
40 | QUnit.ok(true, assertion);
41 | };
42 |
43 | sinon.config = {
44 | injectIntoThis: true,
45 | injectInto: null,
46 | properties: ["spy", "stub", "mock", "clock", "sandbox"],
47 | useFakeTimers: true,
48 | useFakeServer: false
49 | };
50 |
51 | (function (global) {
52 | var qTest = QUnit.test;
53 |
54 | QUnit.test = global.test = function (testName, expected, callback, async) {
55 | if (arguments.length === 2) {
56 | callback = expected;
57 | expected = null;
58 | }
59 |
60 | return qTest(testName, expected, sinon.test(callback), async);
61 | };
62 | }(this));
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery-plugin",
3 | "version": "0.0.0-ignored",
4 | "engines": {
5 | "node": ">= 0.8.0"
6 | },
7 | "scripts": {
8 | "test": "grunt qunit"
9 | },
10 | "devDependencies": {
11 | "grunt-contrib-jshint": "~0.10.0",
12 | "grunt-contrib-concat": "~0.4.0",
13 | "grunt-contrib-uglify": "~0.4.0",
14 | "grunt-contrib-watch": "~0.6.1",
15 | "grunt-contrib-clean": "~0.5.0",
16 | "grunt": "~0.4.4",
17 | "grunt-qunit-istanbul": "~0.4.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "browser": true,
3 | "jquery": true,
4 |
5 | "bitwise": true,
6 | "curly": true,
7 | "eqeqeq": true,
8 | "forin": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "noempty": true,
15 | "nonew": true,
16 | "plusplus": true,
17 | "quotmark": "single",
18 | "undef": true,
19 | "unused": true,
20 | "strict": true,
21 | "trailing": true,
22 | "maxparams": 4,
23 | "maxlen": 80
24 | }
25 |
--------------------------------------------------------------------------------
/src/django-superformset.js:
--------------------------------------------------------------------------------
1 | /*
2 | * django-superformset
3 | * https://github.com/jgerigmeyer/jquery-django-superformset
4 | *
5 | * Based on jQuery Formset 1.1r14
6 | * by Stanislaus Madueke
7 | *
8 | * Original Portions Copyright (c) 2009 Stanislaus Madueke
9 | * Modifications Copyright (c) 2014 Jonny Gerig Meyer
10 | * Licensed under the BSDv3 license.
11 | */
12 |
13 | (function ($) {
14 |
15 | 'use strict';
16 |
17 | var methods = {
18 | init: function (options) {
19 | var vars = {};
20 | var opts = vars.opts = $.extend({}, $.fn.superformset.defaults, options);
21 | var wrapper = vars.wrapper = $(this);
22 | var rows = vars.rows = wrapper.find(opts.rowSel);
23 | var container = vars.container = rows.closest(opts.containerSel);
24 | vars.totalForms = wrapper
25 | .find('input[id$="' + opts.prefix + '-TOTAL_FORMS"]');
26 | vars.maxForms = wrapper
27 | .find('input[id$="' + opts.prefix + '-MAX_NUM_FORMS"]');
28 |
29 | // Clone the form template to generate new form instances
30 | var tpl = vars.tpl = container.find(opts.formTemplate).clone(true);
31 | container.find(opts.formTemplate).find('[required], .required')
32 | .removeAttr('required').removeData('required-by').addClass('required');
33 | tpl.removeAttr('id').find('input, select, textarea').filter('[required]')
34 | .addClass('required').removeAttr('required');
35 |
36 | // Add delete-trigger and insert-above-trigger (if applicable) to template
37 | methods.addDeleteTrigger(tpl, opts.canDelete, vars);
38 | methods.addInsertAboveTrigger(tpl, vars);
39 |
40 | // Iterate over existing rows...
41 | rows.each(function () {
42 | var thisRow = $(this);
43 | // Add delete-trigger and insert-above-trigger to existing rows
44 | methods.addDeleteTrigger(
45 | thisRow,
46 | (opts.canDelete && !opts.deleteOnlyNew),
47 | vars
48 | );
49 | methods.addInsertAboveTrigger(thisRow, vars);
50 | // Attaches handlers watching for changes to inputs,
51 | // ...to add/remove ``required`` attr
52 | methods.watchForChangesToOptionalIfEmptyRow(thisRow, vars);
53 | });
54 |
55 | // Unless using auto-added rows, add and/or activate trigger to add rows
56 | if (!opts.autoAdd) {
57 | methods.activateAddTrigger(vars);
58 | }
59 |
60 | // Add extra empty row, if applicable
61 | if (opts.alwaysShowExtra && opts.autoAdd) {
62 | methods.autoAddRow(vars);
63 | wrapper.closest('form').submit(function () {
64 | $(this).find(opts.rowSel).filter('.extra-row')
65 | .find('input, select, textarea').each(function () {
66 | $(this).removeAttr('name');
67 | }
68 | );
69 | });
70 | }
71 |
72 | return wrapper;
73 | },
74 |
75 | activateAddTrigger: function (vars) {
76 | var opts = vars.opts;
77 | var addButton;
78 | if (vars.wrapper.find(opts.addTriggerSel).length) {
79 | addButton = vars.addButton = vars.wrapper.find(opts.addTriggerSel);
80 | } else {
81 | addButton = vars.addButton = $(opts.addTrigger).appendTo(vars.wrapper);
82 | }
83 | // Hide the add-trigger if we've reach the maxForms limit
84 | if (!methods.showAddButton(vars)) { addButton.hide(); }
85 | addButton.click(function (e) {
86 | var trigger = $(this);
87 | var formCount = parseInt(vars.totalForms.val(), 10);
88 | var newRow = vars.tpl.clone(true).addClass('new-row');
89 | var lastRow = vars.wrapper.find(opts.rowSel).last();
90 | newRow.find('input, select, textarea').filter('.required')
91 | .attr('required', 'required');
92 | if (opts.addAnimationSpeed) {
93 | if (lastRow.length) {
94 | newRow.hide().insertAfter(lastRow);
95 | } else {
96 | newRow.hide().insertBefore(trigger);
97 | }
98 | newRow.animate(
99 | {'height': 'toggle', 'opacity': 'toggle'},
100 | opts.addAnimationSpeed
101 | );
102 | } else {
103 | if (lastRow.length) {
104 | newRow.insertAfter(lastRow).show();
105 | } else {
106 | newRow.insertBefore(trigger).show();
107 | }
108 | }
109 | newRow.find('input, select, textarea, label').each(function () {
110 | methods.updateElementIndex($(this), opts.prefix, formCount);
111 | });
112 | // Attaches handlers watching for changes to inputs,
113 | // ...to add/remove ``required`` attr
114 | methods.watchForChangesToOptionalIfEmptyRow(newRow, vars);
115 | vars.totalForms.val(formCount + 1);
116 | // Check if we've exceeded the maximum allowed number of forms:
117 | if (!methods.showAddButton(vars)) { trigger.hide(); }
118 | // If a post-add callback was supplied, call it with the added form:
119 | if (opts.addedCallback) { opts.addedCallback(newRow); }
120 | e.preventDefault();
121 | });
122 | },
123 |
124 | // Attaches handlers watching for changes to inputs,
125 | // ...to add/remove ``required`` attr
126 | watchForChangesToOptionalIfEmptyRow: function (row, vars) {
127 | var opts = vars.opts;
128 | if (opts.optionalIfEmpty && row.is(opts.optionalIfEmptySel)) {
129 | var inputs = row.find('input, select, textarea');
130 | inputs.filter('[required], .required').removeAttr('required')
131 | .data('required-by', opts.prefix).addClass('required');
132 | row.data('original-vals', inputs.serialize());
133 | inputs.not(opts.deleteTriggerSel).change(function () {
134 | methods.updateRequiredFields(row, vars);
135 | });
136 | }
137 | },
138 |
139 | // Replace ``-__prefix__`` with correct index in for, id, name attrs
140 | updateElementIndex: function (elem, prefix, ndx) {
141 | var idRegex = new RegExp('(' + prefix + '-(\\d+|__prefix__))');
142 | var replacement = prefix + '-' + ndx;
143 | if (elem.attr('for')) {
144 | elem.attr('for', elem.attr('for').replace(idRegex, replacement));
145 | }
146 | if (elem.attr('id')) {
147 | elem.attr('id', elem.attr('id').replace(idRegex, replacement));
148 | }
149 | if (elem.attr('name')) {
150 | elem.attr('name', elem.attr('name').replace(idRegex, replacement));
151 | }
152 | },
153 |
154 | // Check whether we can add more rows
155 | showAddButton: function (vars) {
156 | return (
157 | vars.maxForms.val() === '' ||
158 | (vars.maxForms.val() - vars.totalForms.val() > 0)
159 | );
160 | },
161 |
162 | // Add delete trigger to end of a row, or activate existing delete-trigger
163 | addDeleteTrigger: function (row, canDelete, vars) {
164 | var opts = vars.opts;
165 | if (canDelete) {
166 | // Add a delete-trigger to remove the row from the DOM
167 | $(opts.deleteTrigger).appendTo(row).click(function (e) {
168 | var thisRow = $(this).closest(opts.rowSel);
169 | var rows, i;
170 | var updateSequence = function (rows, i) {
171 | rows.eq(i).find('input, select, textarea, label').each(function () {
172 | methods.updateElementIndex($(this), opts.prefix, i);
173 | });
174 | };
175 | var removeRow = function () {
176 | thisRow.remove();
177 | // Update the TOTAL_FORMS count:
178 | rows = vars.wrapper.find(opts.rowSel);
179 | vars.totalForms.val(rows.not('.extra-row').length);
180 | // Update names and IDs for all child controls,
181 | // ...so they remain in sequence.
182 | for (i = 0; i < rows.length; i = i + 1) {
183 | updateSequence(rows, i);
184 | }
185 | // If a post-delete callback was provided, call it with deleted form
186 | if (opts.removedCallback) { opts.removedCallback(thisRow); }
187 | };
188 | if (opts.removeAnimationSpeed) {
189 | $.when(
190 | thisRow.animate(
191 | {'height': 'toggle', 'opacity': 'toggle'},
192 | opts.removeAnimationSpeed
193 | )
194 | ).done(removeRow);
195 | } else {
196 | removeRow();
197 | }
198 | e.preventDefault();
199 | });
200 | } else {
201 | // If we're dealing with an inline formset,
202 | // ...just remove :required attrs when marking a row deleted
203 | row.find(opts.deleteTriggerSel).change(function () {
204 | var trigger = $(this);
205 | var thisRow = trigger.closest(opts.rowSel);
206 | if (trigger.prop('checked')) {
207 | thisRow.addClass(opts.deletedRowClass);
208 | thisRow.find('[required]').removeAttr('required')
209 | .addClass('deleted-required');
210 | // If a post-delete callback was provided, call it with deleted form
211 | if (opts.removedCallback) { opts.removedCallback(thisRow); }
212 | } else {
213 | thisRow.removeClass(opts.deletedRowClass);
214 | thisRow.find('.deleted-required').attr('required', 'required')
215 | .removeClass('deleted-required');
216 | }
217 | });
218 | }
219 | },
220 |
221 | // Add insert-above trigger before a row, if ``insertAboveTrigger: true``
222 | addInsertAboveTrigger: function (row, vars) {
223 | var opts = vars.opts;
224 | if (opts.insertAbove) {
225 | $(opts.insertAboveTrigger).prependTo(row).click(function (e) {
226 | var thisRow = $(this).closest(opts.rowSel);
227 | var formCount = parseInt(vars.totalForms.val(), 10);
228 | var newRow = vars.tpl.clone(true).addClass('new-row');
229 | var rows, i;
230 | var updateSequence = function (rows, i) {
231 | rows.eq(i).find('input, select, textarea, label').each(function () {
232 | methods.updateElementIndex($(this), opts.prefix, i);
233 | });
234 | };
235 | newRow.find('input, select, textarea').filter('.required')
236 | .attr('required', 'required');
237 | if (opts.addAnimationSpeed) {
238 | newRow.hide().insertBefore(thisRow).animate(
239 | {'height': 'toggle', 'opacity': 'toggle'}, opts.addAnimationSpeed
240 | );
241 | } else {
242 | newRow.insertBefore(thisRow).show();
243 | }
244 | // Update the TOTAL_FORMS count:
245 | rows = vars.wrapper.find(opts.rowSel);
246 | vars.totalForms.val(formCount + 1);
247 | // Update names and IDs for child controls so they remain in sequence.
248 | for (i = 0; i < rows.length; i = i + 1) {
249 | updateSequence(rows, i);
250 | }
251 | // Attaches handlers watching for changes to inputs,
252 | // ...to add/remove ``required`` attr
253 | methods.watchForChangesToOptionalIfEmptyRow(newRow, vars);
254 | // Check if we've exceeded the maximum allowed number of rows:
255 | if (!methods.showAddButton(vars)) { $(this).hide(); }
256 | // If a post-add callback was supplied, call it with the added form:
257 | if (opts.addedCallback) { opts.addedCallback(newRow); }
258 | $(this).blur();
259 | e.preventDefault();
260 | });
261 | }
262 | },
263 |
264 | // Add a row automatically
265 | autoAddRow: function (vars) {
266 | var opts = vars.opts;
267 | var formCount = parseInt(vars.totalForms.val(), 10);
268 | var newRow = vars.tpl.clone(true);
269 | var rows = vars.wrapper.find(opts.rowSel);
270 | if (opts.addAnimationSpeed) {
271 | newRow.hide().css('opacity', 0).insertAfter(rows.last())
272 | .addClass('extra-row').animate(
273 | {'height': 'toggle', 'opacity': '0.5'},
274 | opts.addAnimationSpeed
275 | );
276 | } else {
277 | newRow.css('opacity', 0.5).insertAfter(rows.last())
278 | .addClass('extra-row');
279 | }
280 | // When the extra-row receives focus...
281 | newRow.find('input, select, textarea, label').one('focus', function () {
282 | var el = $(this);
283 | var thisRow = el.closest(opts.rowSel);
284 | // fade it in
285 | thisRow.removeClass('extra-row').css('opacity', 1);
286 | // add "required" to appropriate inputs if not an "optionalIfEmpty" row
287 | if (
288 | el.hasClass('required') &&
289 | !(opts.optionalIfEmpty && newRow.is(opts.optionalIfEmptySel))
290 | ) {
291 | el.attr('required', 'required');
292 | }
293 | // update the totalForms count
294 | vars.totalForms.val(
295 | vars.wrapper.find(opts.rowSel).not('.extra-row').length
296 | );
297 | // fade in the delete-trigger
298 | if (opts.deleteOnlyActive) {
299 | thisRow.find(opts.deleteTriggerSel).fadeIn();
300 | }
301 | // and auto-add another extra-row
302 | if (
303 | methods.showAddButton(vars) &&
304 | thisRow.is(vars.wrapper.find(opts.rowSel).last())
305 | ) {
306 | methods.autoAddRow(vars);
307 | }
308 | }).each(function () {
309 | var el = $(this);
310 | methods.updateElementIndex(el, opts.prefix, formCount);
311 | el.filter('[required]').removeAttr('required').addClass('required');
312 | });
313 | // Attaches handlers watching for changes to inputs,
314 | // ...to add/remove ``required`` attr
315 | methods.watchForChangesToOptionalIfEmptyRow(newRow, vars);
316 | // Hide the delete-trigger initially, if ``deleteOnlyActive: true``
317 | if (opts.deleteOnlyActive) {
318 | newRow.find(opts.deleteTriggerSel).hide();
319 | }
320 | // If a post-add callback was supplied, call it with the added form
321 | if (opts.addedCallback) { opts.addedCallback(newRow); }
322 | },
323 |
324 | // Check if inputs have changed from original state,
325 | // ...and update ``required`` attr accordingly
326 | updateRequiredFields: function (row, vars) {
327 | var opts = vars.opts;
328 | var inputs = row.find('input, select, textarea');
329 | var relevantInputs = inputs.filter(function () {
330 | return $(this).data('required-by') === opts.prefix;
331 | });
332 | var state = inputs.serialize();
333 | var originalState = row.data('original-vals');
334 | if (state === originalState) {
335 | relevantInputs.removeAttr('required');
336 | } else {
337 | relevantInputs.filter('.required').not('.deleted-required')
338 | .attr('required', 'required');
339 | }
340 | },
341 |
342 | // Expose internal methods to allow stubbing in tests
343 | exposeMethods: function () {
344 | return methods;
345 | }
346 | };
347 |
348 | $.fn.superformset = function (method) {
349 | if (methods[method]) {
350 | return methods[method].apply(
351 | this,
352 | Array.prototype.slice.call(arguments, 1)
353 | );
354 | } else if (typeof method === 'object' || !method) {
355 | return methods.init.apply(this, arguments);
356 | } else {
357 | $.error('Method ' + method + ' does not exist on jQuery.superformset');
358 | }
359 | };
360 |
361 | /* Setup plugin defaults */
362 | $.fn.superformset.defaults = {
363 | prefix: 'form', // The form prefix for your django formset
364 | containerSel: 'form', // Container selector
365 | // ...(must contain rows and formTemplate)
366 | rowSel: '.dynamic-form', // Selector to match each row in a formset
367 | formTemplate: '.empty-form .dynamic-form',
368 | // Selector for empty form template to be
369 | // ...cloned to generate new form instances
370 | // ...Must be outside element on which formset
371 | // ...is called, but within containerSel
372 | deleteTrigger: 'remove',
373 | // The HTML "remove" link added to the end of
374 | // ...each form-row (if ``canDelete: true``)
375 | deleteTriggerSel: '.remove-row',
376 | // Selector for HTML "remove" links
377 | // ...Used to target existing delete-trigger,
378 | // ...or to target ``deleteTrigger``
379 | addTrigger: 'add',
380 | // The HTML "add" link added to the end of all
381 | // ...forms if no ``addTriggerSel``
382 | addTriggerSel: null, // Selector for trigger to add a new row
383 | // ...Used to target existing trigger
384 | // ...if provided, ``addTrigger`` is ignored
385 | addedCallback: null, // Fn called each time a new form row is added
386 | removedCallback: null, // Fn called each time a form row is deleted
387 | deletedRowClass: 'deleted', // Add to deleted row if ``canDelete: false``
388 | addAnimationSpeed: 'normal', // Speed (ms) to animate adding rows
389 | // ...If false, new rows appear w/o animation
390 | removeAnimationSpeed: 'fast', // Speed (ms) to animate removing rows
391 | // ...If false, new rows disappear w/o anim.
392 | autoAdd: false, // If true, the "add" link will be removed,
393 | // ...and a row will be automatically added
394 | // ...when text is entered in the final
395 | // ...textarea of the last row
396 | alwaysShowExtra: false, // If true, an extra (empty) row will always
397 | // ...be displayed (req. ``autoAdd: true``)
398 | deleteOnlyActive: false, // If true, extra empty rows cannot be removed
399 | // ...until they acquire focus
400 | // ...(requires ``alwaysShowExtra: true``)
401 | canDelete: false, // If false, rows cannot be removed from DOM.
402 | // ...``deleteTriggerSel`` will remove
403 | // ...``required`` attr from fields within a
404 | // ..."deleted" row.
405 | // ...deleted rows should be hidden via CSS
406 | deleteOnlyNew: false, // If true, only newly-added rows can be
407 | // ...deleted (requires ``canDelete: true``)
408 | insertAbove: false, // If true, ``insertAboveTrigger`` will be
409 | // ...added to the end of each form-row
410 | insertAboveTrigger:
411 | 'insert',
412 | // The HTML "insert" link add to the end of
413 | // ...each row (req. ``insertAbove: true``)
414 | optionalIfEmpty: true, // If true, required fields in a row will be
415 | // ...optional until changed from initial vals
416 | optionalIfEmptySel: '[data-empty-permitted="true"]'
417 | // Selector for rows to apply optionalIfEmpty
418 | // ...logic (req. ``optionalIfEmpty: true``)
419 | };
420 | }(jQuery));
421 |
--------------------------------------------------------------------------------
/test/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "browser": true,
3 | "jquery": true,
4 |
5 | "bitwise": true,
6 | "curly": true,
7 | "eqeqeq": true,
8 | "forin": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "noempty": true,
15 | "nonew": true,
16 | "plusplus": true,
17 | "quotmark": "single",
18 | "undef": true,
19 | "unused": true,
20 | "strict": true,
21 | "trailing": true,
22 |
23 | "predef": [
24 | "QUnit",
25 | "module",
26 | "test",
27 | "asyncTest",
28 | "expect",
29 | "start",
30 | "stop",
31 | "ok",
32 | "equal",
33 | "notEqual",
34 | "deepEqual",
35 | "notDeepEqual",
36 | "strictEqual",
37 | "notStrictEqual",
38 | "throws",
39 | "sinon",
40 | "htmlEqual",
41 | "notHtmlEqual"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/test/django-superformset.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Django Superformset Test Suite
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/test/django-superformset_test.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 | 'use strict';
4 |
5 | module('init', {
6 | setup: function () {
7 | this.container = $('#qunit-fixture .container');
8 | this.formlist = this.container.find('.formlist');
9 | this.template = this.container.find('.empty-form .dynamic-form');
10 | this.templateInput = this.template.find('#template-input');
11 | this.modifiedTemplate = this.template.clone().removeAttr('id');
12 | this.modifiedTemplate.find('#template-input').addClass('required').removeAttr('required');
13 | this.row = this.formlist.find('.dynamic-form');
14 | this.methods = this.formlist.superformset('exposeMethods');
15 | sinon.stub(this.methods, 'addDeleteTrigger');
16 | sinon.stub(this.methods, 'addInsertAboveTrigger');
17 | sinon.stub(this.methods, 'watchForChangesToOptionalIfEmptyRow');
18 | sinon.stub(this.methods, 'activateAddTrigger');
19 | sinon.stub(this.methods, 'autoAddRow');
20 | },
21 | teardown: function () {
22 | this.methods.addDeleteTrigger.restore();
23 | this.methods.addInsertAboveTrigger.restore();
24 | this.methods.watchForChangesToOptionalIfEmptyRow.restore();
25 | this.methods.activateAddTrigger.restore();
26 | this.methods.autoAddRow.restore();
27 | }
28 | });
29 |
30 | test('init is chainable', function () {
31 | ok(this.formlist.superformset().is(this.formlist), 'is chainable');
32 | });
33 |
34 | test('removes "required" attr and "required-by" data-attr from template', function () {
35 | this.templateInput.data('required-by', true);
36 | this.formlist.superformset();
37 |
38 | ok(this.templateInput.hasClass('required'), 'template input has class "required"');
39 | ok(!this.templateInput.is('[required]'), 'template input no longer has "required" attr');
40 | strictEqual(this.templateInput.data('required'), undefined, 'template input no longer has "required-by" data-attr');
41 | });
42 |
43 | test('calls addDeleteTrigger with template and opts.canDelete', function (assert) {
44 | this.formlist.superformset({canDelete: 'test-delete'});
45 |
46 | ok(this.methods.addDeleteTrigger.called, 'addDeleteTrigger was called');
47 | assert.htmlEqual(this.methods.addDeleteTrigger.args[0][0].get(0).outerHTML, this.modifiedTemplate.get(0).outerHTML, 'addDeleteTrigger was passed modified template');
48 | strictEqual(this.methods.addDeleteTrigger.args[0][1], 'test-delete', 'addDeleteTrigger was passed opts.canDelete');
49 | });
50 |
51 | test('calls addInsertAboveTrigger with template', function (assert) {
52 | this.formlist.superformset();
53 |
54 | ok(this.methods.addInsertAboveTrigger.called, 'addInsertAboveTrigger was called');
55 | assert.htmlEqual(this.methods.addInsertAboveTrigger.args[0][0].get(0).outerHTML, this.modifiedTemplate.get(0).outerHTML, 'addInsertAboveTrigger was passed modified template');
56 | });
57 |
58 | test('calls addDeleteTrigger with each row', function () {
59 | this.formlist.superformset({canDelete: true});
60 |
61 | ok(this.methods.addDeleteTrigger.called, 'addDeleteTrigger was called');
62 | ok(this.methods.addDeleteTrigger.args[1][0].is(this.row), 'addDeleteTrigger was passed row');
63 | });
64 |
65 | test('calls addDeleteTrigger with (opts.canDelete && !opts.deleteOnlyNew)', function () {
66 | this.formlist.superformset();
67 |
68 | ok(!this.methods.addDeleteTrigger.args[1][1], 'addDeleteTrigger was passed "false" as second arg');
69 |
70 | this.methods.addDeleteTrigger.reset();
71 | this.formlist.superformset({canDelete: true});
72 |
73 | ok(this.methods.addDeleteTrigger.args[1][1], 'addDeleteTrigger was passed "true" as second arg');
74 |
75 | this.methods.addDeleteTrigger.reset();
76 | this.formlist.superformset({deleteOnlyNew: true});
77 |
78 | ok(!this.methods.addDeleteTrigger.args[1][1], 'addDeleteTrigger was passed "false" as second arg');
79 |
80 | this.methods.addDeleteTrigger.reset();
81 | this.formlist.superformset({canDelete: true, deleteOnlyNew: true});
82 |
83 | ok(!this.methods.addDeleteTrigger.args[1][1], 'addDeleteTrigger was passed "false" as second arg');
84 | });
85 |
86 | test('calls addInsertAboveTrigger with each row', function () {
87 | this.formlist.superformset();
88 |
89 | ok(this.methods.addInsertAboveTrigger.called, 'addInsertAboveTrigger was called');
90 | ok(this.methods.addInsertAboveTrigger.args[1][0].is(this.row), 'addInsertAboveTrigger was passed row');
91 | });
92 |
93 | test('calls watchForChangesToOptionalIfEmptyRow with each row', function () {
94 | this.formlist.superformset();
95 |
96 | ok(this.methods.watchForChangesToOptionalIfEmptyRow.calledOnce, 'watchForChangesToOptionalIfEmptyRow was called once');
97 | ok(this.methods.watchForChangesToOptionalIfEmptyRow.args[0][0].is(this.row), 'watchForChangesToOptionalIfEmptyRow was passed row');
98 | });
99 |
100 | test('calls activateAddTrigger if opts.autoAdd is falsy', function () {
101 | this.formlist.superformset();
102 |
103 | ok(this.methods.activateAddTrigger.calledOnce, 'activateAddTrigger was called once');
104 | });
105 |
106 | test('does not call activateAddTrigger if opts.autoAdd is truthy', function () {
107 | this.formlist.superformset({autoAdd: true});
108 |
109 | ok(!this.methods.activateAddTrigger.called, 'activateAddTrigger was not called');
110 | });
111 |
112 | test('calls autoAddRow if opts.alwaysShowExtra && opts.autoAdd are truthy', function () {
113 | this.formlist.superformset();
114 |
115 | ok(!this.methods.autoAddRow.called, 'autoAddRow was not called');
116 |
117 | this.methods.autoAddRow.reset();
118 | this.formlist.superformset({alwaysShowExtra: true});
119 |
120 | ok(!this.methods.autoAddRow.called, 'autoAddRow was not called');
121 |
122 | this.methods.autoAddRow.reset();
123 | this.formlist.superformset({autoAdd: true});
124 |
125 | ok(!this.methods.autoAddRow.called, 'autoAddRow was not called');
126 |
127 | this.methods.autoAddRow.reset();
128 | this.formlist.superformset({alwaysShowExtra: true, autoAdd: true});
129 |
130 | ok(this.methods.autoAddRow.calledOnce, 'autoAddRow was called once');
131 | });
132 |
133 | test('removes "name" attr from .extra-row inputs on form submit', function () {
134 | var newRow = this.row.clone().addClass('extra-row').appendTo(this.formlist);
135 | var input = newRow.find('.test-input');
136 | this.container.on('submit', function (e) { e.preventDefault(); });
137 | this.formlist.superformset({alwaysShowExtra: true, autoAdd: true});
138 | this.container.trigger('submit');
139 |
140 | ok(!input.is('[name]'), '.extra-row input no longer has "name" attr');
141 | });
142 |
143 |
144 | module('activateAddTrigger', {
145 | setup: function () {
146 | this.container = $('#qunit-fixture .container');
147 | this.formlist = this.container.find('.formlist');
148 | this.template = this.container.find('.empty-form .dynamic-form').clone().removeAttr('id');
149 | this.template.find('#template-input').addClass('required').removeAttr('required');
150 | this.newRow = this.template.clone().addClass('new-row');
151 | this.newInput = this.newRow.find('#template-input').attr('required', 'required');
152 | this.row = this.formlist.find('.dynamic-form');
153 | this.addButton = this.formlist.find('.add');
154 | this.totalForms = this.formlist.find('#test-form-TOTAL_FORMS');
155 | this.methods = this.formlist.superformset('exposeMethods');
156 | this.vars = {
157 | opts: $.extend({}, $.fn.superformset.defaults),
158 | wrapper: this.formlist,
159 | totalForms: this.totalForms,
160 | tpl: this.template
161 | };
162 | this.vars.opts.addTriggerSel = '.add';
163 | this.vars.opts.addAnimationSpeed = false;
164 | sinon.stub(this.methods, 'showAddButton').returns(true);
165 | sinon.stub(this.methods, 'updateElementIndex');
166 | sinon.stub(this.methods, 'watchForChangesToOptionalIfEmptyRow');
167 | },
168 | teardown: function () {
169 | this.methods.showAddButton.restore();
170 | this.methods.updateElementIndex.restore();
171 | this.methods.watchForChangesToOptionalIfEmptyRow.restore();
172 | }
173 | });
174 |
175 | test('if opts.addTriggerSel is missing, appends opts.addTrigger to wrapper', function () {
176 | delete this.vars.opts.addTriggerSel;
177 | this.formlist.superformset('activateAddTrigger', this.vars);
178 |
179 | ok(this.formlist.find('.add-row').length, '.add-row has been appended to wrapper');
180 | });
181 |
182 | test('hides the addButton if methods.showAddButton returns false', function () {
183 | this.methods.showAddButton.returns(false);
184 | this.formlist.superformset('activateAddTrigger', this.vars);
185 |
186 | ok(!this.addButton.is(':visible'), 'addButton has been hidden');
187 | });
188 |
189 | test('does not hide addButton if methods.showAddButton returns true', function () {
190 | this.formlist.superformset('activateAddTrigger', this.vars);
191 |
192 | ok(this.addButton.is(':visible'), 'addButton has not been hidden');
193 | });
194 |
195 | test('inserts new row after last row on addButton click', function () {
196 | var expected = this.newRow.html();
197 | this.formlist.superformset('activateAddTrigger', this.vars);
198 | this.addButton.trigger('click');
199 |
200 | strictEqual(this.row.next().html(), expected, 'new-row has been appended to formlist');
201 | });
202 |
203 | test('inserts new row before addButton if no rows exist on addButton click', function () {
204 | var expected = this.newRow.html();
205 | this.formlist.superformset('activateAddTrigger', this.vars);
206 | this.row.remove();
207 | this.addButton.trigger('click');
208 |
209 | strictEqual(this.addButton.prev().html(), expected, 'new-row has been inserted above addButton');
210 | });
211 |
212 | test('animates display of new row if opts.addAnimationSpeed', function () {
213 | sinon.stub($.fn, 'animate');
214 | this.vars.opts.addAnimationSpeed = 'test-speed';
215 | this.formlist.superformset('activateAddTrigger', this.vars);
216 | this.addButton.trigger('click');
217 |
218 | ok(this.row.next().animate.calledOnce, 'new-row .animate was called once');
219 | ok(this.row.next().animate.calledWith({'height': 'toggle', 'opacity': 'toggle'}, 'test-speed'), 'new-row .animate was passed addAnimationSpeed');
220 |
221 | $.fn.animate.restore();
222 | });
223 |
224 | test('animates display of new row if opts.addAnimationSpeed and no rows exist', function () {
225 | sinon.stub($.fn, 'animate');
226 | this.vars.opts.addAnimationSpeed = 'test-speed';
227 | this.formlist.superformset('activateAddTrigger', this.vars);
228 | this.row.remove();
229 | this.addButton.trigger('click');
230 |
231 | ok(this.addButton.prev().animate.calledOnce, 'new-row .animate was called once');
232 | ok(this.addButton.prev().animate.calledWith({'height': 'toggle', 'opacity': 'toggle'}, 'test-speed'), 'new-row .animate was passed addAnimationSpeed');
233 |
234 | $.fn.animate.restore();
235 | });
236 |
237 | test('calls updateElementIndex for each input in new row', function (assert) {
238 | this.formlist.superformset('activateAddTrigger', this.vars);
239 | this.addButton.trigger('click');
240 |
241 | ok(this.methods.updateElementIndex.calledOnce, 'updateElementIndex was called once');
242 | assert.htmlEqual(this.methods.updateElementIndex.args[0][0].get(0).outerHTML, this.newInput.get(0).outerHTML, 'updateElementIndex was passed new row input');
243 | strictEqual(this.methods.updateElementIndex.args[0][1], 'form', 'updateElementIndex was passed prefix');
244 | strictEqual(this.methods.updateElementIndex.args[0][2], 1, 'updateElementIndex was passed zero-indexed number of rows');
245 | });
246 |
247 | test('calls watchForChangesToOptionalIfEmptyRow with new row', function (assert) {
248 | this.formlist.superformset('activateAddTrigger', this.vars);
249 | this.addButton.trigger('click');
250 |
251 | ok(this.methods.watchForChangesToOptionalIfEmptyRow.calledOnce, 'watchForChangesToOptionalIfEmptyRow was called once');
252 | assert.htmlEqual(this.methods.watchForChangesToOptionalIfEmptyRow.args[0][0].get(0).outerHTML, this.newRow.get(0).outerHTML, 'watchForChangesToOptionalIfEmptyRow was passed new row');
253 | });
254 |
255 | test('increments totalForms by 1', function () {
256 | this.formlist.superformset('activateAddTrigger', this.vars);
257 | this.addButton.trigger('click');
258 |
259 | strictEqual(this.totalForms.val(), '2', 'totalForms is now 2');
260 | });
261 |
262 | test('hides the addButton if methods.showAddButton returns false', function () {
263 | this.formlist.superformset('activateAddTrigger', this.vars);
264 |
265 | ok(this.addButton.is(':visible'), 'addButton is visible');
266 |
267 | this.methods.showAddButton.returns(false);
268 | this.addButton.trigger('click');
269 |
270 | ok(!this.addButton.is(':visible'), 'addButton has been hidden');
271 | });
272 |
273 | test('calls opts.addedCallback with new row', function (assert) {
274 | var callback = this.vars.opts.addedCallback = sinon.spy();
275 | this.formlist.superformset('activateAddTrigger', this.vars);
276 | this.addButton.trigger('click');
277 |
278 | ok(callback.calledOnce, 'addedCallback was called once');
279 | assert.htmlEqual(callback.args[0][0].get(0).outerHTML, this.newRow.get(0).outerHTML, 'addedCallback was passed new row');
280 | });
281 |
282 |
283 | module('watchForChangesToOptionalIfEmptyRow', {
284 | setup: function () {
285 | this.container = $('#qunit-fixture .container');
286 | this.formlist = this.container.find('.formlist');
287 | this.row = this.formlist.find('.dynamic-form').attr('data-empty-permitted', true);
288 | this.input = this.row.find('.test-input');
289 | this.totalForms = this.formlist.find('#test-form-TOTAL_FORMS');
290 | this.methods = this.formlist.superformset('exposeMethods');
291 | this.vars = {
292 | opts: $.extend({}, $.fn.superformset.defaults)
293 | };
294 | sinon.stub(this.methods, 'updateRequiredFields');
295 | },
296 | teardown: function () {
297 | this.methods.updateRequiredFields.restore();
298 | }
299 | });
300 |
301 | test('removes "required" attr, adds .required class and "required-by" data-attr to row input', function () {
302 | this.formlist.superformset('watchForChangesToOptionalIfEmptyRow', this.row, this.vars);
303 |
304 | ok(!this.input.is('[required]'), 'input no longer has "required" attr');
305 | ok(this.input.hasClass('required'), 'input has .required');
306 | strictEqual(this.input.data('required-by'), 'form', 'input data-required-by is prefix');
307 | });
308 |
309 | test('saves serialized inputs in row data-original-vals', function () {
310 | this.formlist.superformset('watchForChangesToOptionalIfEmptyRow', this.row, this.vars);
311 |
312 | strictEqual(this.row.data('original-vals'), this.input.serialize(), 'row data-original-vals has serialized inputs');
313 | });
314 |
315 | test('does nothing if opts.optionalIfEmpty is falsy or row is not opts.optionalIfEmpty', function () {
316 | this.vars.opts.optionalIfEmpty = false;
317 | this.formlist.superformset('watchForChangesToOptionalIfEmptyRow', this.row, this.vars);
318 |
319 | ok(this.input.is('[required]'), 'input is still :required');
320 |
321 | this.vars.opts.optionalIfEmpty = true;
322 | this.vars.opts.optionalIfEmptySel = '.optional';
323 | this.formlist.superformset('watchForChangesToOptionalIfEmptyRow', this.row, this.vars);
324 |
325 | ok(this.input.is('[required]'), 'input is still :required');
326 | });
327 |
328 | test('calls updateRequiredFields with row when input changes', function () {
329 | this.vars.opts.deleteTriggerSel = '[id$="-TOTAL_FORMS"]';
330 | this.formlist.superformset('watchForChangesToOptionalIfEmptyRow', this.row, this.vars);
331 | this.totalForms.trigger('change');
332 |
333 | ok(!this.methods.updateRequiredFields.called, 'updateRequiredFields was not called');
334 |
335 | this.input.trigger('change');
336 |
337 | ok(this.methods.updateRequiredFields.calledOnce, 'updateRequiredFields was called once');
338 | ok(this.methods.updateRequiredFields.calledWith(this.row), 'updateRequiredFields was passed row');
339 | });
340 |
341 |
342 | module('updateElementIndex', {
343 | setup: function () {
344 | this.container = $('#qunit-fixture .container');
345 | this.input = $('');
346 | this.label = $('');
347 | this.methods = this.container.superformset('exposeMethods');
348 | }
349 | });
350 |
351 | test('replaces "__prefix__" with index in "for" attr', function () {
352 | this.container.superformset('updateElementIndex', this.label, 'test', '2');
353 |
354 | strictEqual(this.label.attr('for'), 'test-2-input', '__prefix__ replaced in "for" attr');
355 | });
356 |
357 | test('replaces "__prefix__" with index in "id" attr', function () {
358 | this.container.superformset('updateElementIndex', this.input, 'test', '2');
359 |
360 | strictEqual(this.input.attr('id'), 'test-2-input', '__prefix__ replaced in "id" attr');
361 | });
362 |
363 | test('replaces "__prefix__" with index in "name" attr', function () {
364 | this.container.superformset('updateElementIndex', this.input, 'test', '2');
365 |
366 | strictEqual(this.input.attr('name'), 'test-2-name', '__prefix__ replaced in "name" attr');
367 | });
368 |
369 |
370 | module('showAddButton', {
371 | setup: function () {
372 | this.container = $('#qunit-fixture .container');
373 | this.vars = {
374 | totalForms: this.container.find('#test-form-TOTAL_FORMS'),
375 | maxForms: this.container.find('#test-form-MAX_NUM_FORMS')
376 | };
377 | this.methods = this.container.superformset('exposeMethods');
378 | }
379 | });
380 |
381 | test('returns true if maxForms has no val', function () {
382 | this.vars.maxForms.val('');
383 | var result = this.container.superformset('showAddButton', this.vars);
384 |
385 | ok(result, 'true');
386 | });
387 |
388 | test('returns false if maxForms minus totalForms is zero', function () {
389 | this.vars.maxForms.val('1');
390 | var result = this.container.superformset('showAddButton', this.vars);
391 |
392 | ok(!result, 'false');
393 | });
394 |
395 | test('returns true if maxForms minus totalForms is greater than zero', function () {
396 | var result = this.container.superformset('showAddButton', this.vars);
397 |
398 | ok(result, 'true');
399 | });
400 |
401 |
402 | module('addDeleteTrigger', {
403 | setup: function () {
404 | this.container = $('#qunit-fixture .container');
405 | this.formlist = this.container.find('.formlist');
406 | this.row = this.formlist.find('.dynamic-form');
407 | this.row2 = this.row.clone().addClass('extra-row').insertAfter(this.row);
408 | this.input = this.row.find('.test-input');
409 | this.totalForms = this.formlist.find('#test-form-TOTAL_FORMS');
410 | this.methods = this.formlist.superformset('exposeMethods');
411 | this.vars = {
412 | opts: $.extend({}, $.fn.superformset.defaults),
413 | wrapper: this.formlist,
414 | totalForms: this.totalForms
415 | };
416 | this.vars.opts.removeAnimationSpeed = false;
417 | sinon.stub(this.methods, 'updateElementIndex');
418 | sinon.spy($.fn, 'animate');
419 | $.fx.off = true;
420 | },
421 | teardown: function () {
422 | this.methods.updateElementIndex.restore();
423 | $.fn.animate.restore();
424 | $.fx.off = false;
425 | }
426 | });
427 |
428 | test('if canDelete, appends deleteTrigger to row', function () {
429 | this.formlist.superformset('addDeleteTrigger', this.row, true, this.vars);
430 |
431 | ok(this.row.find('.remove-row').length, '.remove-row has been added to row');
432 | });
433 |
434 | test('if canDelete, removes row on deleteTrigger click', function () {
435 | this.formlist.superformset('addDeleteTrigger', this.row, true, this.vars);
436 | this.row.find('.remove-row').trigger('click');
437 |
438 | ok(!this.formlist.find(this.row).length, 'row has been removed');
439 | });
440 |
441 | test('if canDelete, updates totalForms on deleteTrigger click', function () {
442 | this.formlist.superformset('addDeleteTrigger', this.row, true, this.vars);
443 | this.row.find('.remove-row').trigger('click');
444 |
445 | strictEqual(this.vars.totalForms.val(), '0', 'totalForms val is now zero');
446 | });
447 |
448 | test('if canDelete, calls updateElementIndex on each remaining input after deleteTrigger click', function () {
449 | this.formlist.superformset('addDeleteTrigger', this.row, true, this.vars);
450 | this.row.find('.remove-row').trigger('click');
451 |
452 | ok(this.methods.updateElementIndex.calledOnce, 'updateElementIndex was called once');
453 | ok(this.methods.updateElementIndex.args[0][0].is(this.row2.find('.test-input')), 'updateElementIndex was passed input');
454 | strictEqual(this.methods.updateElementIndex.args[0][1], 'form', 'updateElementIndex was passed opts.prefix');
455 | strictEqual(this.methods.updateElementIndex.args[0][2], 0, 'updateElementIndex was passed input index');
456 | });
457 |
458 | test('if canDelete and removeAnimationSpeed, removes row after animation on deleteTrigger click', function () {
459 | this.vars.opts.removeAnimationSpeed = 100;
460 | this.formlist.superformset('addDeleteTrigger', this.row, true, this.vars);
461 | this.row.find('.remove-row').trigger('click');
462 |
463 | ok(this.row.animate.calledOnce, '$.fn.animate was called on row');
464 | ok(this.row.animate.calledWith({'height': 'toggle', 'opacity': 'toggle'}, 100), '$.fn.animate was passed opts');
465 | ok(!this.formlist.find(this.row).length, 'row has been removed');
466 | });
467 |
468 | test('if canDelete, calls opts.removedCallback with row on deleteTrigger click', function () {
469 | var callback = this.vars.opts.removedCallback = sinon.spy();
470 | this.formlist.superformset('addDeleteTrigger', this.row, true, this.vars);
471 | this.row.find('.remove-row').trigger('click');
472 |
473 | ok(callback.calledOnce, 'removedCallback was called once');
474 | ok(callback.args[0][0].is(this.row), 'removedCallback was passed row');
475 | });
476 |
477 | test('when deleteTriggerSel is checked, calls opts.removedCallback with row', function () {
478 | var callback = this.vars.opts.removedCallback = sinon.spy();
479 | var deleteTrigger = $('').appendTo(this.row);
480 | this.formlist.superformset('addDeleteTrigger', this.row, false, this.vars);
481 | deleteTrigger.trigger('change');
482 |
483 | ok(callback.calledOnce, 'removedCallback was called once');
484 | ok(callback.args[0][0].is(this.row), 'removedCallback was passed row');
485 | });
486 |
487 | test('when deleteTriggerSel is checked, adds .deleted to row', function () {
488 | var deleteTrigger = $('').appendTo(this.row);
489 | this.formlist.superformset('addDeleteTrigger', this.row, false, this.vars);
490 | deleteTrigger.trigger('change');
491 |
492 | ok(this.row.hasClass('deleted'), 'row has .deleted');
493 | });
494 |
495 | test('when deleteTriggerSel is checked, removes :required attr and adds .deleted-required', function () {
496 | var deleteTrigger = $('').appendTo(this.row);
497 | this.formlist.superformset('addDeleteTrigger', this.row, false, this.vars);
498 | deleteTrigger.trigger('change');
499 |
500 | ok(!this.input.is('[required]'), 'input is no longer :required');
501 | ok(this.input.hasClass('deleted-required'), 'input has .deleted-required');
502 | });
503 |
504 | test('when deleteTriggerSel is unchecked, adds :required attr and removes .deleted-required', function () {
505 | var deleteTrigger = $('').appendTo(this.row);
506 | this.formlist.superformset('addDeleteTrigger', this.row, false, this.vars);
507 | deleteTrigger.trigger('change');
508 |
509 | strictEqual(this.input.attr('required'), 'required', 'input is now :required');
510 | ok(!this.input.hasClass('deleted-required'), 'input no longer has .deleted-required');
511 | });
512 |
513 | test('when deleteTriggerSel is unchecked, removes .deleted from row', function () {
514 | var deleteTrigger = $('').appendTo(this.row);
515 | this.formlist.superformset('addDeleteTrigger', this.row, false, this.vars);
516 | deleteTrigger.trigger('change');
517 |
518 | ok(!this.row.hasClass('deleted'), 'row does not have .deleted');
519 | });
520 |
521 |
522 | module('addInsertAboveTrigger', {
523 | setup: function () {
524 | this.container = $('#qunit-fixture .container');
525 | this.formlist = this.container.find('.formlist');
526 | this.row = this.formlist.find('.dynamic-form');
527 | this.input = this.row.find('.test-input');
528 | this.template = this.container.find('.empty-form .dynamic-form').clone().removeAttr('id');
529 | this.template.find('#template-input').addClass('required').removeAttr('required');
530 | this.newRow = this.template.clone().addClass('new-row');
531 | this.newRow.find('#template-input').attr('required', 'required');
532 | this.totalForms = this.formlist.find('#test-form-TOTAL_FORMS');
533 | this.methods = this.formlist.superformset('exposeMethods');
534 | this.vars = {
535 | opts: $.extend({}, $.fn.superformset.defaults),
536 | wrapper: this.formlist,
537 | totalForms: this.totalForms,
538 | tpl: this.template
539 | };
540 | this.vars.opts.addAnimationSpeed = false;
541 | this.vars.opts.insertAbove = true;
542 | sinon.stub(this.methods, 'updateElementIndex');
543 | sinon.stub(this.methods, 'showAddButton').returns(true);
544 | sinon.stub(this.methods, 'watchForChangesToOptionalIfEmptyRow');
545 | sinon.spy($.fn, 'animate');
546 | $.fx.off = true;
547 | },
548 | teardown: function () {
549 | this.methods.updateElementIndex.restore();
550 | this.methods.showAddButton.restore();
551 | this.methods.watchForChangesToOptionalIfEmptyRow.restore();
552 | $.fn.animate.restore();
553 | $.fx.off = false;
554 | }
555 | });
556 |
557 | test('prepends insertAboveTrigger to row', function () {
558 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
559 |
560 | ok(this.row.find('.insert-row').length, '.insert-row has been added to row');
561 | });
562 |
563 | test('does nothing if opts.insertAbove is falsy', function () {
564 | this.vars.opts.insertAbove = false;
565 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
566 |
567 | ok(!this.row.find('.insert-row').length, '.insert-row has not been added to row');
568 | });
569 |
570 | test('inserts new row above triggered row', function (assert) {
571 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
572 | this.row.find('.insert-row').trigger('click');
573 | var result = this.row.prev();
574 |
575 | assert.htmlEqual(result.get(0).outerHTML, this.newRow.get(0).outerHTML, 'new row was added above triggered row');
576 | ok(result.is(':visible'), 'new row is visible');
577 | });
578 |
579 | test('if addAnimationSpeed, inserts row after animation', function (assert) {
580 | this.vars.opts.addAnimationSpeed = 100;
581 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
582 | this.row.find('.insert-row').trigger('click');
583 | var result = this.row.prev();
584 |
585 | ok(result.animate.calledOnce, '$.fn.animate was called on row');
586 | ok(result.animate.calledWith({'height': 'toggle', 'opacity': 'toggle'}, 100), '$.fn.animate was passed opts');
587 | ok(result.is(':visible'), 'new row is visible');
588 | assert.htmlEqual(result.removeAttr('style').get(0).outerHTML, this.newRow.get(0).outerHTML, 'new row was added above triggered row');
589 | });
590 |
591 | test('updates totalForms', function () {
592 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
593 | this.row.find('.insert-row').trigger('click');
594 |
595 | strictEqual(this.vars.totalForms.val(), '2', 'totalForms val is now two');
596 | });
597 |
598 | test('calls updateElementIndex on each input', function () {
599 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
600 | this.row.find('.insert-row').trigger('click');
601 | var result = this.row.prev();
602 |
603 | ok(this.methods.updateElementIndex.calledTwice, 'updateElementIndex was called twice');
604 | ok(this.methods.updateElementIndex.args[0][0].is(result.find('.test-input')), 'updateElementIndex was passed new input');
605 | strictEqual(this.methods.updateElementIndex.args[0][1], 'form', 'updateElementIndex was passed opts.prefix');
606 | strictEqual(this.methods.updateElementIndex.args[0][2], 0, 'updateElementIndex was passed new input index');
607 | ok(this.methods.updateElementIndex.args[1][0].is(this.input), 'updateElementIndex was passed existing input');
608 | strictEqual(this.methods.updateElementIndex.args[1][1], 'form', 'updateElementIndex was passed opts.prefix');
609 | strictEqual(this.methods.updateElementIndex.args[1][2], 1, 'updateElementIndex was passed existing input index');
610 | });
611 |
612 | test('calls watchForChangesToOptionalIfEmptyRow with new row', function (assert) {
613 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
614 | this.row.find('.insert-row').trigger('click');
615 |
616 | ok(this.methods.watchForChangesToOptionalIfEmptyRow.calledOnce, 'watchForChangesToOptionalIfEmptyRow was called once');
617 | assert.htmlEqual(this.methods.watchForChangesToOptionalIfEmptyRow.args[0][0].get(0).outerHTML, this.newRow.get(0).outerHTML, 'watchForChangesToOptionalIfEmptyRow was passed new row');
618 | });
619 |
620 | test('hides the addButton if methods.showAddButton returns false', function () {
621 | this.methods.showAddButton.returns(false);
622 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
623 | var trigger = this.row.find('.insert-row').trigger('click');
624 |
625 | ok(!trigger.is(':visible'), 'insertAbove trigger has been hidden');
626 | });
627 |
628 | test('does not hide addButton if methods.showAddButton returns true', function () {
629 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
630 | var trigger = this.row.find('.insert-row').trigger('click');
631 |
632 | ok(trigger.is(':visible'), 'insertAbove trigger has not been hidden');
633 | });
634 |
635 | test('calls opts.addedCallback with new row', function (assert) {
636 | var callback = this.vars.opts.addedCallback = sinon.spy();
637 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
638 | this.row.find('.insert-row').trigger('click');
639 |
640 | ok(callback.calledOnce, 'addedCallback was called once');
641 | assert.htmlEqual(callback.args[0][0].get(0).outerHTML, this.newRow.get(0).outerHTML, 'addedCallback was passed new row');
642 | });
643 |
644 | test('blurs trigger', function () {
645 | sinon.spy($.fn, 'blur');
646 | this.formlist.superformset('addInsertAboveTrigger', this.row, this.vars);
647 | var trigger = this.row.find('.insert-row').trigger('click');
648 |
649 | ok(trigger.blur.calledOnce, 'trigger.blur was called once');
650 |
651 | $.fn.blur.restore();
652 | });
653 |
654 |
655 | module('autoAddRow', {
656 | setup: function () {
657 | this.container = $('#qunit-fixture .container');
658 | this.formlist = this.container.find('.formlist');
659 | this.row = this.formlist.find('.dynamic-form');
660 | this.input = this.row.find('.test-input');
661 | this.template = this.container.find('.empty-form .dynamic-form').clone().removeAttr('id');
662 | this.newRow = this.template.clone().addClass('extra-row').css('opacity', 0.5);
663 | this.newRow.find('.test-input').addClass('required').removeAttr('required');
664 | this.totalForms = this.formlist.find('#test-form-TOTAL_FORMS');
665 | this.methods = this.formlist.superformset('exposeMethods');
666 | this.vars = {
667 | opts: $.extend({}, $.fn.superformset.defaults),
668 | wrapper: this.formlist,
669 | totalForms: this.totalForms,
670 | tpl: this.template
671 | };
672 | this.vars.opts.addAnimationSpeed = false;
673 | this.vars.opts.optionalIfEmpty = false;
674 | sinon.stub(this.methods, 'updateElementIndex');
675 | sinon.stub(this.methods, 'showAddButton').returns(true);
676 | sinon.stub(this.methods, 'watchForChangesToOptionalIfEmptyRow');
677 | sinon.spy($.fn, 'animate');
678 | $.fx.off = true;
679 | },
680 | teardown: function () {
681 | this.methods.updateElementIndex.restore();
682 | this.methods.showAddButton.restore();
683 | this.methods.watchForChangesToOptionalIfEmptyRow.restore();
684 | $.fn.animate.restore();
685 | $.fx.off = false;
686 | }
687 | });
688 |
689 | test('adds new row with opacity: 0.5 after last row', function (assert) {
690 | this.formlist.superformset('autoAddRow', this.vars);
691 | var result = this.row.next();
692 |
693 | assert.htmlEqual(result.get(0).outerHTML, this.newRow.get(0).outerHTML, 'new row was added after last row');
694 | ok(result.is(':visible'), 'new row is visible');
695 | });
696 |
697 | test('calls updateElementIndex with each new row', function () {
698 | this.formlist.superformset('autoAddRow', this.vars);
699 | var input = this.row.next().find('.test-input');
700 |
701 | ok(this.methods.updateElementIndex.calledOnce, 'updateElementIndex was called once');
702 | ok(this.methods.updateElementIndex.args[0][0].is(input), 'updateElementIndex was passed new row input');
703 | strictEqual(this.methods.updateElementIndex.args[0][1], 'form', 'updateElementIndex was passed prefix');
704 | strictEqual(this.methods.updateElementIndex.args[0][2], 1, 'updateElementIndex was passed new row zero-based index');
705 | });
706 |
707 | test('removes :required attr and adds .required to inputs in new row', function () {
708 | this.formlist.superformset('autoAddRow', this.vars);
709 | var input = this.row.next().find('.test-input');
710 |
711 | ok(!input.is('[required]'), 'new input is not :required');
712 | ok(input.hasClass('required'), 'new input has class .required');
713 | });
714 |
715 | test('if addAnimationSpeed, adds row after animation', function (assert) {
716 | this.vars.opts.addAnimationSpeed = 100;
717 | this.formlist.superformset('autoAddRow', this.vars);
718 | var result = this.row.next();
719 |
720 | ok(result.animate.calledOnce, '$.fn.animate was called on row');
721 | ok(result.animate.calledWith({'height': 'toggle', 'opacity': '0.5'}, 100), '$.fn.animate was passed opts');
722 | assert.htmlEqual(result.get(0).outerHTML, this.newRow.get(0).outerHTML, 'new row was added after last row');
723 | ok(result.is(':visible'), 'new row is visible');
724 | });
725 |
726 | test('on focus, remove .extra-row and opacity: 0.5 from new row', function () {
727 | this.formlist.superformset('autoAddRow', this.vars);
728 | var row = this.row.next();
729 | row.find('.test-input').trigger('focus');
730 |
731 | ok(!row.hasClass('extra-row'), 'new row no longer has class .extra-row');
732 | strictEqual(row.css('opacity'), '1', 'new row now has opacity: 1');
733 | });
734 |
735 | test('on focus, add :required to .required inputs', function () {
736 | this.formlist.superformset('autoAddRow', this.vars);
737 | var input = this.row.next().find('.test-input').trigger('focus');
738 |
739 | ok(input.is('[required]'), 'input is now :required');
740 | });
741 |
742 | test('on focus, add :required to .required inputs', function () {
743 | this.vars.opts.optionalIfEmpty = true;
744 | this.formlist.superformset('autoAddRow', this.vars);
745 | var input = this.row.next().find('.test-input').trigger('focus');
746 |
747 | ok(input.is('[required]'), 'input is now :required');
748 | });
749 |
750 | test('on focus, add :required to .required inputs', function () {
751 | this.formlist.superformset('autoAddRow', this.vars);
752 | var row = this.row.next().attr('data-empty-permitted', true);
753 | var input = row.find('.test-input').trigger('focus');
754 |
755 | ok(input.is('[required]'), 'input is now :required');
756 | });
757 |
758 | test('on focus, do not add :required to .required inputs if optionalIfEmpty row', function () {
759 | this.vars.opts.optionalIfEmpty = true;
760 | this.formlist.superformset('autoAddRow', this.vars);
761 | var row = this.row.next().attr('data-empty-permitted', true);
762 | var input = row.find('.test-input').trigger('focus');
763 |
764 | ok(!input.is('[required]'), 'input is still not :required');
765 | });
766 |
767 | test('updates totalForms', function () {
768 | this.formlist.superformset('autoAddRow', this.vars);
769 | this.formlist.prepend('');
770 | this.row.next().find('.test-input').trigger('focus');
771 |
772 | strictEqual(this.vars.totalForms.val(), '2', 'totalForms val is now two');
773 | });
774 |
775 | test('shows deleteTrigger if deleteOnlyActive', function () {
776 | this.vars.opts.deleteOnlyActive = true;
777 | this.formlist.superformset('autoAddRow', this.vars);
778 | var row = this.row.next();
779 | $('').hide().appendTo(row);
780 | var remove = row.find('.remove-row');
781 | row.find('.test-input').trigger('focus');
782 |
783 | ok(remove.is(':visible'), 'deleteTrigger is visible');
784 | });
785 |
786 | test('does not show deleteTrigger if deleteOnlyActive is falsy', function () {
787 | this.formlist.superformset('autoAddRow', this.vars);
788 | var row = this.row.next();
789 | $('').hide().appendTo(row);
790 | var remove = row.find('.remove-row');
791 | row.find('.test-input').trigger('focus');
792 |
793 | ok(!remove.is(':visible'), 'deleteTrigger is visible');
794 | });
795 |
796 | test('calls autoAddRow if showAddButton returns true and row is last row', function () {
797 | this.formlist.superformset('autoAddRow', this.vars);
798 | sinon.stub(this.methods, 'autoAddRow');
799 | this.row.next().find('.test-input').trigger('focus');
800 |
801 | ok(this.methods.autoAddRow.calledOnce, 'autoAddRow was called once');
802 |
803 | this.methods.autoAddRow.restore();
804 | });
805 |
806 | test('does not call autoAddRow if showAddButton returns false', function () {
807 | this.methods.showAddButton.returns(false);
808 | this.formlist.superformset('autoAddRow', this.vars);
809 | sinon.stub(this.methods, 'autoAddRow');
810 | this.row.next().find('.test-input').trigger('focus');
811 |
812 | ok(!this.methods.autoAddRow.called, 'autoAddRow was not called');
813 |
814 | this.methods.autoAddRow.restore();
815 | });
816 |
817 | test('does not call autoAddRow if row is not last row', function () {
818 | this.formlist.superformset('autoAddRow', this.vars);
819 | sinon.stub(this.methods, 'autoAddRow');
820 | this.formlist.append('');
821 | this.row.next().find('.test-input').trigger('focus');
822 |
823 | ok(!this.methods.autoAddRow.called, 'autoAddRow was not called');
824 |
825 | this.methods.autoAddRow.restore();
826 | });
827 |
828 | test('calls watchForChangesToOptionalIfEmptyRow with new row', function (assert) {
829 | this.formlist.superformset('autoAddRow', this.vars);
830 | var row = this.row.next();
831 |
832 | ok(this.methods.watchForChangesToOptionalIfEmptyRow.calledOnce, 'watchForChangesToOptionalIfEmptyRow was called once');
833 | assert.htmlEqual(this.methods.watchForChangesToOptionalIfEmptyRow.args[0][0].get(0).outerHTML, row.get(0).outerHTML, 'watchForChangesToOptionalIfEmptyRow was passed new row');
834 | });
835 |
836 | test('hides deleteTrigger if deleteOnlyActive', function () {
837 | this.vars.opts.deleteOnlyActive = true;
838 | $('').appendTo(this.template);
839 | this.formlist.superformset('autoAddRow', this.vars);
840 | var remove = this.row.next().find('.remove-row');
841 |
842 | ok(!remove.is(':visible'), 'deleteTrigger is not visible');
843 | });
844 |
845 | test('does not hide deleteTrigger if deleteOnlyActive is falsy', function () {
846 | $('').appendTo(this.template);
847 | this.formlist.superformset('autoAddRow', this.vars);
848 | var remove = this.row.next().find('.remove-row');
849 |
850 | ok(remove.is(':visible'), 'deleteTrigger is still visible');
851 | });
852 |
853 | test('calls opts.addedCallback with new row', function (assert) {
854 | var callback = this.vars.opts.addedCallback = sinon.spy();
855 | this.formlist.superformset('autoAddRow', this.vars);
856 | var row = this.row.next();
857 |
858 | ok(callback.calledOnce, 'addedCallback was called once');
859 | assert.htmlEqual(callback.args[0][0].get(0).outerHTML, row.get(0).outerHTML, 'addedCallback was passed new row');
860 | });
861 |
862 |
863 | module('updateRequiredFields', {
864 | setup: function () {
865 | this.container = $('#qunit-fixture .container');
866 | this.formlist = this.container.find('.formlist');
867 | this.row = this.formlist.find('.dynamic-form');
868 | this.input = this.row.find('.test-input');
869 | this.input.data('required-by', 'form');
870 | this.row.data('original-vals', this.input.serialize());
871 | this.methods = this.formlist.superformset('exposeMethods');
872 | this.vars = {
873 | opts: $.extend({}, $.fn.superformset.defaults)
874 | };
875 | }
876 | });
877 |
878 | test('removes :required from inputs if serialized data matches row data-original-vals', function () {
879 | this.formlist.superformset('updateRequiredFields', this.row, this.vars);
880 |
881 | ok(!this.input.is('[required]'), 'input is no longer :required');
882 | });
883 |
884 | test('does not remove :required from inputs where data-required-by !== opts prefix', function () {
885 | this.input.removeData('required-by');
886 | this.formlist.superformset('updateRequiredFields', this.row, this.vars);
887 |
888 | ok(this.input.is('[required]'), 'input is still :required');
889 | });
890 |
891 | test('adds :required to inputs if serialized data does not match row data-original-vals', function () {
892 | this.row.removeData('original-vals');
893 | this.input.removeAttr('required').addClass('required');
894 | this.formlist.superformset('updateRequiredFields', this.row, this.vars);
895 |
896 | strictEqual(this.input.attr('required'), 'required', 'input is :required');
897 | });
898 |
899 | test('does not add :required to inputs with .deleted-required', function () {
900 | this.row.removeData('original-vals');
901 | this.input.removeAttr('required').addClass('required deleted-required');
902 | this.formlist.superformset('updateRequiredFields', this.row, this.vars);
903 |
904 | ok(!this.input.is('[required]'), 'input is not :required');
905 | });
906 |
907 |
908 | module('superformset methods', {
909 | setup: function () {
910 | this.container = $('#qunit-fixture');
911 | this.methods = this.container.superformset('exposeMethods');
912 | this.initStub = sinon.stub(this.methods, 'init');
913 | },
914 | teardown: function () {
915 | this.initStub.restore();
916 | }
917 | });
918 |
919 | test('if no args, calls init method', function () {
920 | this.container.superformset();
921 |
922 | ok(this.initStub.calledOnce, 'init was called once');
923 | });
924 |
925 | test('if first arg is an object, calls init method with args', function () {
926 | this.container.superformset({test: 'data'}, 'more');
927 |
928 | ok(this.initStub.calledOnce, 'init was called once');
929 | ok(this.initStub.calledWith({test: 'data'}, 'more'), 'init was passed args');
930 | });
931 |
932 | test('if first arg is a method, calls method with remaining args', function () {
933 | this.container.superformset('init', {test: 'data'}, 'more');
934 |
935 | ok(this.initStub.calledOnce, 'init was called once');
936 | ok(this.initStub.calledWith({test: 'data'}, 'more'), 'init was passed remaining args');
937 | });
938 |
939 | test('if first arg not a method or object, returns an error', function () {
940 | sinon.stub($, 'error');
941 | this.container.superformset('test');
942 |
943 | ok(!this.initStub.called, 'init was not called');
944 | ok($.error.calledOnce, '$.error was called once');
945 | ok($.error.calledWith('Method test does not exist on jQuery.superformset'), '$.error was passed error msg');
946 |
947 | $.error.restore();
948 | });
949 |
950 | }(jQuery));
951 |
--------------------------------------------------------------------------------