├── .gitignore ├── .jshintrc ├── LICENSE.md ├── README.md ├── bower.json ├── complex-repeater.html ├── gruntfile.js ├── index.html ├── index.pre.html ├── jquery-1.11.1.js ├── jquery.repeater.js ├── jquery.repeater.min.js ├── nested-repeater.html ├── package.json ├── repeater.html ├── repeater.jquery.json ├── src ├── intro.js ├── jquery.input.js ├── lib.js ├── outro.js └── repeater.js ├── test-post-parse.php └── test ├── complex.js ├── echo.php ├── index.html ├── index.pre.html ├── nested.js ├── qunit-1.14.0.css ├── qunit-1.14.0.js ├── test-lib.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // http://www.jshint.com/docs/options/ 3 | "curly": true, 4 | "immed": true, 5 | "latedef": true, 6 | "unused": true, 7 | "trailing": true, 8 | "boss": true, 9 | "loopfunc": true, 10 | "node": true, 11 | "browser": true, 12 | "jquery": true, 13 | "newcap": false, 14 | "predef": ["React"] 15 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Brian Detering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repeater 2 | [![CDNJS](https://img.shields.io/cdnjs/v/jquery.repeater.svg)](https://cdnjs.com/libraries/jquery.repeater) 3 | 4 | Creates an interface to add and remove a repeatable group of input elements. 5 | 6 | ### [Demo](http://briandetering.net/repeater) 7 | 8 | `bower install jquery.repeater --save` 9 | `npm install jquery.repeater --save` 10 | 11 | ## Templates 12 | 13 | Repeater uses the first "data-repeater-item" as a template for added items. 14 | 15 | ## Rewritten Name Attributes. 16 | 17 | Repeater rewrites your name attributes to avoid collisions within the same form. 18 | (since the name attributes will be repeated). In the example below, the 19 | name attributes would be renamed `group-a[0][text-input]` and `group-a[1][text-input]`. 20 | 21 | Checkbox inputs and Multiple Select inputs will have an additional `[]` appended. So for example a checkbox 22 | with name `foo` would be mapped to `group-a[0][foo][]`. 23 | 24 | Names get reindexed if an item is added or deleted. 25 | 26 | ## Example 27 | 28 | ```html 29 |
30 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 |
48 | 49 | 50 | 51 | 100 | ``` 101 | 102 | ## Nested Example 103 | 104 | ```html 105 | 106 |
107 |
108 |
109 | 110 | 111 | 112 | 113 |
114 |
115 |
116 | 117 | 118 |
119 |
120 | 121 |
122 | 123 |
124 |
125 | 126 |
127 | 128 | 129 | 130 | 146 | ``` 147 | 148 | 149 | ## repeaterVal 150 | 151 | Get a structured object of repeater values, without submitting the form. 152 | 153 | The rewritten name attributes of the form `group[index][name]` work well 154 | when submitting to a server that knows how to parse this format, but not as well 155 | when trying to grab the values via javascript. 156 | 157 | The `repeaterVal` method can be called on a repeater group and will parse the 158 | renamed attributes into something more easily digestible 159 | 160 | ```javascript 161 | // setup the repeater 162 | $('.repeater').repeater(); 163 | //get the values of the inputs as a formatted object 164 | $('.repeater').repeaterVal(); 165 | ``` 166 | 167 | ## setList 168 | 169 | You can set repeater list data after it has been initialized. 170 | 171 | ```javascript 172 | var $repeater = $('.repeater').repeater(); 173 | $repeater.setList([ 174 | { 175 | 'text-input': 'set-a', 176 | 'inner-group': [{ 'inner-text-input': 'set-b' }] 177 | }, 178 | { 'text-input': 'set-foo' } 179 | ]); 180 | ``` 181 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.repeater", 3 | "main": "jquery.repeater.js", 4 | "version": "1.2.2", 5 | "homepage": "https://github.com/DubFriend/jquery.repeater", 6 | "description": "repeatable form input interface", 7 | "keywords": [ 8 | "input", 9 | "repeat", 10 | "multiple", 11 | "form" 12 | ], 13 | "authors": [ 14 | "Brian Detering" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ], 24 | "dependencies": { 25 | "jquery": ">=1.11" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /complex-repeater.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var bannerTemplate = '' + 3 | '// <%= pkg.name %> version <%= pkg.version %>\n' + 4 | '// <%= pkg.repository.url %>\n' + 5 | '// (<%= pkg.license %>) <%= grunt.template.today("dd-mm-yyyy") %>\n' + 6 | '// <%= pkg.author %>\n'; 7 | 8 | grunt.initConfig({ 9 | pkg: grunt.file.readJSON('package.json'), 10 | 11 | preprocess : { 12 | options: { 13 | context : { 14 | DEBUG: true 15 | } 16 | }, 17 | test : { 18 | src : 'test/index.pre.html', 19 | dest : 'test/index.html' 20 | }, 21 | index: { 22 | src: 'index.pre.html', 23 | dest: 'index.html' 24 | } 25 | }, 26 | 27 | concat: { 28 | options: { 29 | separator: '\n', 30 | banner: bannerTemplate 31 | }, 32 | dist: { 33 | src: [ 34 | 'src/intro.js', 35 | 'src/lib.js', 36 | 'src/jquery.input.js', 37 | 'src/repeater.js', 38 | 'src/outro.js' 39 | ], 40 | dest: '<%= pkg.name %>.js' 41 | } 42 | }, 43 | 44 | uglify: { 45 | options: { banner: bannerTemplate }, 46 | dist: { 47 | files: { '<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>'] } 48 | } 49 | }, 50 | 51 | qunit: { 52 | // http://stackoverflow.com/questions/22409002/qunitphantomjs-ajax-success-handler-not-called-in-grunt-using-qunit-with-phant 53 | options : { 54 | '--web-security': false, 55 | '--local-to-remote-url-access': true 56 | }, 57 | all: ['test/index.html'] 58 | }, 59 | 60 | watch: { 61 | scripts: { 62 | files: ['**/*'], 63 | tasks: ['preprocess', 'concat', 'uglify', 'qunit'], 64 | options: { spawn: true } 65 | } 66 | } 67 | 68 | }); 69 | 70 | grunt.loadNpmTasks('grunt-preprocess'); 71 | grunt.loadNpmTasks('grunt-contrib-concat'); 72 | grunt.loadNpmTasks('grunt-contrib-uglify'); 73 | grunt.loadNpmTasks('grunt-contrib-watch'); 74 | grunt.loadNpmTasks('grunt-contrib-qunit'); 75 | 76 | grunt.registerTask('default', ['preprocess', 'concat', 'uglify', 'qunit']); 77 | grunt.registerTask('test', ['preprocess', 'concat', 'uglify', 'qunit']); 78 | }; 79 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Repeater 6 | jquery.repeater 7 | 13 | 14 | 15 |

Repeater

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 105 | 106 | 107 |
108 |
109 | 110 |
111 | 112 |

Nested

113 |
114 |
115 |
116 | 117 | 118 |
119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 |
131 | 132 | 133 | 134 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /index.pre.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Repeater 6 | jquery.repeater 7 | 13 | 14 | 15 |

Repeater

16 | 17 |

Nested

