PageSourceSearch

https://live.invitario.com/js/jquery.collection.js

js invitario.com collected 2026-09-25 15:03:01 UTC 47,498 bytes, 1,008 lines download raw bytes

1/*
2 * jquery.collection.js
3 *
4 * Copyright (c) 2042 alain tiemblo <alain at fuz dot org>
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
7 * "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9 * following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12 *
13 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
15 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
16 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 */
18
19;
20(function ($) {
21
22    $.fn.collection = function (options) {
23
24        var defaults = {
25            container: 'body',
26            allow_up: true,
27            up: '<a href="#">&#x25B2;</a>',
28            before_up: function (collection, element) {
29                return true;
30            },
31            after_up: function (collection, element) {
32                return true;
33            },
34            allow_down: true,
35            down: '<a href="#">&#x25BC;</a>',
36            before_down: function (collection, element) {
37                return true;
38            },
39            after_down: function (collection, element) {
40                return true;
41            },
42            allow_add: true,
43            add: '<a href="#">[ + ]</a>',
44            before_add: function (collection, element) {
45                return true;
46            },
47            after_add: function (collection, element) {
48                return true;
49            },
50            allow_remove: true,
51            remove: '<a href="#">[ - ]</a>',
52            before_remove: function (collection, element) {
53                return true;
54            },
55            after_remove: function (collection, element) {
56                return true;
57            },
58            allow_duplicate: false,
59            duplicate: '<a href="#">[ # ]</a>',
60            before_duplicate: function (collection, element) {
61                return true;
62            },
63            after_duplicate: function (collection, element) {
64                return true;
65            },
66            before_init: function (collection) {
67            },
68            after_init: function (collection) {
69            },
70            min: 0,
71            max: 100,
72            add_at_the_end: false,
73            prefix: 'collection',
74            prototype_name: '__name__',
75            name_prefix: null,
76            elements_selector: '> div, > fieldset',
77            elements_parent_selector: '%id%',
78            children: null,
79            init_with_n_elements: 0,
80            hide_useless_buttons: true,
81            drag_drop: true,
82            drag_drop_options: {
83                'placeholder': 'ui-state-highlight'
84            },
85            drag_drop_start: function (event, ui) {
86                return true;
87            },
88            drag_drop_update: function (event, ui) {
89                return true;
90            },
91            custom_add_location: false,
92            action_container_tag: 'div',
93            fade_in: true,
94            fade_out: true,
95            position_field_selector: null,
96            preserve_names: false
97        };
98
99        // used to generate random id attributes when required and missing
100        var randomNumber = function () {
101            var rand = '' + Math.random() * 1000 * new Date().getTime();
102            return rand.replace('.', '').split('').sort(function () {
103                return 0.5 - Math.random();
104            }).join('');
105        };
106
107        // return an element's id, after generating one when missing
108        var getOrCreateId = function (prefix, obj) {
109            if (!obj.attr('id')) {
110                var generated_id;
111                do {
112                    generated_id = prefix + '_' + randomNumber();
113                } while ($('#' + generated_id).length > 0);
114                obj.attr('id', generated_id);
115            }
116            return obj.attr('id');
117        };
118
119        // return a field value whatever the field type
120        var getFieldValue = function (selector) {
121            try {
122                var jqElem = $(selector);
123            } catch (e) {
124                return null;
125            }
126            if (jqElem.length === 0) {
127                return null;
128            } else if (jqElem.is('input[type="checkbox"]')) {
129                return (jqElem.prop('checked') === true ? true : false);
130            } else if (jqElem.is('input[type="radio"]') && jqElem.attr('name') !== undefined) {
131                return $('input[name="' + jqElem.attr('name') + '"]:checked').val();
132            } else if (jqElem.prop('value') !== undefined) {
133                return jqElem.val();
134            } else {
135                return jqElem.html();
136            }
137        };
138
139        // set a field value in accordance to the field type
140        var putFieldValue = function (selector, value, physical) {
141            try {
142                var jqElem = $(selector);
143            } catch (e) {
144                return;
145            }
146            if (jqElem.length === 0) {
147                return;
148            } else if (jqElem.is('input[type="checkbox"]')) {
149                if (value) {
150                    jqElem.attr('checked', true);
151                } else {
152                    jqElem.removeAttr('checked');
153                }
154            } else if (jqElem.prop('value') !== undefined) {
155                if (physical) {
156                    jqElem.attr('value', value);
157                } else {
158                    jqElem.val(value);
159                }
160            } else {
161                jqElem.html(value);
162            }
163        };
164
165        // a callback set in an event will be considered failed if it
166        // returns false, null, or 0.
167        var trueOrUndefined = function (value) {
168            return undefined === value || value;
169        };
170
171        // used to change element indexes in arbitary id attributes
172        var pregQuote = function (string) {
173            return (string + '').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
174        };
175
176        // if we need to change CollectionType_field_42_value to CollectionType_field_84_value, this method
177        // will change it in id="CollectionType_field_42_value", but also data-id="CollectionType_field_42_value"
178        // or anywhere else just in case it could be used otherwise.
179        var replaceAttrData = function (elements, index, toReplace, replaceWith) {
180
181            var replaceAttrDataNode = function (node) {
182                var jqNode = $(node);
183                if (typeof node === 'object' && 'attributes' in node) {
184                    $.each(node.attributes, function (i, attrib) {
185                        if ($.type(attrib.value) === 'string') {
186                            jqNode.attr(attrib.name.replace(toReplace, replaceWith), attrib.value.replace(toReplace, replaceWith));
187                        }
188                    });
189                }
190                if (jqNode.length > 0) {
191                    $.each(jqNode.data(), function (name, value) {
192                        if ($.type(value) === 'string') {
193                            jqNode.data(name.replace(toReplace, replaceWith), value.replace(toReplace, replaceWith));
194                        }
195                    });
196                }
197            };
198
199            var element = elements.eq(index);
200            replaceAttrDataNode(element[0]);
201            element.find('*').each(function () {
202                replaceAttrDataNode(this);
203            });
204        };
205
206        // replace element names and indexes in the collection, in Symfony, names are always in format
207        // CollectionType[field][42][value] and ids are in format CollectionType_field_42_value;
208        // so we need to change both.
209        var changeElementIndex = function (collection, elements, settings, index, oldIndex, newIndex) {
210            var toReplace = new RegExp(pregQuote(settings.name_prefix + '[' + oldIndex + ']'), 'g');
211            var replaceWith = settings.name_prefix + '[' + newIndex + ']';
212
213            if (settings.children) {
214                $.each(settings.children, function (key, child) {
215                    var childCollection = collection.find(child.selector).eq(index);
216                    var childSettings = childCollection.data('collection-settings');
217                    if (childSettings) {
218                        childSettings.name_prefix = childSettings.name_prefix.replace(toReplace, replaceWith);
219                        childCollection.data('collection-settings', childSettings);
220                    }
221                });
222            }
223
224            replaceAttrData(elements, index, toReplace, replaceWith);
225
226            toReplace = new RegExp(pregQuote(collection.attr('id') + '_' + oldIndex), 'g');
227            replaceWith = collection.attr('id') + '_' + newIndex;
228
229            if (settings.children) {
230                $.each(settings.children, function (key, child) {
231                    var childCollection = collection.find(child.selector).eq(index);
232                    var childSettings = childCollection.data('collection-settings');
233                    if (childSettings) {
234                        childSettings.elements_parent_selector = childSettings.elements_parent_selector.replace(toReplace, replaceWith);
235                        childSettings.elements_selector = childSettings.elements_selector.replace(toReplace, replaceWith);
236                        childSettings.prefix = childSettings.prefix.replace(toReplace, replaceWith);
237                        childCollection.data('collection-settings', childSettings);
238                    }
239                });
240            }
241
242            replaceAttrData(elements, index, toReplace, replaceWith);
243        };
244
245        // same as above, but will replace element names and indexes in an html string instead
246        // of in a dom element.
247        var changeHtmlIndex = function (collection, settings, html, oldIndex, newIndex, oldKey, newKey) {
248            var toReplace = new RegExp(pregQuote(settings.name_prefix + '[' + oldKey + ']'), 'g');
249            var replaceWith = settings.name_prefix + '[' + newKey + ']';
250            html = html.replace(toReplace, replaceWith);
251
252            toReplace = new RegExp(pregQuote(collection.attr('id') + '_' + oldIndex), 'g');
253            replaceWith = collection.attr('id') + '_' + newIndex;
254            html = html.replace(toReplace, replaceWith);
255
256            return html;
257        };
258
259        // sometimes, setting a value will only be made in memory and not
260        // physically in the dom; and we need the full dom when we want
261        // to duplicate a field.
262        var putFieldValuesInDom = function (element) {
263            $(element).find(':input').each(function (index, inputObj) {
264                putFieldValue(inputObj, getFieldValue(inputObj), true);
265            });
266        };
267
268        // this method does the whole magic: in a collection, if we want to
269        // move elements and keep element positions in the backend, we should
270        // either move element names or element contents, but not both! thus,
271        // if you just move elements in the dom, you keep field names and data
272        // attached and nothing will change in the backend.
273        var swapElements = function (collection, elements, oldIndex, newIndex) {
274
275            var settings = collection.data('collection-settings');
276
277            if (!settings.position_field_selector && !settings.preserve_names) {
278                changeElementIndex(collection, elements, settings, oldIndex, oldIndex, '__swap__');
279                changeElementIndex(collection, elements, settings, newIndex, newIndex, oldIndex);
280                changeElementIndex(collection, elements, settings, oldIndex, '__swap__', newIndex);
281            }
282
283            elements.eq(oldIndex).insertBefore(elements.eq(newIndex));
284            if (newIndex > oldIndex) {
285                elements.eq(newIndex).insertBefore(elements.eq(oldIndex));
286            } else {
287                elements.eq(newIndex).insertAfter(elements.eq(oldIndex));
288            }
289
290            return collection.find(settings.elements_selector);
291        };
292
293        // moving an element down of 3 rows means increasing its index of 3, and
294        // decreasing the 2 ones between of 1. Example: 0-A 1-B 2-C 3-D:
295        // moving B to 3 becomes 0-A 1-C 2-D 3-B
296        var swapElementsUp = function (collection, elements, settings, oldIndex, newIndex) {
297            for (var i = oldIndex + 1; (i <= newIndex); i++) {
298                elements = swapElements(collection, elements, i, i - 1);
299            }
300            return collection.find(settings.elements_selector);
301        };
302
303        // moving an element up of 3 rows means decreasing its index of 3, and
304        // increasing the 2 ones between of 1. Example: 0-A 1-B 2-C 3-D:
305        // moving D to 1 becomes 0-A 1-D 2-B 3-C
306        var swapElementsDown = function (collection, elements, settings, oldIndex, newIndex) {
307            for (var i = oldIndex - 1; (i >= newIndex); i--) {
308                elements = swapElements(collection, elements, i, i + 1);
309            }
310            return collection.find(settings.elements_selector);
311        };
312
313        // if we create an element at position 2, all element indexes from 2 to N
314        // should be increased. for example, in 0-A 1-B 2-C 3-D, adding X at position
315        // 1 will create 0-A 1-X 2-B 3-C 4-D
316        var shiftElementsUp = function (collection, elements, settings, index) {
317            for (var i = index + 1; i < elements.length; i++) {
318                elements = swapElements(collection, elements, i - 1, i);
319            }
320            return collection.find(settings.elements_selector);
321        };
322
323        // if we remove an element at position 3, all element indexes from 3 to N
324        // should be decreased. for example, in 0-A 1-B 2-C 3-D, removing B will create
325        // 0-A 1-C 2-D
326        var shiftElementsDown = function (collection, elements, settings, index) {
327            for (var i = elements.length - 2; i > index; i--) {
328                elements = swapElements(collection, elements, i + 1, i);
329            }
330            return collection.find(settings.elements_selector);
331        };
332
333        // this method creates buttons for each action, according to all options set
334        // (buttons enabled, minimum/maximum of elements not yet reached, rescue
335        // button creation when no more elements are remaining...)
336        var dumpCollectionActions = function (collection, settings, isInitialization, event) {
337            var elementsParent = $(settings.elements_parent_selector);
338            var init = elementsParent.find('.' + settings.prefix + '-tmp').length === 0;
339            var elements = collection.find(settings.elements_selector);
340
341            // add a rescue button that will appear only if collection is emptied
342            if (settings.allow_add) {
343                if (init) {
344                    elementsParent.append('<span class="' + settings.prefix + '-tmp"></span>');
345                    if (settings.add) {
346                        elementsParent.append(
347                            $(settings.add)
348                                .addClass(settings.prefix + '-action ' + settings.prefix + '-rescue-add')
349                                .data('collection', collection.attr('id'))
350                        );
351                    }
352                }
353            }
354
355            // initializes the collection with a minimal number of elements
356            if (isInitialization) {
357                collection.data('collection-offset', elements.length);
358
359                var container = $(settings.container);
360                var button = collection.find('.' + settings.prefix + '-add, .' + settings.prefix + '-rescue-add, .' + settings.prefix + '-duplicate').first();
361                var secure = 0;
362                while (elements.length < settings.init_with_n_elements) {
363                    secure++;
364                    var element = elements.length > 0 ? elements.last() : undefined;
365                    var index = elements.length - 1;
366                    elements = doAdd(container, button, collection, settings, elements, element, index, false);
367                    if (secure > settings.init_with_n_elements) {
368                        console.error('Infinite loop, element selector (' + settings.elements_selector + ') not found !');
369                        break;
370                    }
371                }
372
373                collection.data('collection-offset', elements.length);
374            }
375
376            // make buttons appear/disappear in each elements of the collection according to options
377            // (enabled, min/max...) and logic (for example, do not put a move up button on the first
378            // element of the collection)
379            elements.each(function (index) {
380                var element = $(this);
381
382                if (isInitialization) {
383                    element.data('index', index);
384                }
385
386                var actions = element.find('.' + settings.prefix + '-actions').addBack().filter('.' + settings.prefix + '-actions');
387                if (actions.length === 0) {
388                    actions = $('<' + settings.action_container_tag + ' class="' + settings.prefix + '-actions"></' + settings.action_container_tag + '>');
389
390                    element.append(actions);
391                }
392
393                var delta = 0;
394                if (event === 'remove' && settings.fade_out) {
395                    delta = 1;
396                }
397
398                var buttons = [
399                    {
400                        'enabled': settings.allow_remove,
401                        'selector': settings.prefix + '-remove',
402                        'html': settings.remove,
403                        'condition': elements.length - delta > settings.min
404                    }, {
405                        'enabled': settings.allow_up,
406                        'selector': settings.prefix + '-up',
407                        'html': settings.up,
408                        'condition': elements.length - delta > 1 && elements.index(element) - delta > 0
409                    }, {
410                        'enabled': settings.allow_down,
411                        'selector': settings.prefix + '-down',
412                        'html': settings.down,
413                        'condition': elements.length - delta > 1 && elements.index(element) !== elements.length - 1
414                    }, {
415                        'enabled': settings.allow_add && !settings.add_at_the_end && !settings.custom_add_location,
416                        'selector': settings.prefix + '-add',
417                        'html': settings.add,
418                        'condition': elements.length - delta < settings.max
419                    }, {
420                        'enabled': settings.allow_duplicate,
421                        'selector': settings.prefix + '-duplicate',
422                        'html': settings.duplicate,
423                        'condition': elements.length - delta < settings.max
424                    }
425                ];
426
427                $.each(buttons, function (i, button) {
428                    if (button.enabled) {
429                        var action = element.find('.' + button.selector);
430                        if (action.length === 0 && button.html) {
431                            action = $(button.html)
432                                .appendTo(actions)
433                                .addClass(button.selector);
434                        }
435                        if (button.condition) {
436                            action.removeClass(settings.prefix + '-action-disabled');
437                            if (settings.hide_useless_buttons) {
438                                action.css('display', '');
439                            }
440                        } else {
441                            action.addClass(settings.prefix + '-action-disabled');
442                            if (settings.hide_useless_buttons) {
443                                action.css('display', 'none');
444                            }
445                        }
446                        action
447                            .addClass(settings.prefix + '-action')
448                            .data('collection', collection.attr('id'))
449                            .data('element', getOrCreateId(collection.attr('id') + '_' + index, element));
450                    } else {
451                        element.find('.' + button.selector).css('display', 'none');
452                    }
453                });
454
455            }); // elements.each
456
457            // make the rescue button appear / disappear according to options (add_at_the_end) and
458            // logic (no more elements on the collection)
459            if (settings.allow_add) {
460
461                var delta = 0;
462                if (event === 'remove' && settings.fade_out) {
463                    delta = 1;
464                }
465
466                var rescueAdd = collection.find('.' + settings.prefix + '-rescue-add').css('display', '').removeClass(settings.prefix + '-action-disabled');
467                var adds = collection.find('.' + settings.prefix + '-add');
468                if (!settings.add_at_the_end && adds.length > delta || settings.custom_add_location) {
469                    rescueAdd.css('display', 'none');
470                } else if (event === 'remove' && settings.fade_out) {
471                    rescueAdd.css('display', 'none');
472                    rescueAdd.fadeIn('fast');
473                }
474                if (elements.length - delta >= settings.max) {
475                    rescueAdd.addClass(settings.prefix + '-action-disabled');
476                    if (settings.hide_useless_buttons) {
477                        collection.find('.' + settings.prefix + '-add, .' + settings.prefix + '-rescue-add, .' + settings.prefix + '-duplicate').css('display', 'none');
478                    }
479                }
480            }
481
482        }; // dumpCollectionActions
483
484        // this plugin supports nested collections, and this method enables them when the
485        // parent collection is initialized. see
486        // http://symfony-collection.fuz.org/symfony3/advanced/collectionOfCollections
487        var enableChildrenCollections = function (collection, element, settings) {
488            if (settings.children) {
489                $.each(settings.children, function (index, childrenSettings) {
490                    if (!childrenSettings.selector) {
491                        console.log("jquery.collection.js: given collection " + collection.attr('id') + " has children collections, but children's root selector is undefined.");
492                        return true;
493                    }
494                    if (element !== null) {
495                        element.find(childrenSettings.selector).collection(childrenSettings);
496                    } else {
497                        collection.find(childrenSettings.selector).collection(childrenSettings);
498                    }
499                });
500            }
501        };
502
503        // this method handles a click on "add" buttons, it increases all following element indexes of
504        // 1 position and insert a new one in the index that becomes free. if click has been made on a
505        // "duplicate" button, all element values are then inserted. finally, callbacks let user cancel
506        // those actions if needed.
507        var doAdd = function (container, that, collection, settings, elements, element, index, isDuplicate) {
508            if (elements.length < settings.max && (isDuplicate && trueOrUndefined(settings.before_duplicate(collection, element)) || trueOrUndefined(settings.before_add(collection, element)))) {
509                var prototype = collection.data('prototype');
510                var freeIndex = collection.data('collection-offset');
511
512                collection.data('collection-offset', freeIndex + 1);
513
514                if (index === -1) {
515                    index = elements.length - 1;
516                }
517                var regexp = new RegExp(pregQuote(settings.prototype_name), 'g');
518                var freeKey = freeIndex;
519
520                if (settings.preserve_names) {
521                    freeKey = collection.data('collection-free-key');
522
523                    if (freeKey === undefined) {
524                        freeKey = findFreeNumericKey(settings, elements);
525                    }
526
527                    collection.data('collection-free-key', freeKey + 1);
528                }
529
530                var code = $(prototype.replace(regexp, freeKey)).data('index', freeIndex);
531                setRightPrefix(settings, code);
532
533                var elementsParent = $(settings.elements_parent_selector);
534                var tmp = elementsParent.find('> .' + settings.prefix + '-tmp');
535                var id = $(code).find('[id]').first().attr('id');
536
537                if (isDuplicate) {
538                    var oldElement = elements.eq(index);
539                    putFieldValuesInDom(oldElement);
540                    var oldHtml = $("<div/>").append(oldElement.clone()).html();
541                    var oldIndex = settings.preserve_names || settings.position_field_selector ? oldElement.data('index') : index;
542                    var oldKey = settings.preserve_names ? getElementKey(settings, oldElement) : oldIndex;
543                    var newHtml = changeHtmlIndex(collection, settings, oldHtml, oldIndex, freeIndex, oldKey, freeKey);
544
545                    code = $('<div/>').html(newHtml).contents().data('index', freeIndex);
546                    if (settings.fade_in) {
547                        code.hide();
548                    }
549                    tmp.before(code).find(settings.prefix + '-actions').remove();
550                } else {
551                    if (settings.fade_in) {
552                        code.hide();
553                    }
554
555                    tmp.before(code);
556                }
557
558                elements = collection.find(settings.elements_selector);
559
560                var action = code.find('.' + settings.prefix + '-add, .' + settings.prefix + '-duplicate');
561                if (action.length > 0) {
562                    action.addClass(settings.prefix + '-action').data('collection', collection.attr('id'));
563                }
564
565                if (!settings.add_at_the_end && index + 1 !== freeIndex) {
566                    elements = doMove(collection, settings, elements, code, freeIndex, index + 1);
567                } else {
568                    dumpCollectionActions(collection, settings, false);
569                }
570
571                enableChildrenCollections(collection, code, settings);
572
573                if ((isDuplicate && !trueOrUndefined(settings.after_duplicate(collection, code))) || !trueOrUndefined(settings.after_add(collection, code))) {
574                    if (index !== -1) {
575                        elements = shiftElementsUp(collection, elements, settings, index + 1);
576                    }
577                    code.remove();
578                }
579            }
580
581            if (code !== undefined && settings.fade_in) {
582                code.fadeIn('fast', function () {
583                    if (settings.position_field_selector) {
584                        doRewritePositions(settings, elements);
585                    }
586                });
587            } else {
588                if (settings.position_field_selector) {
589                    return doRewritePositions(settings, elements);
590                }
591            }
592
593            return elements;
594        };
595
596        // removes the current element when clicking on a "delete" button and decrease all following
597        // indexes from 1 position.
598        var doDelete = function (collection, settings, elements, element, index) {
599            if (elements.length > settings.min && trueOrUndefined(settings.before_remove(collection, element))) {
600                var deletion = function () {
601                    var toDelete = element;
602                    if (!settings.preserve_names) {
603                        elements = shiftElementsUp(collection, elements, settings, index);
604                        toDelete = elements.last();
605                    }
606                    var backup = toDelete.clone({withDataAndEvents: true}).show();
607                    toDelete.remove();
608                    if (!trueOrUndefined(settings.after_remove(collection, backup))) {
609                        var elementsParent = $(settings.elements_parent_selector);
610                        elementsParent.find('> .' + settings.prefix + '-tmp').before(backup);
611                        elements = collection.find(settings.elements_selector);
612                        elements = shiftElementsDown(collection, elements, settings, index - 1);
613                    }
614                    if (settings.position_field_selector) {
615                        doRewritePositions(settings, elements);
616                    }
617                };
618                if (settings.fade_out) {
619                    element.fadeOut('fast', function () {
620                        deletion();
621                    });
622                } else {
623                    deletion();
624                }
625            }
626
627            return elements;
628        };
629
630        // reverse current element and the previous one (so the current element
631        // appears one place higher)
632        var doUp = function (collection, settings, elements, element, index) {
633            if (index !== 0 && trueOrUndefined(settings.before_up(collection, element))) {
634                elements = swapElements(collection, elements, index, index - 1);
635                if (!trueOrUndefined(settings.after_up(collection, element))) {
636                    elements = swapElements(collection, elements, index - 1, index);
637                }
638            }
639
640            if (settings.position_field_selector) {
641                return doRewritePositions(settings, elements);
642            }
643
644            return elements;
645        };
646
647        // reverse the current element and the next one (so the current element
648        // appears one place lower)
649        var doDown = function (collection, settings, elements, element, index) {
650            if (index !== (elements.length - 1) && trueOrUndefined(settings.before_down(collection, element))) {
651                elements = swapElements(collection, elements, index, index + 1);
652                if (!trueOrUndefined(settings.after_down(collection, elements))) {
653                    elements = swapElements(collection, elements, index + 1, index);
654                }
655            }
656
657            if (settings.position_field_selector) {
658                return doRewritePositions(settings, elements);
659            }
660
661            return elements;
662        };
663
664        // move an element from a position to an arbitrary new position
665        var doMove = function (collection, settings, elements, element, oldIndex, newIndex) {
666            if (1 === Math.abs(newIndex - oldIndex)) {
667                elements = swapElements(collection, elements, oldIndex, newIndex);
668                if (!(newIndex - oldIndex > 0 ? trueOrUndefined(settings.after_up(collection, element)) : trueOrUndefined(settings.after_down(collection, element)))) {
669                    elements = swapElements(collection, elements, newIndex, oldIndex);
670                }
671            } else {
672                if (oldIndex < newIndex) {
673                    elements = swapElementsUp(collection, elements, settings, oldIndex, newIndex);
674                    if (!(newIndex - oldIndex > 0 ? trueOrUndefined(settings.after_up(collection, element)) : trueOrUndefined(settings.after_down(collection, element)))) {
675                        elements = swapElementsDown(collection, elements, settings, newIndex, oldIndex);
676                    }
677                } else {
678                    elements = swapElementsDown(collection, elements, settings, oldIndex, newIndex);
679                    if (!(newIndex - oldIndex > 0 ? trueOrUndefined(settings.after_up(collection, element)) : trueOrUndefined(settings.after_down(collection, element)))) {
680                        elements = swapElementsUp(collection, elements, settings, newIndex, oldIndex);
681                    }
682                }
683            }
684            dumpCollectionActions(collection, settings, false);
685
686            if (settings.position_field_selector) {
687                return doRewritePositions(settings, elements);
688            }
689
690            return elements;
691        };
692
693        var doRewritePositions = function (settings, elements) {
694            $(elements).each(function () {
695                var element = $(this);
696                putFieldValue(element.find(settings.position_field_selector), elements.index(element));
697            });
698
699            return elements;
700        };
701
702        var getElementKey = function (settings, element) {
703            var name = element.find(':input[name^="' + settings.name_prefix + '["]').attr('name');
704
705            return name.slice(settings.name_prefix.length + 1).split(']', 1)[
7050];
706        };
707
708        var findFreeNumericKey = function (settings, elements) {
709            var freeKey = 0;
710
711            elements.each(function () {
712                var key = getElementKey(settings, $(this));
713
714                if (/^0|[1-9]\d*$/.test(key) && key >= freeKey) {
715                    freeKey = Number(key) + 1;
716                }
717            });
718
719            return freeKey;
720        };
721
722        var setRightPrefix = function (settings, container) {
723            var suffixes = [
724                '-action',
725                '-action-disabled',
726                '-actions',
727                '-add',
728                '-down',
729                '-duplicate',
730                '-remove',
731                '-rescue-add',
732                '-tmp',
733                '-up'
734            ];
735
736            $.each(suffixes, function () {
737                var suffix = this;
738                container.each(function () {
739                    var that = $(this);
740                    if (that.hasClass(settings.user_prefix + suffix)) {
741                        that.addClass(settings.prefix + suffix);
742                    }
743                    that.find('*').each(function () {
744                        var here = $(this);
745                        if (here.hasClass(settings.user_prefix + suffix)) {
746                            here.addClass(settings.prefix + suffix);
747                        }
748                    });
749                });
750            });
751        };
752
753        // we're in a $.fn., so in $('.collection').collection(), $(this) equals $('.collection')
754        var elems = $(this);
755
756        // at least one, but why not several collections should be raised
757        if (elems.length === 0) {
758            console.log("jquery.collection.js: given collection selector does not exist.");
759            return false;
760        }
761
762        elems.each(function () {
763
764            var settings = $.extend(true, {}, defaults, options);
765
766            // usage of $.fn.on events using a static container just in case there would be some
767            // ajax interactions inside the collection
768            if ($(settings.container).length === 0) {
769                console.log("jquery.collection.js: a container should exist to handle events (basically, a <body> tag).");
770                return false;
771            }
772
773            // it is possible to use this plugin with a selector that will contain the collection id
774            // in a data attribute
775            var elem = $(this);
776            if (elem.data('collection') !== undefined) {
777                var collection = $('#' + elem.data('collection'));
778                if (collection.length === 0) {
779                    console.log("jquery.collection.js: given collection id does not exist.");
780                    return true;
781                }
782            } else {
783                collection = elem;
784            }
785            collection = $(collection);
786
787            // when adding elements to a collection, we should be aware of the node that will contain them
788            settings.elements_parent_selector = settings.elements_parent_selector.replace('%id%', '#' + getOrCreateId('', collection));
789            if (!settings.elements_parent_selector) {
790                settings.elements_parent_selector = '#' + getOrCreateId('', collection);
791                if ($(settings.elements_parent_selector).length === 0) {
792                    console.log("jquery.collection.js: given elements parent selector does not return any object.");
793                    return true;
794                }
795            }
796
797            // On nested collections, prefix is the same for all children leading to very
798            // random and unexepcted issues, so we merge prefix with current collection id.
799            settings.user_prefix = settings.prefix;
800            settings.prefix = collection.attr('id') + '-' + settings.user_prefix;
801            setRightPrefix(settings, collection);
802
803            // enforcing logic between options
804            if (!settings.allow_add) {
805                settings.allow_duplicate = false;
806                settings.add_at_the_end = false;
807            }
808            if (settings.init_with_n_elements > settings.max) {
809                settings.init_with_n_elements = settings.max;
810            }
811            if (settings.min && (!settings.init_with_n_elements || settings.init_with_n_elements < settings.min)) {
812                settings.init_with_n_elements = settings.min;
813            }
814
815            if (!settings.action_container_tag) {
816                console.log("jquery.collection.js: action_container_tag needs to be set.");
817                return true;
818            }
819
820            // user callback
821            settings.before_init(collection);
822
823            // prototype required to create new elements in the collection
824            if (collection.data('prototype') === null) {
825                console.log("jquery.collection.js: given collection field has no prototype, check that your field has the prototype option set to true.");
826                return true;
827            }
828
829            // all the following data attributes are automatically available thanks to
830            // jquery.collection.html.twig form theme
831            if (collection.data('prototype-name') !== undefined) {
832                settings.prototype_name = collection.data('prototype-name');
833            }
834            if (collection.data('allow-add') !== undefined) {
835                settings.allow_add = collection.data('allow-add');
836                settings.allow_duplicate = collection.data('allow-add') ? settings.allow_duplicate : false;
837            }
838            if (collection.data('allow-remove') !== undefined) {
839                settings.allow_remove = collection.data('allow-remove');
840            }
841            if (collection.data('name-prefix') !== undefined) {
842                settings.name_prefix = collection.data('name-prefix');
843            }
844            if (collection.data('allow-min') !== undefined) {
845                settings.min = collection.data('allow-min');
846            }
847            if (collection.data('allow-max') !== undefined) {
848                settings.max = collection.data('allow-max');
849            }
850
851            // prototype-name required for nested collections, where collection id prefix
852            // isn't guessable (see https://github.com/symfony/symfony/issues/13837)
853            if (!settings.name_prefix) {
854                console.log("jquery.collection.js: the prefix used in descendant field names is mandatory, you can set it using 2 ways:");
855                console.log("jquery.collection.js: - use the form theme given with this plugin source");
856                console.log("jquery.collection.js: - set name_prefix option to  '{{ formView.myCollectionField.vars.full_name }}'");
857                return true;
858            }
859
860            // if preserve_names option is set, we should enforce many options to avoid
861            // having inconsistencies between the UI and the Symfony result
862            if (settings.preserve_names) {
863                settings.allow_up = false;
864                settings.allow_down = false;
865                settings.drag_drop = false;
866                settings.add_at_the_end = true;
867            }
868
869            // drag & drop support: this is a bit more complex than pressing "up" or
870            // "down" buttons because we can move elements more than one place ahead
871            // or below...
872            if ((typeof jQuery.ui !== 'undefined' && typeof jQuery.ui.sortable !== 'undefined')
873                && collection.data('sortable')) {
874                collection.sortable('disable');
875            }
876            if (settings.drag_drop && settings.allow_up && settings.allow_down) {
877                var oldPosition;
878                var newPosition;
879                if (typeof jQuery.ui === 'undefined' || typeof jQuery.ui.sortable === 'undefined') {
880                    settings.drag_drop = false;
881                } else {
882                    collection.sortable($.extend(true, {}, {
883                        start: function (event, ui) {
884                            var elements = collection.find(settings.elements_selector);
885                            var element = ui.item;
886                            var that = $(this);
887                            if (!trueOrUndefined(settings.drag_drop_start(event, ui, elements, element))) {
888                                that.sortable("cancel");
889                                return;
890                            }
891                            ui.placeholder.height(ui.item.height());
892                            ui.placeholder.width(ui.item.width());
893                            oldPosition = elements.index(element);
894                        },
895                        update: function (event, ui) {
896                            var elements = collection.find(settings.elements_selector);
897                            var element = ui.item;
898                            var that = $(this);
899                            that.sortable("cancel");
900                            if (false === settings.drag_drop_update(event, ui, elements, element) || !(newPosition - oldPosition > 0 ? trueOrUndefined(settings.before_up(collection, element)) : trueOrUndefined(settings.before_down(collection, element)))) {
901                                return;
902                            }
903                            newPosition = elements.index(element);
904                            elements = collection.find(settings.elements_selector);
905                            doMove(collection, settings, elements, element, oldPosition, newPosition);
906                        }
907                    }, settings.drag_drop_options));
908                }
909            }
910
911            collection.data('collection-settings', settings);
912
913            // events on buttons using a "static" container so even newly
914            // created/ajax downloaded buttons doesn't need further initialization
915            var container = $(settings.container);
916            container
917                .off('click', '.' + settings.prefix + '-action')
918                .on('click', '.' + settings.prefix + '-action', function (e) {
919
920                    var that = $(this);
921
922                    var collection = $('#' + that.data('collection'));
923                    var settings = collection.data('collection-settings');
924
925                    if (undefined === settings) {
926                        var collection = $('#' + that.data('collection')).find('.' + that.data('collection') + '-collection');
927                        var settings = collection.data('collection-settings');
928                        if (undefined === settings) {
929                            throw "Can't find collection: " + that.data('collection');
930                        }
931                    }
932
933                    var elements = collection.find(settings.elements_selector);
934                    var element = that.data('element') ? $('#' + that.data('element')) : undefined;
935                    var index = element && element.length ? elements.index(element) : -1;
936                    var event = null;
937
938                    var isDuplicate = that.is('.' + settings.prefix + '-duplicate');
939                    if ((that.is('.' + settings.prefix + '-add') || that.is('.' + settings.prefix + '-rescue-add') || isDuplicate) && settings.allow_add) {
940                        event = 'add';
941                        elements = doAdd(container, that, collection, settings, elements, element, index, isDuplicate);
942                    }
943
944                    if (that.is('.' + settings.prefix + '-remove') && settings.allow_remove) {
945                        event = 'remove';
946                        elements = doDelete(collection, settings, elements, element, index);
947                    }
948
949                    if (that.is('.' + settings.prefix + '-up') && settings.allow_up) {
950                        event = 'up';
951                        elements = doUp(collection, settings, elements, element, index);
952                    }
953
954                    if (that.is('.' + settings.prefix + '-down') && settings.allow_down) {
955                        event = 'down';
956                        elements = doDown(collection, settings, elements, element, index);
957                    }
958
959                    dumpCollectionActions(collection, settings, false, event);
960                    e.preventDefault();
961                }); // .on
962
963            dumpCollectionActions(collection, settings, true);
964            enableChildrenCollections(collection, null, settings);
965
966            // if collection elements are given in the wrong order, plugin
967            // must reorder them graphically
968            if (settings.position_field_selector) {
969                var array = [];
970                var elements = collection.find(settings.elements_selector);
971                elements.each(function (index) {
972                    var that = $(this);
973                    array.push({
974                        position: parseFloat(getFieldValue(that.find(settings.position_field_selector))),
975                        element: that
976                    });
977                });
978
979                var sorter = function (a, b) {
980                    return (a.position < b.position ? -1 : (a.position > b.position ? 1 : 0));
981                };
982                array.sort(sorter);
983
984                $.each(array, function (newIndex, object) {
985                    var ids = [];
986                    $(elements).each(function (index) {
987                        ids.push($(this).attr('id'));
988                    });
989
990                    var element = object.element;
991                    var oldIndex = $.inArray(element.attr('id'), ids);
992
993                    if (newIndex !== oldIndex) {
994                        elements = doMove(collection, settings, elements, element, oldIndex, newIndex);
995                        putFieldValue(element.find(settings.position_field_selector), elements.index(element));
996                    }
997                });
998            } // if (settings.position_field_selector) {
999
1000            settings.after_init(collection);
1001
1002        }); // elem.each
1003
1004        return true;
1005    }; // $.fn.collection
1006
1007})
1008(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.