├── README.md ├── doc ├── cover.png ├── howTo.md └── tryIt.md ├── example ├── public │ └── js │ │ ├── textCollection.js │ │ └── textListCollection.js └── views │ ├── complex.html │ └── simple.html └── src └── collection.js /README.md: -------------------------------------------------------------------------------- 1 | # Javascript to handle Symfony form collections 2 | 3 | ![Example collection](doc/cover.png) 4 | 5 | ## Goal 6 | 7 | The aim of [collection.js](src/collection.js) is to provide the 8 | minimum javascript code to handle the 9 | [form collection](http://symfony.com/doc/current/reference/forms/types/collection.html) 10 | of symfony. I strongly advise you to do it yourself at least once by 11 | following the 12 | [cookbook](http://symfony.com/doc/current/cookbook/form/form_collections.html). 13 | 14 | This work relies on [jQuery](https://jquery.com/). 15 | 16 | ## Presentation 17 | 18 | This repository only aims at presenting 19 | [collection.js](src/collection.js). There are two examples. 20 | 21 | [simple.html](example/view/simple.html) is an example of were you 22 | should use [collection.js](src/collection.js). With no extra code you 23 | get a working form collection based on your symfony template. 24 | 25 | [complex.html](example/view/complex.html) is not necessarily a case 26 | were you should use [collection.js](src/collection.js). As there are 27 | lot of specific behaviors, there is no real need for an external 28 | part. However it exists to present how you can add your specific 29 | behaviors on top of [collection.js](src/collection.js). 30 | 31 | ## Try it 32 | 33 | To try it on your computer please read [this](doc/tryIt.md). 34 | For more information please read the [doc](doc/howTo.md). 35 | -------------------------------------------------------------------------------- /doc/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanlucc/sf-form-collection-js/6fdc76f5625f2e656c65beccb5a97d36d625e02f/doc/cover.png -------------------------------------------------------------------------------- /doc/howTo.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | I'll assume you are familiar with form collection. 4 | 5 | You will need: 6 | - a container: top html block with all elements which interact with the collection; 7 | - a collection holder: direct parent of the items of your collection; 8 | - a prototype: necessary html to create a new item; 9 | - a placeholder: pattern in the prototype to replace with a unique id at item creation; 10 | - a button to add an item; 11 | - a button to delete an item. 12 | 13 | ## Simple case 14 | 15 | I will explain the details of how 16 | [simple.html](../example/view/simple.html) works. 17 | - The container has the class `collection_container`. 18 | `collection.js` automatically initiate a `CollectionManager` on element with this class. 19 | - The collection holder has the class `collection_holder`. 20 | - The prototype is stored in `data-prototype` of the collection holder 21 | (default in symfony). 22 | - The placeholder is `__name__` (default in symfony). 23 | - The delete buttons have the class `delete_item` and the add button 24 | has the class `add_item`. 25 | - The items have the class `item`. 26 | 27 | The index used to replace the placeholder is intern to the 28 | `CollectionManager` and initialized with the children count of the 29 | holder. By default new elements are appended to the collection 30 | holder. 31 | 32 | I guess that's enough for default cases. 33 | 34 | ## Basic customization 35 | 36 | For some reason you might want to change some parameters. In order to 37 | do so, you have two choices. Either create the `CollectionManager` 38 | manually and provide an option object as argument. or use data on 39 | your container. 40 | 41 | ### Use data on the container 42 | 43 | ``` 44 |
45 | ... 46 |
47 | ``` 48 | 49 | ### Create CollectionManager manually 50 | 51 | Assuming `$collectionContainer` is the jQuery object associated to 52 | your container: 53 | 54 | ``` 55 | $collectionContainer.manageCollection({'option-name': 'option-value'}); 56 | ``` 57 | 58 | If you use both customizations, the data customization will be 59 | overridden by the one of the creation. 60 | 61 | ## Basic options 62 | 63 | - `insert-method`: can be `append` (default), `prepend`, `before` or `after`. 64 | [Append](http://api.jquery.com/append/) and 65 | [preprend](http://api.jquery.com/prepend/) uses jQuery to add the new 66 | item in the collection holder. [Before](http://api.jquery.com/before/) 67 | and [after](http://api.jquery.com/after/) uses jQuery to add the new 68 | item next to the chose item. In order to use one of those two method, 69 | each item must contain its own add button. 70 | - `prototype-name`: default `prototype`. 71 | The name of the data attached to the collection holder which contains the prototype. 72 | - `index-placeholder`: default `__name__`. 73 | The placeholder of the item index in the prototype. It must not 74 | contain regexp special characters. 75 | - `item-closest-selector`: default `.item`. 76 | The selector used to find the item from the click event target of 77 | delete. The same selector is used to add an item with the `before` or 78 | `after` insert method. 79 | - `collection-holder-selector`: default `.collection_holder`. 80 | The selector used to find the collection holder in the container. 81 | - `add-button-selector`: default `.add_item`. 82 | The selector used to find the add button(s) in the container. 83 | - `delete-button-selector`: default `.delete_item`. 84 | The selector used to find the delete buttons in the container. 85 | - `name`: aims to give an easy way to customize almost all other options. 86 | 87 | `name` effect: 88 | 89 | | option | `name` undefined | `name` = 'tag' | 90 | |------------------------------|----------------------|--------------------------| 91 | | `prototype-name` | `prototype` | `tag-prototype` | 92 | | `index-placeholder` | `__name__` | `__tag_name__` | 93 | | `item-closest-selector` | `.item` | `.tag_item` | 94 | | `collection-holder-selector` | `.collection_holder` | `.tag_collection_holder` | 95 | | `add-button-selector` | `.add_item` | `.add_tag_item` | 96 | | `delete-button-selector` | `.delete_item` | `.delete_tag_item` | 97 | 98 | If you provide a `name` and an option changed by `name`, the provided 99 | option override the option modified by `name`. 100 | 101 | All `selector` options can be any 102 | [selector](https://api.jquery.com/category/selectors/) valid in 103 | jQuery. 104 | 105 | ## Access the CollectionManager 106 | 107 | When you manage a collection manually, the created `CollectionManager` 108 | is returned (in an array) and you can access it. 109 | 110 | ``` 111 | var collectionManager = $collectionContainer.manageCollection()[0]; 112 | ``` 113 | 114 | Get the manager allows you to use its methods. 115 | - `getHolder`: returns the collection holder. 116 | 117 | var $collectionHolder = $(collectionManager.getHolder()); 118 | - `append`: append a new item to the holder. You can optionnaly provide the jQuery object you want to append. 119 | 120 | collectionManager.append(); 121 | or 122 | 123 | var $item = $('create your item'); 124 | collectionManager.append($item); 125 | - `prepend`: same as append but use prepend. 126 | - `before`: add an item before the provided element. You can optionnaly provide the jQuery object you want to append. 127 | 128 | var $elementBeforeWhichItemWillBeAdded = $('create here'); 129 | collectionManager.before($elementBeforeWhichItemWillBeAdded) 130 | or 131 | 132 | var $elementBeforeWhichItemWillBeAdded = $('create here'); 133 | var $item = $('create your item'); 134 | collectionManager.before($elementBeforeWhichItemWillBeAdded, $item); 135 | - `after`: same as before but use after. 136 | - `deleteItem`: delete the provided item. 137 | 138 | var $itemToDelete = $collectionHolder.find('item to delete'); 139 | collectionManager.deleteItem($itemToDelete); 140 | - `addItem`: add an item. This method takes three optional arguments. 141 | For each argument, if you provide `null`, the default option will be used. 142 | - The method can be any value of the `insert-method` option, it is used to override the option chose at `CollectionManager` creation. 143 | - The item to add. 144 | - The reference item used only if method is `before` or `after`. 145 | 146 | Examples: 147 | - Add an item with the default method (`append` or `prepend`) 148 | 149 | collectionManager.addItem(); 150 | - Add the given item using the default method (`append` or `preprend`) 151 | 152 | var $item = $('create your item'); 153 | collectionManager.addItem(null, $item); 154 | - Add an item relatively to another using default method (`before` or `after`) 155 | 156 | var $referenceItem = $('create here'); 157 | collectionManager.addItem(null, null, $referenceItem); 158 | - Add the given item relatively to another using default method (`before` or `after`) 159 | 160 | var $item = $('create your item'); 161 | var $referenceItem = $('create here'); 162 | collectionManager.addItem(null, $item, $referenceItem); 163 | - Append an item 164 | 165 | collectionManager.addItem('append'); 166 | - Put an item after another 167 | 168 | var $referenceItem = $('create here'); 169 | collectionManager.addItem('after', null, $referenceItem); 170 | 171 | The difference between addItem and the four specialized method 172 | (`append`, `preprend`, `before`, `after`) is that `addItem` like 173 | `deleteItem` fire events and the other four do not. If you do not wish 174 | to fire events on delete, you just need to use 175 | [remove](https://api.jquery.com/remove/) yourself. 176 | 177 | ## Use events 178 | 179 | You must wonder how you can add your custom javascript behaviors such as: 180 | - keep at least one (empty) item in the collection; 181 | - add javascript behavior to the created elements 182 | (e.g. a datepicker on an input, or create a nested collection); 183 | - disable add if there are "invalid" items; 184 | - duplicate an item instead of creating a new one; 185 | - store deleted elements somewhere else in the DOM. 186 | 187 | You will have to use the events. 188 | 189 | ### Customize event names 190 | 191 | - `before-add-event-name`: default `before-add`, the name of the event triggered on 192 | container before an item is added; 193 | - `after-add-event-name`: default `after-add`, the name of the event triggered on 194 | container after an item is added; 195 | - `before-delete-event-name`: default `before-delete`, the name of the event triggered on 196 | container before an item is deleted; 197 | - `after-delete-event-name`: default `after-delete`, the name of the event triggered on 198 | container after an item is deleted. 199 | 200 | 201 | The events name are also affected by the `name` option. Alternatively 202 | you can use `event-name` which overrides `name` and affect the event 203 | names in the same way. 204 | 205 | `name` effect: 206 | 207 | | option | `name` and `event-name` undefined | `name` or `event-name` = 'tag' | 208 | |----------------------------|-----------------------------------|--------------------------------| 209 | | `before-add-event-name` | `before-add` | `tag.before-add` | 210 | | `after-add-event-name` | `after-add` | `tag.after-add` | 211 | | `before-delete-event-name` | `before-delete` | `tag.before-delete` | 212 | | `after-delete-event-name` | `after-delete` | `tag.after-delete` | 213 | 214 | ### Register to an event 215 | 216 | ``` 217 | $collectionContainer.on('event-name', function(event) { 218 | ... 219 | }); 220 | ``` 221 | 222 | 223 | ### Before add 224 | 225 | If you do not want to add an item, use `preventDefault` on the event: 226 | 227 | ``` 228 | beforeAddEvent.preventDefault(); 229 | ``` 230 | 231 | The event has some arguments that you can use to customize the behavior. 232 | - `itemToAdd`: the item that will be added; 233 | - `method`: the method that will be used to add the item; 234 | - `referenceItem`: the reference item or undefined if the clicked 235 | button is not inside an item. 236 | 237 | ``` 238 | var $icon = $('my beautifull icon'); 239 | var $item = $(beforeAddEvent.itemToAdd); 240 | $item.append($icon); 241 | beforeAddEvent.itemToAdd = $item; 242 | 243 | beforeAddEvent.method = 'after'; 244 | 245 | beforeAddEvent.referenceItem = $(beforeAddEvent.referenceItem).siblings().first(); 246 | ``` 247 | 248 | ### After add 249 | 250 | You have access to the added item in `addedItem`. 251 | 252 | ``` 253 | var $icon = $('my beautifull icon'); 254 | $(afterAddEvent.addedItem).prepend($icon); 255 | ``` 256 | 257 | ### Before delete 258 | 259 | If you do not want to remove an item, use `preventDefault` on the event: 260 | 261 | ``` 262 | beforeDeleteEvent.preventDefault(); 263 | ``` 264 | 265 | The event has the item that will be deleted in `itemToDelete` and you 266 | can modify it. 267 | 268 | ``` 269 | beforeDeleteEvent.itemToDelete = $(beforeDeleteEvent.itemToDelete).next(); 270 | ``` 271 | 272 | ### After delete 273 | 274 | You have access to the deleted item in `deletedItem`. 275 | 276 | ``` 277 | $('body').append(afterDeleteEvent.deletedItem); 278 | ``` 279 | 280 | ### Common argument 281 | 282 | If you do not call `addItem` or `deleteItem` yourself, you also have access to the 283 | original click event in `clickEvent` in the four events. 284 | -------------------------------------------------------------------------------- /doc/tryIt.md: -------------------------------------------------------------------------------- 1 | # Launch the examples 2 | 3 | Get the repository `git clone git@github.com:jeanlucc/sf-form-collection-js.git`. 4 | 5 | `cd collection/example/views` and open the `simple.html` and 6 | `complex.html` files in your browser. 7 | 8 | Alternatively you can enter the url directly in your browser: 9 | 10 | ``` 11 | file:///path/to/sf-form-collection-js/example/views/simple.html 12 | file:///path/to/sf-form-collection-js/example/views/complex.html 13 | ``` 14 | -------------------------------------------------------------------------------- /example/public/js/textCollection.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var TEXT = window.TEXT || {}; 3 | window.TEXT = TEXT; 4 | 5 | TEXT.TextCollection = function (collectionContainer) { 6 | this.$collectionContainer = $(collectionContainer); 7 | 8 | this.init(); 9 | } 10 | 11 | TEXT.TextCollection.prototype = { 12 | init: function (collectionContainer, options) { 13 | this.collectionManager = this.$collectionContainer.manageCollection({ 14 | 'name': 'text', 15 | 'index-placeholder': '__name__', 16 | 'insert-method': 'before', 17 | })[0]; 18 | 19 | this.$collectionContainer.on('text.before-delete', function(event) { 20 | this.beforeDelete(event); 21 | }.bind(this)); 22 | 23 | this.$collectionContainer.on('text.before-add', function(event) { 24 | this.beforeAdd(event); 25 | }.bind(this)); 26 | 27 | if (this.collectionManager.getHolder().children().length === 0) { 28 | this.collectionManager.addItem('append'); 29 | } 30 | }, 31 | 32 | beforeAdd: function(event) { 33 | var validItemCount = 0; 34 | this.collectionManager.getHolder().children().each(function(index, item) { 35 | if (this.isItemValid(item)) { 36 | ++validItemCount; 37 | } 38 | }.bind(this)); 39 | 40 | if (validItemCount !== this.collectionManager.getHolder().children().length) { 41 | event.preventDefault(); 42 | } 43 | 44 | var $itemToAdd = $(event.itemToAdd); 45 | $itemToAdd.find('input').val('before'); 46 | 47 | event.itemToAdd = $itemToAdd; 48 | }, 49 | 50 | beforeDelete: function(event) { 51 | if (this.collectionManager.getHolder().children().length === 1) { 52 | event.preventDefault(); 53 | $input = $(event.itemToDelete.find('input')); 54 | if ($input.val() !== '') { 55 | $input.val(''); 56 | } 57 | } 58 | }, 59 | 60 | isItemValid: function(item) { 61 | return $(item).find('input').val() !== ''; 62 | }, 63 | }; 64 | }); 65 | -------------------------------------------------------------------------------- /example/public/js/textListCollection.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var TEXT = window.TEXT || {}; 3 | window.TEXT = TEXT; 4 | 5 | TEXT.TextListCollection = function (collectionContainer) { 6 | this.$collectionContainer = $(collectionContainer); 7 | this.$navTabContainer = this.$collectionContainer.find('.nav-tabs'); 8 | this.$tabContentContainer = this.$collectionContainer.find('.tab-content'); 9 | 10 | this.init(); 11 | } 12 | 13 | TEXT.TextListCollection.prototype = { 14 | init: function (collectionContainer, options) { 15 | this.collectionManager = this.$collectionContainer.manageCollection({ 16 | 'name': 'textlist', 17 | 'index-placeholder': '__parent_name__', 18 | 'item-closest-selector': 'li', 19 | })[0]; 20 | 21 | this.$collectionContainer.on('textlist.after-add', function(event) { 22 | this.afterAdd(event); 23 | }.bind(this)); 24 | 25 | this.$collectionContainer.on('textlist.before-delete', function(event) { 26 | this.beforeDelete(event); 27 | }.bind(this)); 28 | 29 | this.$collectionContainer.on('textlist.after-delete', function(event) { 30 | this.afterDelete(event); 31 | }.bind(this)); 32 | 33 | this.$navTabContainer.on('click', 'a', function(event) { 34 | event.preventDefault(); 35 | $(event.target).tab('show'); 36 | }); 37 | 38 | if (this.collectionManager.getHolder().children().length === 0) { 39 | this.collectionManager.addItem(); 40 | } 41 | }, 42 | 43 | afterAdd: function(event) { 44 | var $addedItem = $(event.addedItem); 45 | var $navTab = $addedItem.first(); 46 | var $textCollectionContainer = $addedItem.last(); 47 | 48 | $navTab.remove(); 49 | this.$navTabContainer.append($navTab); 50 | 51 | $textCollectionContainer.data('text-collection-manager', new TEXT.TextCollection($textCollectionContainer)); 52 | 53 | $navTab.find('a').trigger('click'); 54 | }, 55 | 56 | beforeDelete: function(event) { 57 | if (this.$navTabContainer.children().length === 1) { 58 | event.preventDefault(); 59 | return; 60 | } 61 | 62 | if (!confirm('Do you really want to delete this list?')) { 63 | event.preventDefault(); 64 | return; 65 | } 66 | 67 | var $navTab = $(event.itemToDelete); 68 | var $textCollectionContainer = this.$tabContentContainer.find($navTab.find('a').attr('href')); 69 | 70 | textCollectionManager = $textCollectionContainer.data('text-collection-manager').collectionManager; 71 | textCollectionManager.getHolder().children().each(function(index, item) { 72 | textCollectionManager.deleteItem($(item)); 73 | }); 74 | 75 | $textCollectionContainer.remove(); 76 | }, 77 | 78 | afterDelete: function(event) { 79 | this.$navTabContainer.children().first().find('a').trigger('click'); 80 | }, 81 | }; 82 | 83 | var $collectionContainer = $('.textlist_collection_container'); 84 | if ($collectionContainer.length > 0) { 85 | new TEXT.TextListCollection($collectionContainer); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /example/views/complex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test collection 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |
20 |
21 |

