effective_bootstrap 0.9.7 → 0.9.12

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: afa31a3a961a18463d0423a23d8183d32209faccb0eb8ace234ffb450f4c9b8c
4
- data.tar.gz: 864e64da0b5de9736c9479c4196b9dec9dc644242ab9c5adc83ae4a5926e5f20
3
+ metadata.gz: 04eb70a546766f19237112eeffeea4fb2233f8dd49d4e68e1cca3ddd082357a3
4
+ data.tar.gz: 93becbeda35eac032da1f86038407ef56aecc3544f0b0ab62d8b73c267890a1e
5
5
  SHA512:
6
- metadata.gz: adc955a825172b9f165c71f34a70f46d7ef106ed21f6f7ec85f8017f59fee10fabb3275e44ddd609920adf5af189c4091c266b673011acd38299d68c03e4d6ae
7
- data.tar.gz: 23db1eeb78dd48a5a889523294d2717fbcda5be775ee06b64a5e2219a56827fe67cc27ca34f1916e1277b5646caf553683c4a2c27a63aa14849219671afe5157
6
+ metadata.gz: 59150e0f2465c7b5182c82adbd43ab30ad349dc2dd4c624942a383f6d63ec1d74444a4311375400ea61a98ab649d90d937fa3d7e38f0f3217133855034f0c6a0
7
+ data.tar.gz: 4cf279bae151632a6de342f07031bf1fe2d3da7e10a0f8a2d52630326a6e5ed4b358dbe0d3c3bea602b14201a9d4d4745f61a30424a0333df8f205b786f4d754
data/README.md CHANGED
@@ -42,8 +42,8 @@ Add the following to your `application.js`:
42
42
  And to your `application.scss`:
43
43
 
44
44
  ```sass
45
- @import 'bootstrap';
46
- @import 'effective_bootstrap';
45
+ @import 'bootstrap'
46
+ @import 'effective_bootstrap'
47
47
  ```
48
48
 
49
49
  ## View Helpers
@@ -362,6 +362,54 @@ And then in any form, instead of a text area:
362
362
  = f.editor :body
