effective_bootstrap 0.9.8 → 0.9.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3f89e397f06193b9e69ed5e2ef79c992a8c73705548cbb28e33d379738553d7
4
- data.tar.gz: 97869e53c24d7d47c28a19d13bf95095f7e7bdf61122f24b44e2a85491f6380c
3
+ metadata.gz: e330704dd9363570d7981034a08e14d29347feae21c7374ce073e322ea6cd5ea
4
+ data.tar.gz: d618f09047887bec221b95eb3af1b47be80d3914505f82a88e81d7d138e149a6
5
5
  SHA512:
6
- metadata.gz: b2c68c7fec202c9dce7bab99b1783a4ca4283565fb4f1f8f23bfc9efb88f884508d161422f1effecbdd6e39ec37aaf1ef4c2c2eaba53f92ab4ff6ea30bc285c0
7
- data.tar.gz: 7907170bbaa34401f4df5604db70f07047299450d3072181e85e8a0fedcd6b77cfbdcbef717f0fd50687984812c83e8a7885162ef7d7820170b830ed2f46e989
6
+ metadata.gz: c0ac3afa88825a5af188d069a7a9a41a4ab677890068408d80fd39a62928306e9172a81d4dd0dc20e586d0f2755e172e78a07eaedc138c92bf2e318f3a8d4d2a
7
+ data.tar.gz: 332482707c9bf94e22c7be4e04a16ad2367a4bdb8fac87f11d8240b6e5591af9471e9880586211c92db6c40dbf47238ac026b5f4a06e6d0ffb68f07d7ccf0467
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.
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  module EffectiveFormBuilderHelper
2
2
  def effective_form_with(**options, &block)
3
3
  # Compute the default ID
4
- subject = Array(options[:scope] || options[:model]).last
5
- class_name = subject.class.name.underscore
4
+ subject = Array(options[:model] || options[:scope]).last
5
+ class_name = (options[:scope] || 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
@@ -1,3 +1,3 @@
1
1
  module EffectiveBootstrap
2
- VERSION = '0.9.8'.freeze
2
+ VERSION = '0.9.13'.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.8
4
+ version: 0.9.13
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-09 00:00:00.000000000 Z
11
+ date: 2021-02-16 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
@@ -419,6 +433,9 @@ files:
419
433
  - app/assets/javascripts/effective_editor/quill.js
420
434
  - app/assets/javascripts/effective_file/initialize.js.coffee
421
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
422
439
  - app/assets/javascripts/effective_integer/initialize.js.coffee
423
440
  - app/assets/javascripts/effective_integer/input.js
424
441
  - app/assets/javascripts/effective_number_text/initialize.js.coffee
@@ -584,6 +601,7 @@ files:
584
601
  - app/assets/stylesheets/effective_editor/overrides.scss
585
602
  - app/assets/stylesheets/effective_editor/quill.scss
586
603
  - app/assets/stylesheets/effective_file/input.scss
604
+ - app/assets/stylesheets/effective_has_many/input.scss
587
605
  - app/assets/stylesheets/effective_radio/input.scss
588
606
  - app/assets/stylesheets/effective_rich_text_area/input.scss
589
607
  - app/assets/stylesheets/effective_select/bootstrap-theme.css
@@ -613,6 +631,7 @@ files:
613
631
  - app/models/effective/form_inputs/file_field.rb
614
632
  - app/models/effective/form_inputs/float_field.rb
615
633
  - app/models/effective/form_inputs/form_group.rb
634
+ - app/models/effective/form_inputs/has_many.rb
616
635
  - app/models/effective/form_inputs/hidden_field.rb
617
636
  - app/models/effective/form_inputs/integer_field.rb
618
637
  - app/models/effective/form_inputs/number_field.rb
@@ -668,7 +687,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
668
687
  - !ruby/object:Gem::Version
669
688
  version: '0'
670
689
  requirements: []
671
- rubygems_version: 3.1.4
690
+ rubygems_version: 3.1.2
672
691
  signing_key:
673
692
  specification_version: 4
674
693
  summary: Everything you need to get set up with bootstrap 4.