|
1 |
/**
|
|
2 |
* jQuery Formset 1.3-pre
|
|
3 |
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
|
|
4 |
* @requires jQuery 1.2.6 or later
|
|
5 |
*
|
|
6 |
* Copyright (c) 2009, Stanislaus Madueke
|
|
7 |
* All rights reserved.
|
|
8 |
*
|
|
9 |
* Licensed under the New BSD License
|
|
10 |
* See: http://www.opensource.org/licenses/bsd-license.php
|
|
11 |
*/
|
|
12 |
;(function($) {
|
|
13 |
$.fn.formset = function(opts)
|
|
14 |
{
|
|
15 |
var options = $.extend({}, $.fn.formset.defaults, opts),
|
|
16 |
flatExtraClasses = options.extraClasses.join(' '),
|
|
17 |
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
|
|
18 |
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
|
|
19 |
childElementSelector = 'input,select,textarea,label,div',
|
|
20 |
$$ = $(this),
|
|
21 |
|
|
22 |
applyExtraClasses = function(row, ndx) {
|
|
23 |
if (options.extraClasses) {
|
|
24 |
row.removeClass(flatExtraClasses);
|
|
25 |
row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
|
|
26 |
}
|
|
27 |
},
|
|
28 |
|
|
29 |
updateElementIndex = function(elem, prefix, ndx) {
|
|
30 |
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
|
|
31 |
replacement = prefix + '-' + ndx + '-';
|
|
32 |
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
|
|
33 |
if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
|
|
34 |
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
|
|
35 |
},
|
|
36 |
|
|
37 |
hasChildElements = function(row) {
|
|
38 |
return row.find(childElementSelector).length > 0;
|
|
39 |
},
|
|
40 |
|
|
41 |
showAddButton = function() {
|
|
42 |
return maxForms.length == 0 || // For Django versions pre 1.2
|
|
43 |
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0))
|
|
44 |
},
|
|
45 |
|
|
46 |
insertDeleteLink = function(row) {
|
|
47 |
if (row.is('TR')) {
|
|
48 |
// If the forms are laid out in table rows, insert
|
|
49 |
// the remove button into the last table cell:
|
|
50 |
row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>');
|
|
51 |
} else if (row.is('UL') || row.is('OL')) {
|
|
52 |
// If they're laid out as an ordered/unordered list,
|
|
53 |
// insert an <li> after the last list item:
|
|
54 |
row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>');
|
|
55 |
} else {
|
|
56 |
// Otherwise, just insert the remove button as the
|
|
57 |
// last child element of the form's container:
|
|
58 |
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>');
|
|
59 |
}
|
|
60 |
row.find('a.' + options.deleteCssClass).click(function() {
|
|
61 |
var row = $(this).parents('.' + options.formCssClass),
|
|
62 |
del = row.find('input:hidden[id $= "-DELETE"]'),
|
|
63 |
buttonRow = row.siblings("a." + options.addCssClass + ', .' + options.formCssClass + '-add'),
|
|
64 |
forms;
|
|
65 |
if (del.length) {
|
|
66 |
// We're dealing with an inline formset.
|
|
67 |
// Rather than remove this form from the DOM, we'll mark it as deleted
|
|
68 |
// and hide it, then let Django handle the deleting:
|
|
69 |
del.val('on');
|
|
70 |
row.hide();
|
|
71 |
forms = $('.' + options.formCssClass).not(':hidden');
|
|
72 |
} else {
|
|
73 |
row.remove();
|
|
74 |
// Update the TOTAL_FORMS count:
|
|
75 |
forms = $('.' + options.formCssClass).not('.formset-custom-template');
|
|
76 |
totalForms.val(forms.length);
|
|
77 |
}
|
|
78 |
for (var i=0, formCount=forms.length; i<formCount; i++) {
|
|
79 |
// Apply `extraClasses` to form rows so they're nicely alternating:
|
|
80 |
applyExtraClasses(forms.eq(i), i);
|
|
81 |
if (!del.length) {
|
|
82 |
// Also update names and IDs for all child controls (if this isn't
|
|
83 |
// a delete-able inline formset) so they remain in sequence:
|
|
84 |
forms.eq(i).find(childElementSelector).each(function() {
|
|
85 |
updateElementIndex($(this), options.prefix, i);
|
|
86 |
});
|
|
87 |
}
|
|
88 |
}
|
|
89 |
// Check if we need to show the add button:
|
|
90 |
if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show();
|
|
91 |
// If a post-delete callback was provided, call it with the deleted form:
|
|
92 |
if (options.removed) options.removed(row);
|
|
93 |
return false;
|
|
94 |
});
|
|
95 |
};
|
|
96 |
|
|
97 |
$$.each(function(i) {
|
|
98 |
var row = $(this),
|
|
99 |
del = row.find('input:checkbox[id $= "-DELETE"]');
|
|
100 |
if (del.length) {
|
|
101 |
// If you specify "can_delete = True" when creating an inline formset,
|
|
102 |
// Django adds a checkbox to each form in the formset.
|
|
103 |
// Replace the default checkbox with a hidden field:
|
|
104 |
if (del.is(':checked')) {
|
|
105 |
// If an inline formset containing deleted forms fails validation, make sure
|
|
106 |
// we keep the forms hidden (thanks for the bug report and suggested fix Mike)
|
|
107 |
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />');
|
|
108 |
row.hide();
|
|
109 |
} else {
|
|
110 |
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
|
|
111 |
}
|
|
112 |
// Hide any labels associated with the DELETE checkbox:
|
|
113 |
$('label[for="' + del.attr('id') + '"]').hide();
|
|
114 |
del.remove();
|
|
115 |
}
|
|
116 |
if (hasChildElements(row)) {
|
|
117 |
row.addClass(options.formCssClass);
|
|
118 |
if (row.is(':visible')) {
|
|
119 |
insertDeleteLink(row);
|
|
120 |
applyExtraClasses(row, i);
|
|
121 |
}
|
|
122 |
}
|
|
123 |
});
|
|
124 |
|
|
125 |
if ($$.length) {
|
|
126 |
var hideAddButton = !showAddButton(),
|
|
127 |
addButton, template;
|
|
128 |
if (options.formTemplate) {
|
|
129 |
// If a form template was specified, we'll clone it to generate new form instances:
|
|
130 |
template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
|
|
131 |
template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
|
|
132 |
template.find(childElementSelector).each(function() {
|
|
133 |
updateElementIndex($(this), options.prefix, '__prefix__');
|
|
134 |
});
|
|
135 |
insertDeleteLink(template);
|
|
136 |
} else {
|
|
137 |
// Otherwise, use the last form in the formset; this works much better if you've got
|
|
138 |
// extra (>= 1) forms (thnaks to justhamade for pointing this out):
|
|
139 |
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
|
|
140 |
template.find('input:hidden[id $= "-DELETE"]').remove();
|
|
141 |
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
|
|
142 |
template.find(childElementSelector).not(options.keepFieldValues).each(function() {
|
|
143 |
var elem = $(this);
|
|
144 |
// If this is a checkbox or radiobutton, uncheck it.
|
|
145 |
// This fixes Issue 1, reported by Wilson.Andrew.J:
|
|
146 |
if (elem.is('input:checkbox') || elem.is('input:radio')) {
|
|
147 |
elem.attr('checked', false);
|
|
148 |
} else {
|
|
149 |
elem.val('');
|
|
150 |
}
|
|
151 |
});
|
|
152 |
}
|
|
153 |
// FIXME: Perhaps using $.data would be a better idea?
|
|
154 |
options.formTemplate = template;
|
|
155 |
|
|
156 |
if ($$.attr('tagName') == 'TR') {
|
|
157 |
// If forms are laid out as table rows, insert the
|
|
158 |
// "add" button in a new table row:
|
|
159 |
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
|
|
160 |
buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>')
|
|
161 |
.addClass(options.formCssClass + '-add');
|
|
162 |
$$.parent().append(buttonRow);
|
|
163 |
if (hideAddButton) buttonRow.hide();
|
|
164 |
addButton = buttonRow.find('a');
|
|
165 |
} else {
|
|
166 |
// Otherwise, insert it immediately after the last form:
|
|
167 |
$$.filter(':last').after('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>');
|
|
168 |
addButton = $$.filter(':last').next();
|
|
169 |
if (hideAddButton) addButton.hide();
|
|
170 |
}
|
|
171 |
addButton.click(function() {
|
|
172 |
var formCount = parseInt(totalForms.val()),
|
|
173 |
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
|
|
174 |
buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this);
|
|
175 |
applyExtraClasses(row, formCount);
|
|
176 |
row.insertBefore(buttonRow).show();
|
|
177 |
row.find(childElementSelector).each(function() {
|
|
178 |
updateElementIndex($(this), options.prefix, formCount);
|
|
179 |
});
|
|
180 |
totalForms.val(formCount + 1);
|
|
181 |
// Check if we've exceeded the maximum allowed number of forms:
|
|
182 |
if (!showAddButton()) buttonRow.hide();
|
|
183 |
// If a post-add callback was supplied, call it with the added form:
|
|
184 |
if (options.added) options.added(row);
|
|
185 |
return false;
|
|
186 |
});
|
|
187 |
}
|
|
188 |
|
|
189 |
return $$;
|
|
190 |
}
|
|
191 |
|
|
192 |
/* Setup plugin defaults */
|
|
193 |
$.fn.formset.defaults = {
|
|
194 |
prefix: 'form', // The form prefix for your django formset
|
|
195 |
formTemplate: null, // The jQuery selection cloned to generate new form instances
|
|
196 |
addText: 'add another', // Text for the add link
|
|
197 |
deleteText: 'remove', // Text for the delete link
|
|
198 |
addCssClass: 'add-row', // CSS class applied to the add link
|
|
199 |
deleteCssClass: 'delete-row', // CSS class applied to the delete link
|
|
200 |
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
|
|
201 |
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
|
|
202 |
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
|
|
203 |
added: null, // Function called each time a new form is added
|
|
204 |
removed: null // Function called each time a form is deleted
|
|
205 |
};
|
|
206 |
})(jQuery)
|