cocooned 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ };