- Author:
- Aneon
- Posted:
- May 5, 2009
- Language:
- JavaScript
- Version:
- Not specified
- Score:
- 9 (after 9 ratings)
This jQuery javascript enables dynamic add/delete of rows in tabular inlines. It adds a "+" icon at the bottom of the inline to allow addition of new rows, and replaces the default delete checkbox with a "x" icon for deletion, giving you the possibility to add/delete rows instantly without reloading the page.
In addition, it gives you drag-n-drop ordering functionality with a named position model field using jQuery UI Sortable.
Usage (see below for example):
Just include the javascript on your admin page, together with jQuery, and it'll automatically affect all tabular inlines. Optionally, also include jQuery UI Sortable and an Integer field in your inline model named "position" (or whatever you set "position_field" to), which will automatically hide the position field and enable drag-n-drop sorting.
Developed for:
- jQuery 1.3.2
- jQuery UI 1.7.1
- Django trunk (tested in Django v1.0.2)
- (Might work with other versions with or without adjustments, but not tested)
Settings (in top of javascript):
- "position_field" is the name of an integer model field that is used for ordering the inline model. If left empty or not found, the drag-n-drop functionality is dropped. Defaults to "position".
- "add_link_html" for custom look of "add"-buttons. Defaults to Django's built-in "+" image icon.
- "delete_link_html" for custom look of "delete"-buttons. Defaults to Django's built-in "x" image icon.
Use example:
admin.py:
class NameInline(admin.TabularInline):
model = Name
extra = 1
class PersonAdmin(admin.ModelAdmin):
inlines = [NameInline]
class Media:
js = ['js/jquery-1.3.2.min.js', 'js/ui/ui.core.js',
'js/ui/ui.sortable.js', 'js/dynamic_inlines_with_sort.js',]
css = { 'all' : ['css/dynamic_inlines_with_sort.css'], }
admin.site.register(Person, PersonAdmin)
models.py:
class Person(models.Model):
year_born = models.PositiveIntegerField(_('year born'), null=True, blank=True)
class Name(models.Model):
profile = models.ForeignKey(Profile, verbose_name=_('profile'))
position = models.PositiveIntegerField(_('position'), default=0)
name = models.CharField(_('name'), max_length=100)
class Meta:
ordering = ('position',)
dynamic_inlines_with_sort.css:
/* To make row height of saved items same as others */
.inline-group .tabular tr.has_original td { padding-top:0.5em; }
.inline-group .tabular tr.has_original td.original p { display:none; }
Please post bugs in comments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | /* dynamic_inlines_with_sort.js */
/* Created in May 2009 by Hannes Rydén */
/* Use, distribute and modify freely */
// "Add"-link html code. Defaults to Django's "+" image icon, but could use text instead.
add_link_html = '<img src="/media/admin/img/admin/icon_addlink.gif" ' +
'width="10" height="10" alt="Add new row" style="margin:0.5em 1em;" />';
// "Delete"-link html code. Defaults to Django's "x" image icon, but could use text instead.
delete_link_html = '<img src="/media/admin/img/admin/icon_deletelink.gif" ' +
'width="10" height="10" alt="Delete row" style="margin-top:0.5em" />';
position_field = 'position'; // Name of inline model field (integer) used for ordering. Defaults to "position".
jQuery(function($) {
// This script is applied to all TABULAR inlines
$('div.inline-group div.tabular').each(function() {
table = $(this).find('table');
// Hide initial extra row and prepare it to be used as a template for new rows
add_template = table.find('tr:last');
add_template.addClass('add_template').hide();
table.prepend(add_template);
// Hide initial deleted rows
table.find('td.delete input:checkbox:checked').parent('td').parent('tr').addClass('deleted_row').hide();
// "Add"-button in bottom of inline for adding new rows
$(this).find('fieldset').after('<a class="add" href="#">' + add_link_html + '</a>');
$(this).find('a.add').click(function(){
old_item = $(this).parent().find('table tr.add_template')
new_item = old_item.clone(true);
create_delete_button(new_item.find('td.delete'));
new_item.removeClass('add_template').show();
$(this).parent().find('table').append(new_item);
update_positions($(this).parent().find('table'), true);
// Place for special code to re-enable javascript widgets after clone (e.g. an ajax-autocomplete field)
// Fictive example: new_item.find('.autocomplete').each(function() { $(this).triggerHandler('autocomplete'); });
}).removeAttr('href').css('cursor', 'pointer');
// "Delete"-buttons for each row that replaces the default checkbox
table.find('tr:not(.add_template) td.delete').each(function() {
create_delete_button($(this));
});
// Drag and drop functionality - only used if a position field exists
if (position_field != '' && table.find('td').is('.' + position_field))
{
// Hide "position"-field (both td:s and th:s)
$(this).find('td.' + position_field).hide();
td_pos_field_index = table.find('tbody tr td').index($(this).find('td.' + position_field));
$(this).find('th:eq(' + (td_pos_field_index-1) + ')').hide();
// Hide "original"-field and set any colspan to 1 (why show in the first case?)
$(this).find('td.original').hide();
$(this).find('th[colspan]').removeAttr('colspan');
// Make table sortable using jQuery UI Sortable
table.sortable({
items: 'tr:has(td)',
tolerance: 'pointer',
axis: 'y',
cancel: 'input,button,select,a',
helper: 'clone',
update: function() {
update_positions($(this));
}
});
// Re-order <tr>:s based on the "position"-field values.
// This is a very simple ordering which only works with correct position number sequences,
// which the rest of this script (hopefully) guarantees.
rows = [];
table.find('tbody tr').each(function() {
position = $(this).find('td.' + position_field + ' input').val();
rows[position] = $(this);
// Add move cursor to table row.
// Also remove row coloring, as it confuses when using drag-and-drop for ordering
table.find('tr:has(td)').css('cursor', 'move').removeClass('row1').removeClass('row2');
});
for (var i in rows) { table.append(rows[i]); } // Move <tr> to its correct position
update_positions($(this), true);
}
else
position_field = '';
});
});
// Function for creating fancy delete buttons
function create_delete_button(td)
{
// Replace checkbox with image
td.find('input:checkbox').hide();
td.append('<a class="delete" href="#">' + delete_link_html + '</a>');
td.find('a.delete').click(function(){
current_row = $(this).parent('td').parent('tr');
table = current_row.parent().parent();
if (current_row.is('.has_original')) // This row has already been saved once, so we must keep checkbox
{
$(this).prev('input').attr('checked', true);
current_row.addClass('deleted_row').hide();
}
else // This row has never been saved so we can just remove the element completely
{
current_row.remove();
}
update_positions(table, true);
}).removeAttr('href').css('cursor', 'pointer');
}
// Updates "position"-field values based on row order in table
function update_positions(table, update_ids)
{
even = true
num_rows = 0
position = 0;
// Set correct position: Filter through all trs, excluding first th tr and last hidden template tr
table.find('tbody tr:not(.add_template):not(.deleted_row)').each(function() {
if (position_field != '')
{
// Update position field
$(this).find('td.' + position_field + ' input').val(position + 1);
position++;
}
else
{
// Update row coloring
$(this).removeClass('row1 row2');
if (even)
{
$(this).addClass('row1');
even = false;
}
else
{
$(this).addClass('row2');
even = true;
}
}
});
table.find('tbody tr.has_original').each(function() {
num_rows++;
});
table.find('tbody tr:not(.has_original):not(.add_template)').each(function() {
if (update_ids) update_id_fields($(this), num_rows);
num_rows++;
});
table.find('tbody tr.add_template').each(function() {
if (update_ids) update_id_fields($(this), num_rows)
num_rows++;
});
table.parent().parent('div.tabular').find("input[id$='TOTAL_FORMS']").val(num_rows);
}
// Updates actual id and name attributes of inputs, selects and so on.
// Required for Django validation to keep row order.
function update_id_fields(row, new_position)
{
// Fix IDs, names etc.
// <select ...>
row.find('select').each(function() {
// id=...
old_id = $(this).attr('id').toString();
new_id = old_id.replace(/([^ ]+\-)[0-9]+(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('id', new_id)
// name=...
old_id = $(this).attr('name').toString();
new_id = old_id.replace(/([^ ]+\-)[0-9]+(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('name', new_id)
});
// <input ...>
row.find('input').each(function() {
// id=...
old_id = $(this).attr('id').toString();
new_id = old_id.replace(/([^ ]+\-)[0-9]+(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('id', new_id)
// name=...
old_id = $(this).attr('name').toString();
new_id = old_id.replace(/([^ ]+\-)[0-9]+(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('name', new_id)
});
// <a ...>
row.find('a').each(function() {
// id=...
old_id = $(this).attr('id').toString();
new_id = old_id.replace(/([^ ]+\-)[0-9]+(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('id', new_id)
});
// Are there other element types...? Add here.
}
|
More like this
- Django Collapsed Stacked Inlines by applecat 1 year, 9 months ago
- Django Collapsed Stacked Inlines by mkarajohn 3 years, 10 months ago
- Dynamically adding forms to a formset. OOP version. by halfnibble 9 years, 6 months ago
- Convert multiple select for m2m to multiple checkboxes in django admin form by abidibo 11 years, 7 months ago
- Django admin inline ordering - javascript only implementation by ojhilt 12 years ago
Comments
Work perfectly for me with django 1.0.2,jquery 1.3.2 and without jquery ui. Thank you very much.
#
That's good to hear. Thanks for the report!
#
I tested drag-n-drop sorting in Django v1.0.2 as well and it seems to work fine.
#
I tried the sorting system. The correct order is always saved in the db, but to override the default ordering in the admin (that is alphabetically on the ImageField in my case), I added this subclass to my «inline» model:
#
The javascript should order the table rows at page load based on the position field, regardless of which order they're presented in (on the admin page that is). But I haven't really tested that functionality so you're probably best of by adding class Meta: ordering = ('position',) to your inline model, or it might not work as expected. I've added this to the description example.
#
it's don't work with date widget. Same help? Thanks
#
doesn't clone calendar widgets properly :(
#
Drag and drop doesn't work for inlines with textareas. Here is a patch to correct the problem:
#
One more minor bug I found.
'option' needs to be added to the cancel list. Without it trying to scroll select boxes using arrow keys doesn't work correctly.
#
Really cool man, Thanks! One thing, i'd avoid using the <img> tag in the link_html and use css to add the icons instead.
#
works fine except for textareas, which are not "selectable", fix: table.sortable({ items: 'tr:has(td)', tolerance: 'pointer', axis: 'y', cancel: 'input,button,select,a,textarea', helper: 'clone', update: function() { update_positions($(this)); } });
#
Have all these patches been taken into account in the code? Is the developer planning on addressing the issues raised (e.g. date selectors not working)?
#
Hi. This is a very cool snippet. Only problem I have is I cant copy the javascript functions associated with a field.I am enabling the code right under the explanation "Place for special code to re-enable javascript widgets after clone" new_item.find('.autocomplete').each(function() { $(this).triggerHandler('autocomplete'); });
I am using the autocomplete function here
This link
I really need this autocomplete for cloned instances. Is there a way to fix this?
Thanks
#
Please login first before commenting.