├── .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 | [](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 |
48 |
49 |
50 |
51 |
100 | ```
101 |
102 | ## Nested Example
103 |
104 | ```html
105 |
106 |
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 | create
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 |
111 |
112 | Nested
113 |
114 |
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 |
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 |
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 | = json_encode($_POST, JSON_PRETTY_PRINT); ?>
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 | "" +
347 | " " +
348 | "
" +
349 | " " +
350 | " ";
351 | }
352 |
353 | tests = id( "qunit-tests" );
354 | banner = id( "qunit-banner" );
355 | result = id( "qunit-testresult" );
356 |
357 | if ( tests ) {
358 | tests.innerHTML = "";
359 | }
360 |
361 | if ( banner ) {
362 | banner.className = "";
363 | }
364 |
365 | if ( result ) {
366 | result.parentNode.removeChild( result );
367 | }
368 |
369 | if ( tests ) {
370 | result = document.createElement( "p" );
371 | result.id = "qunit-testresult";
372 | result.className = "result";
373 | tests.parentNode.insertBefore( result, tests );
374 | result.innerHTML = "Running... ";
375 | }
376 | },
377 |
378 | // Resets the test setup. Useful for tests that modify the DOM.
379 | /*
380 | DEPRECATED: Use multiple tests instead of resetting inside a test.
381 | Use testStart or testDone for custom cleanup.
382 | This method will throw an error in 2.0, and will be removed in 2.1
383 | */
384 | reset: function() {
385 | var fixture = id( "qunit-fixture" );
386 | if ( fixture ) {
387 | fixture.innerHTML = config.fixture;
388 | }
389 | },
390 |
391 | // Safe object type checking
392 | is: function( type, obj ) {
393 | return QUnit.objectType( obj ) === type;
394 | },
395 |
396 | objectType: function( obj ) {
397 | if ( typeof obj === "undefined" ) {
398 | return "undefined";
399 | }
400 |
401 | // Consider: typeof null === object
402 | if ( obj === null ) {
403 | return "null";
404 | }
405 |
406 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
407 | type = match && match[1] || "";
408 |
409 | switch ( type ) {
410 | case "Number":
411 | if ( isNaN(obj) ) {
412 | return "nan";
413 | }
414 | return "number";
415 | case "String":
416 | case "Boolean":
417 | case "Array":
418 | case "Date":
419 | case "RegExp":
420 | case "Function":
421 | return type.toLowerCase();
422 | }
423 | if ( typeof obj === "object" ) {
424 | return "object";
425 | }
426 | return undefined;
427 | },
428 |
429 | push: function( result, actual, expected, message ) {
430 | if ( !config.current ) {
431 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
432 | }
433 |
434 | var output, source,
435 | details = {
436 | module: config.current.module,
437 | name: config.current.testName,
438 | result: result,
439 | message: message,
440 | actual: actual,
441 | expected: expected
442 | };
443 |
444 | message = escapeText( message ) || ( result ? "okay" : "failed" );
445 | message = "" + message + " ";
446 | output = message;
447 |
448 | if ( !result ) {
449 | expected = escapeText( QUnit.jsDump.parse(expected) );
450 | actual = escapeText( QUnit.jsDump.parse(actual) );
451 | output += "Expected: " + expected + " ";
452 |
453 | if ( actual !== expected ) {
454 | output += "Result: " + actual + " ";
455 | output += "Diff: " + QUnit.diff( expected, actual ) + " ";
456 | }
457 |
458 | source = sourceFromStacktrace();
459 |
460 | if ( source ) {
461 | details.source = source;
462 | output += "Source: " + escapeText( source ) + " ";
463 | }
464 |
465 | output += "
";
466 | }
467 |
468 | runLoggingCallbacks( "log", QUnit, details );
469 |
470 | config.current.assertions.push({
471 | result: !!result,
472 | message: output
473 | });
474 | },
475 |
476 | pushFailure: function( message, source, actual ) {
477 | if ( !config.current ) {
478 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) );
479 | }
480 |
481 | var output,
482 | details = {
483 | module: config.current.module,
484 | name: config.current.testName,
485 | result: false,
486 | message: message
487 | };
488 |
489 | message = escapeText( message ) || "error";
490 | message = "" + message + " ";
491 | output = message;
492 |
493 | output += "";
494 |
495 | if ( actual ) {
496 | output += "Result: " + escapeText( actual ) + " ";
497 | }
498 |
499 | if ( source ) {
500 | details.source = source;
501 | output += "Source: " + escapeText( source ) + " ";
502 | }
503 |
504 | output += "
";
505 |
506 | runLoggingCallbacks( "log", QUnit, details );
507 |
508 | config.current.assertions.push({
509 | result: false,
510 | message: output
511 | });
512 | },
513 |
514 | url: function( params ) {
515 | params = extend( extend( {}, QUnit.urlParams ), params );
516 | var key,
517 | querystring = "?";
518 |
519 | for ( key in params ) {
520 | if ( hasOwn.call( params, key ) ) {
521 | querystring += encodeURIComponent( key ) + "=" +
522 | encodeURIComponent( params[ key ] ) + "&";
523 | }
524 | }
525 | return window.location.protocol + "//" + window.location.host +
526 | window.location.pathname + querystring.slice( 0, -1 );
527 | },
528 |
529 | extend: extend,
530 | id: id,
531 | addEvent: addEvent,
532 | addClass: addClass,
533 | hasClass: hasClass,
534 | removeClass: removeClass
535 | // load, equiv, jsDump, diff: Attached later
536 | });
537 |
538 | /**
539 | * @deprecated: Created for backwards compatibility with test runner that set the hook function
540 | * into QUnit.{hook}, instead of invoking it and passing the hook function.
541 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
542 | * Doing this allows us to tell if the following methods have been overwritten on the actual
543 | * QUnit object.
544 | */
545 | extend( QUnit.constructor.prototype, {
546 |
547 | // Logging callbacks; all receive a single argument with the listed properties
548 | // run test/logs.html for any related changes
549 | begin: registerLoggingCallback( "begin" ),
550 |
551 | // done: { failed, passed, total, runtime }
552 | done: registerLoggingCallback( "done" ),
553 |
554 | // log: { result, actual, expected, message }
555 | log: registerLoggingCallback( "log" ),
556 |
557 | // testStart: { name }
558 | testStart: registerLoggingCallback( "testStart" ),
559 |
560 | // testDone: { name, failed, passed, total, runtime }
561 | testDone: registerLoggingCallback( "testDone" ),
562 |
563 | // moduleStart: { name }
564 | moduleStart: registerLoggingCallback( "moduleStart" ),
565 |
566 | // moduleDone: { name, failed, passed, total }
567 | moduleDone: registerLoggingCallback( "moduleDone" )
568 | });
569 |
570 | if ( !defined.document || document.readyState === "complete" ) {
571 | config.autorun = true;
572 | }
573 |
574 | QUnit.load = function() {
575 | runLoggingCallbacks( "begin", QUnit, {} );
576 |
577 | // Initialize the config, saving the execution queue
578 | var banner, filter, i, j, label, len, main, ol, toolbar, val, selection,
579 | urlConfigContainer, moduleFilter, userAgent,
580 | numModules = 0,
581 | moduleNames = [],
582 | moduleFilterHtml = "",
583 | urlConfigHtml = "",
584 | oldconfig = extend( {}, config );
585 |
586 | QUnit.init();
587 | extend(config, oldconfig);
588 |
589 | config.blocking = false;
590 |
591 | len = config.urlConfig.length;
592 |
593 | for ( i = 0; i < len; i++ ) {
594 | val = config.urlConfig[i];
595 | if ( typeof val === "string" ) {
596 | val = {
597 | id: val,
598 | label: val
599 | };
600 | }
601 | config[ val.id ] = QUnit.urlParams[ val.id ];
602 | if ( !val.value || typeof val.value === "string" ) {
603 | urlConfigHtml += "" + val.label + " ";
611 | } else {
612 | urlConfigHtml += "" + val.label +
615 | ": ";
619 | selection = false;
620 | if ( QUnit.is( "array", val.value ) ) {
621 | for ( j = 0; j < val.value.length; j++ ) {
622 | urlConfigHtml += "" + escapeText( val.value[j] ) + " ";
627 | }
628 | } else {
629 | for ( j in val.value ) {
630 | if ( hasOwn.call( val.value, j ) ) {
631 | urlConfigHtml += "" + escapeText( val.value[j] ) + " ";
636 | }
637 | }
638 | }
639 | if ( config[ val.id ] && !selection ) {
640 | urlConfigHtml += "" +
642 | escapeText( config[ val.id ] ) +
643 | " ";
644 | }
645 | 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 += "Module: < All Modules > ";
660 |
661 |
662 | for ( i = 0; i < numModules; i++) {
663 | moduleFilterHtml += "" + escapeText(moduleNames[i]) + " ";
666 | }
667 | 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 |
--------------------------------------------------------------------------------