363
363
  ```
364
364
 
365
+ ## Custom has_many
366
+
367
+ This custom form input was inspired by [cocoon](https://github.com/nathanvda/cocoon) but works with more magic.
368
+
369
+ This nested form builder allows has_many resources to be created, updated, destroyed and reordered.
370
+
371
+ Just add `has_many` and `accepts_nested_attributes_for` like normal and then use it in the form:
372
+
373
+ ```ruby
374
+ class Author < ApplicationRecord
375
+ has_many :books
376
+ accepts_nested_attributes_for :books, allow_destroy: true
377
+ end
378
+ ```
379
+
380
+ and
381
+
382
+ ```haml
383
+ = effective_form_with(model: author) do |f|
384
+ = f.text_field :name
385
+
386
+ = f.has_many :books do |fb|
387
+ = fb.text_field :title
388
+ = fb.date_field :published_at
389
+ ```
390
+
391
+ If `:books` can be destroyed, a hidden field `_destroy` will automatically be added to each set of fields and a Remove button will be displayed to remove the item.
392
+
393
+ If the `Book` model has an integer `position` attribute, a hidden field `position` will automatically be added to each set of fields and a Reorder button will be displayed to drag&drop reorder items.
394
+
395
+ If the has_many collection is blank?, `.build()` will be automatically called, unless `build: false` is passed.
396
+
397
+ Any errors on the has_many name will be displayed unless `errors: false` is passed.
398
+
399
+ You can customize this behaviour by passing the following:
400
+
401
+ ```haml
402
+ = f.has_many :books, add: true, remove: true, reorder: true, build: true, errors: true do |fb|
403
+ = fb.text_field :title
404
+ ```
405
+
406
+ or add an html class:
407
+
408
+ ```haml
409
+ = f.has_many :books, class: 'tight' do |fb|
410
+ = fb.text_field :title
411
+ ```
412
+
365
413
  ## Custom percent_field
366
414
 
367
415
  This custom form input uses no 3rd party jQuery plugins.
@@ -0,0 +1 @@
1
+ //= link_directory ../images/icons
@@ -15,6 +15,7 @@
15
15
  //= require ./effective_ck_editor/input
16
16
 
17
17
  //= require ./effective_file/input
18
+ //= require ./effective_has_many/input
18
19
  //= require ./effective_integer/input
19
20
  //= require ./effective_number_text/input
20
21
  //= require ./effective_percent/input
@@ -2,3 +2,10 @@
2
2
  $(document).on 'cocoon:before-remove', (event, $obj) ->
3
3
  $(event.target).data('remove-timeout', 1000)
4
4
  $obj.fadeOut('slow')
5
+
6
+ # Open all external trix links in a new window.
7
+ $(document).on 'click', '.trix-content a', (event) ->
8
+ obj = event.currentTarget
9
+
10
+ if obj.host != window.location.host && !obj.isContentEditable
11
+ obj.setAttribute('target', '_blank')
@@ -0,0 +1,77 @@
1
+ assignPositions = (target) ->
2
+ $hasMany = $(target)
3
+ return unless $hasMany.length > 0
4
+
5
+ $fields = $hasMany.children('.has-many-fields:not(.marked-for-destruction)')
6
+ positions = $fields.find("input[name$='[position]']").map(-> this.value).get()
7
+
8
+ if positions.length > 0
9
+ index = Math.min.apply(Math, positions) || 0
10
+
11
+ $fields.each((i, obj) ->
12
+ $(obj).find("input[name$='[position]']").first().val(index)
13
+ index = index + 1
14
+ )
15
+
16
+ true
17
+
18
+ (this.EffectiveBootstrap || {}).effective_has_many = ($element, options) ->
19
+ if options.sortable
20
+ $element.sortable(
21
+ containerSelector: '.form-has-many',
22
+ itemSelector: '.has-many-fields',
23
+ handle: '.has-many-move'
24
+ placeholder: "<div class='has-many-placeholder' />",
25
+ onDrop: ($item, container, _super) =>
26
+ assignPositions(container.target)
27
+ _super($item, container)
28
+ )
29
+
30
+ $(document).on 'click', '[data-effective-form-has-many-add]', (event) ->
31
+ event.preventDefault()
32
+
33
+ $obj = $(event.currentTarget)
34
+ $hasMany = $obj.closest('.form-has-many')
35
+ return unless $hasMany.length > 0
36
+
37
+ uid = (new Date).valueOf()
38
+ template = $obj.data('effective-form-has-many-template').replace(/HASMANYINDEX/g, uid)
39
+
40
+ $fields = $(template).hide().fadeIn('fast')
41
+ EffectiveBootstrap.initialize($fields)
42
+ $obj.closest('.has-many-links').before($fields)
43
+
44
+ assignPositions($hasMany)
45
+ true
46
+
47
+ $(document).on 'click', '[data-effective-form-has-many-remove]', (event) ->
48
+ event.preventDefault()
49
+
50
+ $obj = $(event.currentTarget)
51
+ $hasMany = $obj.closest('.form-has-many')
52
+ return unless $hasMany.length > 0
53
+
54
+ $input = $obj.siblings("input[name$='[_destroy]']").first()
55
+ $fields = $obj.closest('.has-many-fields').first()
56
+
57
+ if $input.length > 0
58
+ $input.val('true')
59
+ $fields.addClass('marked-for-destruction').fadeOut('fast')
60
+ else
61
+ $fields.fadeOut('fast', -> this.remove())
62
+
63
+ assignPositions($hasMany)
64
+ true
65
+
66
+ $(document).on 'click', '[data-effective-form-has-many-reorder]', (event) ->
67
+ event.preventDefault()
68
+
69
+ $obj = $(event.currentTarget)
70
+ $hasMany = $obj.closest('.form-has-many')
71
+ return unless $hasMany.length > 0
72
+
73
+ $fields = $hasMany.children('.has-many-fields:not(.marked-for-destruction)')
74
+ return unless $fields.length > 1
75
+
76
+ $hasMany.toggleClass('reordering')
77
+ true
@@ -0,0 +1,2 @@
1
+ //= require ./jquery.sortable
2
+ //= require ./initialize
@@ -0,0 +1,696 @@
1
+ /* ===================================================
2
+ * jquery-sortable.js v0.9.13
3
+ * http://johnny.github.com/jquery-sortable/
4
+ * ===================================================
5
+ * Copyright (c) 2012 Jonas von Andrian
6
+ * All rights reserved.
7
+ *
8
+ * Redistribution and use in source and binary forms, with or without
9
+ * modification, are permitted provided that the following conditions are met:
10
+ * * Redistributions of source code must retain the above copyright
11
+ * notice, this list of conditions and the following disclaimer.
12
+ * * Redistributions in binary form must reproduce the above copyright
13
+ * notice, this list of conditions and the following disclaimer in the
14
+ * documentation and/or other materials provided with the distribution.
15
+ * * The name of the author may not be used to endorse or promote products
16
+ * derived from this software without specific prior written permission.
17
+ *
18
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
22
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ * ========================================================== */
29
+
30
+
31
+ !function ( $, window, pluginName, undefined){
32
+ var containerDefaults = {
33
+ // If true, items can be dragged from this container
34
+ drag: true,
35
+ // If true, items can be dropped onto this container
36
+ drop: true,
37
+ // Exclude items from being draggable, if the
38
+ // selector matches the item
39
+ exclude: "",
40
+ // If true, search for nested containers within an item.If you nest containers,
41
+ // either the original selector with which you call the plugin must only match the top containers,
42
+ // or you need to specify a group (see the bootstrap nav example)
43
+ nested: true,
44
+ // If true, the items are assumed to be arranged vertically
45
+ vertical: true
46
+ }, // end container defaults
47
+ groupDefaults = {
48
+ // This is executed after the placeholder has been moved.
49
+ // $closestItemOrContainer contains the closest item, the placeholder
50
+ // has been put at or the closest empty Container, the placeholder has
51
+ // been appended to.
52
+ afterMove: function ($placeholder, container, $closestItemOrContainer) {
53
+ },
54
+ // The exact css path between the container and its items, e.g. "> tbody"
55
+ containerPath: "",
56
+ // The css selector of the containers
57
+ containerSelector: "ol, ul",
58
+ // Distance the mouse has to travel to start dragging
59
+ distance: 0,
60
+ // Time in milliseconds after mousedown until dragging should start.
61
+ // This option can be used to prevent unwanted drags when clicking on an element.
62
+ delay: 0,
63
+ // The css selector of the drag handle
64
+ handle: "",
65
+ // The exact css path between the item and its subcontainers.
66
+ // It should only match the immediate items of a container.
67
+ // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
68
+ itemPath: "",
69
+ // The css selector of the items
70
+ itemSelector: "li",
71
+ // The class given to "body" while an item is being dragged
72
+ bodyClass: "dragging",
73
+ // The class giving to an item while being dragged
74
+ draggedClass: "dragged",
75
+ // Check if the dragged item may be inside the container.
76
+ // Use with care, since the search for a valid container entails a depth first search
77
+ // and may be quite expensive.
78
+ isValidTarget: function ($item, container) {
79
+ return true
80
+ },
81
+ // Executed before onDrop if placeholder is detached.
82
+ // This happens if pullPlaceholder is set to false and the drop occurs outside a container.
83
+ onCancel: function ($item, container, _super, event) {
84
+ },
85
+ // Executed at the beginning of a mouse move event.
86
+ // The Placeholder has not been moved yet.
87
+ onDrag: function ($item, position, _super, event) {
88
+ $item.css(position)
89
+ },
90
+ // Called after the drag has been started,
91
+ // that is the mouse button is being held down and
92
+ // the mouse is moving.
93
+ // The container is the closest initialized container.
94
+ // Therefore it might not be the container, that actually contains the item.
95
+ onDragStart: function ($item, container, _super, event) {
96
+ $item.css({
97
+ height: $item.outerHeight(),
98
+ width: $item.outerWidth()
99
+ })
100
+ $item.addClass(container.group.options.draggedClass)
101
+ $("body").addClass(container.group.options.bodyClass)
102
+ },
103
+ // Called when the mouse button is being released
104
+ onDrop: function ($item, container, _super, event) {
105
+ $item.removeClass(container.group.options.draggedClass).removeAttr("style")
106
+ $("body").removeClass(container.group.options.bodyClass)
107
+ // START MONKEY PATCH (for submitting form when heading positions are changed on touchscreen)
108
+ $item.trigger('movimento:drop:complete', $item)
109
+ // END MONKEY PATCH
110
+ },
111
+ // Called on mousedown. If falsy value is returned, the dragging will not start.
112
+ // Ignore if element clicked is input, select or textarea
113
+ onMousedown: function ($item, _super, event) {
114
+ if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
115
+ event.preventDefault()
116
+ return true
117
+ }
118
+ },
119
+ // The class of the placeholder (must match placeholder option markup)
120
+ placeholderClass: "placeholder",
121
+ // Template for the placeholder. Can be any valid jQuery input
122
+ // e.g. a string, a DOM element.
123
+ // The placeholder must have the class "placeholder"
124
+ placeholder: '<li class="placeholder"></li>',
125
+ // If true, the position of the placeholder is calculated on every mousemove.
126
+ // If false, it is only calculated when the mouse is above a container.
127
+ pullPlaceholder: true,
128
+ // Specifies serialization of the container group.
129
+ // The pair $parent/$children is either container/items or item/subcontainers.
130
+ serialize: function ($parent, $children, parentIsContainer) {
131
+ var result = $.extend({}, $parent.data())
132
+
133
+ if(parentIsContainer)
134
+ return [$children]
135
+ else if ($children[0]){
136
+ result.children = $children
137
+ }
138
+
139
+ delete result.subContainers
140
+ delete result.sortable
141
+
142
+ return result
143
+ },
144
+ // Set tolerance while dragging. Positive values decrease sensitivity,
145
+ // negative values increase it.
146
+ tolerance: 0
147
+ }, // end group defaults
148
+ containerGroups = {},
149
+ groupCounter = 0,
150
+ emptyBox = {
151
+ left: 0,
152
+ top: 0,
153
+ bottom: 0,
154
+ right:0
155
+ },
156
+ eventNames = {
157
+ start: "touchstart.sortable mousedown.sortable",
158
+ drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
159
+ drag: "touchmove.sortable mousemove.sortable",
160
+ scroll: "scroll.sortable"
161
+ },
162
+ subContainerKey = "subContainers"
163
+
164
+ /*
165
+ * a is Array [left, right, top, bottom]
166
+ * b is array [left, top]
167
+ */
168
+ function d(a,b) {
169
+ var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
170
+ y = Math.max(0, a[2] - b[1], b[1] - a[3])
171
+ return x+y;
172
+ }
173
+
174
+ function setDimensions(array, dimensions, tolerance, useOffset) {
175
+ var i = array.length,
176
+ offsetMethod = useOffset ? "offset" : "position"
177
+ tolerance = tolerance || 0
178
+
179
+ while(i--){
180
+ var el = array[i].el ? array[i].el : $(array[i]),
181
+ // use fitting method
182
+ pos = el[offsetMethod]()
183
+ pos.left += parseInt(el.css('margin-left'), 10)
184
+ pos.top += parseInt(el.css('margin-top'),10)
185
+ dimensions[i] = [
186
+ pos.left - tolerance,
187
+ pos.left + el.outerWidth() + tolerance,
188
+ pos.top - tolerance,
189
+ pos.top + el.outerHeight() + tolerance
190
+ ]
191
+ }
192
+ }
193
+
194
+ function getRelativePosition(pointer, element) {
195
+ var offset = element.offset()
196
+ return {
197
+ left: pointer.left - offset.left,
198
+ top: pointer.top - offset.top
199
+ }
200
+ }
201
+
202
+ function sortByDistanceDesc(dimensions, pointer, lastPointer) {
203
+ pointer = [pointer.left, pointer.top]
204
+ lastPointer = lastPointer && [lastPointer.left, lastPointer.top]
205
+
206
+ var dim,
207
+ i = dimensions.length,
208
+ distances = []
209
+
210
+ while(i--){
211
+ dim = dimensions[i]
212
+ distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
213
+ }
214
+ distances = distances.sort(function (a,b) {
215
+ return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
216
+ })
217
+
218
+ // last entry is the closest
219
+ return distances
220
+ }
221
+
222
+ function ContainerGroup(options) {
223
+ this.options = $.extend({}, groupDefaults, options)
224
+ this.containers = []
225
+
226
+ if(!this.options.rootGroup){
227
+ this.scrollProxy = $.proxy(this.scroll, this)
228
+ this.dragProxy = $.proxy(this.drag, this)
229
+ this.dropProxy = $.proxy(this.drop, this)
230
+ this.placeholder = $(this.options.placeholder)
231
+
232
+ if(!options.isValidTarget)
233
+ this.options.isValidTarget = undefined
234
+ }
235
+ }
236
+
237
+ ContainerGroup.get = function (options) {
238
+ if(!containerGroups[options.group]) {
239
+ if(options.group === undefined)
240
+ options.group = groupCounter ++
241
+
242
+ containerGroups[options.group] = new ContainerGroup(options)
243
+ }
244
+
245
+ return containerGroups[options.group]
246
+ }
247
+
248
+ ContainerGroup.prototype = {
249
+ dragInit: function (e, itemContainer) {
250
+ this.$document = $(itemContainer.el[0].ownerDocument)
251
+
252
+ // get item to drag
253
+ var closestItem = $(e.target).closest(this.options.itemSelector);
254
+ // using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
255
+ // this may also be helpful in instantiating multidrag.
256
+ if (closestItem.length) {
257
+ this.item = closestItem;
258
+ this.itemContainer = itemContainer;
259
+ if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
260
+ return;
261
+ }
262
+ this.setPointer(e);
263
+ this.toggleListeners('on');
264
+ this.setupDelayTimer();
265
+ this.dragInitDone = true;
266
+ }
267
+ },
268
+ drag: function (e) {
269
+ if(!this.dragging){
270
+ if(!this.distanceMet(e) || !this.delayMet)
271
+ return
272
+
273
+ this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
274
+ this.item.before(this.placeholder)
275
+ this.dragging = true
276
+ }
277
+
278
+ this.setPointer(e)
279
+ // place item under the cursor
280
+ this.options.onDrag(this.item,
281
+ getRelativePosition(this.pointer, this.item.offsetParent()),
282
+ groupDefaults.onDrag,
283
+ e)
284
+
285
+ var p = this.getPointer(e),
286
+ box = this.sameResultBox,
287
+ t = this.options.tolerance
288
+
289
+ if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
290
+ if(!this.searchValidTarget()){
291
+ this.placeholder.detach()
292
+ this.lastAppendedItem = undefined
293
+ }
294
+ },
295
+ drop: function (e) {
296
+ this.toggleListeners('off')
297
+
298
+ this.dragInitDone = false
299
+
300
+ if(this.dragging){
301
+ // processing Drop, check if placeholder is detached
302
+ if(this.placeholder.closest("html")[0]){
303
+ this.placeholder.before(this.item).detach()
304
+ } else {
305
+ this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
306
+ }
307
+ this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)
308
+
309
+ // cleanup
310
+ this.clearDimensions()
311
+ this.clearOffsetParent()
312
+ this.lastAppendedItem = this.sameResultBox = undefined
313
+ this.dragging = false
314
+ }
315
+ },
316
+ searchValidTarget: function (pointer, lastPointer) {
317
+ if(!pointer){
318
+ pointer = this.relativePointer || this.pointer
319
+ lastPointer = this.lastRelativePointer || this.lastPointer
320
+ }
321
+
322
+ var distances = sortByDistanceDesc(this.getContainerDimensions(),
323
+ pointer,
324
+ lastPointer),
325
+ i = distances.length
326
+
327
+ while(i--){
328
+ var index = distances[i][0],
329
+ distance = distances[i][1]
330
+
331
+ if(!distance || this.options.pullPlaceholder){
332
+ var container = this.containers[index]
333
+ if(!container.disabled){
334
+ if(!this.$getOffsetParent()){
335
+ var offsetParent = container.getItemOffsetParent()
336
+ pointer = getRelativePosition(pointer, offsetParent)
337
+ lastPointer = getRelativePosition(lastPointer, offsetParent)
338
+ }
339
+ if(container.searchValidTarget(pointer, lastPointer))
340
+ return true
341
+ }
342
+ }
343
+ }
344
+ if(this.sameResultBox)
345
+ this.sameResultBox = undefined
346
+ },
347
+ movePlaceholder: function (container, item, method, sameResultBox) {
348
+ var lastAppendedItem = this.lastAppendedItem
349
+ if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
350
+ return;
351
+
352
+ item[method](this.placeholder)
353
+ this.lastAppendedItem = item
354
+ this.sameResultBox = sameResultBox
355
+ this.options.afterMove(this.placeholder, container, item)
356
+ },
357
+ getContainerDimensions: function () {
358
+ if(!this.containerDimensions)
359
+ setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
360
+ return this.containerDimensions
361
+ },
362
+ getContainer: function (element) {
363
+ return element.closest(this.options.containerSelector).data(pluginName)
364
+ },
365
+ $getOffsetParent: function () {
366
+ if(this.offsetParent === undefined){
367
+ var i = this.containers.length - 1,
368
+ offsetParent = this.containers[i].getItemOffsetParent()
369
+
370
+ if(!this.options.rootGroup){
371
+ while(i--){
372
+ if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
373
+ // If every container has the same offset parent,
374
+ // use position() which is relative to this parent,
375
+ // otherwise use offset()
376
+ // compare #setDimensions
377
+ offsetParent = false
378
+ break;
379
+ }
380
+ }
381
+ }
382
+
383
+ this.offsetParent = offsetParent
384
+ }
385
+ return this.offsetParent
386
+ },
387
+ setPointer: function (e) {
388
+ var pointer = this.getPointer(e)
389
+
390
+ if(this.$getOffsetParent()){
391
+ var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
392
+ this.lastRelativePointer = this.relativePointer
393
+ this.relativePointer = relativePointer
394
+ }
395
+
396
+ this.lastPointer = this.pointer
397
+ this.pointer = pointer
398
+ },
399
+ distanceMet: function (e) {
400
+ var currentPointer = this.getPointer(e)
401
+ return (Math.max(
402
+ Math.abs(this.pointer.left - currentPointer.left),
403
+ Math.abs(this.pointer.top - currentPointer.top)
404
+ ) >= this.options.distance)
405
+ },
406
+ getPointer: function(e) {
407
+ var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
408
+ return {
409
+ left: e.pageX || o.pageX,
410
+ top: e.pageY || o.pageY
411
+ }
412
+ },
413
+ setupDelayTimer: function () {
414
+ var that = this
415
+ this.delayMet = !this.options.delay
416
+
417
+ // init delay timer if needed
418
+ if (!this.delayMet) {
419
+ clearTimeout(this._mouseDelayTimer);
420
+ this._mouseDelayTimer = setTimeout(function() {
421
+ that.delayMet = true
422
+ }, this.options.delay)
423
+ }
424
+ },
425
+ scroll: function (e) {
426
+ this.clearDimensions()
427
+ this.clearOffsetParent() // TODO is this needed?
428
+ },
429
+ toggleListeners: function (method) {
430
+ var that = this,
431
+ events = ['drag','drop','scroll']
432
+
433
+ $.each(events,function (i,event) {
434
+ that.$document[method](eventNames[event], that[event + 'Proxy'])
435
+ })
436
+ },
437
+ clearOffsetParent: function () {
438
+ this.offsetParent = undefined
439
+ },
440
+ // Recursively clear container and item dimensions
441
+ clearDimensions: function () {
442
+ this.traverse(function(object){
443
+ object._clearDimensions()
444
+ })
445
+ },
446
+ traverse: function(callback) {
447
+ callback(this)
448
+ var i = this.containers.length
449
+ while(i--){
450
+ this.containers[i].traverse(callback)
451
+ }
452
+ },
453
+ _clearDimensions: function(){
454
+ this.containerDimensions = undefined
455
+ },
456
+ _destroy: function () {
457
+ containerGroups[this.options.group] = undefined
458
+ }
459
+ }
460
+
461
+ function Container(element, options) {
462
+ this.el = element
463
+ this.options = $.extend( {}, containerDefaults, options)
464
+
465
+ this.group = ContainerGroup.get(this.options)
466
+ this.rootGroup = this.options.rootGroup || this.group
467
+ this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector
468
+
469
+ var itemPath = this.rootGroup.options.itemPath
470
+ this.target = itemPath ? this.el.find(itemPath) : this.el
471
+
472
+ this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))
473
+
474
+ if(this.options.drop)
475
+ this.group.containers.push(this)
476
+ }
477
+
478
+ Container.prototype = {
479
+ dragInit: function (e) {
480
+ var rootGroup = this.rootGroup
481
+
482
+ if( !this.disabled &&
483
+ !rootGroup.dragInitDone &&
484
+ this.options.drag &&
485
+ this.isValidDrag(e)) {
486
+ rootGroup.dragInit(e, this)
487
+ }
488
+ },
489
+ isValidDrag: function(e) {
490
+ return e.which == 1 ||
491
+ e.type == "touchstart" && e.originalEvent.touches.length == 1
492
+ },
493
+ searchValidTarget: function (pointer, lastPointer) {
494
+ var distances = sortByDistanceDesc(this.getItemDimensions(),
495
+ pointer,
496
+ lastPointer),
497
+ i = distances.length,
498
+ rootGroup = this.rootGroup,
499
+ validTarget = !rootGroup.options.isValidTarget ||
500
+ rootGroup.options.isValidTarget(rootGroup.item, this)
501
+
502
+ if(!i && validTarget){
503
+ rootGroup.movePlaceholder(this, this.target, "append")
504
+ return true
505
+ } else
506
+ while(i--){
507
+ var index = distances[i][0],
508
+ distance = distances[i][1]
509
+ if(!distance && this.hasChildGroup(index)){
510
+ var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
511
+ if(found)
512
+ return true
513
+ }
514
+ else if(validTarget){
515
+ this.movePlaceholder(index, pointer)
516
+ return true
517
+ }
518
+ }
519
+ },
520
+ movePlaceholder: function (index, pointer) {
521
+ var item = $(this.items[index]),
522
+ dim = this.itemDimensions[index],
523
+ method = "after",
524
+ width = item.outerWidth(),
525
+ height = item.outerHeight(),
526
+ offset = item.offset(),
527
+ sameResultBox = {
528
+ left: offset.left,
529
+ right: offset.left + width,
530
+ top: offset.top,
531
+ bottom: offset.top + height
532
+ }
533
+ if(this.options.vertical){
534
+ var yCenter = (dim[2] + dim[3]) / 2,
535
+ inUpperHalf = pointer.top <= yCenter
536
+ if(inUpperHalf){
537
+ method = "before"
538
+ sameResultBox.bottom -= height / 2
539
+ } else
540
+ sameResultBox.top += height / 2
541
+ } else {
542
+ var xCenter = (dim[0] + dim[1]) / 2,
543
+ inLeftHalf = pointer.left <= xCenter
544
+ if(inLeftHalf){
545
+ method = "before"
546
+ sameResultBox.right -= width / 2
547
+ } else
548
+ sameResultBox.left += width / 2
549
+ }
550
+ if(this.hasChildGroup(index))
551
+ sameResultBox = emptyBox
552
+ this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
553
+ },
554
+ getItemDimensions: function () {
555
+ if(!this.itemDimensions){
556
+ this.items = this.$getChildren(this.el, "item").filter(
557
+ ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
558
+ ).get()
559
+ setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
560
+ }
561
+ return this.itemDimensions
562
+ },
563
+ getItemOffsetParent: function () {
564
+ var offsetParent,
565
+ el = this.el
566
+ // Since el might be empty we have to check el itself and
567
+ // can not do something like el.children().first().offsetParent()
568
+ if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed")
569
+ offsetParent = el
570
+ else
571
+ offsetParent = el.offsetParent()
572
+ return offsetParent
573
+ },
574
+ hasChildGroup: function (index) {
575
+ return this.options.nested && this.getContainerGroup(index)
576
+ },
577
+ getContainerGroup: function (index) {
578
+ var childGroup = $.data(this.items[index], subContainerKey)
579
+ if( childGroup === undefined){
580
+ var childContainers = this.$getChildren(this.items[index], "container")
581
+ childGroup = false
582
+
583
+ if(childContainers[0]){
584
+ var options = $.extend({}, this.options, {
585
+ rootGroup: this.rootGroup,
586
+ group: groupCounter ++
587
+ })
588
+ childGroup = childContainers[pluginName](options).data(pluginName).group
589
+ }
590
+ $.data(this.items[index], subContainerKey, childGroup)
591
+ }
592
+ return childGroup
593
+ },
594
+ $getChildren: function (parent, type) {
595
+ var options = this.rootGroup.options,
596
+ path = options[type + "Path"],
597
+ selector = options[type + "Selector"]
598
+
599
+ parent = $(parent)
600
+ if(path)
601
+ parent = parent.find(path)
602
+
603
+ return parent.children(selector)
604
+ },
605
+ _serialize: function (parent, isContainer) {
606
+ var that = this,
607
+ childType = isContainer ? "item" : "container",
608
+
609
+ children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
610
+ return that._serialize($(this), !isContainer)
611
+ }).get()
612
+
613
+ return this.rootGroup.options.serialize(parent, children, isContainer)
614
+ },
615
+ traverse: function(callback) {
616
+ $.each(this.items || [], function(item){
617
+ var group = $.data(this, subContainerKey)
618
+ if(group)
619
+ group.traverse(callback)
620
+ });
621
+
622
+ callback(this)
623
+ },
624
+ _clearDimensions: function () {
625
+ this.itemDimensions = undefined
626
+ },
627
+ _destroy: function() {
628
+ var that = this;
629
+
630
+ this.target.off(eventNames.start, this.handle);
631
+ this.el.removeData(pluginName)
632
+
633
+ if(this.options.drop)
634
+ this.group.containers = $.grep(this.group.containers, function(val){
635
+ return val != that
636
+ })
637
+
638
+ $.each(this.items || [], function(){
639
+ $.removeData(this, subContainerKey)
640
+ })
641
+ }
642
+ }
643
+
644
+ var API = {
645
+ enable: function() {
646
+ this.traverse(function(object){
647
+ object.disabled = false
648
+ })
649
+ },
650
+ disable: function (){
651
+ this.traverse(function(object){
652
+ object.disabled = true
653
+ })
654
+ },
655
+ serialize: function () {
656
+ return this._serialize(this.el, true)
657
+ },
658
+ refresh: function() {
659
+ this.traverse(function(object){
660
+ object._clearDimensions()
661
+ })
662
+ },
663
+ destroy: function () {
664
+ this.traverse(function(object){
665
+ object._destroy();
666
+ })
667
+ }
668
+ }
669
+
670
+ $.extend(Container.prototype, API)
671
+
672
+ /**
673
+ * jQuery API
674
+ *
675
+ * Parameters are
676
+ * either options on init
677
+ * or a method name followed by arguments to pass to the method
678
+ */
679
+ $.fn[pluginName] = function(methodOrOptions) {
680
+ var args = Array.prototype.slice.call(arguments, 1)
681
+
682
+ return this.map(function(){
683
+ var $t = $(this),
684
+ object = $t.data(pluginName)
685
+
686
+ if(object && API[methodOrOptions])
687
+ return API[methodOrOptions].apply(object, args) || this
688
+ else if(!object && (methodOrOptions === undefined ||
689
+ typeof methodOrOptions === "object"))
690
+ $t.data(pluginName, new Container($t, methodOrOptions))
691
+
692
+ return this
693
+ });
694
+ };
695
+
696
+ }(jQuery, window, 'sortable');
@@ -3,6 +3,7 @@
3
3
  @import 'effective_checks/input';
4
4
  @import 'effective_datetime/input';
5
5
  @import 'effective_file/input';
6
+ @import 'effective_has_many/input';
6
7
  @import 'effective_rich_text_area/input';
7
8
  @import 'effective_select/input';
8
9
  @import 'effective_select_or_text/input';
@@ -4,5 +4,5 @@
4
4
  font-size: 0.75rem;
5
5
  margin: 0.25rem 0;
6
6
 
7
- a { padding: 0.25rem 0; }
7
+ a { padding: 0.5rem 0; }
8
8
  }
@@ -0,0 +1,42 @@
1
+ body.dragging,
2
+ body.dragging * {
3
+ cursor: move !important;
4
+ }
5
+
6
+ .form-has-many {
7
+ .has-many-placeholder {
8
+ position: relative;
9
+ height: 2rem;
10
+
11
+ &:before {
12
+ position: absolute;
13
+ content: '';
14
+ background-image: asset-data-url('icons/arrow-right-circle.svg');
15
+ background-repeat: no-repeat;
16
+ height: 2rem;
17
+ width: 2rem;
18
+ }
19
+ }
20
+
21
+ .has-many-fields.dragged {
22
+ position: absolute;
23
+ opacity: 0;
24
+ z-index: 2000;
25
+ .has-many-move { display: none; }
26
+ }
27
+
28
+ .has-many-move svg { margin-top: 6px; }
29
+ .has-many-move { display: none; }
30
+
31
+ .has-many-remove { margin-top: 1.5rem; }
32
+ .has-many-move { margin-top: 1.5rem; }
33
+ }
34
+
35
+ .form-has-many.reordering {
36
+ .has-many-move { display: inline-block; }
37
+ }
38
+
39
+ .form-has-many.tight {
40
+ .has-many-remove { margin-top: 0; }
41
+ .has-many-move { margin-top: 0; }
42
+ }
@@ -2,7 +2,7 @@ module EffectiveFormBuilderHelper
2
2
  def effective_form_with(**options, &block)
3
3
  # Compute the default ID
4
4
  subject = Array(options[:scope] || options[:model]).last
5
- class_name = subject.class.name.underscore
5
+ class_name = subject.class.name.parameterize.underscore
6
6
  unique_id = options.except(:model).hash.abs
7
7
 
8
8
  html_id = if subject.kind_of?(Symbol)
@@ -56,6 +56,17 @@ module EffectiveFormBuilderHelper
56
56
  # Assign default ID
57
57
  options[:id] ||= (options[:html].delete(:id) || html_id) unless options.key?(:id)
58
58
 
59
+ # Assign url if engine present
60
+ options[:url] ||= if options[:engine] && options[:model].present?
61
+ resource = Effective::Resource.new(options[:model])
62
+
63
+ if subject.respond_to?(:persisted?) && subject.persisted?
64
+ resource.action_path(:update, subject)
65
+ elsif subject.respond_to?(:new_record?) && subject.new_record?
66
+ resource.action_path(:create)
67
+ end
68
+ end
69
+
59
70
  without_error_proc do
60
71
  form_with(**options.merge(builder: Effective::FormBuilder), &block)
61
72
  end
@@ -203,5 +203,19 @@ module Effective
203
203
  Effective::FormLogics::ShowIfAny.new(*args, builder: self).to_html(&block)
204
204
  end
205
205
 
206
+ # Has Many
207
+ def has_many(name, collection = nil, options = {}, &block)
208
+ association = object.class.reflect_on_all_associations.find { |a| a.name == name }
209
+ raise("expected #{object.class.name} to has_many :#{name}") if association.blank?
210
+
211
+ nested_attributes_options = (object.class.nested_attributes_options || {})[name]
212
+ raise("expected #{object.class.name} to accepts_nested_attributes_for :#{name}") if nested_attributes_options.blank?
213
+
214
+ options = collection if collection.kind_of?(Hash)
215
+ options.merge!(collection: collection) if collection && !collection.kind_of?(Hash)
216
+
217
+ Effective::FormInputs::HasMany.new(name, options, builder: self).to_html(&block)
218
+ end
219
+
206
220
  end
207
221
  end
@@ -50,18 +50,29 @@ module Effective
50
50
  end
51
51
 
52
52
  def build_label
53
- return BLANK if options[:label] == false
53
+ return BLANK if options[:label] == false && !actions?
54
54
  return BLANK if name.kind_of?(NilClass)
55
55
 
56
- text = (options[:label].delete(:text) || (object.class.human_attribute_name(name) if object) || BLANK).html_safe
56
+ text = begin
57
+ if options[:label] == false
58
+ nil
59
+ elsif options[:label].key?(:text)
60
+ options[:label].delete(:text)
61
+ elsif object.present?
62
+ object.class.human_attribute_name(name)
63
+ end || BLANK
64
+ end.html_safe
65
+
66
+ actions = if !disabled? && actions?
67
+ content_tag(:div, class: 'effective-checks-actions text-muted') do
68
+ link_to('Select All', '#', 'data-effective-checks-all': true) + ' - ' + link_to('Select None', '#', 'data-effective-checks-none': true)
69
+ end
70
+ end
57
71
 
58
72
  content_tag(:label, options[:label]) do
59
- text + content_tag(:div, class: 'effective-checks-actions text-muted') do
60
- unless disabled? || !actions?
61
- link_to('Select All', '#', 'data-effective-checks-all': true) + ' - ' + link_to('Select None', '#', 'data-effective-checks-none': true)
62
- end
63
- end
73
+ [text, actions].compact.join.html_safe
64
74
  end
75
+
65
76
  end
66
77
 
67
78
  def build_item(builder)
@@ -91,7 +102,7 @@ module Effective
91
102
 
92
103
  def actions? # default true
93
104
  return @actions unless @actions.nil?
94
- @actions = (options.delete(:actions) != false)
105
+ @actions = (options[:input].delete(:actions) != false)
95
106
  end
96
107
 
97
108
  end
@@ -0,0 +1,207 @@
1
+ module Effective
2
+ module FormInputs
3
+ class HasMany < Effective::FormInput
4
+ BLANK = ''.html_safe
5
+
6
+ def to_html(&block)
7
+ object.send(name).build() if build? && collection.blank?
8
+
9
+ errors = (@builder.error(name) if errors?) || BLANK
10
+
11
+ errors + content_tag(:div, options[:input]) do
12
+ has_many_fields_for(block) + has_many_links_for(block)
13
+ end
14
+ end
15
+
16
+ def input_html_options
17
+ { class: 'form-has-many mb-4' }
18
+ end
19
+
20
+ def input_js_options
21
+ { sortable: true }
22
+ end
23
+
24
+ def collection
25
+ Array(options[:input][:collection] || object.send(name))
26
+ end
27
+
28
+ # cards: false
29
+ def display
30
+ @display ||= (options[:input].delete(:cards) ? :cards : :rows)
31
+ end
32
+
33
+ # build: true
34
+ def build?
35
+ return @build unless @build.nil?
36
+
37
+ @build ||= begin
38
+ build = options[:input].delete(:build)
39
+ build.nil? ? true : build
40
+ end
41
+ end
42
+
43
+ # add: true
44
+ def add?
45
+ return @add unless @add.nil?
46
+
47
+ @add ||= begin
48
+ add = options[:input].delete(:add)
49
+ add.nil? ? true : add
50
+ end
51
+ end
52
+
53
+ # errors: true
54
+ def errors?
55
+ return @errors unless @errors.nil?
56
+
57
+ @errors ||= begin
58
+ errors = options[:input].delete(:errors)
59
+ errors.nil? ? true : errors
60
+ end
61
+ end
62
+
63
+ # remove: true
64
+ def remove?
65
+ return @remove unless @remove.nil?
66
+
67
+ @remove ||= begin
68
+ remove = options[:input].delete(:remove)
69
+
70
+ if remove != nil
71
+ remove
72
+ else
73
+ opts = (object.class.nested_attributes_options[name] || {})
74
+ opts[:update_only] != true && opts[:allow_destroy] != false
75
+ end
76
+ end
77
+ end
78
+
79
+ # reorder: true
80
+ def reorder?
81
+ return @reorder unless @reorder.nil?
82
+
83
+ @reorder ||= begin
84
+ reorder = options[:input].delete(:reorder)
85
+
86
+ if reorder != nil
87
+ reorder
88
+ else
89
+ build_resource().class.columns_hash['position']&.type == :integer
90
+ end
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def has_many_fields_for(block)
97
+ collection.map { |resource| render_resource(resource, block) }.join.html_safe
98
+ end
99
+
100
+ def has_many_links_for(block)
101
+ return BLANK unless add? || reorder?
102
+
103
+ content_tag(:div, class: 'has-many-links text-center mt-2') do
104
+ [*(link_to_add(block) if add?), *(link_to_reorder(block) if reorder?)].join(' ').html_safe
105
+ end
106
+ end
107
+
108
+ def render_resource(resource, block)
109
+ remove = BLANK
110
+
111
+ content = @builder.fields_for(name, resource) do |form|
112
+ fields = block.call(form)
113
+
114
+ remove += form.super_hidden_field(:_destroy) if remove? && resource.persisted?
115
+ remove += form.super_hidden_field(:position) if reorder? && !fields.include?('][position]')
116
+
117
+ fields
118
+ end
119
+
120
+ remove += link_to_remove(resource) if remove?
121
+
122
+ content_tag(:div, render_fields(content, remove), class: 'has-many-fields')
123
+ end
124
+
125
+ def render_fields(content, remove)
126
+ case display
127
+ when :rows
128
+ content_tag(:div, class: 'form-row') do
129
+ (reorder? ? content_tag(:div, has_many_move, class: 'col-auto') : BLANK) +
130
+ content_tag(:div, content, class: 'col mr-auto') +
131
+ content_tag(:div, remove, class: 'col-auto')
132
+ end
133
+ when :cards
134
+ raise('unsupported')
135
+ else
136
+ content + remove
137
+ end
138
+ end
139
+
140
+ def render_template(block)
141
+ resource = build_resource()
142
+ index = collection.length
143
+
144
+ html = render_resource(resource, block)
145
+
146
+ unless html.include?("#{name}_attributes][#{index}]")
147
+ raise('unexpected index. unable to render resource template.')
148
+ end
149
+
150
+ html.gsub!("#{name}_attributes][#{index}]", "#{name}_attributes][HASMANYINDEX]")
151
+ html.gsub!("#{name}_attributes_#{index}_", "#{name}_attributes_HASMANYINDEX_")
152
+
153
+ html.html_safe
154
+ end
155
+
156
+ def link_to_add(block)
157
+ content_tag(
158
+ :button,
159
+ icon('plus-circle') + 'Add Another',
160
+ class: 'has-many-add btn btn-secondary',
161
+ title: 'Add Another',
162
+ data: {
163
+ 'effective-form-has-many-add': true,
164
+ 'effective-form-has-many-template': render_template(block)
165
+ }
166
+ )
167
+ end
168
+
169
+ def link_to_reorder(block)
170
+ content_tag(
171
+ :button,
172
+ icon('list') + 'Reorder',
173
+ class: 'has-many-reorder btn btn-secondary',
174
+ title: 'Reorder',
175
+ data: {
176
+ 'effective-form-has-many-reorder': true,
177
+ }
178
+ )
179
+ end
180
+
181
+ def link_to_remove(resource)
182
+ content_tag(
183
+ :button,
184
+ icon('trash-2') + 'Remove',
185
+ class: 'has-many-remove btn btn-danger',
186
+ title: 'Remove',
187
+ data: {
188
+ 'confirm': "Remove #{resource}?",
189
+ 'effective-form-has-many-remove': true,
190
+ }
191
+ )
192
+ end
193
+
194
+ def has_many_move
195
+ @has_many_move ||= content_tag(:span, icon('move'), class: 'has-many-move')
196
+ end
197
+
198
+ def build_resource
199
+ # Using .new() here seems like it should work but it doesn't. It changes the index
200
+ @build_resource ||= object.send(name).build().tap do |resource|
201
+ object.send(name).delete(resource)
202
+ end
203
+ end
204
+
205
+ end
206
+ end
207
+ end
@@ -8,7 +8,7 @@ module EffectiveBootstrap
8
8
  end
9
9
 
10
10
  initializer 'effective_bootstrap.assets' do |app|
11
- app.config.assets.precompile += ['icons/*']
11
+ app.config.assets.precompile += ['effective_bootstrap_manifest.js', 'icons/*']
12
12
  end
13
13
 
14
14
  end
@@ -1,3 +1,3 @@
1
1
  module EffectiveBootstrap
2
- VERSION = '0.9.7'.freeze
2
+ VERSION = '0.9.12'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_bootstrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.7
4
+ version: 0.9.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-07 00:00:00.000000000 Z
11
+ date: 2021-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: haml
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: Everything you need to get set up with bootstrap 4.
98
112
  email:
99
113
  - info@codeandeffect.com
@@ -103,6 +117,7 @@ extra_rdoc_files: []
103
117
  files:
104
118
  - MIT-LICENSE
105
119
  - README.md
120
+ - app/assets/config/effective_bootstrap_manifest.js
106
121
  - app/assets/images/icons/activity.svg
107
122
  - app/assets/images/icons/airplay.svg
108
123
  - app/assets/images/icons/alert-circle.svg
@@ -418,6 +433,9 @@ files:
418
433
  - app/assets/javascripts/effective_editor/quill.js
419
434
  - app/assets/javascripts/effective_file/initialize.js.coffee
420
435
  - app/assets/javascripts/effective_file/input.js
436
+ - app/assets/javascripts/effective_has_many/initialize.js.coffee
437
+ - app/assets/javascripts/effective_has_many/input.js
438
+ - app/assets/javascripts/effective_has_many/jquery.sortable.js
421
439
  - app/assets/javascripts/effective_integer/initialize.js.coffee
422
440
  - app/assets/javascripts/effective_integer/input.js
423
441
  - app/assets/javascripts/effective_number_text/initialize.js.coffee
@@ -583,6 +601,7 @@ files:
583
601
  - app/assets/stylesheets/effective_editor/overrides.scss
584
602
  - app/assets/stylesheets/effective_editor/quill.scss
585
603
  - app/assets/stylesheets/effective_file/input.scss
604
+ - app/assets/stylesheets/effective_has_many/input.scss
586
605
  - app/assets/stylesheets/effective_radio/input.scss
587
606
  - app/assets/stylesheets/effective_rich_text_area/input.scss
588
607
  - app/assets/stylesheets/effective_select/bootstrap-theme.css
@@ -612,6 +631,7 @@ files:
612
631
  - app/models/effective/form_inputs/file_field.rb
613
632
  - app/models/effective/form_inputs/float_field.rb
614
633
  - app/models/effective/form_inputs/form_group.rb
634
+ - app/models/effective/form_inputs/has_many.rb
615
635
  - app/models/effective/form_inputs/hidden_field.rb
616
636
  - app/models/effective/form_inputs/integer_field.rb
617
637
  - app/models/effective/form_inputs/number_field.rb
@@ -667,7 +687,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
667
687
  - !ruby/object:Gem::Version
668
688
  version: '0'
669
689
  requirements: []
670
- rubygems_version: 3.1.4
690
+ rubygems_version: 3.1.2
671
691
  signing_key:
672
692
  specification_version: 4
673
693
  summary: Everything you need to get set up with bootstrap 4.