Please create your nested list

22 |
23 |
24 |
25 |
26 | 28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /example/views/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test simple collection 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |

Please create your simple list

15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
TextAction
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /src/collection.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var CollectionManager = function (collectionContainer, options) { 4 | this.$collectionContainer = $(collectionContainer); 5 | 6 | this.init(collectionContainer, options); 7 | } 8 | 9 | CollectionManager.VALID_INSERT_METHOD = ['append', 'prepend', 'after', 'before']; 10 | 11 | CollectionManager.prototype = { 12 | init: function (collectionContainer, options) { 13 | this.options = this.getOptions(options); 14 | this.$collectionHolder = this.$collectionContainer.find(this.options['collection-holder-selector']); 15 | this.itemPrototype = this.$collectionHolder.data(this.options['prototype-name']); 16 | this.indexPattern = new RegExp(this.options['index-placeholder'], 'g'); 17 | this.index = this.$collectionHolder.children().length; 18 | 19 | if ('string' === typeof this.options['add-button-selector']) { 20 | this.$collectionContainer.on('click', this.options['add-button-selector'], function(event) { 21 | this.addItemHandleEvent(event); 22 | }.bind(this)) 23 | } 24 | 25 | if ('string' === typeof this.options['delete-button-selector']) { 26 | this.$collectionContainer.on('click', this.options['delete-button-selector'], function(event) { 27 | this.deleteItemHandleEvent(event); 28 | }.bind(this)) 29 | } 30 | }, 31 | 32 | addItemHandleEvent: function(event) { 33 | event.preventDefault(); 34 | 35 | this.addItem(null, null, null, event); 36 | }, 37 | 38 | addItem: function(method, itemToAdd, referenceItem, event) { 39 | method = method ? method : this.options['insert-method']; 40 | var $itemToAdd = $(itemToAdd ? itemToAdd : this.createItem()); 41 | var $referenceItem = referenceItem ? $(referenceItem) : this.getClickedItem(event); 42 | 43 | var beforeAddEvent = $.Event(this.options['before-add-event-name']); 44 | beforeAddEvent.method = method; 45 | beforeAddEvent.itemToAdd = $itemToAdd; 46 | beforeAddEvent.referenceItem = $referenceItem; 47 | beforeAddEvent.clickEvent = event; 48 | 49 | this.$collectionContainer.trigger(beforeAddEvent); 50 | if (beforeAddEvent.isDefaultPrevented()) { 51 | return; 52 | } 53 | 54 | var $itemToAdd = $(beforeAddEvent.itemToAdd); 55 | 56 | switch (beforeAddEvent.method) { 57 | case 'append': 58 | this.append($itemToAdd); 59 | break; 60 | case 'prepend': 61 | this.prepend($itemToAdd); 62 | break; 63 | case 'after': 64 | var $referenceItem = $(beforeAddEvent.referenceItem ? beforeAddEvent.referenceItem : this.getClickedItem(event)); 65 | this.after($referenceItem, $itemToAdd); 66 | break; 67 | case 'before': 68 | var $referenceItem = $(beforeAddEvent.referenceItem ? beforeAddEvent.referenceItem : this.getClickedItem(event)); 69 | this.before($referenceItem, $itemToAdd); 70 | break; 71 | default: 72 | console.error(this.options['insert-method'] + ' is not a valid insert-method. Valid insert-method are: ' + CollectionManager.VALID_INSERT_METHOD.join(', ')); 73 | } 74 | 75 | var afterAddEvent = $.Event(this.options['after-add-event-name']); 76 | afterAddEvent.addedItem = $itemToAdd; 77 | afterAddEvent.clickEvent = event; 78 | 79 | this.$collectionContainer.trigger(afterAddEvent); 80 | }, 81 | 82 | append: function(itemToAdd) { 83 | this.$collectionHolder.append(itemToAdd ? itemToAdd : this.createItem()); 84 | }, 85 | 86 | prepend: function(itemToAdd) { 87 | this.$collectionHolder.prepend(itemToAdd ? itemToAdd : this.createItem()); 88 | }, 89 | 90 | after: function(referenceItem, itemToAdd) { 91 | $(referenceItem).after(itemToAdd ? itemToAdd : this.createItem()); 92 | }, 93 | 94 | before: function(referenceItem, itemToAdd) { 95 | $(referenceItem).before(itemToAdd ? itemToAdd : this.createItem()); 96 | }, 97 | 98 | createItem: function() { 99 | var newItem = this.itemPrototype.replace(this.indexPattern, this.index); 100 | this.index = this.index + 1; 101 | 102 | return newItem; 103 | }, 104 | 105 | deleteItemHandleEvent: function (event) { 106 | event.preventDefault(); 107 | 108 | this.deleteItem(null, event); 109 | }, 110 | 111 | deleteItem: function(itemToDelete, event) { 112 | var $itemToDelete = $(itemToDelete ? itemToDelete : this.getClickedItem(event)); 113 | var beforeDeleteEvent = $.Event(this.options['before-delete-event-name']); 114 | beforeDeleteEvent.itemToDelete = $itemToDelete; 115 | beforeDeleteEvent.clickEvent = event; 116 | 117 | this.$collectionContainer.trigger(beforeDeleteEvent); 118 | if (beforeDeleteEvent.isDefaultPrevented()) { 119 | return; 120 | } 121 | 122 | $itemToDelete = $(beforeDeleteEvent.itemToDelete); 123 | $itemToDelete.remove(); 124 | 125 | var afterDeleteEvent = $.Event(this.options['after-delete-event-name']); 126 | afterDeleteEvent.deletedItem = $itemToDelete; 127 | afterDeleteEvent.clickEvent = event; 128 | 129 | this.$collectionContainer.trigger(afterDeleteEvent); 130 | }, 131 | 132 | getClickedItem: function(event) { 133 | if (undefined === event) { 134 | return; 135 | } 136 | var clickedElement = event.target; 137 | var collectionContainer = this.$collectionContainer[0]; 138 | if ($.contains(collectionContainer, clickedElement)) { 139 | return $(clickedElement).closest(this.options['item-closest-selector'], collectionContainer) 140 | } 141 | }, 142 | 143 | getOptions: function (customOptions) { 144 | if ('undefined' !== typeof this.options) { 145 | return this.options; 146 | } 147 | 148 | if ('undefined' === typeof customOptions) { 149 | customOptions = {}; 150 | } 151 | 152 | customOptions = $.extend(this.getDataOptions(), customOptions); 153 | 154 | var options = {}; 155 | $.extend(options, this.getDefaultOptions()); 156 | $.extend(options, this.getNamedOptions('name' in customOptions ? customOptions['name'] : '')); 157 | if ('event-name' in customOptions && customOptions['event-name'].length > 0) { 158 | options['event-name'] = customOptions['event-name'] + '.'; 159 | } 160 | $.extend(options, this.getEventOptions(options['event-name'])); 161 | $.extend(options, customOptions); 162 | 163 | this.validateOptions(options); 164 | 165 | return options; 166 | }, 167 | 168 | getDefaultOptions: function() { 169 | return { 170 | 'insert-method': 'append', 171 | }; 172 | }, 173 | 174 | getNamedOptions: function(name) { 175 | if (name.length === 0) { 176 | return { 177 | 'prototype-name': 'prototype', 178 | 'index-placeholder': '__name__', 179 | 'item-closest-selector': '.item', 180 | 'collection-holder-selector': '.collection_holder', 181 | 'add-button-selector': '.add_item', 182 | 'delete-button-selector': '.delete_item', 183 | 'event-name': '', 184 | }; 185 | } 186 | 187 | return { 188 | 'prototype-name': name + '-prototype', 189 | 'index-placeholder': '__' + name + '_name__', 190 | 'item-closest-selector': '.' + name + '_item', 191 | 'collection-holder-selector': '.' + name + '_collection_holder', 192 | 'add-button-selector': '.add_' + name + '_item', 193 | 'delete-button-selector': '.delete_' + name + '_item', 194 | 'event-name': name + '.', 195 | }; 196 | }, 197 | 198 | getEventOptions: function(eventName) { 199 | return { 200 | 'before-add-event-name': eventName + 'before-add', 201 | 'after-add-event-name': eventName + 'after-add', 202 | 'before-delete-event-name': eventName + 'before-delete', 203 | 'after-delete-event-name': eventName + 'after-delete', 204 | }; 205 | }, 206 | 207 | getDataOptions: function() { 208 | var dataOptionNames = [ 209 | 'name', 210 | 'insert-method', 211 | 'prototype-name', 212 | 'index-placeholder', 213 | 'item-closest-selector', 214 | 'collection-holder-selector', 215 | 'add-button-selector', 216 | 'delete-button-selector', 217 | 'event-name', 218 | 'before-add-event-name', 219 | 'after-add-event-name', 220 | 'before-delete-event-name', 221 | 'after-delete-event-name', 222 | ]; 223 | 224 | var dataOptions = {}; 225 | dataOptionNames.forEach(function(optionName) { 226 | var dataOption = this.$collectionContainer.data(optionName); 227 | if ('undefined' !== typeof dataOption) { 228 | dataOptions[optionName] = dataOption; 229 | } 230 | }.bind(this)); 231 | 232 | return dataOptions; 233 | }, 234 | 235 | validateOptions: function(options) { 236 | if ($.inArray(options['insert-method'], CollectionManager.VALID_INSERT_METHOD) === -1) { 237 | console.error(options['insert-method'] + ' is not a valid insert-method. Valid methods are: ' + CollectionManager.VALID_INSERT_METHOD.join(', ')); 238 | } 239 | }, 240 | 241 | getHolder: function() { 242 | return this.$collectionHolder; 243 | }, 244 | }; 245 | 246 | $.fn.manageCollection = function (options) { 247 | return this.map(function (index, collectionContainer) { 248 | return new CollectionManager(collectionContainer, options); 249 | }) 250 | }; 251 | 252 | $(document).ready(function () { 253 | $('.collection_container').manageCollection(); 254 | }); 255 | }); 256 | --------------------------------------------------------------------------------