cocooned 2.5.0 → 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f2af10b0b746c5011676ba112458b540d192803ca0b1ddf527464b748003ed5
4
- data.tar.gz: 0246debfd5bf517de6b2ae2ef10bc2caa117c732c855943ee998513ba4dd5ead
3
+ metadata.gz: 4452a44114a2a49166816f4966764b27cccf5de838568acb29fc68544fb96878
4
+ data.tar.gz: 7d28fe11d1d06bb28433be2587411471ce79db0983347de07acf40a2e62bc3d7
5
5
  SHA512:
6
- metadata.gz: e6636bfb5b815f29ce6285bd504bc0c9a43196f70d0b0b21e771bee1e67338eec2ff64251012e8f5d67d2d54606c8538c2eb27a57f99ef70242d81bc6d5672e2
7
- data.tar.gz: f8ba105ad6016c0ee6d846344543d7afda37cd7e52a07baa1ab80cc7392bbe0abf8ba32ec59e8201d5c0daa9ab19583973061d174909e27a1be958d157d6d58c
6
+ metadata.gz: 8af0b42463b95aac1f4bf2c1f290a75735a1ebb304fe333f79124da55bb2f0d694c05e23292b3328accb0b70c596205b918a6912757f2e8dbddcd5101f6df4bc
7
+ data.tar.gz: e95c020441e9c26e4941d49ec7e202dc22af173b02af6bf2023a46260004dceee1e0ca9f352bed232a80b085c819a35f26d9994013a21a76ed65f669d2f4f471
data/CHANGELOG.md CHANGED
@@ -6,8 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## Version 3.0.0 (2026-01-27)
10
+
11
+ ## Breaking changes
12
+
13
+ * Respect browser accessibility settings on reduced motion to determinate the default value of the `animate` option (#120)
14
+ `animate` will be set to false by default if the `(prefers-reduced-motion: reduce)` media query matches.
15
+ * Proper instances cleanup through [explicit resource management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management) (#115)
16
+ Introduce a `dispose` method on Cocooned instances to clean up what was done in the `start` method: remove event listeners and delete references to the disposed instance, so it can be cleanly garbage collected.
17
+ This may require you to install a polyfill for `DisposableStack` and `[Symbol.dispose]()` to support older browsers.
18
+
19
+ To not introduce too many breaking changes in a single release, removal of long-time deprecated features have been postponed to Cocooned 4.0 (#122)
20
+
21
+ ## Added
22
+
23
+ * Optional Stimulus integration (#119)
24
+ Introduce two ways to integrate Cocooned with Stimulus: `registerCocoonedContainer` and `useCocooned`. See documentation for details.
25
+
9
26
  ### Changed
10
27
 
28
+ * Add Ruby 4.0 to the test matrix (#112)
29
+
11
30
  ## Version 2.5.0 (2025-10-23)
12
31
 
13
32
  * Upgrade to use `eslint ~> 9` (#103)
@@ -1,3 +1,3 @@
1
1
  //= require 'cocooned'
2
2
 
3
- /* TODO: Remove in 3.0 */
3
+ /* TODO: Remove in 4.0 */
@@ -36,6 +36,153 @@
36
36
  }
37
37
  }
38
38
 
39
+ class Traverser {
40
+ constructor (origin, traversal) {
41
+ this.#origin = origin;
42
+ this.#traversal = traversal;
43
+ }
44
+
45
+ resolve (selector) {
46
+ if (this.#traversal in this.#origin && typeof this.#origin[this.#traversal] === 'function') {
47
+ return this._tryMethod(this.#traversal, selector)
48
+ }
49
+
50
+ if (this.#traversal in this.#origin) {
51
+ return this._tryProperty(this.#traversal)
52
+ }
53
+
54
+ const method = `_${this.#traversal}`;
55
+ if (method in this) {
56
+ return this[method](selector)
57
+ }
58
+
59
+ return null
60
+ }
61
+
62
+ /* Protected and private attributes and methods */
63
+ #origin
64
+ #traversal
65
+
66
+ _tryMethod (method, selector) {
67
+ try {
68
+ const resolved = this.#origin[method](selector);
69
+ if (resolved instanceof HTMLElement) {
70
+ return resolved
71
+ }
72
+ } catch (e) {}
73
+
74
+ return null
75
+ }
76
+
77
+ _tryProperty (property) {
78
+ const resolved = this.#origin[property];
79
+ if (resolved instanceof HTMLElement) {
80
+ return resolved
81
+ }
82
+
83
+ return null
84
+ }
85
+
86
+ _parent (selector) {
87
+ if (this.#origin.parentElement.matches(selector)) {
88
+ return this.#origin.parentElement
89
+ }
90
+ return null
91
+ }
92
+
93
+ _prev (selector) {
94
+ if (this.#origin.previousElementSibling.matches(selector)) {
95
+ return this.#origin.previousElementSibling
96
+ }
97
+ return null
98
+ }
99
+
100
+ _next (selector) {
101
+ if (this.#origin.nextElementSibling.matches(selector)) {
102
+ return this.#origin.nextElementSibling
103
+ }
104
+ return null
105
+ }
106
+
107
+ _siblings (selector) {
108
+ return this.#origin.parentElement.querySelector(selector)
109
+ }
110
+ }
111
+
112
+ class Deprecator {
113
+ logger
114
+ package
115
+ version
116
+
117
+ constructor (version, packageName, logger) {
118
+ this.version = version;
119
+ this.package = packageName;
120
+ this.logger = logger;
121
+ }
122
+
123
+ warn (message, replacement = null) {
124
+ if (message in this.#emitted) {
125
+ return
126
+ }
127
+
128
+ const warning = `${message}. It will be removed from ${this.package} ${this.version}`;
129
+ const alternative = (replacement !== null ? `, use ${replacement} instead` : '');
130
+ this.logger.warn(`DEPRECATION WARNING: ${warning}${alternative}.`);
131
+
132
+ this.#emitted[message] = true;
133
+ }
134
+
135
+ /* Protected and private attributes and methods */
136
+ #emitted = Object.create(null)
137
+ }
138
+
139
+ const deprecators = Object.create(null);
140
+
141
+ function deprecator (version, packageName = 'Cocooned', logger = console) {
142
+ const hash = [version, packageName].join('#');
143
+ if (!(hash in deprecators)) {
144
+ deprecators[hash] = new Deprecator(version, packageName, logger);
145
+ }
146
+
147
+ return deprecators[hash]
148
+ }
149
+
150
+ class Listener {
151
+ constructor (eventTarget, type, listener) {
152
+ this.#eventTarget = eventTarget;
153
+ this.#type = type;
154
+ this.#listener = listener;
155
+
156
+ this.#eventTarget.addEventListener(this.#type, this.#listener);
157
+ }
158
+
159
+ dispose () {
160
+ this.#eventTarget.removeEventListener(this.#type, this.#listener);
161
+ }
162
+
163
+ /* Protected and private attributes and methods */
164
+ #eventTarget
165
+ #type
166
+ #listener
167
+ }
168
+
169
+ disposable(Listener);
170
+
171
+ if (typeof Symbol.dispose !== "symbol") {
172
+ console.warn(`
173
+ Cocooned use Disposable objects but they are not supported by your browser.
174
+ See Cocooned documentation for polyfill options.
175
+ `);
176
+ }
177
+
178
+ function disposable(klass) {
179
+ if (typeof Symbol.dispose !== "symbol") {
180
+ return
181
+ }
182
+
183
+ klass.prototype[Symbol.dispose] = klass.prototype.dispose;
184
+ }
185
+
39
186
  // Borrowed from <https://stackoverflow.com/a/2117523>
40
187
  function uuidv4 () {
41
188
  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
@@ -69,13 +216,22 @@
69
216
  ]
70
217
  }
71
218
 
219
+ const canAnimate = (
220
+ 'animate' in document.createElement('div') &&
221
+ typeof document.createElement('div').animate === 'function'
222
+ );
223
+
224
+ const shouldAnimate = (
225
+ 'matchMedia' in window &&
226
+ !window.matchMedia('(prefers-reduced-motion: reduce)').matches
227
+ );
228
+
72
229
  const instances = Object.create(null);
73
230
 
74
231
  class Base {
75
232
  static get defaultOptions () {
76
- const element = document.createElement('div');
77
233
  return {
78
- animate: ('animate' in element && typeof element.animate === 'function'),
234
+ animate: canAnimate && shouldAnimate,
79
235
  animator: defaultAnimator,
80
236
  duration: 450
81
237
  }
@@ -98,7 +254,7 @@
98
254
 
99
255
  constructor (container, options) {
100
256
  this._container = container;
101
- this._uuid = uuidv4();
257
+ this.__uuid = uuidv4();
102
258
  this._options = this.constructor._normalizeOptions({
103
259
  ...this.constructor.defaultOptions,
104
260
  ...('cocoonedOptions' in container.dataset ? JSON.parse(container.dataset.cocoonedOptions) : {}),
@@ -115,16 +271,31 @@
115
271
  }
116
272
 
117
273
  start () {
118
- this.container.dataset.cocoonedContainer = true;
119
- this.container.dataset.cocoonedUuid = this._uuid;
120
- instances[this._uuid] = this;
274
+ if (!('cocoonedContainer' in this.container.dataset)) {
275
+ deprecator('4.0').warn(
276
+ 'CSS classes based detection is deprecated',
277
+ 'cocooned_container Rails helper to declare containers'
278
+ );
279
+ this.container.dataset.cocoonedContainer = true;
280
+ }
281
+
282
+ this.container.dataset.cocoonedUuid = this.__uuid;
283
+ this._onDispose(() => delete this.container.dataset.cocoonedUuid);
284
+
285
+ instances[this.__uuid] = this;
286
+ this._onDispose(() => delete instances[this.__uuid]);
121
287
 
122
288
  const hideDestroyed = () => { hideMarkedForDestruction(this, this.items); };
123
289
 
124
290
  hideDestroyed();
125
- this.container.ownerDocument.addEventListener('page:load', hideDestroyed);
126
- this.container.ownerDocument.addEventListener('turbo:load', hideDestroyed);
127
- this.container.ownerDocument.addEventListener('turbolinks:load', hideDestroyed);
291
+ this._addEventListener(this.container.ownerDocument, 'page:load', hideDestroyed);
292
+ this._addEventListener(this.container.ownerDocument, 'turbo:load', hideDestroyed);
293
+ this._addEventListener(this.container.ownerDocument, 'turbolinks:load', hideDestroyed);
294
+ }
295
+
296
+ dispose () {
297
+ this._disposer.dispose();
298
+ this._container = null;
128
299
  }
129
300
 
130
301
  notify (node, eventType, eventData) {
@@ -180,8 +351,21 @@
180
351
 
181
352
  _container
182
353
  _options
183
- __uuid
354
+ __disposer
184
355
  __emitter
356
+ __uuid
357
+
358
+ _addEventListener (target, type, listener) {
359
+ this._disposer.use(new Listener(target, type, listener));
360
+ }
361
+
362
+ get _disposer () {
363
+ if (typeof this.__disposer === 'undefined') {
364
+ this.__disposer = new DisposableStack();
365
+ }
366
+
367
+ return this.__disposer
368
+ }
185
369
 
186
370
  get _emitter () {
187
371
  if (typeof this.__emitter === 'undefined') {
@@ -191,6 +375,10 @@
191
375
  return this.__emitter
192
376
  }
193
377
 
378
+ _onDispose (callback) {
379
+ this._disposer.defer(callback);
380
+ }
381
+
194
382
  _selectors (name) {
195
383
  return this.constructor.selectors[name]
196
384
  }
@@ -205,6 +393,8 @@
205
393
  }
206
394
  }
207
395
 
396
+ disposable(Base);
397
+
208
398
  class Trigger {
209
399
  constructor (trigger, cocooned) {
210
400
  this._trigger = trigger;
@@ -277,117 +467,6 @@
277
467
  }
278
468
  }
279
469
 
280
- class Traverser {
281
- constructor (origin, traversal) {
282
- this.#origin = origin;
283
- this.#traversal = traversal;
284
- }
285
-
286
- resolve (selector) {
287
- if (this.#traversal in this.#origin && typeof this.#origin[this.#traversal] === 'function') {
288
- return this._tryMethod(this.#traversal, selector)
289
- }
290
-
291
- if (this.#traversal in this.#origin) {
292
- return this._tryProperty(this.#traversal)
293
- }
294
-
295
- const method = `_${this.#traversal}`;
296
- if (method in this) {
297
- return this[method](selector)
298
- }
299
-
300
- return null
301
- }
302
-
303
- /* Protected and private attributes and methods */
304
- #origin
305
- #traversal
306
-
307
- _tryMethod (method, selector) {
308
- try {
309
- const resolved = this.#origin[method](selector);
310
- if (resolved instanceof HTMLElement) {
311
- return resolved
312
- }
313
- } catch (e) {}
314
-
315
- return null
316
- }
317
-
318
- _tryProperty (property) {
319
- const resolved = this.#origin[property];
320
- if (resolved instanceof HTMLElement) {
321
- return resolved
322
- }
323
-
324
- return null
325
- }
326
-
327
- _parent (selector) {
328
- if (this.#origin.parentElement.matches(selector)) {
329
- return this.#origin.parentElement
330
- }
331
- return null
332
- }
333
-
334
- _prev (selector) {
335
- if (this.#origin.previousElementSibling.matches(selector)) {
336
- return this.#origin.previousElementSibling
337
- }
338
- return null
339
- }
340
-
341
- _next (selector) {
342
- if (this.#origin.nextElementSibling.matches(selector)) {
343
- return this.#origin.nextElementSibling
344
- }
345
- return null
346
- }
347
-
348
- _siblings (selector) {
349
- return this.#origin.parentElement.querySelector(selector)
350
- }
351
- }
352
-
353
- class Deprecator {
354
- logger
355
- package
356
- version
357
-
358
- constructor (version, packageName, logger) {
359
- this.version = version;
360
- this.package = packageName;
361
- this.logger = logger;
362
- }
363
-
364
- warn (message, replacement = null) {
365
- if (message in this.#emitted) {
366
- return
367
- }
368
-
369
- const warning = `${message}. It will be removed from ${this.package} ${this.version}`;
370
- const alternative = (replacement !== null ? `, use ${replacement} instead` : '');
371
- this.logger.warn(`DEPRECATION WARNING: ${warning}${alternative}.`);
372
-
373
- this.#emitted[message] = true;
374
- }
375
-
376
- /* Protected and private attributes and methods */
377
- #emitted = Object.create(null)
378
- }
379
-
380
- const deprecators = Object.create(null);
381
-
382
- function deprecator (version, packageName = 'Cocooned', logger = console) {
383
- const hash = [version, packageName].join('#');
384
- if (!(hash in deprecators)) {
385
- deprecators[hash] = new Deprecator(version, packageName, logger);
386
- }
387
-
388
- return deprecators[hash]
389
- }
390
-
391
470
  class Extractor {
392
471
  constructor (trigger, cocooned) {
393
472
  this.#trigger = trigger;
@@ -466,7 +545,7 @@
466
545
  return this.#trigger.ownerDocument.querySelector(node)
467
546
  }
468
547
 
469
- deprecator('3.0').warn('associationInsertionTraversal is deprecated');
548
+ deprecator('4.0').warn('associationInsertionTraversal is deprecated');
470
549
  const traverser = new Traverser(this.#trigger, this.#dataset.associationInsertionTraversal);
471
550
 
472
551
  return traverser.resolve(node)
@@ -736,12 +815,12 @@
736
815
  .map(element => Add.create(element, this))
737
816
  .filter(trigger => this.toContainer(trigger.insertionNode) === this.container);
738
817
 
739
- this.addTriggers.forEach(add => add.trigger.addEventListener(
740
- 'click',
741
- clickHandler$1((e) => add.handle(e))
742
- ));
818
+ this.addTriggers.forEach(add => {
819
+ this._addEventListener(add.trigger, 'click', clickHandler$1((e) => add.handle(e)));
820
+ });
743
821
 
744
- this.container.addEventListener(
822
+ this._addEventListener(
823
+ this.container,
745
824
  'click',
746
825
  itemDelegatedClickHandler(this, this._selector('triggers.remove'), (e) => {
747
826
  const trigger = new Remove(e.target, this);
@@ -795,7 +874,7 @@
795
874
  return
796
875
  }
797
876
 
798
- this.container.addEventListener('cocooned:before-insert', e => {
877
+ this._addEventListener(this.container, 'cocooned:before-insert', e => {
799
878
  if (this.items.length < this.options.limit) {
800
879
  return
801
880
  }
@@ -943,16 +1022,16 @@
943
1022
  return
944
1023
  }
945
1024
 
946
- this.container.addEventListener('cocooned:after-insert', e => this._reindexer.reindex(e));
947
- this.container.addEventListener('cocooned:after-remove', e => this._reindexer.reindex(e));
948
- this.container.addEventListener('cocooned:after-move', e => this._reindexer.reindex(e));
1025
+ this._addEventListener(this.container, 'cocooned:after-insert', e => this._reindexer.reindex(e));
1026
+ this._addEventListener(this.container, 'cocooned:after-remove', e => this._reindexer.reindex(e));
1027
+ this._addEventListener(this.container, 'cocooned:after-move', e => this._reindexer.reindex(e));
949
1028
  const form = this.container.closest('form');
950
1029
  if (form !== null) {
951
- form.addEventListener('submit', e => this._reindexer.reindex(e));
1030
+ this._addEventListener(form, 'submit', e => this._reindexer.reindex(e));
952
1031
  }
953
1032
 
954
- this.container.addEventListener('click', clickHandler(this, this._selector('triggers.up'), Up));
955
- this.container.addEventListener('click', clickHandler(this, this._selector('triggers.down'), Down));
1033
+ this._addEventListener(this.container, 'click', clickHandler(this, this._selector('triggers.up'), Up));
1034
+ this._addEventListener(this.container, 'click', clickHandler(this, this._selector('triggers.down'), Down));
956
1035
  }
957
1036
 
958
1037
  /* Protected and private attributes and methods */
@@ -1051,7 +1130,7 @@
1051
1130
 
1052
1131
  $(() => cocoonAutoStart($));
1053
1132
 
1054
- deprecator('3.0').warn(
1133
+ deprecator('4.0').warn(
1055
1134
  'Loading @notus.sh/cocooned/cocooned is deprecated',
1056
1135
  '@notus.sh/cocooned/jquery, @notus.sh/cocooned or `@notus.sh/cocooned/src/cocooned/cocooned`'
1057
1136
  );
@@ -2,4 +2,4 @@
2
2
  *= require cocooned
3
3
  */
4
4
 
5
- /* TODO: Remove in 3.0 */
5
+ /* TODO: Remove in 4.0 */
@@ -1 +1 @@
1
- /* TODO: Remove in 3.0 */
1
+ /* TODO: Remove in 4.0 */
data/cocooned.gemspec CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |spec|
13
13
 
14
14
  spec.summary = 'Form builder agnostic handling of Rails nested forms'
15
15
  spec.description = <<-DESC.gsub(/\s+/, ' ')
16
- Easier nested form in Rails with capabilities to add, remove, reorder or limit nested items.
17
- Works with standard Rails form builder, Formtastic or SimpleForm, and with or without jQuery.
16
+ Rails nested form made easy, with standard Rails form builder, Formtastic or SimpleForm.
17
+ Unobtrusive and zero-dependencies JavaScript included, with optional integration with jQuery and Stimulus.
18
18
  DESC
19
19
  spec.homepage = 'https://github.com/notus-sh/cocooned'
20
20
 
@@ -42,7 +42,7 @@ Gem::Specification.new do |spec|
42
42
 
43
43
  spec.add_dependency 'rails', '>= 7.2', '< 8.2'
44
44
 
45
- spec.add_development_dependency 'bundler', '~> 2.1'
45
+ spec.add_development_dependency 'bundler', '>= 2.1'
46
46
  spec.add_development_dependency 'rake', '~> 13.0'
47
47
  spec.add_development_dependency 'rspec', '~> 3.11'
48
48
  end
@@ -26,14 +26,14 @@ module Cocooned
26
26
  cocooned_add_item_link(...)
27
27
  end
28
28
  deprecate link_to_add_association: 'Use :cocooned_add_link instead',
29
- deprecator: Deprecation['3.0']
29
+ deprecator: Deprecation['4.0']
30
30
 
31
31
  # @deprecated: Please use {#cocooned_remove_item_link} instead
32
32
  def link_to_remove_association(...)
33
33
  cocooned_remove_item_link(...)
34
34
  end
35
35
  deprecate link_to_remove_association: 'Use :cocooned_remove_item_link instead',
36
- deprecator: Deprecation['3.0']
36
+ deprecator: Deprecation['4.0']
37
37
  end
38
38
  end
39
39
 
@@ -44,7 +44,7 @@ module Cocooned
44
44
  def i18n_namespaces
45
45
  return super unless I18n.exists?(:cocoon)
46
46
 
47
- Deprecation['3.0'].warn 'Support for the :cocoon i18n namespace will be removed in 3.0', caller_locations(3)
47
+ Deprecation['4.0'].warn 'Support for the :cocoon i18n namespace will be removed in 4.0', caller_locations(3)
48
48
  super + %w[cocoon]
49
49
  end
50
50
  end
@@ -57,7 +57,7 @@ module Cocooned
57
57
  def html_data
58
58
  return super unless data_keys.size.positive?
59
59
 
60
- Deprecation['3.0'].warn 'Compatibility with options named data-* will be removed in 3.0', caller_locations(3)
60
+ Deprecation['4.0'].warn 'Compatibility with options named data-* will be removed in 4.0', caller_locations(3)
61
61
  html_data_normalize super.merge(data_options)
62
62
  end
63
63
 
@@ -78,7 +78,7 @@ module Cocooned
78
78
 
79
79
  def association_options
80
80
  if options.key? :insertion_traversal
81
- Deprecation['3.0'].warn 'Support for the :insertion_traversal will be removed in 3.0', caller_locations(3)
81
+ Deprecation['4.0'].warn 'Support for the :insertion_traversal will be removed in 4.0', caller_locations(3)
82
82
  end
83
83
 
84
84
  super
@@ -91,7 +91,7 @@ module Cocooned
91
91
  def renderer_options
92
92
  return super unless options.key?(:render_options)
93
93
 
94
- Deprecation['3.0'].warn 'Support for :render_options will be removed in 3.0', caller_locations(3)
94
+ Deprecation['4.0'].warn 'Support for :render_options will be removed in 4.0', caller_locations(3)
95
95
  legacy_options = options.delete(:render_options)
96
96
 
97
97
  super.tap do |opts|
@@ -32,10 +32,8 @@ module Cocooned
32
32
  def cocooned_container(*args, &)
33
33
  options = args.extract_options!.dup
34
34
  name = args.shift || :div
35
- defaults = cocooned_wrapper_defaults(options, %w[cocooned-container], :'cocooned-container')
36
- defaults[:data][:cocooned_options] = options.extract!(:limit, :reorderable).to_json
37
35
 
38
- content_tag(name, *args, **options.deep_merge(defaults), &)
36
+ content_tag(name, *args, **cocooned_container_options(options), &)
39
37
  end
40
38
 
41
39
  # Wrap content with the expected markup for a Cocooned item.
@@ -56,17 +54,30 @@ module Cocooned
56
54
  def cocooned_item(*args, &)
57
55
  options = args.extract_options!.dup
58
56
  name = args.shift || :div
59
- defaults = cocooned_wrapper_defaults(options, %w[cocooned-item nested-fields], :'cocooned-item')
60
57
 
61
- content_tag(name, *args, **options.deep_merge(defaults), &)
58
+ content_tag(name, *args, **cocooned_item_options(options), &)
62
59
  end
63
60
 
64
61
  protected
65
62
 
66
- def cocooned_wrapper_defaults(options, additional_classes, mark)
67
- classes = Array.wrap(options.delete(:class)).flat_map { |k| k.to_s.split }.compact_blank
63
+ def cocooned_container_options(options)
64
+ options.deep_merge(
65
+ class: token_list(options.delete(:class), %w[cocooned-container]),
66
+ data: {
67
+ controller: [options.dig(:data, :controller), :cocooned].compact_blank.map(&:to_s).join(' '),
68
+ cocooned_container: true,
69
+ cocooned_options: options.extract!(:limit, :reorderable).to_json
70
+ }
71
+ )
72
+ end
68
73
 
69
- { class: (classes + additional_classes), data: { mark => true } }
74
+ def cocooned_item_options(options)
75
+ options.deep_merge(
76
+ class: token_list(options.delete(:class), %w[cocooned-item nested-fields]),
77
+ data: {
78
+ cocooned_item: true
79
+ }
80
+ )
70
81
  end
71
82
  end
72
83
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cocooned
4
- VERSION = '2.5.0'
4
+ VERSION = '3.0.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocooned
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gaël-Ian Havard
@@ -34,14 +34,14 @@ dependencies:
34
34
  name: bundler
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.1'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: '2.1'
47
47
  - !ruby/object:Gem::Dependency
@@ -72,9 +72,9 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '3.11'
75
- description: " Easier nested form in Rails with capabilities to add, remove, reorder
76
- or limit nested items. Works with standard Rails form builder, Formtastic or SimpleForm,
77
- and with or without jQuery. "
75
+ description: " Rails nested form made easy, with standard Rails form builder, Formtastic
76
+ or SimpleForm. Unobtrusive and zero-dependencies JavaScript included, with optional
77
+ integration with jQuery and Stimulus. "
78
78
  email:
79
79
  - gael-ian@notus.sh
80
80
  - nathan@dixis.com
@@ -136,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
138
  requirements: []
139
- rubygems_version: 3.6.9
139
+ rubygems_version: 4.0.3
140
140
  specification_version: 4
141
141
  summary: Form builder agnostic handling of Rails nested forms
142
142
  test_files: []