cocooned 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # Cocooned
2
+
3
+ [![Build Status](https://travis-ci.org/notus-sh/cocooned.png?branch=master)](https://travis-ci.org/notus-sh/cocooned)
4
+
5
+ Cocooned makes it easier to handle nested forms in a Rails project.
6
+
7
+ Cocooned is form builder-agnostic: it works with standard Rails (>= 4.0, < 6.0), [Formtastic](https://github.com/justinfrench/formtastic) or [SimpleForm](https://github.com/plataformatec/simple_form).
8
+
9
+ ## Some Background
10
+
11
+ Cocooned is a fork of [Cocoon](https://github.com/nathanvda/cocoon) by [Nathan Van der Auwera](https://github.com/nathanvda).
12
+
13
+ He and all Cocoon contributors did a great job to maintain it for years. Many thanks to them!
14
+
15
+ But last time I checked, the project seemed to not have been actively maintained for a long time and many pull requests, even simple ones, were on hold. As I needed a more than what Cocoon provided at this time, I had the choice to either maintain an extension or to fork it and integrate everything that was waiting and more.
16
+
17
+ Cocooned is almost a complete rewrite of Cocoon, with more functionnalities and (I hope) a more fluent API.
18
+
19
+ **For now, Cocooned is completely compatible with Cocoon and can be used as a drop-in replacement.**
20
+ Just change the name of the gem in your Gemfile and you're done. It will work the same (but will add a bunch of deprecation warning to your logs).
21
+
22
+ **The compatibility layer with the original Cocoon API will be dropped in Cocooned 2.0.**
23
+
24
+ ## Prerequisites
25
+
26
+ Cocooned depends on jQuery, Ruby (>= 2.2) and Rails (>= 4.0, < 6.0).
27
+
28
+ ## Installation
29
+
30
+ Inside your `Gemfile` add the following:
31
+
32
+ ```ruby
33
+ gem "cocooned"
34
+ ```
35
+
36
+ You must also require `cocooned` in your `application.js` and `application.css`, so it compiles with the asset pipeline.
37
+
38
+ ## Usage
39
+
40
+ For all the following examples, we will consider modelisation of an administrable list with items.
41
+ Here are the two ActiveRecord models : `List` and `Item`:
42
+
43
+ ```ruby
44
+ class Item < ApplicationRecord
45
+ has_many :items, inverse_of: :list
46
+ accepts_nested_attributes_for :items, reject_if: :all_blank, allow_destroy: true
47
+ end
48
+
49
+ class Item < ApplicationRecord
50
+ belongs_to :list
51
+ end
52
+ ```
53
+
54
+ We will build a form where we can dynamically add and remove items to a list.
55
+
56
+ ### Strong Parameters Gotcha
57
+
58
+ To destroy nested models, Rails uses a virtual attribute called `_destroy`.
59
+ When `_destroy` is set, the nested model will be deleted. If the record has previously been persisted, Rails generate and use an automatic `id` field to fetch the wannabe destroyed record.
60
+
61
+ When using Rails > 4.0 (or strong parameters), you need to explicitly add both `:id` and `:_destroy` to the list of permitted parameters.
62
+
63
+ E.g. in your `ListsController`:
64
+
65
+ ```ruby
66
+ def list_params
67
+ params.require(:list).permit(:name, tasks_attributes: [:id, :description, :done, :_destroy])
68
+ end
69
+ ```
70
+
71
+ ### Basic form
72
+
73
+ _Please note examples in this section are written with the [`haml` templating language](http://haml.info/)._
74
+
75
+ [Rails natively supports nested forms](https://guides.rubyonrails.org/form_helpers.html#nested-forms) but does not support adding or removing nested items.
76
+
77
+ ```haml
78
+ / `app/views/lists/_form.html.haml`
79
+ = form_for @list do |f|
80
+ = f.input :name
81
+
82
+ %h3 Items
83
+ = f.fields_for :tasks do |item_form|
84
+ / This block is repeated for every task in @list.items
85
+ = item_form.label :description
86
+ = item_form.text_field :description
87
+ = item_form.check_box :done
88
+
89
+ = f.submit "Save"
90
+ ```
91
+
92
+ To enable Cocooned on this first, we need to:
93
+
94
+ 1. Move the nested form to a partial
95
+ 2. Add a way to add a new item to the collection
96
+ 3. Add a way to remove an item from the collection
97
+ 4. Initialize Cocooned to handle this form
98
+
99
+ Let's do it.
100
+
101
+ #### 1. Move the nested form to a partial
102
+
103
+ We now have two files:
104
+
105
+ ```haml
106
+ / `app/views/lists/_form.html.haml`
107
+ = form_for @list do |form|
108
+ = form.input :name
109
+
110
+ %h3 Items
111
+ = form.fields_for :items do |item_form|
112
+ = render 'item_fields', f: item_form
113
+
114
+ = form.submit "Save"
115
+ ```
116
+
117
+ ```haml
118
+ / `app/views/lists/_item_fields.html.haml`
119
+ = f.label :description
120
+ = f.text_field :description
121
+ = f.check_box :done
122
+ ```
123
+
124
+ #### 2. Add a way to add a new item to the collection
125
+
126
+ ```haml
127
+ / `app/views/lists/_form.html.haml`
128
+ = form_for @list do |form|
129
+ = form.input :name
130
+
131
+ %h3 Items
132
+ #items
133
+ = form.fields_for :tasks do |item_form|
134
+ = render 'item_fields', f: item_form
135
+ .links
136
+ = cocooned_add_item_link 'Add an item', form, :items
137
+
138
+ = form.submit "Save"
139
+ ```
140
+
141
+ By default, a new item will be inserted just before the immediate parent of the 'Add an item' link. You can have a look at the documentation of `cocooned_add_item_link` for more information about how to change that but we'll keep it simple for now.
142
+
143
+ #### 3. Add a way to remove an item from the collection
144
+
145
+ ```haml
146
+ / `app/views/lists/_item_fields.html.haml`
147
+ .cocooned-item
148
+ = f.label :description
149
+ = f.text_field :description
150
+ = f.check_box :done
151
+ = cocooned_remove_item_link 'Remove', f
152
+ ```
153
+
154
+ The `.cocooned-item` class is required for the `cocooned_remove_item_link` to work correctly.
155
+
156
+ #### 4. Initialize Cocooned to handle this form
157
+
158
+ Cocooned will detect on page load forms it should handle and initialize itself.
159
+ This detection is based on the presence of a `data-cocooned-options` attribute on the nested forms container.
160
+
161
+ ```haml
162
+ / `app/views/lists/_form.html.haml`
163
+ = form_for @list do |form|
164
+ = form.input :name
165
+
166
+ %h3 Items
167
+ #items{ :data => { cocooned_options: {}.to_json } }
168
+ = form.fields_for :tasks do |item_form|
169
+ = render 'item_fields', f: item_form
170
+ .links
171
+ = cocooned_add_item_link 'Add an item', form, :items
172
+
173
+ = form.submit "Save"
174
+ ```
175
+
176
+ And we're done!
177
+
178
+ ### Wait, what's the point of `data-cocooned-options` if it's to be empty?
179
+
180
+ For simple use cases as the one we just demonstrated, the `data-cocooned-options` attributes only triggers the Cocooned initialization on page load. But you can use it to pass additional options to the Cocooned javascript and enable plugins.
181
+
182
+ For now, Cocooned supports two plugins:
183
+
184
+ * **Limit**, to set a maximum limit of items that can be added to the association
185
+ * **Reorderable**, that will automatically update `position` fields when you add or remove an item or when you reorder associated items.
186
+
187
+ #### The limit plugin
188
+
189
+ The limit plugin is autoloaded when needed and does not require anything more than you specifiying the maximum number of items allowed in the association.
190
+
191
+ ```haml
192
+ / `app/views/lists/_form.html.haml`
193
+ = form_for @list do |form|
194
+ = form.input :name
195
+
196
+ %h3 Items
197
+ #items{ :data => { cocooned_options: { limit: 12 }.to_json } }
198
+ = form.fields_for :tasks do |item_form|
199
+ = render 'item_fields', f: item_form
200
+ .links
201
+ = cocooned_add_item_link 'Add an item', form, :items
202
+
203
+ = form.submit "Save"
204
+ ```
205
+
206
+ #### The reorderable plugin
207
+
208
+ The reorderable plugin is autoloaded when activated and does not support any particular options.
209
+
210
+ ```haml
211
+ / `app/views/lists/_form.html.haml`
212
+ = form_for @list do |form|
213
+ = form.input :name
214
+
215
+ %h3 Items
216
+ #items{ :data => { cocooned_options: { reorderable: true }.to_json } }
217
+ = form.fields_for :tasks do |item_form|
218
+ = render 'item_fields', f: item_form
219
+ .links
220
+ = cocooned_add_item_link 'Add an item', form, :items
221
+
222
+ = form.submit "Save"
223
+ ```
224
+
225
+ However, you need to edit your nested partial to add the links that allow your users to move an item up or down in the collection and to add a `position` field.
226
+
227
+ ```haml
228
+ / `app/views/lists/_item_fields.html.haml`
229
+ .cocooned-item
230
+ = f.label :description
231
+ = f.text_field :description
232
+ = f.check_box :done
233
+ = f.hidden_field :position
234
+ = cocooned_move_item_up_link 'Up', f
235
+ = cocooned_move_item_down_link 'Down', f
236
+ = cocooned_remove_item_link 'Remove', f
237
+ ```
238
+
239
+ Also, remember the strong parameters gotcha we mentioned earlier.
240
+
241
+ Of course, it means your model must have a `position` attribute you will use to sort collections.
242
+
243
+ ## How it works
244
+
245
+ Cocooned defines some helper functions:
246
+
247
+ * `cocooned_add_item_link` will build a link that, when clicked, dynamically adds a new partial form for the given association. [Have a look at the documentation for available options](https://github.com/notus-sh/cocooned/blob/master/lib/cocooned/helpers.rb#L21).
248
+ * `cocooned_remove_item_link` will build a link that, when clicked, dynamically removes the surrounding partial form. [Have a look at the documentation for available options](https://github.com/notus-sh/cocooned/blob/master/lib/cocooned/helpers.rb#L143).
249
+ * `cocooned_move_item_up_link` and `cocooned_move_item_down_link` will build links that, when clicked, will move the surrounding partial form one step up or down in the collection. [Have a look at the documentation for available options](https://github.com/notus-sh/cocooned/blob/master/lib/cocooned/helpers.rb#L178).
250
+
251
+ ### Javascript callbacks
252
+
253
+ When your collection is modified, the following events can be triggered:
254
+
255
+ * `cocooned:before-insert`: called before inserting a new nested child, can be [canceled](#canceling-an-action)
256
+ * `cocooned:after-insert`: called after inserting
257
+ * `cocooned:before-remove`: called before removing the nested child, can be [canceled](#canceling-an-action)
258
+ * `cocooned:after-remove`: called after removal
259
+
260
+ The limit plugin can trigger its own event:
261
+
262
+ * `cocooned:limit-reached`: called when the limit is reached (before a new item will be inserted)
263
+
264
+ And so does the reorderable plugin:
265
+
266
+ * `cocooned:before-move`: called before moving the nested child, can be [canceled](#canceling-an-action)
267
+ * `cocooned:after-move`: called after moving
268
+ * `cocooned:before-reindex`: called before updating the `position` fields of nested items, can be [canceled](#canceling-an-action) (even if I honestly don't know why you would)
269
+ * `cocooned:after-reindex`: called after `position` fields update
270
+
271
+ To listen to the events in your JavaScript:
272
+
273
+ ```javascript
274
+ $('#container').on('cocooned:before-insert', function(event, node, cocoonedInstance) {
275
+ /* Do something */
276
+ });
277
+ ```
278
+
279
+ An event handler is called with 3 arguments:
280
+
281
+ The event `event` is an instance of `jQuery.Event` and carry some additional data:
282
+
283
+ * `event.link`, the clicked link
284
+ * `event.node`, the nested item that will be added, removed or moved, as a jQuery object. This is null for `cocooned:limit-reached` and `cocooned:*-reindex` events
285
+ * `event.nodes`, the nested items that will be or just have been reindexed on `cocooned:*-reindex` events, as a jQuery object. Null otherwise.
286
+ * `event.cocooned`, the Cocooned javascript object instance handling the nested association.
287
+
288
+ The `node` argument is the same jQuery object as `event.node`.
289
+ The `cocooned` argument is the same as `event.cocooned`.
290
+
291
+ #### Canceling an action
292
+
293
+ You can cancel an action within the `cocooned:before-<action>` callback by calling `event.preventDefault()` or `event.stopPropagation()`.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ # Test suites
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task default: :spec
10
+
11
+ require 'jasmine'
12
+ load 'jasmine/tasks/jasmine.rake'
13
+
14
+ # Linters
15
+ require 'rubocop/rake_task'
16
+ RuboCop::RakeTask.new do |task|
17
+ task.options = ['--config', 'config/linters/ruby.yml']
18
+ end
19
+
20
+ eslint_args = ['--no-eslintrc', '--config config/linters/js.json']
21
+ eslint_path = ['app/assets/**/*.js', 'spec/javascripts/**/*.js', 'spec/dummy/app/assets/**/*.js']
22
+
23
+ namespace :eslint do
24
+ desc 'Auto-correct JavaScript files'
25
+ task :auto_correct do
26
+ system("yarnpkg run eslint #{(eslint_args + ['--fix']).join(' ')} #{eslint_path.join(' ')}")
27
+ end
28
+ end
29
+
30
+ desc 'Lint JavaScript code'
31
+ task :eslint do
32
+ system("yarnpkg run eslint #{eslint_args.join(' ')} #{eslint_path.join(' ')}")
33
+ end
34
+
35
+ # Documentation
36
+ require 'rdoc/task'
37
+
38
+ Rake::RDocTask.new(:rdoc) do |rdoc|
39
+ rdoc.rdoc_dir = 'rdoc'
40
+ rdoc.title = 'Cocooned'
41
+ rdoc.options << '--line-numbers' << '--inline-source'
42
+ rdoc.rdoc_files.include('README.rdoc')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
@@ -0,0 +1,14 @@
1
+ /* globals Cocooned */
2
+ //= require 'cocooned'
3
+
4
+ // Compatibility with the original Cocoon
5
+ function initCocoon () {
6
+ $(Cocooned.prototype.selector('add')).each(function (_i, addLink) {
7
+ var container = Cocooned.prototype.findContainer(addLink);
8
+ var limit = parseInt($(addLink).data('limit'), 10) || false;
9
+
10
+ container.cocooned({ limit: limit });
11
+ });
12
+ }
13
+
14
+ $(initCocoon);
@@ -0,0 +1,284 @@
1
+ var Cocooned = function (container, options) {
2
+ this.container = jQuery(container);
3
+ this.options = jQuery.extend({}, this.defaultOptions(), (options || {}));
4
+
5
+ // Autoload plugins
6
+ for (var moduleName in Cocooned.Plugins) {
7
+ if (Cocooned.Plugins.hasOwnProperty(moduleName)) {
8
+ var module = Cocooned.Plugins[moduleName];
9
+ var optionName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
10
+
11
+ if (this.options[optionName]) {
12
+ for (var method in module) {
13
+ if (module.hasOwnProperty(method) && typeof module[method] === 'function') {
14
+ this[method] = module[method];
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ this.init();
22
+ };
23
+
24
+ Cocooned.Plugins = {};
25
+ Cocooned.prototype = {
26
+
27
+ elementsCounter: 0,
28
+
29
+ // Compatibility with Cocoon
30
+ // TODO: Remove in 2.0 (Only Cocoon namespaces).
31
+ namespaces: {
32
+ events: ['cocooned', 'cocoon']
33
+ },
34
+
35
+ // Compatibility with Cocoon
36
+ // TODO: Remove in 2.0 (Only Cocoon class names).
37
+ classes: {
38
+ // Actions link
39
+ add: ['cocooned-add', 'add_fields'],
40
+ remove: ['cocooned-remove', 'remove_fields'],
41
+ up: ['cocooned-move-up'],
42
+ down: ['cocooned-move-down'],
43
+ // Containers
44
+ container: ['cocooned-container'],
45
+ item: ['cocooned-item', 'nested-fields'],
46
+ },
47
+
48
+ defaultOptions: function () {
49
+ var options = {};
50
+
51
+ for (var moduleName in Cocooned.Plugins) {
52
+ if (Cocooned.Plugins.hasOwnProperty(moduleName)) {
53
+ var module = Cocooned.Plugins[moduleName];
54
+ var optionName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
55
+
56
+ options[optionName] = module.defaultOptionValue;
57
+ }
58
+ }
59
+
60
+ return options;
61
+ },
62
+
63
+ notify: function (node, eventType, eventData) {
64
+ return !(this.namespaces.events.some(function(namespace) {
65
+ var namespacedEventType = [namespace, eventType].join(':');
66
+ var event = jQuery.Event(namespacedEventType, eventData);
67
+
68
+ node.trigger(event, [eventData.node, eventData.cocooned]);
69
+
70
+ return (event.isPropagationStopped() || event.isDefaultPrevented());
71
+ }));
72
+ },
73
+
74
+ selector: function (type, selector) {
75
+ var s = selector || '&';
76
+ return this.classes[type].map(function(klass) { return s.replace(/&/, '.' + klass); }).join(', ');
77
+ },
78
+
79
+ namespacedNativeEvents: function (type) {
80
+ var namespaces = this.namespaces.events.map(function(ns) { return '.' + ns; });
81
+ namespaces.unshift(type);
82
+ return namespaces.join('');
83
+ },
84
+
85
+ buildId: function () {
86
+ return (new Date().getTime() + this.elementsCounter++);
87
+ },
88
+
89
+ buildContentNode: function (content) {
90
+ var id = this.buildId();
91
+ var html = (content || this.content);
92
+ var braced = '[' + id + ']';
93
+ var underscored = '_' + id + '_';
94
+
95
+ ['associations', 'association'].forEach(function (a) {
96
+ html = html.replace(this.regexps[a]['braced'], braced + '$1');
97
+ html = html.replace(this.regexps[a]['underscored'], underscored + '$1');
98
+ }, this);
99
+
100
+ return $(html);
101
+ },
102
+
103
+ getInsertionNode: function (adder) {
104
+ var $adder = $(adder);
105
+ var insertionNode = $adder.data('association-insertion-node');
106
+ var insertionTraversal = $adder.data('association-insertion-traversal');
107
+
108
+ if (!insertionNode) {
109
+ return $adder.parent();
110
+ }
111
+
112
+ if (typeof insertionNode === 'function') {
113
+ return insertionNode($adder);
114
+ }
115
+
116
+ if (insertionTraversal) {
117
+ return $adder[insertionTraversal](insertionNode);
118
+ }
119
+
120
+ return insertionNode === 'this' ? $adder : $(insertionNode);
121
+ },
122
+
123
+ getInsertionMethod: function (adder) {
124
+ var $adder = $(adder);
125
+ return $adder.data('association-insertion-method') || 'before';
126
+ },
127
+
128
+ getItems: function (selector) {
129
+ selector = selector || '';
130
+ var self = this;
131
+ return $(this.selector('item', selector), this.container).filter(function () {
132
+ return ($(this).closest(self.selector('container')).get(0) === self.container.get(0));
133
+ });
134
+ },
135
+
136
+ findContainer: function (addLink) {
137
+ var $adder = $(addLink);
138
+ var insertionNode = this.getInsertionNode($adder);
139
+ var insertionMethod = this.getInsertionMethod($adder);
140
+
141
+ switch (insertionMethod) {
142
+ case 'before':
143
+ case 'after':
144
+ case 'replaceWith':
145
+ return insertionNode.parent();
146
+
147
+ case 'append':
148
+ case 'prepend':
149
+ default:
150
+ return insertionNode;
151
+ }
152
+ },
153
+
154
+ findItem: function (removeLink) {
155
+ return $(removeLink).closest(this.selector('item'));
156
+ },
157
+
158
+ init: function () {
159
+ var self = this;
160
+
161
+ this.addLinks = $(this.selector('add')).filter(function () {
162
+ var container = self.findContainer(this);
163
+ return (container.get(0) === self.container.get(0));
164
+ });
165
+
166
+ var addLink = $(this.addLinks.get(0));
167
+
168
+ this.content = addLink.data('association-insertion-template');
169
+ this.regexps = {
170
+ association: {
171
+ braced: new RegExp('\\[new_' + addLink.data('association') + '\\](.*?\\s)', 'g'),
172
+ underscored: new RegExp('_new_' + addLink.data('association') + '_(\\w*)', 'g')
173
+ },
174
+ associations: {
175
+ braced: new RegExp('\\[new_' + addLink.data('associations') + '\\](.*?\\s)', 'g'),
176
+ underscored: new RegExp('_new_' + addLink.data('associations') + '_(\\w*)', 'g')
177
+ }
178
+ };
179
+
180
+ this.initUi();
181
+ this.bindEvents();
182
+ },
183
+
184
+ initUi: function () {
185
+ var self = this;
186
+
187
+ if (!this.container.attr('id')) {
188
+ this.container.attr('id', this.buildId());
189
+ }
190
+ this.container.addClass(this.classes['container'].join(' '));
191
+
192
+ $(function () { self.hideMarkedForDestruction(); });
193
+ $(document).on('page:load turbolinks:load', function () { self.hideMarkedForDestruction(); });
194
+ },
195
+
196
+ bindEvents: function () {
197
+ var self = this;
198
+
199
+ // Bind add links
200
+ this.addLinks.on(
201
+ this.namespacedNativeEvents('click'),
202
+ function (e) {
203
+ e.preventDefault();
204
+ self.add(this);
205
+ });
206
+
207
+ // Bind remove links
208
+ // (Binded on document instead of container to not bypass click handler defined in jquery_ujs)
209
+ $(document).on(
210
+ this.namespacedNativeEvents('click'),
211
+ this.selector('remove', '#' + this.container.attr('id') + ' &'),
212
+ function (e) {
213
+ e.preventDefault();
214
+ self.remove(this);
215
+ });
216
+
217
+ // Bind options events
218
+ $.each(this.options, function (name, value) {
219
+ var bindMethod = 'bind' + name.charAt(0).toUpperCase() + name.slice(1);
220
+ if (value && self[bindMethod]) {
221
+ self[bindMethod]();
222
+ }
223
+ });
224
+ },
225
+
226
+ add: function (adder) {
227
+ var $adder = $(adder);
228
+ var insertionMethod = this.getInsertionMethod($adder);
229
+ var insertionNode = this.getInsertionNode($adder);
230
+ var contentTemplate = $adder.data('association-insertion-template');
231
+ var count = parseInt($adder.data('count'), 10) || 1;
232
+
233
+ for (var i = 0; i < count; i++) {
234
+ var contentNode = this.buildContentNode(contentTemplate);
235
+ var eventData = { link: $adder, node: contentNode, cocooned: this };
236
+ var afterNode = (insertionMethod === 'replaceWith' ? contentNode : insertionNode);
237
+
238
+ // Insertion can be prevented through a 'cocooned:before-insert' event handler
239
+ if (!this.notify(insertionNode, 'before-insert', eventData)) {
240
+ return false;
241
+ }
242
+
243
+ insertionNode[insertionMethod](contentNode);
244
+
245
+ this.notify(afterNode, 'after-insert', eventData);
246
+ }
247
+ },
248
+
249
+ remove: function (remover) {
250
+ var self = this;
251
+ var $remover = $(remover);
252
+ var nodeToDelete = this.findItem($remover);
253
+ var triggerNode = nodeToDelete.parent();
254
+ var eventData = { link: $remover, node: nodeToDelete, cocooned: this };
255
+
256
+ // Deletion can be prevented through a 'cocooned:before-remove' event handler
257
+ if (!this.notify(triggerNode, 'before-remove', eventData)) {
258
+ return false;
259
+ }
260
+
261
+ var timeout = triggerNode.data('remove-timeout') || 0;
262
+
263
+ setTimeout(function () {
264
+ if ($remover.hasClass('dynamic')) {
265
+ nodeToDelete.remove();
266
+ } else {
267
+ nodeToDelete.find('input[required], select[required]').each(function (index, element) {
268
+ $(element).removeAttr('required');
269
+ });
270
+ $remover.siblings('input[type=hidden][name$="[_destroy]"]').val('true');
271
+ nodeToDelete.hide();
272
+ }
273
+ self.notify(triggerNode, 'after-remove', eventData);
274
+ }, timeout);
275
+ },
276
+
277
+ hideMarkedForDestruction: function () {
278
+ var self = this;
279
+ $(this.selector('remove', '&.existing.destroyed'), this.container).each(function (i, removeLink) {
280
+ var node = self.findItem(removeLink);
281
+ node.hide();
282
+ });
283
+ }
284
+ };
@@ -0,0 +1,10 @@
1
+ ;(function(w, $) {
2
+ 'use strict';
3
+
4
+ $(function() {
5
+ $('*[data-cocooned-options]').each(function(i, el) {
6
+ $(el).cocooned($(el).data('cocooned-options'));
7
+ });
8
+ });
9
+
10
+ })(window, jQuery);
@@ -0,0 +1,20 @@
1
+ ;(function(w, $) {
2
+ 'use strict';
3
+
4
+ $.fn.cocooned = function(options) {
5
+ return this.each(function() {
6
+ var container = $(this);
7
+
8
+ if (typeof container.data('cocooned') !== 'undefined') {
9
+ if (typeof w.console !== 'undefined') {
10
+ w.console.warn('Cocooned already initialized on this element.');
11
+ w.console.debug(container);
12
+ }
13
+ return;
14
+ }
15
+
16
+ var cocooned = new Cocooned(container, options);
17
+ container.data('cocooned', cocooned);
18
+ });
19
+ };
20
+ })(window, jQuery);
@@ -0,0 +1,22 @@
1
+ Cocooned.Plugins.Limit = {
2
+
3
+ defaultOptionValue: false,
4
+
5
+ bindLimit: function() {
6
+ this.limit = this.options['limit'];
7
+ this.container.on('cocooned:before-insert', function(e) {
8
+ var cocooned = e.cocooned;
9
+ if (cocooned.getLength() < cocooned.limit) {
10
+ return;
11
+ }
12
+
13
+ e.stopPropagation();
14
+ var eventData = { link: e.link, node: e.node, cocooned: cocooned };
15
+ cocooned.notify(cocooned.container, 'limit-reached', eventData);
16
+ });
17
+ },
18
+
19
+ getLength: function() {
20
+ return this.getItems('&:visible').length;
21
+ }
22
+ };