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="#">▲</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="#">▼</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.