18 | 19 | 20 | 21 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /jquery.repeater.js: -------------------------------------------------------------------------------- 1 | // jquery.repeater version 1.2.2 2 | // https://github.com/DubFriend/jquery.repeater 3 | // (MIT) 15-04-2022 4 | // Brian Detering (http://www.briandetering.net/) 5 | (function ($) { 6 | 'use strict'; 7 | 8 | var identity = function (x) { 9 | return x; 10 | }; 11 | 12 | var isArray = function (value) { 13 | return $.isArray(value); 14 | }; 15 | 16 | var isObject = function (value) { 17 | return !isArray(value) && (value instanceof Object); 18 | }; 19 | 20 | var isNumber = function (value) { 21 | return value instanceof Number; 22 | }; 23 | 24 | var isFunction = function (value) { 25 | return value instanceof Function; 26 | }; 27 | 28 | var indexOf = function (object, value) { 29 | return $.inArray(value, object); 30 | }; 31 | 32 | var inArray = function (array, value) { 33 | return indexOf(array, value) !== -1; 34 | }; 35 | 36 | var foreach = function (collection, callback) { 37 | for(var i in collection) { 38 | if(collection.hasOwnProperty(i)) { 39 | callback(collection[i], i, collection); 40 | } 41 | } 42 | }; 43 | 44 | 45 | var last = function (array) { 46 | return array[array.length - 1]; 47 | }; 48 | 49 | var argumentsToArray = function (args) { 50 | return Array.prototype.slice.call(args); 51 | }; 52 | 53 | var extend = function () { 54 | var extended = {}; 55 | foreach(argumentsToArray(arguments), function (o) { 56 | foreach(o, function (val, key) { 57 | extended[key] = val; 58 | }); 59 | }); 60 | return extended; 61 | }; 62 | 63 | var mapToArray = function (collection, callback) { 64 | var mapped = []; 65 | foreach(collection, function (value, key, coll) { 66 | mapped.push(callback(value, key, coll)); 67 | }); 68 | return mapped; 69 | }; 70 | 71 | var mapToObject = function (collection, callback, keyCallback) { 72 | var mapped = {}; 73 | foreach(collection, function (value, key, coll) { 74 | key = keyCallback ? keyCallback(key, value) : key; 75 | mapped[key] = callback(value, key, coll); 76 | }); 77 | return mapped; 78 | }; 79 | 80 | var map = function (collection, callback, keyCallback) { 81 | return isArray(collection) ? 82 | mapToArray(collection, callback) : 83 | mapToObject(collection, callback, keyCallback); 84 | }; 85 | 86 | var pluck = function (arrayOfObjects, key) { 87 | return map(arrayOfObjects, function (val) { 88 | return val[key]; 89 | }); 90 | }; 91 | 92 | var filter = function (collection, callback) { 93 | var filtered; 94 | 95 | if(isArray(collection)) { 96 | filtered = []; 97 | foreach(collection, function (val, key, coll) { 98 | if(callback(val, key, coll)) { 99 | filtered.push(val); 100 | } 101 | }); 102 | } 103 | else { 104 | filtered = {}; 105 | foreach(collection, function (val, key, coll) { 106 | if(callback(val, key, coll)) { 107 | filtered[key] = val; 108 | } 109 | }); 110 | } 111 | 112 | return filtered; 113 | }; 114 | 115 | var call = function (collection, functionName, args) { 116 | return map(collection, function (object, name) { 117 | return object[functionName].apply(object, args || []); 118 | }); 119 | }; 120 | 121 | //execute callback immediately and at most one time on the minimumInterval, 122 | //ignore block attempts 123 | var throttle = function (minimumInterval, callback) { 124 | var timeout = null; 125 | return function () { 126 | var that = this, args = arguments; 127 | if(timeout === null) { 128 | timeout = setTimeout(function () { 129 | timeout = null; 130 | }, minimumInterval); 131 | callback.apply(that, args); 132 | } 133 | }; 134 | }; 135 | 136 | 137 | var mixinPubSub = function (object) { 138 | object = object || {}; 139 | var topics = {}; 140 | 141 | object.publish = function (topic, data) { 142 | foreach(topics[topic], function (callback) { 143 | callback(data); 144 | }); 145 | }; 146 | 147 | object.subscribe = function (topic, callback) { 148 | topics[topic] = topics[topic] || []; 149 | topics[topic].push(callback); 150 | }; 151 | 152 | object.unsubscribe = function (callback) { 153 | foreach(topics, function (subscribers) { 154 | var index = indexOf(subscribers, callback); 155 | if(index !== -1) { 156 | subscribers.splice(index, 1); 157 | } 158 | }); 159 | }; 160 | 161 | return object; 162 | }; 163 | 164 | // jquery.input version 0.0.0 165 | // https://github.com/DubFriend/jquery.input 166 | // (MIT) 09-04-2014 167 | // Brian Detering (http://www.briandetering.net/) 168 | (function ($) { 169 | 'use strict'; 170 | 171 | var createBaseInput = function (fig, my) { 172 | var self = mixinPubSub(), 173 | $self = fig.$; 174 | 175 | self.getType = function () { 176 | throw 'implement me (return type. "text", "radio", etc.)'; 177 | }; 178 | 179 | self.$ = function (selector) { 180 | return selector ? $self.find(selector) : $self; 181 | }; 182 | 183 | self.disable = function () { 184 | self.$().prop('disabled', true); 185 | self.publish('isEnabled', false); 186 | }; 187 | 188 | self.enable = function () { 189 | self.$().prop('disabled', false); 190 | self.publish('isEnabled', true); 191 | }; 192 | 193 | my.equalTo = function (a, b) { 194 | return a === b; 195 | }; 196 | 197 | my.publishChange = (function () { 198 | var oldValue; 199 | return function (e, domElement) { 200 | var newValue = self.get(); 201 | if(!my.equalTo(newValue, oldValue)) { 202 | self.publish('change', { e: e, domElement: domElement }); 203 | } 204 | oldValue = newValue; 205 | }; 206 | }()); 207 | 208 | return self; 209 | }; 210 | 211 | 212 | var createInput = function (fig, my) { 213 | var self = createBaseInput(fig, my); 214 | 215 | self.get = function () { 216 | return self.$().val(); 217 | }; 218 | 219 | self.set = function (newValue) { 220 | self.$().val(newValue); 221 | }; 222 | 223 | self.clear = function () { 224 | self.set(''); 225 | }; 226 | 227 | my.buildSetter = function (callback) { 228 | return function (newValue) { 229 | callback.call(self, newValue); 230 | }; 231 | }; 232 | 233 | return self; 234 | }; 235 | 236 | var createInputText = function (fig) { 237 | var my = {}, 238 | self = createInput(fig, my); 239 | 240 | self.getType = function () { 241 | return 'text'; 242 | }; 243 | 244 | self.$().on('change keyup keydown', function (e) { 245 | my.publishChange(e, this); 246 | }); 247 | 248 | return self; 249 | }; 250 | 251 | var inputEqualToArray = function (a, b) { 252 | a = isArray(a) ? a : [a]; 253 | b = isArray(b) ? b : [b]; 254 | 255 | var isEqual = true; 256 | if(a.length !== b.length) { 257 | isEqual = false; 258 | } 259 | else { 260 | foreach(a, function (value) { 261 | if(!inArray(b, value)) { 262 | isEqual = false; 263 | } 264 | }); 265 | } 266 | 267 | return isEqual; 268 | }; 269 | 270 | var createInputButton = function (fig) { 271 | var my = {}, 272 | self = createInput(fig, my); 273 | 274 | self.getType = function () { 275 | return 'button'; 276 | }; 277 | 278 | self.$().on('change', function (e) { 279 | my.publishChange(e, this); 280 | }); 281 | 282 | return self; 283 | }; 284 | 285 | var createInputCheckbox = function (fig) { 286 | var my = {}, 287 | self = createInput(fig, my); 288 | 289 | self.getType = function () { 290 | return 'checkbox'; 291 | }; 292 | 293 | self.get = function () { 294 | var values = []; 295 | self.$().filter(':checked').each(function () { 296 | values.push($(this).val()); 297 | }); 298 | return values; 299 | }; 300 | 301 | self.set = function (newValues) { 302 | newValues = isArray(newValues) ? newValues : [newValues]; 303 | 304 | self.$().each(function () { 305 | $(this).prop('checked', false); 306 | }); 307 | 308 | foreach(newValues, function (value) { 309 | self.$().filter('[value="' + value + '"]') 310 | .prop('checked', true); 311 | }); 312 | }; 313 | 314 | my.equalTo = inputEqualToArray; 315 | 316 | self.$().change(function (e) { 317 | my.publishChange(e, this); 318 | }); 319 | 320 | return self; 321 | }; 322 | 323 | var createInputEmail = function (fig) { 324 | var my = {}, 325 | self = createInputText(fig, my); 326 | 327 | self.getType = function () { 328 | return 'email'; 329 | }; 330 | 331 | return self; 332 | }; 333 | 334 | var createInputFile = function (fig) { 335 | var my = {}, 336 | self = createBaseInput(fig, my); 337 | 338 | self.getType = function () { 339 | return 'file'; 340 | }; 341 | 342 | self.get = function () { 343 | return last(self.$().val().split('\\')); 344 | }; 345 | 346 | self.clear = function () { 347 | // http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery 348 | this.$().each(function () { 349 | $(this).wrap('
').closest('form').get(0).reset(); 350 | $(this).unwrap(); 351 | }); 352 | }; 353 | 354 | self.$().change(function (e) { 355 | my.publishChange(e, this); 356 | // self.publish('change', self); 357 | }); 358 | 359 | return self; 360 | }; 361 | 362 | var createInputHidden = function (fig) { 363 | var my = {}, 364 | self = createInput(fig, my); 365 | 366 | self.getType = function () { 367 | return 'hidden'; 368 | }; 369 | 370 | self.$().change(function (e) { 371 | my.publishChange(e, this); 372 | }); 373 | 374 | return self; 375 | }; 376 | var createInputMultipleFile = function (fig) { 377 | var my = {}, 378 | self = createBaseInput(fig, my); 379 | 380 | self.getType = function () { 381 | return 'file[multiple]'; 382 | }; 383 | 384 | self.get = function () { 385 | // http://stackoverflow.com/questions/14035530/how-to-get-value-of-html-5-multiple-file-upload-variable-using-jquery 386 | var fileListObject = self.$().get(0).files || [], 387 | names = [], i; 388 | 389 | for(i = 0; i < (fileListObject.length || 0); i += 1) { 390 | names.push(fileListObject[i].name); 391 | } 392 | 393 | return names; 394 | }; 395 | 396 | self.clear = function () { 397 | // http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery 398 | this.$().each(function () { 399 | $(this).wrap('').closest('form').get(0).reset(); 400 | $(this).unwrap(); 401 | }); 402 | }; 403 | 404 | self.$().change(function (e) { 405 | my.publishChange(e, this); 406 | }); 407 | 408 | return self; 409 | }; 410 | 411 | var createInputMultipleSelect = function (fig) { 412 | var my = {}, 413 | self = createInput(fig, my); 414 | 415 | self.getType = function () { 416 | return 'select[multiple]'; 417 | }; 418 | 419 | self.get = function () { 420 | return self.$().val() || []; 421 | }; 422 | 423 | self.set = function (newValues) { 424 | self.$().val( 425 | newValues === '' ? [] : isArray(newValues) ? newValues : [newValues] 426 | ); 427 | }; 428 | 429 | my.equalTo = inputEqualToArray; 430 | 431 | self.$().change(function (e) { 432 | my.publishChange(e, this); 433 | }); 434 | 435 | return self; 436 | }; 437 | 438 | var createInputPassword = function (fig) { 439 | var my = {}, 440 | self = createInputText(fig, my); 441 | 442 | self.getType = function () { 443 | return 'password'; 444 | }; 445 | 446 | return self; 447 | }; 448 | 449 | var createInputRadio = function (fig) { 450 | var my = {}, 451 | self = createInput(fig, my); 452 | 453 | self.getType = function () { 454 | return 'radio'; 455 | }; 456 | 457 | self.get = function () { 458 | return self.$().filter(':checked').val() || null; 459 | }; 460 | 461 | self.set = function (newValue) { 462 | if(!newValue) { 463 | self.$().each(function () { 464 | $(this).prop('checked', false); 465 | }); 466 | } 467 | else { 468 | self.$().filter('[value="' + newValue + '"]').prop('checked', true); 469 | } 470 | }; 471 | 472 | self.$().change(function (e) { 473 | my.publishChange(e, this); 474 | }); 475 | 476 | return self; 477 | }; 478 | 479 | var createInputRange = function (fig) { 480 | var my = {}, 481 | self = createInput(fig, my); 482 | 483 | self.getType = function () { 484 | return 'range'; 485 | }; 486 | 487 | self.$().change(function (e) { 488 | my.publishChange(e, this); 489 | }); 490 | 491 | return self; 492 | }; 493 | 494 | var createInputSelect = function (fig) { 495 | var my = {}, 496 | self = createInput(fig, my); 497 | 498 | self.getType = function () { 499 | return 'select'; 500 | }; 501 | 502 | self.$().change(function (e) { 503 | my.publishChange(e, this); 504 | }); 505 | 506 | return self; 507 | }; 508 | 509 | var createInputTextarea = function (fig) { 510 | var my = {}, 511 | self = createInput(fig, my); 512 | 513 | self.getType = function () { 514 | return 'textarea'; 515 | }; 516 | 517 | self.$().on('change keyup keydown', function (e) { 518 | my.publishChange(e, this); 519 | }); 520 | 521 | return self; 522 | }; 523 | 524 | var createInputURL = function (fig) { 525 | var my = {}, 526 | self = createInputText(fig, my); 527 | 528 | self.getType = function () { 529 | return 'url'; 530 | }; 531 | 532 | return self; 533 | }; 534 | 535 | var createInputColor = function(fig) { 536 | var my = {}, 537 | self = createInputText(fig, my); 538 | 539 | self.getType = function () { 540 | return 'color'; 541 | }; 542 | 543 | return self; 544 | }; 545 | 546 | var createInputDate = function(fig) { 547 | var my = {}, 548 | self = createInputText(fig, my); 549 | 550 | self.getType = function () { 551 | return 'date'; 552 | }; 553 | 554 | return self; 555 | }; 556 | 557 | var createInputDateTimeLocal = function(fig) { 558 | var my = {}, 559 | self = createInputText(fig, my); 560 | 561 | self.getType = function () { 562 | return 'datetime-local'; 563 | }; 564 | 565 | return self; 566 | }; 567 | 568 | var createInputMonth = function(fig) { 569 | var my = {}, 570 | self = createInputText(fig, my); 571 | 572 | self.getType = function () { 573 | return 'month'; 574 | }; 575 | 576 | return self; 577 | }; 578 | 579 | var createInputNumber = function(fig) { 580 | var my = {}, 581 | self = createInputText(fig, my); 582 | 583 | self.getType = function () { 584 | return 'number'; 585 | }; 586 | 587 | return self; 588 | }; 589 | 590 | var createInputSearch = function(fig) { 591 | var my = {}, 592 | self = createInputText(fig, my); 593 | 594 | self.getType = function () { 595 | return 'search'; 596 | }; 597 | 598 | return self; 599 | }; 600 | 601 | var createInputTel = function(fig) { 602 | var my = {}, 603 | self = createInputText(fig, my); 604 | 605 | self.getType = function () { 606 | return 'tel'; 607 | }; 608 | 609 | return self; 610 | }; 611 | 612 | var createInputTime = function(fig) { 613 | var my = {}, 614 | self = createInputText(fig, my); 615 | 616 | self.getType = function () { 617 | return 'time'; 618 | }; 619 | 620 | return self; 621 | }; 622 | 623 | var createInputWeek = function(fig) { 624 | var my = {}, 625 | self = createInputText(fig, my); 626 | 627 | self.getType = function () { 628 | return 'week'; 629 | }; 630 | 631 | return self; 632 | }; 633 | 634 | 635 | var buildFormInputs = function (fig) { 636 | var inputs = {}, 637 | $self = fig.$; 638 | var constructor = fig.constructorOverride || { 639 | color:createInputColor, 640 | date:createInputDate, 641 | datetimeLocal:createInputDateTimeLocal, 642 | month:createInputMonth, 643 | number:createInputNumber, 644 | search:createInputSearch, 645 | tel:createInputTel, 646 | time:createInputTime, 647 | week:createInputWeek, 648 | button: createInputButton, 649 | text: createInputText, 650 | url: createInputURL, 651 | email: createInputEmail, 652 | password: createInputPassword, 653 | range: createInputRange, 654 | textarea: createInputTextarea, 655 | select: createInputSelect, 656 | 'select[multiple]': createInputMultipleSelect, 657 | radio: createInputRadio, 658 | checkbox: createInputCheckbox, 659 | file: createInputFile, 660 | 'file[multiple]': createInputMultipleFile, 661 | hidden: createInputHidden 662 | }; 663 | 664 | var addInputsBasic = function (type, selector) { 665 | var $input = isObject(selector) ? selector : $self.find(selector); 666 | 667 | $input.each(function () { 668 | var name = $(this).attr('name'); 669 | inputs[name] = constructor[type]({ 670 | $: $(this) 671 | }); 672 | }); 673 | }; 674 | 675 | var addInputsGroup = function (type, selector) { 676 | var names = [], 677 | $input = isObject(selector) ? selector : $self.find(selector); 678 | 679 | if(isObject(selector)) { 680 | inputs[$input.attr('name')] = constructor[type]({ 681 | $: $input 682 | }); 683 | } 684 | else { 685 | // group by name attribute 686 | $input.each(function () { 687 | if(indexOf(names, $(this).attr('name')) === -1) { 688 | names.push($(this).attr('name')); 689 | } 690 | }); 691 | 692 | foreach(names, function (name) { 693 | inputs[name] = constructor[type]({ 694 | $: $self.find('input[name="' + name + '"]') 695 | }); 696 | }); 697 | } 698 | }; 699 | 700 | 701 | if($self.is('input, select, textarea')) { 702 | if($self.is('input[type="button"], button, input[type="submit"]')) { 703 | addInputsBasic('button', $self); 704 | } 705 | else if($self.is('textarea')) { 706 | addInputsBasic('textarea', $self); 707 | } 708 | else if( 709 | $self.is('input[type="text"]') || 710 | $self.is('input') && !$self.attr('type') 711 | ) { 712 | addInputsBasic('text', $self); 713 | } 714 | else if($self.is('input[type="password"]')) { 715 | addInputsBasic('password', $self); 716 | } 717 | else if($self.is('input[type="email"]')) { 718 | addInputsBasic('email', $self); 719 | } 720 | else if($self.is('input[type="url"]')) { 721 | addInputsBasic('url', $self); 722 | } 723 | else if($self.is('input[type="color"]')) { 724 | addInputsBasic('color', $self); 725 | } 726 | else if($self.is('input[type="date"]')) { 727 | addInputsBasic('date', $self); 728 | } 729 | else if($self.is('input[type="datetime-local"]')) { 730 | addInputsBasic('datetimeLocal', $self); 731 | } 732 | else if($self.is('input[type="month"]')) { 733 | addInputsBasic('month', $self); 734 | } 735 | else if($self.is('input[type="number"]')) { 736 | addInputsBasic('number', $self); 737 | } 738 | else if($self.is('input[type="search"]')) { 739 | addInputsBasic('search', $self); 740 | } 741 | else if($self.is('input[type="tel"]')) { 742 | addInputsBasic('tel', $self); 743 | } 744 | else if($self.is('input[type="time"]')) { 745 | addInputsBasic('time', $self); 746 | } 747 | else if($self.is('input[type="week"]')) { 748 | addInputsBasic('week', $self); 749 | } 750 | else if($self.is('input[type="range"]')) { 751 | addInputsBasic('range', $self); 752 | } 753 | else if($self.is('select')) { 754 | if($self.is('[multiple]')) { 755 | addInputsBasic('select[multiple]', $self); 756 | } 757 | else { 758 | addInputsBasic('select', $self); 759 | } 760 | } 761 | else if($self.is('input[type="file"]')) { 762 | if($self.is('[multiple]')) { 763 | addInputsBasic('file[multiple]', $self); 764 | } 765 | else { 766 | addInputsBasic('file', $self); 767 | } 768 | } 769 | else if($self.is('input[type="hidden"]')) { 770 | addInputsBasic('hidden', $self); 771 | } 772 | else if($self.is('input[type="radio"]')) { 773 | addInputsGroup('radio', $self); 774 | } 775 | else if($self.is('input[type="checkbox"]')) { 776 | addInputsGroup('checkbox', $self); 777 | } 778 | else { 779 | //in all other cases default to a "text" input interface. 780 | addInputsBasic('text', $self); 781 | } 782 | } 783 | else { 784 | addInputsBasic('button', 'input[type="button"], button, input[type="submit"]'); 785 | addInputsBasic('text', 'input[type="text"]'); 786 | addInputsBasic('color', 'input[type="color"]'); 787 | addInputsBasic('date', 'input[type="date"]'); 788 | addInputsBasic('datetimeLocal', 'input[type="datetime-local"]'); 789 | addInputsBasic('month', 'input[type="month"]'); 790 | addInputsBasic('number', 'input[type="number"]'); 791 | addInputsBasic('search', 'input[type="search"]'); 792 | addInputsBasic('tel', 'input[type="tel"]'); 793 | addInputsBasic('time', 'input[type="time"]'); 794 | addInputsBasic('week', 'input[type="week"]'); 795 | addInputsBasic('password', 'input[type="password"]'); 796 | addInputsBasic('email', 'input[type="email"]'); 797 | addInputsBasic('url', 'input[type="url"]'); 798 | addInputsBasic('range', 'input[type="range"]'); 799 | addInputsBasic('textarea', 'textarea'); 800 | addInputsBasic('select', 'select:not([multiple])'); 801 | addInputsBasic('select[multiple]', 'select[multiple]'); 802 | addInputsBasic('file', 'input[type="file"]:not([multiple])'); 803 | addInputsBasic('file[multiple]', 'input[type="file"][multiple]'); 804 | addInputsBasic('hidden', 'input[type="hidden"]'); 805 | addInputsGroup('radio', 'input[type="radio"]'); 806 | addInputsGroup('checkbox', 'input[type="checkbox"]'); 807 | } 808 | 809 | return inputs; 810 | }; 811 | 812 | $.fn.inputVal = function (newValue) { 813 | var $self = $(this); 814 | 815 | var inputs = buildFormInputs({ $: $self }); 816 | 817 | if($self.is('input, textarea, select')) { 818 | if(typeof newValue === 'undefined') { 819 | return inputs[$self.attr('name')].get(); 820 | } 821 | else { 822 | inputs[$self.attr('name')].set(newValue); 823 | return $self; 824 | } 825 | } 826 | else { 827 | if(typeof newValue === 'undefined') { 828 | return call(inputs, 'get'); 829 | } 830 | else { 831 | foreach(newValue, function (value, inputName) { 832 | inputs[inputName].set(value); 833 | }); 834 | return $self; 835 | } 836 | } 837 | }; 838 | 839 | $.fn.inputOnChange = function (callback) { 840 | var $self = $(this); 841 | var inputs = buildFormInputs({ $: $self }); 842 | foreach(inputs, function (input) { 843 | input.subscribe('change', function (data) { 844 | callback.call(data.domElement, data.e); 845 | }); 846 | }); 847 | return $self; 848 | }; 849 | 850 | $.fn.inputDisable = function () { 851 | var $self = $(this); 852 | call(buildFormInputs({ $: $self }), 'disable'); 853 | return $self; 854 | }; 855 | 856 | $.fn.inputEnable = function () { 857 | var $self = $(this); 858 | call(buildFormInputs({ $: $self }), 'enable'); 859 | return $self; 860 | }; 861 | 862 | $.fn.inputClear = function () { 863 | var $self = $(this); 864 | call(buildFormInputs({ $: $self }), 'clear'); 865 | return $self; 866 | }; 867 | 868 | }(jQuery)); 869 | 870 | $.fn.repeaterVal = function () { 871 | var parse = function (raw) { 872 | var parsed = []; 873 | 874 | foreach(raw, function (val, key) { 875 | var parsedKey = []; 876 | if(key !== "undefined") { 877 | parsedKey.push(key.match(/^[^\[]*/)[0]); 878 | parsedKey = parsedKey.concat(map( 879 | key.match(/\[[^\]]*\]/g), 880 | function (bracketed) { 881 | return bracketed.replace(/[\[\]]/g, ''); 882 | } 883 | )); 884 | 885 | parsed.push({ 886 | val: val, 887 | key: parsedKey 888 | }); 889 | } 890 | }); 891 | 892 | return parsed; 893 | }; 894 | 895 | var build = function (parsed) { 896 | if( 897 | parsed.length === 1 && 898 | (parsed[0].key.length === 0 || parsed[0].key.length === 1 && !parsed[0].key[0]) 899 | ) { 900 | return parsed[0].val; 901 | } 902 | 903 | foreach(parsed, function (p) { 904 | p.head = p.key.shift(); 905 | }); 906 | 907 | var grouped = (function () { 908 | var grouped = {}; 909 | 910 | foreach(parsed, function (p) { 911 | if(!grouped[p.head]) { 912 | grouped[p.head] = []; 913 | } 914 | grouped[p.head].push(p); 915 | }); 916 | 917 | return grouped; 918 | }()); 919 | 920 | var built; 921 | 922 | if(/^[0-9]+$/.test(parsed[0].head)) { 923 | built = []; 924 | foreach(grouped, function (group) { 925 | built.push(build(group)); 926 | }); 927 | } 928 | else { 929 | built = {}; 930 | foreach(grouped, function (group, key) { 931 | built[key] = build(group); 932 | }); 933 | } 934 | 935 | return built; 936 | }; 937 | 938 | return build(parse($(this).inputVal())); 939 | }; 940 | 941 | $.fn.repeater = function (fig) { 942 | fig = fig || {}; 943 | 944 | var setList; 945 | 946 | $(this).each(function () { 947 | 948 | var $self = $(this); 949 | 950 | var show = fig.show || function () { 951 | $(this).show(); 952 | }; 953 | 954 | var hide = fig.hide || function (removeElement) { 955 | removeElement(); 956 | }; 957 | 958 | var $list = $self.find('[data-repeater-list]').first(); 959 | 960 | var $filterNested = function ($items, repeaters) { 961 | return $items.filter(function () { 962 | return repeaters ? 963 | $(this).closest( 964 | pluck(repeaters, 'selector').join(',') 965 | ).length === 0 : true; 966 | }); 967 | }; 968 | 969 | var $items = function () { 970 | return $filterNested($list.find('[data-repeater-item]'), fig.repeaters); 971 | }; 972 | 973 | var $itemTemplate = $list.find('[data-repeater-item]') 974 | .first().clone().hide(); 975 | 976 | var $firstDeleteButton = $filterNested( 977 | $filterNested($(this).find('[data-repeater-item]'), fig.repeaters) 978 | .first().find('[data-repeater-delete]'), 979 | fig.repeaters 980 | ); 981 | 982 | if(fig.isFirstItemUndeletable && $firstDeleteButton) { 983 | $firstDeleteButton.remove(); 984 | } 985 | 986 | var getGroupName = function () { 987 | var groupName = $list.data('repeater-list'); 988 | return fig.$parent ? 989 | fig.$parent.data('item-name') + '[' + groupName + ']' : 990 | groupName; 991 | }; 992 | 993 | var initNested = function ($listItems) { 994 | if(fig.repeaters) { 995 | $listItems.each(function () { 996 | var $item = $(this); 997 | foreach(fig.repeaters, function (nestedFig) { 998 | $item.find(nestedFig.selector).repeater(extend( 999 | nestedFig, { $parent: $item } 1000 | )); 1001 | }); 1002 | }); 1003 | } 1004 | }; 1005 | 1006 | var $foreachRepeaterInItem = function (repeaters, $item, cb) { 1007 | if(repeaters) { 1008 | foreach(repeaters, function (nestedFig) { 1009 | cb.call($item.find(nestedFig.selector)[0], nestedFig); 1010 | }); 1011 | } 1012 | }; 1013 | 1014 | var setIndexes = function ($items, groupName, repeaters) { 1015 | $items.each(function (index) { 1016 | var $item = $(this); 1017 | $item.data('item-name', groupName + '[' + index + ']'); 1018 | $filterNested($item.find('[name]'), repeaters) 1019 | .each(function () { 1020 | var $input = $(this); 1021 | // match non empty brackets (ex: "[foo]") 1022 | var matches = $input.attr('name').match(/\[[^\]]+\]/g); 1023 | 1024 | var name = matches ? 1025 | // strip "[" and "]" characters 1026 | last(matches).replace(/\[|\]/g, '') : 1027 | $input.attr('name'); 1028 | 1029 | 1030 | var newName = groupName + '[' + index + '][' + name + ']' + 1031 | ($input.is(':checkbox') || $input.attr('multiple') ? '[]' : ''); 1032 | 1033 | $input.attr('name', newName); 1034 | 1035 | $foreachRepeaterInItem(repeaters, $item, function (nestedFig) { 1036 | var $repeater = $(this); 1037 | setIndexes( 1038 | $filterNested($repeater.find('[data-repeater-item]'), nestedFig.repeaters || []), 1039 | groupName + '[' + index + ']' + 1040 | '[' + $repeater.find('[data-repeater-list]').first().data('repeater-list') + ']', 1041 | nestedFig.repeaters 1042 | ); 1043 | }); 1044 | }); 1045 | }); 1046 | 1047 | $list.find('input[name][checked]') 1048 | .removeAttr('checked') 1049 | .prop('checked', true); 1050 | }; 1051 | 1052 | setIndexes($items(), getGroupName(), fig.repeaters); 1053 | initNested($items()); 1054 | if(fig.initEmpty) { 1055 | $items().remove(); 1056 | } 1057 | 1058 | if(fig.ready) { 1059 | fig.ready(function () { 1060 | setIndexes($items(), getGroupName(), fig.repeaters); 1061 | }); 1062 | } 1063 | 1064 | var appendItem = (function () { 1065 | var setItemsValues = function ($item, data, repeaters) { 1066 | if(data || fig.defaultValues) { 1067 | var inputNames = {}; 1068 | $filterNested($item.find('[name]'), repeaters).each(function () { 1069 | var key = $(this).attr('name').match(/\[([^\]]*)(\]|\]\[\])$/)[1]; 1070 | inputNames[key] = $(this).attr('name'); 1071 | }); 1072 | 1073 | $item.inputVal(map( 1074 | filter(data || fig.defaultValues, function (val, name) { 1075 | return inputNames[name]; 1076 | }), 1077 | identity, 1078 | function (name) { 1079 | return inputNames[name]; 1080 | } 1081 | )); 1082 | } 1083 | 1084 | 1085 | $foreachRepeaterInItem(repeaters, $item, function (nestedFig) { 1086 | var $repeater = $(this); 1087 | $filterNested( 1088 | $repeater.find('[data-repeater-item]'), 1089 | nestedFig.repeaters 1090 | ) 1091 | .each(function () { 1092 | var fieldName = $repeater.find('[data-repeater-list]').data('repeater-list'); 1093 | if(data && data[fieldName]) { 1094 | var $template = $(this).clone(); 1095 | $repeater.find('[data-repeater-item]').remove(); 1096 | foreach(data[fieldName], function (data) { 1097 | var $item = $template.clone(); 1098 | setItemsValues( 1099 | $item, 1100 | data, 1101 | nestedFig.repeaters || [] 1102 | ); 1103 | $repeater.find('[data-repeater-list]').append($item); 1104 | }); 1105 | } 1106 | else { 1107 | setItemsValues( 1108 | $(this), 1109 | nestedFig.defaultValues, 1110 | nestedFig.repeaters || [] 1111 | ); 1112 | } 1113 | }); 1114 | }); 1115 | 1116 | }; 1117 | 1118 | return function ($item, data) { 1119 | $list.append($item); 1120 | setIndexes($items(), getGroupName(), fig.repeaters); 1121 | $item.find('[name]').each(function () { 1122 | $(this).inputClear(); 1123 | }); 1124 | setItemsValues($item, data || fig.defaultValues, fig.repeaters); 1125 | }; 1126 | }()); 1127 | 1128 | var addItem = function (data) { 1129 | var $item = $itemTemplate.clone(); 1130 | appendItem($item, data); 1131 | if(fig.repeaters) { 1132 | initNested($item); 1133 | } 1134 | show.call($item.get(0)); 1135 | }; 1136 | 1137 | setList = function (rows) { 1138 | $items().remove(); 1139 | foreach(rows, addItem); 1140 | }; 1141 | 1142 | $filterNested($self.find('[data-repeater-create]'), fig.repeaters).click(function () { 1143 | addItem(); 1144 | }); 1145 | 1146 | $list.on('click', '[data-repeater-delete]', function () { 1147 | var self = $(this).closest('[data-repeater-item]').get(0); 1148 | hide.call(self, function () { 1149 | $(self).remove(); 1150 | setIndexes($items(), getGroupName(), fig.repeaters); 1151 | }); 1152 | }); 1153 | }); 1154 | 1155 | this.setList = setList; 1156 | 1157 | return this; 1158 | }; 1159 | 1160 | }(jQuery)); -------------------------------------------------------------------------------- /jquery.repeater.min.js: -------------------------------------------------------------------------------- 1 | // jquery.repeater version 1.2.2 2 | // https://github.com/DubFriend/jquery.repeater 3 | // (MIT) 15-04-2022 4 | // Brian Detering (http://www.briandetering.net/) 5 | 6 | !function(a){"use strict";var b=function(a){return a},c=function(b){return a.isArray(b)},d=function(a){return!c(a)&&a instanceof Object},e=function(b,c){return a.inArray(c,b)},f=function(a,b){return-1!==e(a,b)},g=function(a,b){for(var c in a)a.hasOwnProperty(c)&&b(a[c],c,a)},h=function(a){return a[a.length-1]},i=function(a){return Array.prototype.slice.call(a)},j=function(){var a={};return g(i(arguments),function(b){g(b,function(b,c){a[c]=b})}),a},k=function(a,b){var c=[];return g(a,function(a,d,e){c.push(b(a,d,e))}),c},l=function(a,b,c){var d={};return g(a,function(a,e,f){e=c?c(e,a):e,d[e]=b(a,e,f)}),d},m=function(a,b,d){return c(a)?k(a,b):l(a,b,d)},n=function(a,b){return m(a,function(a){return a[b]})},o=function(a,b){var d;return c(a)?(d=[],g(a,function(a,c,e){b(a,c,e)&&d.push(a)})):(d={},g(a,function(a,c,e){b(a,c,e)&&(d[c]=a)})),d},p=function(a,b,c){return m(a,function(a,d){return a[b].apply(a,c||[])})},q=function(a){a=a||{};var b={};return a.publish=function(a,c){g(b[a],function(a){a(c)})},a.subscribe=function(a,c){b[a]=b[a]||[],b[a].push(c)},a.unsubscribe=function(a){g(b,function(b){var c=e(b,a);-1!==c&&b.splice(c,1)})},a};!function(a){var b=function(a,b){var c=q(),d=a.$;return c.getType=function(){throw'implement me (return type. "text", "radio", etc.)'},c.$=function(a){return a?d.find(a):d},c.disable=function(){c.$().prop("disabled",!0),c.publish("isEnabled",!1)},c.enable=function(){c.$().prop("disabled",!1),c.publish("isEnabled",!0)},b.equalTo=function(a,b){return a===b},b.publishChange=function(){var a;return function(d,e){var f=c.get();b.equalTo(f,a)||c.publish("change",{e:d,domElement:e}),a=f}}(),c},i=function(a,c){var d=b(a,c);return d.get=function(){return d.$().val()},d.set=function(a){d.$().val(a)},d.clear=function(){d.set("")},c.buildSetter=function(a){return function(b){a.call(d,b)}},d},j=function(a){var b={},c=i(a,b);return c.getType=function(){return"text"},c.$().on("change keyup keydown",function(a){b.publishChange(a,this)}),c},k=function(a,b){a=c(a)?a:[a],b=c(b)?b:[b];var d=!0;return a.length!==b.length?d=!1:g(a,function(a){f(b,a)||(d=!1)}),d},l=function(a){var b={},c=i(a,b);return c.getType=function(){return"button"},c.$().on("change",function(a){b.publishChange(a,this)}),c},m=function(b){var d={},e=i(b,d);return e.getType=function(){return"checkbox"},e.get=function(){var b=[];return e.$().filter(":checked").each(function(){b.push(a(this).val())}),b},e.set=function(b){b=c(b)?b:[b],e.$().each(function(){a(this).prop("checked",!1)}),g(b,function(a){e.$().filter('[value="'+a+'"]').prop("checked",!0)})},d.equalTo=k,e.$().change(function(a){d.publishChange(a,this)}),e},n=function(a){var b=j(a);return b.getType=function(){return"email"},b},o=function(c){var d={},e=b(c,d);return e.getType=function(){return"file"},e.get=function(){return h(e.$().val().split("\\"))},e.clear=function(){this.$().each(function(){a(this).wrap("").closest("form").get(0).reset(),a(this).unwrap()})},e.$().change(function(a){d.publishChange(a,this)}),e},r=function(a){var b={},c=i(a,b);return c.getType=function(){return"hidden"},c.$().change(function(a){b.publishChange(a,this)}),c},s=function(c){var d={},e=b(c,d);return e.getType=function(){return"file[multiple]"},e.get=function(){var a,b=e.$().get(0).files||[],c=[];for(a=0;a<(b.length||0);a+=1)c.push(b[a].name);return c},e.clear=function(){this.$().each(function(){a(this).wrap("").closest("form").get(0).reset(),a(this).unwrap()})},e.$().change(function(a){d.publishChange(a,this)}),e},t=function(a){var b={},d=i(a,b);return d.getType=function(){return"select[multiple]"},d.get=function(){return d.$().val()||[]},d.set=function(a){d.$().val(""===a?[]:c(a)?a:[a])},b.equalTo=k,d.$().change(function(a){b.publishChange(a,this)}),d},u=function(a){var b=j(a);return b.getType=function(){return"password"},b},v=function(b){var c={},d=i(b,c);return d.getType=function(){return"radio"},d.get=function(){return d.$().filter(":checked").val()||null},d.set=function(b){b?d.$().filter('[value="'+b+'"]').prop("checked",!0):d.$().each(function(){a(this).prop("checked",!1)})},d.$().change(function(a){c.publishChange(a,this)}),d},w=function(a){var b={},c=i(a,b);return c.getType=function(){return"range"},c.$().change(function(a){b.publishChange(a,this)}),c},x=function(a){var b={},c=i(a,b);return c.getType=function(){return"select"},c.$().change(function(a){b.publishChange(a,this)}),c},y=function(a){var b={},c=i(a,b);return c.getType=function(){return"textarea"},c.$().on("change keyup keydown",function(a){b.publishChange(a,this)}),c},z=function(a){var b=j(a);return b.getType=function(){return"url"},b},A=function(a){var b=j(a);return b.getType=function(){return"color"},b},B=function(a){var b=j(a);return b.getType=function(){return"date"},b},C=function(a){var b=j(a);return b.getType=function(){return"datetime-local"},b},D=function(a){var b=j(a);return b.getType=function(){return"month"},b},E=function(a){var b=j(a);return b.getType=function(){return"number"},b},F=function(a){var b=j(a);return b.getType=function(){return"search"},b},G=function(a){var b=j(a);return b.getType=function(){return"tel"},b},H=function(a){var b=j(a);return b.getType=function(){return"time"},b},I=function(a){var b=j(a);return b.getType=function(){return"week"},b},J=function(b){var c={},f=b.$,h=b.constructorOverride||{color:A,date:B,datetimeLocal:C,month:D,number:E,search:F,tel:G,time:H,week:I,button:l,text:j,url:z,email:n,password:u,range:w,textarea:y,select:x,"select[multiple]":t,radio:v,checkbox:m,file:o,"file[multiple]":s,hidden:r},i=function(b,e){(d(e)?e:f.find(e)).each(function(){var d=a(this).attr("name");c[d]=h[b]({$:a(this)})})},k=function(b,i){var j=[],k=d(i)?i:f.find(i);d(i)?c[k.attr("name")]=h[b]({$:k}):(k.each(function(){-1===e(j,a(this).attr("name"))&&j.push(a(this).attr("name"))}),g(j,function(a){c[a]=h[b]({$:f.find('input[name="'+a+'"]')})}))};return f.is("input, select, textarea")?f.is('input[type="button"], button, input[type="submit"]')?i("button",f):f.is("textarea")?i("textarea",f):f.is('input[type="text"]')||f.is("input")&&!f.attr("type")?i("text",f):f.is('input[type="password"]')?i("password",f):f.is('input[type="email"]')?i("email",f):f.is('input[type="url"]')?i("url",f):f.is('input[type="color"]')?i("color",f):f.is('input[type="date"]')?i("date",f):f.is('input[type="datetime-local"]')?i("datetimeLocal",f):f.is('input[type="month"]')?i("month",f):f.is('input[type="number"]')?i("number",f):f.is('input[type="search"]')?i("search",f):f.is('input[type="tel"]')?i("tel",f):f.is('input[type="time"]')?i("time",f):f.is('input[type="week"]')?i("week",f):f.is('input[type="range"]')?i("range",f):f.is("select")?f.is("[multiple]")?i("select[multiple]",f):i("select",f):f.is('input[type="file"]')?f.is("[multiple]")?i("file[multiple]",f):i("file",f):f.is('input[type="hidden"]')?i("hidden",f):f.is('input[type="radio"]')?k("radio",f):f.is('input[type="checkbox"]')?k("checkbox",f):i("text",f):(i("button",'input[type="button"], button, input[type="submit"]'),i("text",'input[type="text"]'),i("color",'input[type="color"]'),i("date",'input[type="date"]'),i("datetimeLocal",'input[type="datetime-local"]'),i("month",'input[type="month"]'),i("number",'input[type="number"]'),i("search",'input[type="search"]'),i("tel",'input[type="tel"]'),i("time",'input[type="time"]'),i("week",'input[type="week"]'),i("password",'input[type="password"]'),i("email",'input[type="email"]'),i("url",'input[type="url"]'),i("range",'input[type="range"]'),i("textarea","textarea"),i("select","select:not([multiple])"),i("select[multiple]","select[multiple]"),i("file",'input[type="file"]:not([multiple])'),i("file[multiple]",'input[type="file"][multiple]'),i("hidden",'input[type="hidden"]'),k("radio",'input[type="radio"]'),k("checkbox",'input[type="checkbox"]')),c};a.fn.inputVal=function(b){var c=a(this),d=J({$:c});return c.is("input, textarea, select")?void 0===b?d[c.attr("name")].get():(d[c.attr("name")].set(b),c):void 0===b?p(d,"get"):(g(b,function(a,b){d[b].set(a)}),c)},a.fn.inputOnChange=function(b){var c=a(this),d=J({$:c});return g(d,function(a){a.subscribe("change",function(a){b.call(a.domElement,a.e)})}),c},a.fn.inputDisable=function(){var b=a(this);return p(J({$:b}),"disable"),b},a.fn.inputEnable=function(){var b=a(this);return p(J({$:b}),"enable"),b},a.fn.inputClear=function(){var b=a(this);return p(J({$:b}),"clear"),b}}(jQuery),a.fn.repeaterVal=function(){var b=function(a){if(1===a.length&&(0===a[0].key.length||1===a[0].key.length&&!a[0].key[0]))return a[0].val;g(a,function(a){a.head=a.key.shift()});var c,d=function(){var b={};return g(a,function(a){b[a.head]||(b[a.head]=[]),b[a.head].push(a)}),b}();return/^[0-9]+$/.test(a[0].head)?(c=[],g(d,function(a){c.push(b(a))})):(c={},g(d,function(a,d){c[d]=b(a)})),c};return b(function(a){var b=[];return g(a,function(a,c){var d=[];"undefined"!==c&&(d.push(c.match(/^[^\[]*/)[0]),d=d.concat(m(c.match(/\[[^\]]*\]/g),function(a){return a.replace(/[\[\]]/g,"")})),b.push({val:a,key:d}))}),b}(a(this).inputVal()))},a.fn.repeater=function(c){c=c||{};var d;return a(this).each(function(){var e=a(this),f=c.show||function(){a(this).show()},i=c.hide||function(a){a()},k=e.find("[data-repeater-list]").first(),l=function(b,c){return b.filter(function(){return!c||0===a(this).closest(n(c,"selector").join(",")).length})},p=function(){return l(k.find("[data-repeater-item]"),c.repeaters)},q=k.find("[data-repeater-item]").first().clone().hide(),r=l(l(a(this).find("[data-repeater-item]"),c.repeaters).first().find("[data-repeater-delete]"),c.repeaters);c.isFirstItemUndeletable&&r&&r.remove();var s=function(){var a=k.data("repeater-list");return c.$parent?c.$parent.data("item-name")+"["+a+"]":a},t=function(b){c.repeaters&&b.each(function(){var b=a(this);g(c.repeaters,function(a){b.find(a.selector).repeater(j(a,{$parent:b}))})})},u=function(a,b,c){a&&g(a,function(a){c.call(b.find(a.selector)[0],a)})},v=function(b,c,d){b.each(function(b){var e=a(this);e.data("item-name",c+"["+b+"]"),l(e.find("[name]"),d).each(function(){var f=a(this),g=f.attr("name").match(/\[[^\]]+\]/g),i=g?h(g).replace(/\[|\]/g,""):f.attr("name"),j=c+"["+b+"]["+i+"]"+(f.is(":checkbox")||f.attr("multiple")?"[]":"");f.attr("name",j),u(d,e,function(d){var e=a(this);v(l(e.find("[data-repeater-item]"),d.repeaters||[]),c+"["+b+"]["+e.find("[data-repeater-list]").first().data("repeater-list")+"]",d.repeaters)})})}),k.find("input[name][checked]").removeAttr("checked").prop("checked",!0)};v(p(),s(),c.repeaters),t(p()),c.initEmpty&&p().remove(),c.ready&&c.ready(function(){v(p(),s(),c.repeaters)});var w=function(){var d=function(e,f,h){if(f||c.defaultValues){var i={};l(e.find("[name]"),h).each(function(){var b=a(this).attr("name").match(/\[([^\]]*)(\]|\]\[\])$/)[1];i[b]=a(this).attr("name")}),e.inputVal(m(o(f||c.defaultValues,function(a,b){return i[b]}),b,function(a){return i[a]}))}u(h,e,function(b){var c=a(this);l(c.find("[data-repeater-item]"),b.repeaters).each(function(){var e=c.find("[data-repeater-list]").data("repeater-list");if(f&&f[e]){var h=a(this).clone();c.find("[data-repeater-item]").remove(),g(f[e],function(a){var e=h.clone();d(e,a,b.repeaters||[]),c.find("[data-repeater-list]").append(e)})}else d(a(this),b.defaultValues,b.repeaters||[])})})};return function(b,e){k.append(b),v(p(),s(),c.repeaters),b.find("[name]").each(function(){a(this).inputClear()}),d(b,e||c.defaultValues,c.repeaters)}}(),x=function(a){var b=q.clone();w(b,a),c.repeaters&&t(b),f.call(b.get(0))};d=function(a){p().remove(),g(a,x)},l(e.find("[data-repeater-create]"),c.repeaters).click(function(){x()}),k.on("click","[data-repeater-delete]",function(){var b=a(this).closest("[data-repeater-item]").get(0);i.call(b,function(){a(b).remove(),v(p(),s(),c.repeaters)})})}),this.setList=d,this}}(jQuery); -------------------------------------------------------------------------------- /nested-repeater.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.repeater", 3 | "version": "1.2.2", 4 | "description": "repeatable form input interface", 5 | "main": "jquery.repeater.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "grunt test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/DubFriend/jquery.repeater" 15 | }, 16 | "keywords": [ 17 | "input", 18 | "repeat", 19 | "multiple", 20 | "form" 21 | ], 22 | "author": "Brian Detering (http://www.briandetering.net/)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/DubFriend/jquery.repeater/issues" 26 | }, 27 | "homepage": "https://github.com/DubFriend/jquery.repeater", 28 | "devDependencies": { 29 | "grunt": "^1.0.1", 30 | "grunt-contrib-concat": "^1.0.1", 31 | "grunt-contrib-qunit": "^1.2.0", 32 | "grunt-contrib-uglify": "^2.0.0", 33 | "grunt-contrib-watch": "^1.0.0", 34 | "grunt-preprocess": "^5.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /repeater.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 85 | 86 | 90 | 91 | 92 |
93 |
94 | 95 |
96 | -------------------------------------------------------------------------------- /repeater.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repeater", 3 | "version": "1.2.1", 4 | "title": "Repeater", 5 | "author": { 6 | "name": "Brian Detering", 7 | "url": "http://briandetering.net/", 8 | "email": "BDeterin@gmail.com" 9 | }, 10 | "licenses": [ 11 | { 12 | "type": "MIT", 13 | "url": "http://opensource.org/licenses/MIT" 14 | } 15 | ], 16 | "description": "An interface to add and remove a repeatable group of input elements.", 17 | "keywords": [ 18 | "input", 19 | "repeat", 20 | "multiple", 21 | "form", 22 | "jquery-plugin" 23 | ], 24 | "docs": "https://github.com/DubFriend/jquery.repeater/blob/master/README.md", 25 | "demo": "http://briandetering.net/repeater", 26 | "homepage": "https://github.com/DubFriend/jquery.repeater", 27 | "dependencies": { 28 | "jquery": ">=1.11" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/intro.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | -------------------------------------------------------------------------------- /src/jquery.input.js: -------------------------------------------------------------------------------- 1 | // jquery.input version 0.0.0 2 | // https://github.com/DubFriend/jquery.input 3 | // (MIT) 09-04-2014 4 | // Brian Detering (http://www.briandetering.net/) 5 | (function ($) { 6 | 'use strict'; 7 | 8 | var createBaseInput = function (fig, my) { 9 | var self = mixinPubSub(), 10 | $self = fig.$; 11 | 12 | self.getType = function () { 13 | throw 'implement me (return type. "text", "radio", etc.)'; 14 | }; 15 | 16 | self.$ = function (selector) { 17 | return selector ? $self.find(selector) : $self; 18 | }; 19 | 20 | self.disable = function () { 21 | self.$().prop('disabled', true); 22 | self.publish('isEnabled', false); 23 | }; 24 | 25 | self.enable = function () { 26 | self.$().prop('disabled', false); 27 | self.publish('isEnabled', true); 28 | }; 29 | 30 | my.equalTo = function (a, b) { 31 | return a === b; 32 | }; 33 | 34 | my.publishChange = (function () { 35 | var oldValue; 36 | return function (e, domElement) { 37 | var newValue = self.get(); 38 | if(!my.equalTo(newValue, oldValue)) { 39 | self.publish('change', { e: e, domElement: domElement }); 40 | } 41 | oldValue = newValue; 42 | }; 43 | }()); 44 | 45 | return self; 46 | }; 47 | 48 | 49 | var createInput = function (fig, my) { 50 | var self = createBaseInput(fig, my); 51 | 52 | self.get = function () { 53 | return self.$().val(); 54 | }; 55 | 56 | self.set = function (newValue) { 57 | self.$().val(newValue); 58 | }; 59 | 60 | self.clear = function () { 61 | self.set(''); 62 | }; 63 | 64 | my.buildSetter = function (callback) { 65 | return function (newValue) { 66 | callback.call(self, newValue); 67 | }; 68 | }; 69 | 70 | return self; 71 | }; 72 | 73 | var createInputText = function (fig) { 74 | var my = {}, 75 | self = createInput(fig, my); 76 | 77 | self.getType = function () { 78 | return 'text'; 79 | }; 80 | 81 | self.$().on('change keyup keydown', function (e) { 82 | my.publishChange(e, this); 83 | }); 84 | 85 | return self; 86 | }; 87 | 88 | var inputEqualToArray = function (a, b) { 89 | a = isArray(a) ? a : [a]; 90 | b = isArray(b) ? b : [b]; 91 | 92 | var isEqual = true; 93 | if(a.length !== b.length) { 94 | isEqual = false; 95 | } 96 | else { 97 | foreach(a, function (value) { 98 | if(!inArray(b, value)) { 99 | isEqual = false; 100 | } 101 | }); 102 | } 103 | 104 | return isEqual; 105 | }; 106 | 107 | var createInputButton = function (fig) { 108 | var my = {}, 109 | self = createInput(fig, my); 110 | 111 | self.getType = function () { 112 | return 'button'; 113 | }; 114 | 115 | self.$().on('change', function (e) { 116 | my.publishChange(e, this); 117 | }); 118 | 119 | return self; 120 | }; 121 | 122 | var createInputCheckbox = function (fig) { 123 | var my = {}, 124 | self = createInput(fig, my); 125 | 126 | self.getType = function () { 127 | return 'checkbox'; 128 | }; 129 | 130 | self.get = function () { 131 | var values = []; 132 | self.$().filter(':checked').each(function () { 133 | values.push($(this).val()); 134 | }); 135 | return values; 136 | }; 137 | 138 | self.set = function (newValues) { 139 | newValues = isArray(newValues) ? newValues : [newValues]; 140 | 141 | self.$().each(function () { 142 | $(this).prop('checked', false); 143 | }); 144 | 145 | foreach(newValues, function (value) { 146 | self.$().filter('[value="' + value + '"]') 147 | .prop('checked', true); 148 | }); 149 | }; 150 | 151 | my.equalTo = inputEqualToArray; 152 | 153 | self.$().change(function (e) { 154 | my.publishChange(e, this); 155 | }); 156 | 157 | return self; 158 | }; 159 | 160 | var createInputEmail = function (fig) { 161 | var my = {}, 162 | self = createInputText(fig, my); 163 | 164 | self.getType = function () { 165 | return 'email'; 166 | }; 167 | 168 | return self; 169 | }; 170 | 171 | var createInputFile = function (fig) { 172 | var my = {}, 173 | self = createBaseInput(fig, my); 174 | 175 | self.getType = function () { 176 | return 'file'; 177 | }; 178 | 179 | self.get = function () { 180 | return last(self.$().val().split('\\')); 181 | }; 182 | 183 | self.clear = function () { 184 | // http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery 185 | this.$().each(function () { 186 | $(this).wrap('
').closest('form').get(0).reset(); 187 | $(this).unwrap(); 188 | }); 189 | }; 190 | 191 | self.$().change(function (e) { 192 | my.publishChange(e, this); 193 | // self.publish('change', self); 194 | }); 195 | 196 | return self; 197 | }; 198 | 199 | var createInputHidden = function (fig) { 200 | var my = {}, 201 | self = createInput(fig, my); 202 | 203 | self.getType = function () { 204 | return 'hidden'; 205 | }; 206 | 207 | self.$().change(function (e) { 208 | my.publishChange(e, this); 209 | }); 210 | 211 | return self; 212 | }; 213 | var createInputMultipleFile = function (fig) { 214 | var my = {}, 215 | self = createBaseInput(fig, my); 216 | 217 | self.getType = function () { 218 | return 'file[multiple]'; 219 | }; 220 | 221 | self.get = function () { 222 | // http://stackoverflow.com/questions/14035530/how-to-get-value-of-html-5-multiple-file-upload-variable-using-jquery 223 | var fileListObject = self.$().get(0).files || [], 224 | names = [], i; 225 | 226 | for(i = 0; i < (fileListObject.length || 0); i += 1) { 227 | names.push(fileListObject[i].name); 228 | } 229 | 230 | return names; 231 | }; 232 | 233 | self.clear = function () { 234 | // http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery 235 | this.$().each(function () { 236 | $(this).wrap('').closest('form').get(0).reset(); 237 | $(this).unwrap(); 238 | }); 239 | }; 240 | 241 | self.$().change(function (e) { 242 | my.publishChange(e, this); 243 | }); 244 | 245 | return self; 246 | }; 247 | 248 | var createInputMultipleSelect = function (fig) { 249 | var my = {}, 250 | self = createInput(fig, my); 251 | 252 | self.getType = function () { 253 | return 'select[multiple]'; 254 | }; 255 | 256 | self.get = function () { 257 | return self.$().val() || []; 258 | }; 259 | 260 | self.set = function (newValues) { 261 | self.$().val( 262 | newValues === '' ? [] : isArray(newValues) ? newValues : [newValues] 263 | ); 264 | }; 265 | 266 | my.equalTo = inputEqualToArray; 267 | 268 | self.$().change(function (e) { 269 | my.publishChange(e, this); 270 | }); 271 | 272 | return self; 273 | }; 274 | 275 | var createInputPassword = function (fig) { 276 | var my = {}, 277 | self = createInputText(fig, my); 278 | 279 | self.getType = function () { 280 | return 'password'; 281 | }; 282 | 283 | return self; 284 | }; 285 | 286 | var createInputRadio = function (fig) { 287 | var my = {}, 288 | self = createInput(fig, my); 289 | 290 | self.getType = function () { 291 | return 'radio'; 292 | }; 293 | 294 | self.get = function () { 295 | return self.$().filter(':checked').val() || null; 296 | }; 297 | 298 | self.set = function (newValue) { 299 | if(!newValue) { 300 | self.$().each(function () { 301 | $(this).prop('checked', false); 302 | }); 303 | } 304 | else { 305 | self.$().filter('[value="' + newValue + '"]').prop('checked', true); 306 | } 307 | }; 308 | 309 | self.$().change(function (e) { 310 | my.publishChange(e, this); 311 | }); 312 | 313 | return self; 314 | }; 315 | 316 | var createInputRange = function (fig) { 317 | var my = {}, 318 | self = createInput(fig, my); 319 | 320 | self.getType = function () { 321 | return 'range'; 322 | }; 323 | 324 | self.$().change(function (e) { 325 | my.publishChange(e, this); 326 | }); 327 | 328 | return self; 329 | }; 330 | 331 | var createInputSelect = function (fig) { 332 | var my = {}, 333 | self = createInput(fig, my); 334 | 335 | self.getType = function () { 336 | return 'select'; 337 | }; 338 | 339 | self.$().change(function (e) { 340 | my.publishChange(e, this); 341 | }); 342 | 343 | return self; 344 | }; 345 | 346 | var createInputTextarea = function (fig) { 347 | var my = {}, 348 | self = createInput(fig, my); 349 | 350 | self.getType = function () { 351 | return 'textarea'; 352 | }; 353 | 354 | self.$().on('change keyup keydown', function (e) { 355 | my.publishChange(e, this); 356 | }); 357 | 358 | return self; 359 | }; 360 | 361 | var createInputURL = function (fig) { 362 | var my = {}, 363 | self = createInputText(fig, my); 364 | 365 | self.getType = function () { 366 | return 'url'; 367 | }; 368 | 369 | return self; 370 | }; 371 | 372 | var createInputColor = function(fig) { 373 | var my = {}, 374 | self = createInputText(fig, my); 375 | 376 | self.getType = function () { 377 | return 'color'; 378 | }; 379 | 380 | return self; 381 | }; 382 | 383 | var createInputDate = function(fig) { 384 | var my = {}, 385 | self = createInputText(fig, my); 386 | 387 | self.getType = function () { 388 | return 'date'; 389 | }; 390 | 391 | return self; 392 | }; 393 | 394 | var createInputDateTimeLocal = function(fig) { 395 | var my = {}, 396 | self = createInputText(fig, my); 397 | 398 | self.getType = function () { 399 | return 'datetime-local'; 400 | }; 401 | 402 | return self; 403 | }; 404 | 405 | var createInputMonth = function(fig) { 406 | var my = {}, 407 | self = createInputText(fig, my); 408 | 409 | self.getType = function () { 410 | return 'month'; 411 | }; 412 | 413 | return self; 414 | }; 415 | 416 | var createInputNumber = function(fig) { 417 | var my = {}, 418 | self = createInputText(fig, my); 419 | 420 | self.getType = function () { 421 | return 'number'; 422 | }; 423 | 424 | return self; 425 | }; 426 | 427 | var createInputSearch = function(fig) { 428 | var my = {}, 429 | self = createInputText(fig, my); 430 | 431 | self.getType = function () { 432 | return 'search'; 433 | }; 434 | 435 | return self; 436 | }; 437 | 438 | var createInputTel = function(fig) { 439 | var my = {}, 440 | self = createInputText(fig, my); 441 | 442 | self.getType = function () { 443 | return 'tel'; 444 | }; 445 | 446 | return self; 447 | }; 448 | 449 | var createInputTime = function(fig) { 450 | var my = {}, 451 | self = createInputText(fig, my); 452 | 453 | self.getType = function () { 454 | return 'time'; 455 | }; 456 | 457 | return self; 458 | }; 459 | 460 | var createInputWeek = function(fig) { 461 | var my = {}, 462 | self = createInputText(fig, my); 463 | 464 | self.getType = function () { 465 | return 'week'; 466 | }; 467 | 468 | return self; 469 | }; 470 | 471 | 472 | var buildFormInputs = function (fig) { 473 | var inputs = {}, 474 | $self = fig.$; 475 | var constructor = fig.constructorOverride || { 476 | color:createInputColor, 477 | date:createInputDate, 478 | datetimeLocal:createInputDateTimeLocal, 479 | month:createInputMonth, 480 | number:createInputNumber, 481 | search:createInputSearch, 482 | tel:createInputTel, 483 | time:createInputTime, 484 | week:createInputWeek, 485 | button: createInputButton, 486 | text: createInputText, 487 | url: createInputURL, 488 | email: createInputEmail, 489 | password: createInputPassword, 490 | range: createInputRange, 491 | textarea: createInputTextarea, 492 | select: createInputSelect, 493 | 'select[multiple]': createInputMultipleSelect, 494 | radio: createInputRadio, 495 | checkbox: createInputCheckbox, 496 | file: createInputFile, 497 | 'file[multiple]': createInputMultipleFile, 498 | hidden: createInputHidden 499 | }; 500 | 501 | var addInputsBasic = function (type, selector) { 502 | var $input = isObject(selector) ? selector : $self.find(selector); 503 | 504 | $input.each(function () { 505 | var name = $(this).attr('name'); 506 | inputs[name] = constructor[type]({ 507 | $: $(this) 508 | }); 509 | }); 510 | }; 511 | 512 | var addInputsGroup = function (type, selector) { 513 | var names = [], 514 | $input = isObject(selector) ? selector : $self.find(selector); 515 | 516 | if(isObject(selector)) { 517 | inputs[$input.attr('name')] = constructor[type]({ 518 | $: $input 519 | }); 520 | } 521 | else { 522 | // group by name attribute 523 | $input.each(function () { 524 | if(indexOf(names, $(this).attr('name')) === -1) { 525 | names.push($(this).attr('name')); 526 | } 527 | }); 528 | 529 | foreach(names, function (name) { 530 | inputs[name] = constructor[type]({ 531 | $: $self.find('input[name="' + name + '"]') 532 | }); 533 | }); 534 | } 535 | }; 536 | 537 | 538 | if($self.is('input, select, textarea')) { 539 | if($self.is('input[type="button"], button, input[type="submit"]')) { 540 | addInputsBasic('button', $self); 541 | } 542 | else if($self.is('textarea')) { 543 | addInputsBasic('textarea', $self); 544 | } 545 | else if( 546 | $self.is('input[type="text"]') || 547 | $self.is('input') && !$self.attr('type') 548 | ) { 549 | addInputsBasic('text', $self); 550 | } 551 | else if($self.is('input[type="password"]')) { 552 | addInputsBasic('password', $self); 553 | } 554 | else if($self.is('input[type="email"]')) { 555 | addInputsBasic('email', $self); 556 | } 557 | else if($self.is('input[type="url"]')) { 558 | addInputsBasic('url', $self); 559 | } 560 | else if($self.is('input[type="color"]')) { 561 | addInputsBasic('color', $self); 562 | } 563 | else if($self.is('input[type="date"]')) { 564 | addInputsBasic('date', $self); 565 | } 566 | else if($self.is('input[type="datetime-local"]')) { 567 | addInputsBasic('datetimeLocal', $self); 568 | } 569 | else if($self.is('input[type="month"]')) { 570 | addInputsBasic('month', $self); 571 | } 572 | else if($self.is('input[type="number"]')) { 573 | addInputsBasic('number', $self); 574 | } 575 | else if($self.is('input[type="search"]')) { 576 | addInputsBasic('search', $self); 577 | } 578 | else if($self.is('input[type="tel"]')) { 579 | addInputsBasic('tel', $self); 580 | } 581 | else if($self.is('input[type="time"]')) { 582 | addInputsBasic('time', $self); 583 | } 584 | else if($self.is('input[type="week"]')) { 585 | addInputsBasic('week', $self); 586 | } 587 | else if($self.is('input[type="range"]')) { 588 | addInputsBasic('range', $self); 589 | } 590 | else if($self.is('select')) { 591 | if($self.is('[multiple]')) { 592 | addInputsBasic('select[multiple]', $self); 593 | } 594 | else { 595 | addInputsBasic('select', $self); 596 | } 597 | } 598 | else if($self.is('input[type="file"]')) { 599 | if($self.is('[multiple]')) { 600 | addInputsBasic('file[multiple]', $self); 601 | } 602 | else { 603 | addInputsBasic('file', $self); 604 | } 605 | } 606 | else if($self.is('input[type="hidden"]')) { 607 | addInputsBasic('hidden', $self); 608 | } 609 | else if($self.is('input[type="radio"]')) { 610 | addInputsGroup('radio', $self); 611 | } 612 | else if($self.is('input[type="checkbox"]')) { 613 | addInputsGroup('checkbox', $self); 614 | } 615 | else { 616 | //in all other cases default to a "text" input interface. 617 | addInputsBasic('text', $self); 618 | } 619 | } 620 | else { 621 | addInputsBasic('button', 'input[type="button"], button, input[type="submit"]'); 622 | addInputsBasic('text', 'input[type="text"]'); 623 | addInputsBasic('color', 'input[type="color"]'); 624 | addInputsBasic('date', 'input[type="date"]'); 625 | addInputsBasic('datetimeLocal', 'input[type="datetime-local"]'); 626 | addInputsBasic('month', 'input[type="month"]'); 627 | addInputsBasic('number', 'input[type="number"]'); 628 | addInputsBasic('search', 'input[type="search"]'); 629 | addInputsBasic('tel', 'input[type="tel"]'); 630 | addInputsBasic('time', 'input[type="time"]'); 631 | addInputsBasic('week', 'input[type="week"]'); 632 | addInputsBasic('password', 'input[type="password"]'); 633 | addInputsBasic('email', 'input[type="email"]'); 634 | addInputsBasic('url', 'input[type="url"]'); 635 | addInputsBasic('range', 'input[type="range"]'); 636 | addInputsBasic('textarea', 'textarea'); 637 | addInputsBasic('select', 'select:not([multiple])'); 638 | addInputsBasic('select[multiple]', 'select[multiple]'); 639 | addInputsBasic('file', 'input[type="file"]:not([multiple])'); 640 | addInputsBasic('file[multiple]', 'input[type="file"][multiple]'); 641 | addInputsBasic('hidden', 'input[type="hidden"]'); 642 | addInputsGroup('radio', 'input[type="radio"]'); 643 | addInputsGroup('checkbox', 'input[type="checkbox"]'); 644 | } 645 | 646 | return inputs; 647 | }; 648 | 649 | $.fn.inputVal = function (newValue) { 650 | var $self = $(this); 651 | 652 | var inputs = buildFormInputs({ $: $self }); 653 | 654 | if($self.is('input, textarea, select')) { 655 | if(typeof newValue === 'undefined') { 656 | return inputs[$self.attr('name')].get(); 657 | } 658 | else { 659 | inputs[$self.attr('name')].set(newValue); 660 | return $self; 661 | } 662 | } 663 | else { 664 | if(typeof newValue === 'undefined') { 665 | return call(inputs, 'get'); 666 | } 667 | else { 668 | foreach(newValue, function (value, inputName) { 669 | inputs[inputName].set(value); 670 | }); 671 | return $self; 672 | } 673 | } 674 | }; 675 | 676 | $.fn.inputOnChange = function (callback) { 677 | var $self = $(this); 678 | var inputs = buildFormInputs({ $: $self }); 679 | foreach(inputs, function (input) { 680 | input.subscribe('change', function (data) { 681 | callback.call(data.domElement, data.e); 682 | }); 683 | }); 684 | return $self; 685 | }; 686 | 687 | $.fn.inputDisable = function () { 688 | var $self = $(this); 689 | call(buildFormInputs({ $: $self }), 'disable'); 690 | return $self; 691 | }; 692 | 693 | $.fn.inputEnable = function () { 694 | var $self = $(this); 695 | call(buildFormInputs({ $: $self }), 'enable'); 696 | return $self; 697 | }; 698 | 699 | $.fn.inputClear = function () { 700 | var $self = $(this); 701 | call(buildFormInputs({ $: $self }), 'clear'); 702 | return $self; 703 | }; 704 | 705 | }(jQuery)); 706 | -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | var identity = function (x) { 2 | return x; 3 | }; 4 | 5 | var isArray = function (value) { 6 | return $.isArray(value); 7 | }; 8 | 9 | var isObject = function (value) { 10 | return !isArray(value) && (value instanceof Object); 11 | }; 12 | 13 | var isNumber = function (value) { 14 | return value instanceof Number; 15 | }; 16 | 17 | var isFunction = function (value) { 18 | return value instanceof Function; 19 | }; 20 | 21 | var indexOf = function (object, value) { 22 | return $.inArray(value, object); 23 | }; 24 | 25 | var inArray = function (array, value) { 26 | return indexOf(array, value) !== -1; 27 | }; 28 | 29 | var foreach = function (collection, callback) { 30 | for(var i in collection) { 31 | if(collection.hasOwnProperty(i)) { 32 | callback(collection[i], i, collection); 33 | } 34 | } 35 | }; 36 | 37 | 38 | var last = function (array) { 39 | return array[array.length - 1]; 40 | }; 41 | 42 | var argumentsToArray = function (args) { 43 | return Array.prototype.slice.call(args); 44 | }; 45 | 46 | var extend = function () { 47 | var extended = {}; 48 | foreach(argumentsToArray(arguments), function (o) { 49 | foreach(o, function (val, key) { 50 | extended[key] = val; 51 | }); 52 | }); 53 | return extended; 54 | }; 55 | 56 | var mapToArray = function (collection, callback) { 57 | var mapped = []; 58 | foreach(collection, function (value, key, coll) { 59 | mapped.push(callback(value, key, coll)); 60 | }); 61 | return mapped; 62 | }; 63 | 64 | var mapToObject = function (collection, callback, keyCallback) { 65 | var mapped = {}; 66 | foreach(collection, function (value, key, coll) { 67 | key = keyCallback ? keyCallback(key, value) : key; 68 | mapped[key] = callback(value, key, coll); 69 | }); 70 | return mapped; 71 | }; 72 | 73 | var map = function (collection, callback, keyCallback) { 74 | return isArray(collection) ? 75 | mapToArray(collection, callback) : 76 | mapToObject(collection, callback, keyCallback); 77 | }; 78 | 79 | var pluck = function (arrayOfObjects, key) { 80 | return map(arrayOfObjects, function (val) { 81 | return val[key]; 82 | }); 83 | }; 84 | 85 | var filter = function (collection, callback) { 86 | var filtered; 87 | 88 | if(isArray(collection)) { 89 | filtered = []; 90 | foreach(collection, function (val, key, coll) { 91 | if(callback(val, key, coll)) { 92 | filtered.push(val); 93 | } 94 | }); 95 | } 96 | else { 97 | filtered = {}; 98 | foreach(collection, function (val, key, coll) { 99 | if(callback(val, key, coll)) { 100 | filtered[key] = val; 101 | } 102 | }); 103 | } 104 | 105 | return filtered; 106 | }; 107 | 108 | var call = function (collection, functionName, args) { 109 | return map(collection, function (object, name) { 110 | return object[functionName].apply(object, args || []); 111 | }); 112 | }; 113 | 114 | //execute callback immediately and at most one time on the minimumInterval, 115 | //ignore block attempts 116 | var throttle = function (minimumInterval, callback) { 117 | var timeout = null; 118 | return function () { 119 | var that = this, args = arguments; 120 | if(timeout === null) { 121 | timeout = setTimeout(function () { 122 | timeout = null; 123 | }, minimumInterval); 124 | callback.apply(that, args); 125 | } 126 | }; 127 | }; 128 | 129 | 130 | var mixinPubSub = function (object) { 131 | object = object || {}; 132 | var topics = {}; 133 | 134 | object.publish = function (topic, data) { 135 | foreach(topics[topic], function (callback) { 136 | callback(data); 137 | }); 138 | }; 139 | 140 | object.subscribe = function (topic, callback) { 141 | topics[topic] = topics[topic] || []; 142 | topics[topic].push(callback); 143 | }; 144 | 145 | object.unsubscribe = function (callback) { 146 | foreach(topics, function (subscribers) { 147 | var index = indexOf(subscribers, callback); 148 | if(index !== -1) { 149 | subscribers.splice(index, 1); 150 | } 151 | }); 152 | }; 153 | 154 | return object; 155 | }; 156 | -------------------------------------------------------------------------------- /src/outro.js: -------------------------------------------------------------------------------- 1 | }(jQuery)); -------------------------------------------------------------------------------- /src/repeater.js: -------------------------------------------------------------------------------- 1 | $.fn.repeaterVal = function () { 2 | var parse = function (raw) { 3 | var parsed = []; 4 | 5 | foreach(raw, function (val, key) { 6 | var parsedKey = []; 7 | if(key !== "undefined") { 8 | parsedKey.push(key.match(/^[^\[]*/)[0]); 9 | parsedKey = parsedKey.concat(map( 10 | key.match(/\[[^\]]*\]/g), 11 | function (bracketed) { 12 | return bracketed.replace(/[\[\]]/g, ''); 13 | } 14 | )); 15 | 16 | parsed.push({ 17 | val: val, 18 | key: parsedKey 19 | }); 20 | } 21 | }); 22 | 23 | return parsed; 24 | }; 25 | 26 | var build = function (parsed) { 27 | if( 28 | parsed.length === 1 && 29 | (parsed[0].key.length === 0 || parsed[0].key.length === 1 && !parsed[0].key[0]) 30 | ) { 31 | return parsed[0].val; 32 | } 33 | 34 | foreach(parsed, function (p) { 35 | p.head = p.key.shift(); 36 | }); 37 | 38 | var grouped = (function () { 39 | var grouped = {}; 40 | 41 | foreach(parsed, function (p) { 42 | if(!grouped[p.head]) { 43 | grouped[p.head] = []; 44 | } 45 | grouped[p.head].push(p); 46 | }); 47 | 48 | return grouped; 49 | }()); 50 | 51 | var built; 52 | 53 | if(/^[0-9]+$/.test(parsed[0].head)) { 54 | built = []; 55 | foreach(grouped, function (group) { 56 | built.push(build(group)); 57 | }); 58 | } 59 | else { 60 | built = {}; 61 | foreach(grouped, function (group, key) { 62 | built[key] = build(group); 63 | }); 64 | } 65 | 66 | return built; 67 | }; 68 | 69 | return build(parse($(this).inputVal())); 70 | }; 71 | 72 | $.fn.repeater = function (fig) { 73 | fig = fig || {}; 74 | 75 | var setList; 76 | 77 | $(this).each(function () { 78 | 79 | var $self = $(this); 80 | 81 | var show = fig.show || function () { 82 | $(this).show(); 83 | }; 84 | 85 | var hide = fig.hide || function (removeElement) { 86 | removeElement(); 87 | }; 88 | 89 | var $list = $self.find('[data-repeater-list]').first(); 90 | 91 | var $filterNested = function ($items, repeaters) { 92 | return $items.filter(function () { 93 | return repeaters ? 94 | $(this).closest( 95 | pluck(repeaters, 'selector').join(',') 96 | ).length === 0 : true; 97 | }); 98 | }; 99 | 100 | var $items = function () { 101 | return $filterNested($list.find('[data-repeater-item]'), fig.repeaters); 102 | }; 103 | 104 | var $itemTemplate = $list.find('[data-repeater-item]') 105 | .first().clone().hide(); 106 | 107 | var $firstDeleteButton = $filterNested( 108 | $filterNested($(this).find('[data-repeater-item]'), fig.repeaters) 109 | .first().find('[data-repeater-delete]'), 110 | fig.repeaters 111 | ); 112 | 113 | if(fig.isFirstItemUndeletable && $firstDeleteButton) { 114 | $firstDeleteButton.remove(); 115 | } 116 | 117 | var getGroupName = function () { 118 | var groupName = $list.data('repeater-list'); 119 | return fig.$parent ? 120 | fig.$parent.data('item-name') + '[' + groupName + ']' : 121 | groupName; 122 | }; 123 | 124 | var initNested = function ($listItems) { 125 | if(fig.repeaters) { 126 | $listItems.each(function () { 127 | var $item = $(this); 128 | foreach(fig.repeaters, function (nestedFig) { 129 | $item.find(nestedFig.selector).repeater(extend( 130 | nestedFig, { $parent: $item } 131 | )); 132 | }); 133 | }); 134 | } 135 | }; 136 | 137 | var $foreachRepeaterInItem = function (repeaters, $item, cb) { 138 | if(repeaters) { 139 | foreach(repeaters, function (nestedFig) { 140 | cb.call($item.find(nestedFig.selector)[0], nestedFig); 141 | }); 142 | } 143 | }; 144 | 145 | var setIndexes = function ($items, groupName, repeaters) { 146 | $items.each(function (index) { 147 | var $item = $(this); 148 | $item.data('item-name', groupName + '[' + index + ']'); 149 | $filterNested($item.find('[name]'), repeaters) 150 | .each(function () { 151 | var $input = $(this); 152 | // match non empty brackets (ex: "[foo]") 153 | var matches = $input.attr('name').match(/\[[^\]]+\]/g); 154 | 155 | var name = matches ? 156 | // strip "[" and "]" characters 157 | last(matches).replace(/\[|\]/g, '') : 158 | $input.attr('name'); 159 | 160 | 161 | var newName = groupName + '[' + index + '][' + name + ']' + 162 | ($input.is(':checkbox') || $input.attr('multiple') ? '[]' : ''); 163 | 164 | $input.attr('name', newName); 165 | 166 | $foreachRepeaterInItem(repeaters, $item, function (nestedFig) { 167 | var $repeater = $(this); 168 | setIndexes( 169 | $filterNested($repeater.find('[data-repeater-item]'), nestedFig.repeaters || []), 170 | groupName + '[' + index + ']' + 171 | '[' + $repeater.find('[data-repeater-list]').first().data('repeater-list') + ']', 172 | nestedFig.repeaters 173 | ); 174 | }); 175 | }); 176 | }); 177 | 178 | $list.find('input[name][checked]') 179 | .removeAttr('checked') 180 | .prop('checked', true); 181 | }; 182 | 183 | setIndexes($items(), getGroupName(), fig.repeaters); 184 | initNested($items()); 185 | if(fig.initEmpty) { 186 | $items().remove(); 187 | } 188 | 189 | if(fig.ready) { 190 | fig.ready(function () { 191 | setIndexes($items(), getGroupName(), fig.repeaters); 192 | }); 193 | } 194 | 195 | var appendItem = (function () { 196 | var setItemsValues = function ($item, data, repeaters) { 197 | if(data || fig.defaultValues) { 198 | var inputNames = {}; 199 | $filterNested($item.find('[name]'), repeaters).each(function () { 200 | var key = $(this).attr('name').match(/\[([^\]]*)(\]|\]\[\])$/)[1]; 201 | inputNames[key] = $(this).attr('name'); 202 | }); 203 | 204 | $item.inputVal(map( 205 | filter(data || fig.defaultValues, function (val, name) { 206 | return inputNames[name]; 207 | }), 208 | identity, 209 | function (name) { 210 | return inputNames[name]; 211 | } 212 | )); 213 | } 214 | 215 | 216 | $foreachRepeaterInItem(repeaters, $item, function (nestedFig) { 217 | var $repeater = $(this); 218 | $filterNested( 219 | $repeater.find('[data-repeater-item]'), 220 | nestedFig.repeaters 221 | ) 222 | .each(function () { 223 | var fieldName = $repeater.find('[data-repeater-list]').data('repeater-list'); 224 | if(data && data[fieldName]) { 225 | var $template = $(this).clone(); 226 | $repeater.find('[data-repeater-item]').remove(); 227 | foreach(data[fieldName], function (data) { 228 | var $item = $template.clone(); 229 | setItemsValues( 230 | $item, 231 | data, 232 | nestedFig.repeaters || [] 233 | ); 234 | $repeater.find('[data-repeater-list]').append($item); 235 | }); 236 | } 237 | else { 238 | setItemsValues( 239 | $(this), 240 | nestedFig.defaultValues, 241 | nestedFig.repeaters || [] 242 | ); 243 | } 244 | }); 245 | }); 246 | 247 | }; 248 | 249 | return function ($item, data) { 250 | $list.append($item); 251 | setIndexes($items(), getGroupName(), fig.repeaters); 252 | $item.find('[name]').each(function () { 253 | $(this).inputClear(); 254 | }); 255 | setItemsValues($item, data || fig.defaultValues, fig.repeaters); 256 | }; 257 | }()); 258 | 259 | var addItem = function (data) { 260 | var $item = $itemTemplate.clone(); 261 | appendItem($item, data); 262 | if(fig.repeaters) { 263 | initNested($item); 264 | } 265 | show.call($item.get(0)); 266 | }; 267 | 268 | setList = function (rows) { 269 | $items().remove(); 270 | foreach(rows, addItem); 271 | }; 272 | 273 | $filterNested($self.find('[data-repeater-create]'), fig.repeaters).click(function () { 274 | addItem(); 275 | }); 276 | 277 | $list.on('click', '[data-repeater-delete]', function () { 278 | var self = $(this).closest('[data-repeater-item]').get(0); 279 | hide.call(self, function () { 280 | $(self).remove(); 281 | setIndexes($items(), getGroupName(), fig.repeaters); 282 | }); 283 | }); 284 | }); 285 | 286 | this.setList = setList; 287 | 288 | return this; 289 | }; 290 | -------------------------------------------------------------------------------- /test-post-parse.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
 9 | 
