cocooned 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +194 -0
- data/History.md +253 -0
- data/LICENSE +13 -0
- data/README.md +293 -0
- data/Rakefile +44 -0
- data/app/assets/javascripts/cocoon.js +14 -0
- data/app/assets/javascripts/cocooned/core.js +284 -0
- data/app/assets/javascripts/cocooned/jquery/onload.js +10 -0
- data/app/assets/javascripts/cocooned/jquery/plugin.js +20 -0
- data/app/assets/javascripts/cocooned/plugins/limit.js +22 -0
- data/app/assets/javascripts/cocooned/plugins/reorderable.js +101 -0
- data/app/assets/javascripts/cocooned.js +3 -0
- data/app/assets/stylesheets/cocoon.css +3 -0
- data/app/assets/stylesheets/cocooned.css +9 -0
- data/cocooned.gemspec +37 -0
- data/config/linters/js.json +50 -0
- data/config/linters/ruby.yml +16 -0
- data/gemfiles/Gemfile.rails-4 +8 -0
- data/gemfiles/Gemfile.rails-5 +8 -0
- data/lib/cocooned/association_builder.rb +69 -0
- data/lib/cocooned/helpers/cocoon_compatibility.rb +27 -0
- data/lib/cocooned/helpers/deprecate.rb +49 -0
- data/lib/cocooned/helpers.rb +331 -0
- data/lib/cocooned/railtie.rb +14 -0
- data/lib/cocooned/version.rb +5 -0
- data/lib/cocooned.rb +6 -0
- data/package.json +24 -0
- data/yarn.lock +1052 -0
- metadata +183 -0
@@ -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
|
data/lib/cocooned.rb
ADDED
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
|
+
}
|