/* 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 = 'Add new row'; // "Delete"-link html code. Defaults to Django's "x" image icon, but could use text instead. delete_link_html = 'Delete row'; 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('' + add_link_html + ''); $(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 :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 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('' + delete_link_html + ''); 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. // 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) }); // 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. }