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.
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cocooned/helpers/deprecate'
4
+ require 'cocooned/helpers/cocoon_compatibility'
5
+ require 'cocooned/association_builder'
6
+
7
+ module Cocooned
8
+ # TODO: Remove in 2.0 (Only Cocoon class names).
9
+ HELPER_CLASSES = {
10
+ add: ['cocooned-add', 'add_fields'],
11
+ remove: ['cocooned-remove', 'remove_fields'],
12
+ up: ['cocooned-move-up'],
13
+ down: ['cocooned-move-down']
14
+ }.freeze
15
+
16
+ module Helpers
17
+ # Create aliases to old Cocoon method name
18
+ # TODO: Remove in 2.0
19
+ include Cocooned::Helpers::CocoonCompatibility
20
+
21
+ # Output an action link to add an item in a nested form.
22
+ #
23
+ # ==== Signatures
24
+ #
25
+ # cocooned_add_item_link(label, form, association, options = {})
26
+ # # Explicit name
27
+ #
28
+ # cocooned_add_item_link(form, association, options = {}) do
29
+ # # Name as a block
30
+ # end
31
+ #
32
+ # cocooned_add_item_link(form, association, options = {})
33
+ # # Use default name
34
+ #
35
+ # ==== Parameters
36
+ #
37
+ # `label` is the text to be used as the link label.
38
+ # Just as when you use the Rails builtin helper +link_to+, you can give an explicit
39
+ # label to your link or use a block to build it.
40
+ # If you provide neither an explicit label nor a block, Cocooned will try to use I18n
41
+ # to find a suitable label. This lookup will check the following keys:
42
+ #
43
+ # - `cocooned.{association}.add`
44
+ # - `cocooned.defaults.add`
45
+ #
46
+ # If none of these translation exist, it will fallback to 'Add'.
47
+ #
48
+ #
49
+ # `form` is your form builder. Can be a SimpleForm::Builder, Formtastic::Builder
50
+ # or a standard Rails FormBuilder.
51
+ #
52
+ # `association` is the name of the nested association.
53
+ # Ex: cocooned_add_item_link "Add an item", form, :items
54
+ #
55
+ # ==== Options
56
+ #
57
+ # `options` can be any of the following.
58
+ #
59
+ # Rendering options:
60
+ #
61
+ # - **partial**: the nested form partial.
62
+ # Defaults to `{association.singular_name}_fields`.
63
+ # - **form_name**: name used to access the form builder in the nested form partial.
64
+ # Defaults to `:f`.
65
+ # - **form_options**: options to be passed to the nested form builder. Can be used
66
+ # to specify a wrapper for simple_form_fields if you use its Bootstrap setup, for
67
+ # example.
68
+ # No defaults.
69
+ # - **locals**: a hash of local variables, will be forwarded to the partial.
70
+ # No default.
71
+ #
72
+ # Association options:
73
+ #
74
+ # - **insertion_method** : the jQuery method to be used to insert new items.
75
+ # Can be any of `before`, `after`, `append`, `prepend`, `replaceWith`.
76
+ # Defaults to `before`
77
+ # - **insertion_traversal** and **insertion_node** : respectively the jQuery
78
+ # traversal method and the jQuery compatible selector that will be used to find
79
+ # the insertion node, relative to the generated link.
80
+ # When both are specified, `$(addLink).{traversal}({node})` will be used.
81
+ # When only **insertion_node** is specified, `$({node})` will be used.
82
+ # When only **insertion_traversal** is specified, it will be ignored.
83
+ # When none is specified, `$(addLink).parent()` will be used.
84
+ # - **count**: how many item will be inserted on click.
85
+ # Defaults to 1.
86
+ # - **wrap_object**: anything responding to `call` to be used to wrap the newly build
87
+ # item instance. Can be useful with decorators or special initialisations.
88
+ # Ex: cocooned_add_item_link "Add an item", form, :items,
89
+ # wrap_object: Proc.new { |comment| CommentDecorator.new(comment) })
90
+ # No default.
91
+ # - **force_non_association_create**: force to build instances of the nested model
92
+ # outside association (i.e. without calling `build_{association}` or `{association}.build`)
93
+ # Can be usefull if, for some specific reason, you need an object to _not_ be created
94
+ # on the association, for example if you did not want `after_add` callbacks to be triggered.
95
+ # Defaults to false.
96
+ #
97
+ # Link HTML options:
98
+ #
99
+ # You can pass any option supported by +link_to+. It will be politely forwarded.
100
+ # See the documentation of +link_to+ for more informations.
101
+ #
102
+ # Compatibility options:
103
+ #
104
+ # These options are supported for backward compatibility with the original Cocoon.
105
+ # **Support for these options will be removed in the next major release !**.
106
+ #
107
+ # - **render_options**: A nested Hash originaly used to pass locals and form builder
108
+ # options.
109
+ #
110
+ def cocooned_add_item_link(*args, &block)
111
+ if block_given?
112
+ cocooned_add_item_link(capture(&block), *args)
113
+
114
+ elsif args.first.respond_to?(:object)
115
+ association = args.second
116
+ cocooned_add_item_link(cocooned_default_label(:add, association), *args)
117
+
118
+ else
119
+ name, form, association, html_options = *args
120
+ html_options ||= {}
121
+ html_options = html_options.with_indifferent_access
122
+
123
+ builder_options = cocooned_extract_builder_options!(html_options)
124
+ render_options = cocooned_extract_render_options!(html_options)
125
+
126
+ builder = Cocooned::AssociationBuilder.new(form, association, builder_options)
127
+ rendered = cocooned_render_association(builder, render_options)
128
+
129
+ data = cocooned_extract_data!(html_options).merge!(
130
+ association: builder.singular_name,
131
+ associations: builder.plural_name,
132
+ association_insertion_template: CGI.escapeHTML(rendered.to_str).html_safe
133
+ )
134
+
135
+ html_options[:data] = (html_options[:data] || {}).merge(data)
136
+ html_options[:class] = [Array(html_options.delete(:class)).collect { |k| k.to_s.split(' ') },
137
+ Cocooned::HELPER_CLASSES[:add]].flatten.compact.uniq.join(' ')
138
+
139
+ link_to(name, '#', html_options)
140
+ end
141
+ end
142
+
143
+ # Output an action link to remove an item (and an hidden field to mark
144
+ # it as destroyed if it has already been persisted).
145
+ #
146
+ # ==== Signatures
147
+ #
148
+ # cocooned_remove_item_link(label, form, html_options = {})
149
+ # # Explicit name
150
+ #
151
+ # cocooned_remove_item_link(form, html_options = {}) do
152
+ # # Name as a block
153
+ # end
154
+ #
155
+ # cocooned_remove_item_link(form, html_options = {})
156
+ # # Use default name
157
+ #
158
+ # See the documentation of +link_to+ for valid options.
159
+ def cocooned_remove_item_link(name, form = nil, html_options = {}, &block)
160
+ # rubocop:disable Style/ParallelAssignment
161
+ html_options, form = form, nil if form.is_a?(Hash)
162
+ form, name = name, form if form.nil?
163
+ # rubocop:enable Style/ParallelAssignment
164
+
165
+ return cocooned_remove_item_link(capture(&block), form, html_options) if block_given?
166
+
167
+ association = form.object.class.to_s.tableize
168
+ return cocooned_remove_item_link(cocooned_default_label(:remove, association), form, html_options) if name.nil?
169
+
170
+ html_options[:class] = [html_options[:class], Cocooned::HELPER_CLASSES[:remove]].flatten.compact
171
+ html_options[:class] << (form.object.new_record? ? 'dynamic' : 'existing')
172
+ html_options[:class] << 'destroyed' if form.object.marked_for_destruction?
173
+
174
+ hidden_field_tag("#{form.object_name}[_destroy]", form.object._destroy) <<
175
+ link_to(name, '#', html_options)
176
+ end
177
+
178
+ # Output an action link to move an item up.
179
+ #
180
+ # ==== Signatures
181
+ #
182
+ # cocooned_move_item_up_link(label, form, html_options = {})
183
+ # # Explicit name
184
+ #
185
+ # cocooned_move_item_up_link(form, html_options = {}) do
186
+ # # Name as a block
187
+ # end
188
+ #
189
+ # cocooned_move_item_up_link(form, html_options = {})
190
+ # # Use default name
191
+ #
192
+ # See the documentation of +link_to+ for valid options.
193
+ def cocooned_move_item_up_link(name, form = nil, html_options = {}, &block)
194
+ cocooned_move_item_link(:up, name, form, html_options, &block)
195
+ end
196
+
197
+ # Output an action link to move an item down.
198
+ #
199
+ # ==== Signatures
200
+ #
201
+ # cocooned_move_item_down_link(label, html_options = {})
202
+ # # Explicit name
203
+ #
204
+ # cocooned_move_item_down_link(html_options = {}) do
205
+ # # Name as a block
206
+ # end
207
+ #
208
+ # cocooned_move_item_down_link(html_options = {})
209
+ # # Use default name
210
+ #
211
+ # See the documentation of +link_to+ for valid options.
212
+ def cocooned_move_item_down_link(name, form = nil, html_options = {}, &block)
213
+ cocooned_move_item_link(:down, name, form, html_options, &block)
214
+ end
215
+
216
+ private
217
+
218
+ def cocooned_move_item_link(direction, name, form = nil, html_options = {}, &block)
219
+ form, name = name, form if form.nil?
220
+ return cocooned_move_item_link(direction, capture(&block), form, html_options) if block_given?
221
+ return cocooned_move_item_link(direction, cocooned_default_label(direction), form, html_options) if name.nil?
222
+
223
+ html_options[:class] = [html_options[:class], Cocooned::HELPER_CLASSES[direction]].flatten.compact.join(' ')
224
+ link_to name, '#', html_options
225
+ end
226
+
227
+ def cocooned_default_label(action, association = nil)
228
+ # TODO: Remove in 2.0
229
+ if I18n.exists?(:cocoon)
230
+ msg = Cocooned::Helpers::Deprecate.deprecate_release_message('the :cocoon i18n scope', ':cocooned')
231
+ warn msg
232
+ end
233
+
234
+ keys = ["cocooned.defaults.#{action}", "cocoon.defaults.#{action}"]
235
+ keys.unshift("cocooned.#{association}.#{action}", "cocoon.#{association}.#{action}") unless association.nil?
236
+ keys.collect!(&:to_sym)
237
+ keys << action.to_s.humanize
238
+
239
+ I18n.translate(keys.take(1).first, default: keys.drop(1))
240
+ end
241
+
242
+ def cocooned_render_association(builder, render_options = {})
243
+ locals = (render_options.delete(:locals) || {}).symbolize_keys
244
+ partial = render_options.delete(:partial) || builder.singular_name + '_fields'
245
+ form_name = render_options.delete(:form_name)
246
+ form_options = (render_options.delete(:form_options) || {}).symbolize_keys
247
+ form_options.reverse_merge!(child_index: "new_#{builder.association}")
248
+
249
+ builder.form.send(cocooned_form_method(builder.form),
250
+ builder.association,
251
+ builder.build_object,
252
+ form_options) do |form_builder|
253
+
254
+ partial_options = { form_name.to_sym => form_builder, :dynamic => true }.merge(locals)
255
+ render(partial, partial_options)
256
+ end
257
+ end
258
+
259
+ def cocooned_form_method(form)
260
+ ancestors = form.class.ancestors.map(&:to_s)
261
+ if ancestors.include?('SimpleForm::FormBuilder')
262
+ :simple_fields_for
263
+ elsif ancestors.include?('Formtastic::FormBuilder')
264
+ :semantic_fields_for
265
+ else
266
+ :fields_for
267
+ end
268
+ end
269
+
270
+ def cocooned_extract_builder_options!(html_options)
271
+ %i[wrap_object force_non_association_create].each_with_object({}) do |option_name, opts|
272
+ opts[option_name] = html_options.delete(option_name) if html_options.key?(option_name)
273
+ end
274
+ end
275
+
276
+ def cocooned_extract_render_options!(html_options)
277
+ render_options = { form_name: :f }
278
+
279
+ # TODO: Remove in 2.0
280
+ if html_options.key?(:render_options)
281
+ msg = Cocooned::Helpers::Deprecate.deprecate_release_message(':render_options', :none)
282
+ warn msg
283
+
284
+ options = html_options.delete(:render_options)
285
+ render_options[:locals] = options.delete(:locals) if options.key?(:locals)
286
+ render_options[:form_options] = options
287
+ end
288
+
289
+ %i[locals partial form_name form_options].each_with_object(render_options) do |option_name, opts|
290
+ opts[option_name] = html_options.delete(option_name) if html_options.key?(option_name)
291
+ end
292
+ end
293
+
294
+ def cocooned_extract_data!(html_options)
295
+ data = {
296
+ count: [
297
+ html_options.delete(:count).to_i,
298
+ (html_options.key?(:data) ? html_options[:data].delete(:count) : 0).to_i,
299
+ 1
300
+ ].compact.max,
301
+ association_insertion_node: cocooned_extract_single_data!(html_options, :insertion_node),
302
+ association_insertion_method: cocooned_extract_single_data!(html_options, :insertion_method),
303
+ association_insertion_traversal: cocooned_extract_single_data!(html_options, :insertion_traversal)
304
+ }
305
+
306
+ # Compatibility with the old way to pass data attributes to Rails view helpers
307
+ # Has we build a :data key, they will not be looked up.
308
+ html_options.keys.select { |k| k.to_s.match?(/data[_-]/) }.each_with_object(data) do |data_key, d|
309
+ key = data_key.to_s.gsub(/^data[_-]/, '')
310
+ d[key] = html_options.delete(data_key)
311
+ end
312
+ data.compact
313
+ end
314
+
315
+ def cocooned_extract_single_data!(h, key)
316
+ k = key.to_s
317
+ return h.delete(k) if h.key?(k)
318
+
319
+ # Compatibility alternatives
320
+ # TODO: Remove in 2.0
321
+ return h.delete("association_#{k}") if h.key?("association_#{k}")
322
+ return h.delete("data_association_#{k}") if h.key?("data_association_#{k}")
323
+ return h.delete("data-association-#{k.tr('_', '-')}") if h.key?("data-association-#{k.tr('_', '-')}")
324
+ return nil unless h.key?(:data)
325
+ d = h[:data].with_indifferent_access
326
+ return d.delete("association_#{k}") if d.key?("association_#{k}")
327
+ return d.delete("association-#{k.tr('_', '-')}") if d.key?("association-#{k.tr('_', '-')}")
328
+ nil
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'cocooned/helpers'
5
+
6
+ module Cocooned
7
+ class Railtie < ::Rails::Engine
8
+ initializer 'cocooned.initialize' do |_app|
9
+ ActiveSupport.on_load :action_view do
10
+ ActionView::Base.send :include, Cocooned::Helpers
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocooned
4
+ VERSION = '1.3.0'
5
+ end
data/lib/cocooned.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cocooned/railtie'
4
+
5
+ module Cocooned
6
+ end
data/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "cocooned",
3
+ "version": "1.2.11",
4
+ "private": true,
5
+ "license": "APACHE-2.0",
6
+ "description": "Unobtrusive nested forms handling using jQuery.",
7
+ "homepage": "https://github.com/notus-sh/cocooned#readme",
8
+ "repository": "https://github.com/notus-sh/cocooned",
9
+ "author": "Gaël-Ian Havard <gael-ian@notus.sh>",
10
+ "main": "app/assets/javascripts/cocooned.js",
11
+ "files": ["app/assets/javascripts"],
12
+ "bugs": { "url": "https://github.com/notus-sh/cocooned/issues" },
13
+ "dependencies": {
14
+ "jquery": "^3.3.1"
15
+ },
16
+ "devDependencies": {
17
+ "eslint": "^4.19.1",
18
+ "eslint-config-standard": "^11.0.0",
19
+ "eslint-plugin-import": "^2.8.0",
20
+ "eslint-plugin-node": "^5.2.1",
21
+ "eslint-plugin-promise": "^3.6.0",
22
+ "eslint-plugin-standard": "^3.0.1"
23
+ }
24
+ }