Skip to content

Commit 7cbcf32

Browse files
committed
sortable has-many
1 parent 12b19a7 commit 7cbcf32

File tree

6 files changed

+90
-9
lines changed

6 files changed

+90
-9
lines changed

app/assets/javascripts/active_admin/base.js.coffee

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#= require jquery
22
#= require jquery.ui.widget
33
#= require jquery.ui.datepicker
4+
#= require jquery.ui.sortable
45
#= require jquery_ujs
56
#
67
#= require_self

app/assets/javascripts/active_admin/lib/has_many.js.coffee

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ $ ->
99
e.preventDefault()
1010
parent = $(@).closest '.has_many_container'
1111
to_remove = $(@).closest 'fieldset'
12+
recompute_positions parent
1213

1314
parent.trigger 'has_many_remove:before', [ to_remove ]
1415
to_remove.remove()
@@ -38,4 +39,37 @@ $ ->
3839
regex = new RegExp elem.data('placeholder'), 'g'
3940
html = elem.data('html').replace regex, index
4041

41-
parent.trigger 'has_many_add:after', [ $(html).insertBefore(@) ]
42+
fieldset = $(html).insertBefore(@)
43+
recompute_positions parent
44+
parent.trigger 'has_many_add:after', [ fieldset ]
45+
46+
$(document).on 'change','.has_many_container[data-sortable] :input[name$="[_destroy]"]', ->
47+
recompute_positions $(@).closest '.has_many'
48+
49+
init_sortable()
50+
$(document).on 'has_many_add:after', '.has_many_container', init_sortable
51+
52+
53+
# Helpers
54+
init_sortable = ->
55+
elems = $('.has_many_container[data-sortable]:not(.ui-sortable)')
56+
elems.sortable \
57+
items: '> fieldset',
58+
handle: '> ol > .handle',
59+
stop: recompute_positions
60+
elems.each recompute_positions
61+
62+
recompute_positions = (parent)->
63+
parent = if parent instanceof jQuery then parent else $(@)
64+
input_name = parent.data 'sortable'
65+
position = 0
66+
67+
parent.children('fieldset').each ->
68+
fieldset = $(@)
69+
# when looking for inputs, we ignore inputs from the possibly nested inputs
70+
# so, when defining your has_many, make sure to keep the sortable input at the root of the has_many block
71+
destroy_input = fieldset.find "> ol > .input > :input[name$='[_destroy]']"
72+
sortable_input = fieldset.find "> ol > .input > :input[name$='[#{input_name}]']"
73+
74+
if sortable_input.length
75+
sortable_input.val if destroy_input.is ':checked' then '' else position++

app/assets/stylesheets/active_admin/_forms.css.scss

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ form {
88
border: 0;
99
padding: 10px 0;
1010
margin-bottom: 20px;
11+
position: relative; // required for the absolutely positioned sortable .handle
1112

1213
&.inputs { @include section-background; }
1314

@@ -51,12 +52,31 @@ form {
5152
}
5253
}
5354