10 | 
11 | 12 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /test/complex.js: -------------------------------------------------------------------------------- 1 | QUnit.module('complex-repeater', { 2 | setup: function () { 3 | this.$fixture = $('#qunit-fixture'); 4 | this.$fixture.html($('#template').html()); 5 | this.$repeater = this.$fixture.find('.complex-repeater'); 6 | this.$addButton = this.$repeater.find('[data-repeater-create]'); 7 | this.$fixture.append($('#template').html()); 8 | } 9 | }); 10 | 11 | QUnit.test('add item', function (assert) { 12 | this.$repeater.repeater(); 13 | this.$addButton.click(); 14 | var $items = this.$repeater.find('[data-repeater-item]'); 15 | assert.strictEqual($items.length, 2, 'adds a second item to list'); 16 | 17 | assert.deepEqual( 18 | getNamedInputValues($items.last()), 19 | { 'complex-repeater[1][text-input]': '' }, 20 | 'added items inputs are clear' 21 | ); 22 | 23 | assert.deepEqual( 24 | getNamedInputValues($items.first()), 25 | { 'complex-repeater[0][text-input]': 'A' }, 26 | 'does not clear other inputs' 27 | ); 28 | }); 29 | 30 | QUnit.test('delete item', function (assert) { 31 | this.$repeater.repeater(); 32 | this.$repeater.find('[data-repeater-delete]').first().click(); 33 | assert.strictEqual( 34 | this.$repeater.find('[data-repeater-item]').length, 0, 35 | 'deletes item' 36 | ); 37 | }); 38 | 39 | QUnit.test('delete item that has been added', function (assert) { 40 | this.$repeater.repeater(); 41 | this.$addButton.click(); 42 | assert.strictEqual( 43 | this.$repeater.find('[data-repeater-item]').length, 2, 44 | 'item added' 45 | ); 46 | this.$repeater.find('[data-repeater-delete]').last().click(); 47 | assert.strictEqual( 48 | this.$repeater.find('[data-repeater-item]').length, 1, 49 | 'item deleted' 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /test/echo.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Repeater 6 | 7 | 8 | 9 |
10 |
11 | 12 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /test/index.pre.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Repeater 6 | 7 | 8 | 9 |
10 |
11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/nested.js: -------------------------------------------------------------------------------- 1 | QUnit.module('nested-repeater', { 2 | setup: function () { 3 | this.$fixture = $('#qunit-fixture'); 4 | this.$fixture.html($('#template').html()); 5 | this.$outerRepeater = this.$fixture.find('.outer-repeater'); 6 | this.$innerRepeater = this.$fixture.find('.inner-repeater'); 7 | this.$outerAddButton = this.$fixture.find('.outer-repeater > [data-repeater-create]'); 8 | this.$innerAddButton = this.$fixture.find('.inner-repeater > [data-repeater-create]'); 9 | } 10 | }); 11 | 12 | 13 | QUnit.test('isFirstItemUndeletable configuration option', function (assert) { 14 | this.$outerRepeater.repeater({ 15 | isFirstItemUndeletable: true, 16 | repeaters: [{ 17 | selector: '.inner-repeater', 18 | isFirstItemUndeletable: true 19 | }] 20 | }); 21 | 22 | this.$outerAddButton.click(); 23 | this.$innerAddButton.click(); 24 | 25 | var $outerItems = this.$outerRepeater.find('[data-repeater-list="outer-group"] > [data-repeater-item]'); 26 | var $innerItems = this.$innerRepeater.find('[data-repeater-item]'); 27 | 28 | assert.strictEqual($outerItems.length, 2, 'adds a second item to outer list'); 29 | assert.strictEqual($innerItems.length, 2, 'adds a second item to inner list'); 30 | assert.strictEqual( 31 | this.$outerRepeater.find('[data-repeater-item].outer') 32 | .first().find('[data-repeater-delete].outer').length, 33 | 0, 34 | 'No delete button on first outer item' 35 | ); 36 | 37 | assert.strictEqual( 38 | this.$outerRepeater.find('[data-repeater-item].outer') 39 | .last().find('[data-repeater-delete].outer').length, 40 | 1, 41 | 'Delete button on second outer item' 42 | ); 43 | 44 | assert.strictEqual( 45 | this.$innerRepeater.find('[data-repeater-item]') 46 | .first().find('[data-repeater-delete]').length, 47 | 0, 48 | 'No delete button on first inner item of first outer item' 49 | ); 50 | 51 | assert.strictEqual( 52 | this.$innerRepeater.find('[data-repeater-item]') 53 | .last().find('[data-repeater-delete]').length, 54 | 1, 55 | 'Delete button on second inner item of first outer item' 56 | ); 57 | 58 | assert.strictEqual( 59 | this.$outerRepeater.find('[data-repeater-list="inner-group"]').last() 60 | .find('[data-repeater-item]').first().find('[data-repeater-delete]').length, 61 | 0, 62 | 'No delete button on first inner item of second outer item' 63 | ); 64 | }); 65 | 66 | QUnit.test('setList', function (assert) { 67 | var repeater = this.$outerRepeater.repeater({ 68 | repeaters: [{ selector: '.inner-repeater' }] 69 | }); 70 | repeater.setList([ 71 | { 72 | 'text-input': 'set-a', 73 | 'inner-group': [{ 'inner-text-input': 'set-b' }] 74 | }, 75 | { 76 | 'text-input': 'set-foo', 77 | 'inner-group': [] 78 | } 79 | ]); 80 | 81 | var $items = this.$outerRepeater.find('[data-repeater-list="outer-group"] > [data-repeater-item]'); 82 | 83 | assert.deepEqual( 84 | getNamedInputValues($items.first()), 85 | { 86 | "outer-group[0][text-input]": "set-a", 87 | "outer-group[0][inner-group][0][inner-text-input]": "set-b" 88 | }, 89 | 'set first item' 90 | ); 91 | 92 | assert.deepEqual( 93 | getNamedInputValues($items.last()), 94 | { 95 | "outer-group[1][text-input]": "set-foo" 96 | }, 97 | 'set second item' 98 | ); 99 | }); 100 | 101 | QUnit.test('add item nested outer', function (assert) { 102 | this.$outerRepeater.repeater({ repeaters: [{ selector: '.inner-repeater' }] }); 103 | this.$outerAddButton.click(); 104 | var $items = this.$outerRepeater.find('[data-repeater-list="outer-group"] > [data-repeater-item]'); 105 | 106 | assert.strictEqual($items.length, 2, 'adds a second item to list'); 107 | 108 | assert.strictEqual( 109 | $items.first().find('[data-repeater-list="inner-group"] > [data-repeater-item]').length, 110 | 1, 'does not duplicate first inner repeater' 111 | ); 112 | 113 | assert.strictEqual( 114 | $items.last().find('[data-repeater-list="inner-group"] > [data-repeater-item]').length, 115 | 1, 'does not duplicate last inner repeater' 116 | ); 117 | 118 | assert.deepEqual( 119 | getNamedInputValues($items.first()), 120 | { 121 | "outer-group[0][text-input]": "A", 122 | "outer-group[0][inner-group][0][inner-text-input]": "B" 123 | }, 124 | 'renamed first item' 125 | ); 126 | 127 | assert.deepEqual( 128 | getNamedInputValues($items.last()), 129 | { 130 | "outer-group[1][text-input]": "", 131 | "outer-group[1][inner-group][0][inner-text-input]": "" 132 | }, 133 | 'renamed last item, values cleared' 134 | ); 135 | }); 136 | 137 | QUnit.test('delete added item outer from repeated outer', function (assert) { 138 | this.$outerRepeater.repeater({ repeaters: [{ selector: '.inner-repeater' }] }); 139 | this.$outerAddButton.click(); 140 | 141 | var $lastOuterItem = this.$outerRepeater 142 | .find('[data-repeater-list="outer-group"] > [data-repeater-item]') 143 | .last(); 144 | 145 | $lastOuterItem.find('[data-repeater-delete]').first().click(); 146 | 147 | assert.deepEqual( 148 | getNamedInputValues(this.$outerRepeater), 149 | { 150 | "outer-group[0][text-input]": "A", 151 | "outer-group[0][inner-group][0][inner-text-input]": "B" 152 | } 153 | ); 154 | }); 155 | 156 | QUnit.test('delete added item outer from first outer', function (assert) { 157 | this.$outerRepeater.repeater({ repeaters: [{ selector: '.inner-repeater' }] }); 158 | this.$outerAddButton.click(); 159 | 160 | var $firstOuterItem = this.$outerRepeater 161 | .find('[data-repeater-list="outer-group"] > [data-repeater-item]') 162 | .first(); 163 | 164 | $firstOuterItem.find('[data-repeater-delete]').first().click(); 165 | 166 | assert.deepEqual( 167 | getNamedInputValues(this.$outerRepeater), 168 | { 169 | "outer-group[0][text-input]": "", 170 | "outer-group[0][inner-group][0][inner-text-input]": "" 171 | } 172 | ); 173 | }); 174 | 175 | QUnit.test('add item nested inner', function (assert) { 176 | this.$outerRepeater.repeater({ repeaters: [{ selector: '.inner-repeater' }] }); 177 | this.$innerAddButton.click(); 178 | 179 | assert.strictEqual( 180 | this.$innerRepeater.find('[data-repeater-item]').length, 181 | 2, 'adds item to inner repeater' 182 | ); 183 | 184 | var $items = this.$outerRepeater.find('[data-repeater-list="outer-group"] > [data-repeater-item]'); 185 | 186 | assert.strictEqual($items.length, 1, 'does not add item to outer list'); 187 | 188 | assert.deepEqual( 189 | getNamedInputValues($items.first()), 190 | { 191 | "outer-group[0][text-input]": "A", 192 | "outer-group[0][inner-group][0][inner-text-input]": "B", 193 | "outer-group[0][inner-group][1][inner-text-input]": "", 194 | }, 195 | 'renamed items' 196 | ); 197 | }); 198 | 199 | QUnit.test('add item nested inner from repeated outer', function (assert) { 200 | this.$outerRepeater.repeater({ repeaters: [{ selector: '.inner-repeater' }] }); 201 | this.$outerAddButton.click(); 202 | 203 | var $lastItem = this.$outerRepeater.find('[data-repeater-list="outer-group"] > [data-repeater-item]').last(); 204 | 205 | $lastItem.find('[data-repeater-create]').click(); 206 | 207 | assert.strictEqual( 208 | this.$innerRepeater.find('[data-repeater-item]').length, 209 | 1, 'does not add item to first inner repeater' 210 | ); 211 | 212 | assert.strictEqual( 213 | $lastItem.find('[data-repeater-item]').length, 214 | 2, 'adds item to second inner repeater' 215 | ); 216 | 217 | var $items = this.$outerRepeater.find('[data-repeater-list="outer-group"] > [data-repeater-item]'); 218 | 219 | assert.strictEqual($items.length, 2, 'correct number of terms in outer list'); 220 | 221 | assert.deepEqual( 222 | getNamedInputValues(this.$outerRepeater), 223 | { 224 | "outer-group[0][text-input]": "A", 225 | "outer-group[0][inner-group][0][inner-text-input]": "B", 226 | "outer-group[1][text-input]": "", 227 | "outer-group[1][inner-group][0][inner-text-input]": "", 228 | "outer-group[1][inner-group][1][inner-text-input]": "", 229 | }, 230 | 'renamed items' 231 | ); 232 | }); 233 | 234 | QUnit.test('delete added item nested inner from repeated outer', function (assert) { 235 | this.$outerRepeater.repeater({ repeaters: [{ selector: '.inner-repeater' }] }); 236 | this.$outerAddButton.click(); 237 | 238 | var $lastOuterItem = this.$outerRepeater 239 | .find('[data-repeater-list="outer-group"] > [data-repeater-item]') 240 | .last(); 241 | 242 | $lastOuterItem.find('[data-repeater-create]').click(); 243 | $lastOuterItem.find('[data-repeater-list] [data-repeater-delete]').first().click(); 244 | 245 | assert.deepEqual( 246 | getNamedInputValues(this.$outerRepeater), 247 | { 248 | "outer-group[0][text-input]": "A", 249 | "outer-group[0][inner-group][0][inner-text-input]": "B", 250 | "outer-group[1][text-input]": "", 251 | "outer-group[1][inner-group][0][inner-text-input]": "" 252 | } 253 | ); 254 | }); 255 | 256 | QUnit.test('nested default values first item', function (assert) { 257 | this.$outerRepeater.repeater({ 258 | repeaters: [{ 259 | selector: '.inner-repeater', 260 | defaultValues: { 'inner-text-input': 'foo' } 261 | }] 262 | }); 263 | 264 | this.$innerAddButton.click(); 265 | 266 | assert.deepEqual( 267 | getNamedInputValues(this.$outerRepeater), 268 | { 269 | "outer-group[0][text-input]": "A", 270 | "outer-group[0][inner-group][0][inner-text-input]": "B", 271 | "outer-group[0][inner-group][1][inner-text-input]": "foo" 272 | } 273 | ); 274 | }); 275 | 276 | QUnit.test('nested default values last item', function (assert) { 277 | this.$outerRepeater.repeater({ 278 | repeaters: [{ 279 | selector: '.inner-repeater', 280 | defaultValues: { 'inner-text-input': 'foo' } 281 | }] 282 | }); 283 | 284 | this.$outerAddButton.click(); 285 | var $lastOuterItem = this.$outerRepeater 286 | .find('[data-repeater-list="outer-group"] > [data-repeater-item]') 287 | .last(); 288 | 289 | assert.deepEqual( 290 | getNamedInputValues(this.$outerRepeater), 291 | { 292 | "outer-group[0][text-input]": "A", 293 | "outer-group[0][inner-group][0][inner-text-input]": "B", 294 | "outer-group[1][text-input]": "", 295 | "outer-group[1][inner-group][0][inner-text-input]": "foo" 296 | } 297 | ); 298 | 299 | $lastOuterItem.find('[data-repeater-create]').click(); 300 | 301 | assert.deepEqual( 302 | getNamedInputValues(this.$outerRepeater), 303 | { 304 | "outer-group[0][text-input]": "A", 305 | "outer-group[0][inner-group][0][inner-text-input]": "B", 306 | "outer-group[1][text-input]": "", 307 | "outer-group[1][inner-group][0][inner-text-input]": "foo", 308 | "outer-group[1][inner-group][1][inner-text-input]": "foo" 309 | } 310 | ); 311 | }); 312 | 313 | QUnit.test('repeaterVal nested', function (assert) { 314 | this.$outerRepeater.repeater({ 315 | repeaters: [{ selector: '.inner-repeater' }] 316 | }); 317 | 318 | assert.deepEqual(this.$outerRepeater.repeaterVal(), { 319 | 'outer-group': [{ 320 | 'text-input': 'A', 321 | 'inner-group': [{ 'inner-text-input': 'B' }] 322 | }] 323 | }); 324 | }); 325 | -------------------------------------------------------------------------------- /test/qunit-1.14.0.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 | -------------------------------------------------------------------------------- /test/qunit-1.14.0.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 | "

" + escapeText( document.title ) + "

" + 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 += ""; 452 | 453 | if ( actual !== expected ) { 454 | output += ""; 455 | output += ""; 456 | } 457 | 458 | source = sourceFromStacktrace(); 459 | 460 | if ( source ) { 461 | details.source = source; 462 | output += ""; 463 | } 464 | 465 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 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 += ""; 497 | } 498 | 499 | if ( source ) { 500 | details.source = source; 501 | output += ""; 502 | } 503 | 504 | output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 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 | -------------------------------------------------------------------------------- /test/test-lib.js: -------------------------------------------------------------------------------- 1 | var getNamedInputValues = function ($scope) { 2 | return filter($scope.inputVal(), function (val, key) { 3 | return key !== 'undefined'; 4 | }); 5 | }; 6 | 7 | var generateNameMappedInputValues = function (group, index, defaultValue, override) { 8 | var defaultObject = {}; 9 | defaultObject['group-' + group + '[' + index + '][text-input]'] = defaultValue; 10 | defaultObject['group-' + group + '[' + index + '][date-input]'] = defaultValue; 11 | defaultObject['group-' + group + '[' + index + '][url-input]'] = defaultValue; 12 | defaultObject['group-' + group + '[' + index + '][color-input]'] = defaultValue || '#000000'; 13 | defaultObject['group-' + group + '[' + index + '][datetime-local-input]'] = defaultValue; 14 | defaultObject['group-' + group + '[' + index + '][month-input]'] = defaultValue; 15 | defaultObject['group-' + group + '[' + index + '][number-input]'] = defaultValue; 16 | defaultObject['group-' + group + '[' + index + '][search-input]'] = defaultValue; 17 | defaultObject['group-' + group + '[' + index + '][tel-input]'] = defaultValue; 18 | defaultObject['group-' + group + '[' + index + '][time-input]'] = defaultValue; 19 | defaultObject['group-' + group + '[' + index + '][week-input]'] = defaultValue; 20 | defaultObject['group-' + group + '[' + index + '][textarea-input]'] = defaultValue; 21 | defaultObject['group-' + group + '[' + index + '][select-input]'] = defaultValue || null; 22 | defaultObject['group-' + group + '[' + index + '][radio-input]'] = defaultValue || null; 23 | defaultObject['group-' + group + '[' + index + '][checkbox-input][]'] = defaultValue ? [defaultValue] : []; 24 | defaultObject['group-' + group + '[' + index + '][multiple-select-input][]'] = defaultValue ? [defaultValue] : []; 25 | return $.extend(defaultObject, override || {}); 26 | }; 27 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | QUnit.module('repeater', { 2 | setup: function () { 3 | this.$fixture = $('#qunit-fixture'); 4 | this.$fixture.html($('#template').html()); 5 | this.$repeater = this.$fixture.find('.repeater'); 6 | this.$addButton = this.$repeater.find('[data-repeater-create]'); 7 | 8 | this.$fixture.append($('#template').html()); 9 | this.$secondRepeater = this.$fixture.find('.repeater').last(); 10 | this.$secondRepeater.find('[data-repeater-list]').data( 11 | 'repeater-list', 'group-b' 12 | ); 13 | this.$secondRepeaterAddButton = this.$secondRepeater.find('[data-repeater-create]'); 14 | } 15 | }); 16 | 17 | QUnit.test('add item', function (assert) { 18 | this.$repeater.repeater(); 19 | this.$secondRepeater.repeater(); 20 | this.$addButton.click(); 21 | var $items = this.$repeater.find('[data-repeater-item]'); 22 | assert.strictEqual($items.length, 3, 'adds a third item to list'); 23 | 24 | assert.deepEqual( 25 | getNamedInputValues($items.last()), 26 | generateNameMappedInputValues('a', 2, ''), 27 | 'added items inputs are clear' 28 | ); 29 | 30 | assert.deepEqual( 31 | getNamedInputValues($items.first()), 32 | generateNameMappedInputValues('a', 0, 'A', { 33 | "group-a[0][multiple-select-input][]": ['A', 'B'], 34 | "group-a[0][color-input]":"#aaaaaa", 35 | "group-a[0][date-input]":"2018-05-01", 36 | "group-a[0][datetime-local-input]":"2018-05-12T19:30", 37 | "group-a[0][month-input]":"2018-05", 38 | "group-a[0][number-input]":"42", 39 | "group-a[0][tel-input]":"1112223333", 40 | "group-a[0][time-input]":"13:30", 41 | "group-a[0][url-input]":"https://exemple.com/a", 42 | "group-a[0][week-input]":"2018-W26", 43 | }), 44 | 'does not clear other inputs' 45 | ); 46 | 47 | assert.strictEqual( 48 | this.$secondRepeater.find('[data-repeater-item]').length, 49 | 2, 50 | 'does not add third item to second repeater' 51 | ); 52 | }); 53 | 54 | QUnit.test('instantiate with no first item', function (assert) { 55 | this.$repeater.find('[data-repeater-item]').last().remove(); 56 | this.$repeater.find('[data-repeater-item]').css('display', 'none'); 57 | assert.strictEqual( 58 | this.$repeater.find('[data-repeater-item]').css('display'), 'none', 59 | 'display:none css is set' 60 | ); 61 | this.$repeater.repeater({ initEmpty: true }); 62 | assert.strictEqual( 63 | this.$repeater.find('[data-repeater-item]').length, 0, 64 | 'starts with no items' 65 | ); 66 | this.$addButton.click(); 67 | assert.strictEqual( 68 | this.$repeater.find('[data-repeater-item]').length, 1, 69 | 'still able to create item' 70 | ); 71 | 72 | assert.strictEqual( 73 | this.$repeater.find('[data-repeater-item]').css('display'), 'block', 74 | 'display:none css is not set' 75 | ); 76 | 77 | assert.deepEqual( 78 | getNamedInputValues(this.$repeater.find('[data-repeater-item]')), 79 | generateNameMappedInputValues('a', 0, '', { 80 | "group-a[0][multiple-select-input][]": [] 81 | }), 82 | 'maintains template' 83 | ); 84 | }); 85 | 86 | QUnit.test('instantiate group of repeaters with a single repeater() call', function (assert) { 87 | this.$fixture.find('.repeater').repeater(); 88 | this.$addButton.click(); 89 | var $items = this.$secondRepeater.find('[data-repeater-item]'); 90 | assert.strictEqual( 91 | $items.length, 2, 92 | 'does not add a third item to unclicked list' 93 | ); 94 | }); 95 | 96 | QUnit.test('second repeater add item', function (assert) { 97 | this.$repeater.repeater(); 98 | this.$secondRepeater.repeater(); 99 | this.$secondRepeaterAddButton.click(); 100 | var $items = this.$secondRepeater.find('[data-repeater-item]'); 101 | assert.strictEqual($items.length, 3, 'adds a third item to list'); 102 | assert.deepEqual( 103 | getNamedInputValues($items.last()), 104 | generateNameMappedInputValues('b', 2, ''), 105 | 'added items inputs are clear' 106 | ); 107 | 108 | assert.deepEqual( 109 | getNamedInputValues($items.first()), 110 | generateNameMappedInputValues('b', 0, 'A', { 111 | "group-b[0][multiple-select-input][]": ['A', 'B'], 112 | "group-b[0][color-input]":"#aaaaaa", 113 | "group-b[0][date-input]":"2018-05-01", 114 | "group-b[0][datetime-local-input]":"2018-05-12T19:30", 115 | "group-b[0][month-input]":"2018-05", 116 | "group-b[0][number-input]":"42", 117 | "group-b[0][tel-input]":"1112223333", 118 | "group-b[0][time-input]":"13:30", 119 | "group-b[0][url-input]":"https://exemple.com/a", 120 | "group-b[0][week-input]":"2018-W26", 121 | }), 122 | 'does not clear other inputs' 123 | ); 124 | 125 | assert.strictEqual( 126 | this.$repeater.find('[data-repeater-item]').length, 127 | 2, 128 | 'does not add third item to first repeater' 129 | ); 130 | }); 131 | 132 | QUnit.test('multiple add buttons', function (assert) { 133 | this.$repeater.append( 134 | '
    ' + 135 | 'Another Add Button' + 136 | '
    ' 137 | ); 138 | this.$repeater.repeater(); 139 | this.$repeater.find('.second-add').click(); 140 | var $items = this.$repeater.find('[data-repeater-item]'); 141 | assert.strictEqual($items.length, 3, 'adds a third item to list'); 142 | assert.deepEqual( 143 | getNamedInputValues($items.last()), 144 | generateNameMappedInputValues('a', 2, ''), 145 | 'added items inputs are clear' 146 | ); 147 | 148 | assert.deepEqual( 149 | getNamedInputValues($items.first()), 150 | generateNameMappedInputValues('a', 0, 'A', { 151 | "group-a[0][multiple-select-input][]": ['A', 'B'], 152 | "group-a[0][color-input]":"#aaaaaa", 153 | "group-a[0][date-input]":"2018-05-01", 154 | "group-a[0][datetime-local-input]":"2018-05-12T19:30", 155 | "group-a[0][month-input]":"2018-05", 156 | "group-a[0][number-input]":"42", 157 | "group-a[0][tel-input]":"1112223333", 158 | "group-a[0][time-input]":"13:30", 159 | "group-a[0][url-input]":"https://exemple.com/a", 160 | "group-a[0][week-input]":"2018-W26", 161 | }), 162 | 'does not clear other inputs' 163 | ); 164 | }); 165 | 166 | QUnit.test('add item with default values and rewrite names', function (assert) { 167 | this.$repeater.repeater({ 168 | defaultValues: { 169 | 'text-input': 'foo', 170 | 'checkbox-input': ['A', 'B'], 171 | "multiple-select-input": ['B'] 172 | } 173 | }); 174 | this.$addButton.click(); 175 | assert.deepEqual( 176 | getNamedInputValues(this.$repeater.find('[data-repeater-item]').last()), 177 | generateNameMappedInputValues('a', 2, '', { 178 | 'group-a[2][text-input]': 'foo', 179 | 'group-a[2][checkbox-input][]' : ['A', 'B'], 180 | "group-a[2][multiple-select-input][]": ['B'] 181 | }) 182 | ); 183 | }); 184 | 185 | QUnit.test('delete item', function (assert) { 186 | this.$repeater.repeater(); 187 | this.$repeater.find('[data-repeater-delete]').first().click(); 188 | assert.strictEqual( 189 | this.$repeater.find('[data-repeater-item]').length, 1, 190 | 'only one item remains' 191 | ); 192 | assert.deepEqual( 193 | getNamedInputValues(this.$repeater), 194 | generateNameMappedInputValues('a', 0, 'B', { 195 | "group-a[0][multiple-select-input][]": ['A', 'B'], 196 | "group-a[0][color-input]":"#bbbbbb", 197 | "group-a[0][date-input]":"2019-05-01", 198 | "group-a[0][datetime-local-input]":"2019-05-12T19:30", 199 | "group-a[0][month-input]":"2019-05", 200 | "group-a[0][number-input]":"43", 201 | "group-a[0][tel-input]":"4442223333", 202 | "group-a[0][time-input]":"14:30", 203 | "group-a[0][url-input]":"https://exemple.com/b", 204 | "group-a[0][week-input]":"2019-W26", 205 | }), 206 | 'second input remains and reindexed as first element' 207 | ); 208 | }); 209 | 210 | QUnit.test('delete item that has been added', function (assert) { 211 | this.$repeater.repeater(); 212 | this.$addButton.click(); 213 | assert.strictEqual( 214 | this.$repeater.find('[data-repeater-item]').length, 3, 215 | 'item added' 216 | ); 217 | this.$repeater.find('[data-repeater-delete]').last().click(); 218 | assert.strictEqual( 219 | this.$repeater.find('[data-repeater-item]').length, 2, 220 | 'item deleted' 221 | ); 222 | assert.deepEqual( 223 | getNamedInputValues(this.$repeater.find('[data-repeater-item]').last()), 224 | generateNameMappedInputValues('a', 1, 'B', { 225 | "group-a[1][multiple-select-input][]": ['A', 'B'], 226 | "group-a[1][color-input]":"#bbbbbb", 227 | "group-a[1][date-input]":"2019-05-01", 228 | "group-a[1][datetime-local-input]":"2019-05-12T19:30", 229 | "group-a[1][month-input]":"2019-05", 230 | "group-a[1][number-input]":"43", 231 | "group-a[1][tel-input]":"4442223333", 232 | "group-a[1][time-input]":"14:30", 233 | "group-a[1][url-input]":"https://exemple.com/b", 234 | "group-a[1][week-input]":"2019-W26", 235 | }), 236 | 'second input remains' 237 | ); 238 | }); 239 | 240 | QUnit.asyncTest('custom show callback', function (assert) { 241 | expect(2); 242 | this.$repeater.repeater({ 243 | show: function () { 244 | assert.deepEqual( 245 | getNamedInputValues($(this)), 246 | generateNameMappedInputValues('a', 2, ''), 247 | '"this" set to blank element' 248 | ); 249 | assert.strictEqual($(this).is(':hidden'), true, 'element is hidden'); 250 | QUnit.start(); 251 | } 252 | }); 253 | this.$addButton.click(); 254 | }); 255 | 256 | QUnit.asyncTest('custom hide callback', function (assert) { 257 | expect(5); 258 | var $repeater = this.$repeater; 259 | $repeater.repeater({ 260 | hide: function (removeItem) { 261 | assert.strictEqual($(this).length, 1, 'has one element'); 262 | assert.deepEqual( 263 | getNamedInputValues($(this)), 264 | generateNameMappedInputValues('a', 0, 'A',{ 265 | "group-a[0][multiple-select-input][]": ['A', 'B'], 266 | "group-a[0][color-input]":"#aaaaaa", 267 | "group-a[0][date-input]":"2018-05-01", 268 | "group-a[0][datetime-local-input]":"2018-05-12T19:30", 269 | "group-a[0][month-input]":"2018-05", 270 | "group-a[0][number-input]":"42", 271 | "group-a[0][tel-input]":"1112223333", 272 | "group-a[0][time-input]":"13:30", 273 | "group-a[0][url-input]":"https://exemple.com/a", 274 | "group-a[0][week-input]":"2018-W26", 275 | }), 276 | '"this" is set to first element' 277 | ); 278 | assert.strictEqual( 279 | $(this).is(':hidden'), false, 280 | 'element is not hidden' 281 | ); 282 | assert.ok($.contains(document, this), 'element is attached to dom'); 283 | removeItem(); 284 | assert.ok(!$.contains(document, this), 'element is detached from dom'); 285 | QUnit.start(); 286 | } 287 | }); 288 | this.$repeater.find('[data-repeater-item]').first() 289 | .find('[data-repeater-delete]').click(); 290 | }); 291 | 292 | QUnit.test('isFirstItemUndeletable configuration option', function (assert) { 293 | this.$repeater.repeater({ isFirstItemUndeletable: true }); 294 | 295 | var $firstDeleteButton = this.$repeater.find('[data-repeater-item]') 296 | .first().find('[data-repeater-delete]'); 297 | 298 | assert.strictEqual($firstDeleteButton.length, 0, 'first delete button is removed'); 299 | }); 300 | 301 | QUnit.asyncTest('has ready callback option and setIndexes', function (assert) { 302 | expect(3); 303 | var $list = this.$secondRepeater.find('[data-repeater-list]'); 304 | this.$secondRepeater.repeater({ 305 | ready: function (setIndexes) { 306 | assert.ok(isFunction(setIndexes), 'passed setIndexes function'); 307 | var $lastItem = $list.find('[data-repeater-item]').last(); 308 | $list.prepend($lastItem.clone()); 309 | $lastItem.remove(); 310 | setIndexes(); 311 | 312 | var indeces = $list.find('[name]').map(function () { 313 | return $(this).attr('name').match(/\[([0-9])+\]/)[1]; 314 | }).get(); 315 | 316 | assert.strictEqual(indeces[0], '0'); 317 | assert.strictEqual(indeces[19], '1'); 318 | 319 | QUnit.start(); 320 | } 321 | }); 322 | }); 323 | 324 | QUnit.test('repeaterVal', function (assert) { 325 | this.$repeater.repeater(); 326 | assert.deepEqual(this.$repeater.repeaterVal(), { 327 | "group-a": [ 328 | { 329 | "text-input": "A", 330 | "date-input": "2018-05-01", 331 | "url-input": "https://exemple.com/a", 332 | "color-input": "#aaaaaa", 333 | "datetime-local-input": "2018-05-12T19:30", 334 | "month-input": "2018-05", 335 | "number-input": "42", 336 | "search-input": "A", 337 | "tel-input": "1112223333", 338 | "time-input": "13:30", 339 | "week-input": "2018-W26", 340 | "textarea-input": "A", 341 | "select-input": "A", 342 | "multiple-select-input": ["A", "B"], 343 | "radio-input": "A", 344 | "checkbox-input": ["A"] 345 | }, 346 | { 347 | "text-input": "B", 348 | "date-input": "2019-05-01", 349 | "url-input": "https://exemple.com/b", 350 | "color-input": "#bbbbbb", 351 | "datetime-local-input": "2019-05-12T19:30", 352 | "month-input": "2019-05", 353 | "number-input": "43", 354 | "search-input": "B", 355 | "tel-input": "4442223333", 356 | "time-input": "14:30", 357 | "week-input": "2019-W26", 358 | "textarea-input": "B", 359 | "select-input": "B", 360 | "multiple-select-input": ["A", "B"], 361 | "radio-input": "B", 362 | "checkbox-input": ["B"] 363 | } 364 | ] 365 | }); 366 | }); 367 | --------------------------------------------------------------------------------