cocooned 1.3.0

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