55+
.has_many_container .handle {
56+
position: absolute;
57+
@include icon-size(3em);
58+
top: calc(50% - 3em / 2);
59+
right: 2px;
60+
padding: 0;
61+
cursor: move;
62+
}
63+
64+
.ui-sortable {
65+
// give the handle space!
66+
input[type=text], input[type=password], input[type=email], input[type=number], input[type=url], input[type=tel], textarea {
67+
width: calc(80% - #{$text-input-total-padding} - 2em - 1px);
68+
}
69+
// If a sortable is nested in a sortable, give the parent handle space!
70+
.ui-sortable .has_many_fields {
71+
margin-right: 2em;
72+
}
73+
}
74+
5475
/* Nested Fieldsets and Legends */
5576
li.has_many_container {
5677
fieldset.has_many_fields {
5778
margin: 10px 0;
5879
}
59-
6080
}
6181

6282
fieldset > ol > li {
@@ -87,16 +107,16 @@ form {
87107

88108
/* Text Fields */
89109
input[type=text], input[type=password], input[type=email], input[type=number], input[type=url], input[type=tel], textarea {
90-
width: 76%;
91-
border: 1px solid #c9d0d6;
110+
width: calc(80% - #{$text-input-total-padding});
111+
border: $border-width solid #c9d0d6;
92112
@include rounded;
93113
font-size: 0.95em;
94114
@include sans-family;
95115
outline: none;
96116
padding: 8px $text-input-horizontal-padding 7px;
97117

98118
&:focus {
99-
border: 1px solid #99a2aa;
119+
border: $border-width solid #99a2aa;
100120
@include shadow(0,0,4px,#99a2aa);
101121
}
102122
}
@@ -168,7 +188,7 @@ form {
168188

169189
&.error {
170190
input[type=text], input[type=password], input[type=email], input[type=number], input[type=url], input[type=tel], textarea {
171-
border: 1px solid $error-color;
191+
border: $border-width solid $error-color;
172192
}
173193
}
174194
}

app/assets/stylesheets/active_admin/mixins/_variables.css.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ $form-label-color: $section-header-text-color;
2222
$page-header-text-color: #cdcdcd;
2323

2424
// Sizes
25+
$border-width: 1px !default;
2526
$horizontal-page-margin: 30px !default;
2627
$sidebar-width: 270px !default;
2728
$cell-padding: 5px 10px 3px 10px !default;
2829
$cell-horizontal-padding: 12px !default;
2930
$section-padding: 15px !default;
3031
$text-input-horizontal-padding: 10px !default;
32+
$text-input-total-padding: $text-input-horizontal-padding * 2 + $border-width * 2;
3133

3234
$blank-slate-border: 1px dashed #DADADA

docs/5-forms.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ ActiveAdmin.register Post do
6363
a.input :title
6464
end
6565
end
66+
f.inputs do
67+
f.has_many :taggings, sortable: :position do |t|
68+
t.input :tag
69+
end
70+
end
6671
f.inputs do
6772
f.has_many :comment, new_record: 'Leave Comment' do |b|
6873
b.input :body
@@ -83,6 +88,9 @@ The `:heading` option adds a custom heading. You can hide it entirely by passing
8388
The `:new_record` option controls the visibility of the new record button (shown by default).
8489
If you pass a string, it will be used as the text for the new record button.
8590

91+
The `:sortable` option adds a hidden field and will enable drag & drop sorting of the children. It
92+
expects the name of the column that will store the index of each child.
93+
8694
## DatePicker
8795

8896
ActiveAdmin offers the `datepicker` input, which uses the [jQueryUI Datepicker](http://jqueryui.com/datepicker/).

lib/active_admin/form_builder.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def commit_action_with_cancel_link
5050

5151
def has_many(assoc, options = {}, &block)
5252
# remove options that should not render as attributes
53-
custom_settings = :new_record, :allow_destroy, :heading
53+
custom_settings = :new_record, :allow_destroy, :heading, :sortable
5454
builder_options = {new_record: true}.merge! options.slice *custom_settings
5555
options = {for: assoc }.merge! options.except *custom_settings
5656
options[:class] = [options[:class], "inputs has_many_fields"].compact.join(' ')
@@ -68,9 +68,25 @@ def has_many(assoc, options = {}, &block)
6868
has_many_form.input :_destroy, as: :boolean, wrapper_html: {class: 'has_many_delete'},
6969
label: I18n.t('active_admin.has_many_delete')
7070
end
71+
72+
if builder_options[:sortable]
73+
has_many_form.input builder_options[:sortable], as: :hidden
74+
75+
contents << template.content_tag(:li, class: 'handle') do
76+
Iconic.icon :move_vertical
77+
end
78+
end
79+
7180
contents
7281
end
7382

83+
# make sure that the sortable children sorted in stable ascending order
84+
if column = builder_options[:sortable]
85+
children = object.send(assoc)
86+
children = children.reorder("#{column}, id")
87+
options[:for] = [assoc, children]
88+
end
89+
7490
html = without_wrapper do
7591
unless builder_options.key?(:heading) && !builder_options[:heading]
7692
form_buffers.last << template.content_tag(:h3) do
@@ -84,9 +100,9 @@ def has_many(assoc, options = {}, &block)
84100
end
85101

86102
form_buffers.last << if @already_in_an_inputs_block
87-
template.content_tag :li, html, class: "has_many_container #{assoc}"
103+
template.content_tag :li, html, class: "has_many_container #{assoc}", 'data-sortable' => builder_options[:sortable]
88104
else
89-
template.content_tag :div, html, class: "has_many_container #{assoc}"
105+
template.content_tag :div, html, class: "has_many_container #{assoc}", 'data-sortable' => builder_options[:sortable]
90106
end
91107
end
92108

0 commit comments

Comments
 (0)