18 | | $.fn.formset = function(opts) { |
19 | | var options = $.extend({}, $.fn.formset.defaults, opts); |
20 | | var updateElementIndex = function(el, prefix, ndx) { |
21 | | var id_regex = new RegExp("(" + prefix + "-\\d+)"); |
22 | | var replacement = prefix + "-" + ndx; |
23 | | if ($(el).attr("for")) { |
24 | | $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); |
25 | | } |
26 | | if (el.id) { |
27 | | el.id = el.id.replace(id_regex, replacement); |
28 | | } |
29 | | if (el.name) { |
30 | | el.name = el.name.replace(id_regex, replacement); |
31 | | } |
32 | | }; |
33 | | var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); |
34 | | var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); |
35 | | // only show the add button if we are allowed to add more items |
36 | | var showAddButton = ((maxForms.val() == 0) || ((maxForms.val()-totalForms.val()) > 0)); |
37 | | $(this).each(function(i) { |
38 | | $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); |
39 | | }); |
40 | | if ($(this).length && showAddButton) { |
41 | | var addButton; |
42 | | if ($(this).attr("tagName") == "TR") { |
43 | | // If forms are laid out as table rows, insert the |
44 | | // "add" button in a new table row: |
45 | | var numCols = this.eq(0).children().length; |
46 | | $(this).parent().append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>"); |
47 | | addButton = $(this).parent().find("tr:last a"); |
48 | | } else { |
49 | | // Otherwise, insert it immediately after the last form: |
50 | | $(this).filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>"); |
51 | | addButton = $(this).filter(":last").next().find("a"); |
52 | | } |
53 | | addButton.click(function() { |
54 | | var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); |
55 | | var nextIndex = parseInt(totalForms.val()) + 1; |
56 | | var template = $("#" + options.prefix + "-empty"); |
57 | | var row = template.clone(true).get(0); |
58 | | $(row).removeClass(options.emptyCssClass).removeAttr("id").insertBefore($(template)); |
59 | | $(row).html($(row).html().replace(/__prefix__/g, nextIndex)); |
60 | | $(row).addClass(options.formCssClass).attr("id", options.prefix + nextIndex); |
61 | | if ($(row).is("TR")) { |
62 | | // If the forms are laid out in table rows, insert |
63 | | // the remove button into the last table cell: |
64 | | $(row).children(":last").append('<div><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></div>"); |
65 | | } else if ($(row).is("UL") || $(row).is("OL")) { |
66 | | // If they're laid out as an ordered/unordered list, |
67 | | // insert an <li> after the last list item: |
68 | | $(row).append('<li><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></li>"); |
69 | | } else { |
70 | | // Otherwise, just insert the remove button as the |
71 | | // last child element of the form's container: |
72 | | $(row).children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>"); |
73 | | } |
74 | | $(row).find("input,select,textarea,label").each(function() { |
75 | | updateElementIndex(this, options.prefix, totalForms.val()); |
76 | | }); |
77 | | // Update number of total forms |
78 | | $(totalForms).val(nextIndex); |
79 | | // Hide add button in case we've hit the max, except we want to add infinitely |
80 | | if ((maxForms.val() != 0) && (maxForms.val() <= totalForms.val())) { |
81 | | addButton.parent().hide(); |
82 | | } |
83 | | // The delete button of each row triggers a bunch of other things |
84 | | $(row).find("a." + options.deleteCssClass).click(function() { |
85 | | // Remove the parent form containing this button: |
86 | | var row = $(this).parents("." + options.formCssClass); |
87 | | row.remove(); |
88 | | // If a post-delete callback was provided, call it with the deleted form: |
89 | | if (options.removed) { |
90 | | options.removed(row); |
91 | | } |
92 | | // Update the TOTAL_FORMS form count. |
93 | | var forms = $("." + options.formCssClass); |
94 | | $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); |
95 | | // Show add button again once we drop below max |
96 | | if ((maxForms.val() == 0) || (maxForms.val() >= forms.length)) { |
97 | | addButton.parent().show(); |
98 | | } |
99 | | // Also, update names and ids for all remaining form controls |
100 | | // so they remain in sequence: |
101 | | for (var i=0, formCount=forms.length; i<formCount; i++) |
102 | | { |
103 | | $(forms.get(i)).find("input,select,textarea,label").each(function() { |
104 | | updateElementIndex(this, options.prefix, i); |
105 | | }); |
106 | | } |
107 | | return false; |
108 | | }); |
109 | | // If a post-add callback was supplied, call it with the added form: |
110 | | if (options.added) { |
111 | | options.added($(row)); |
112 | | } |
113 | | return false; |
114 | | }); |
115 | | } |
116 | | return this; |
117 | | } |
118 | | /* Setup plugin defaults */ |
119 | | $.fn.formset.defaults = { |
120 | | prefix: "form", // The form prefix for your django formset |
121 | | addText: "add another", // Text for the add link |
122 | | deleteText: "remove", // Text for the delete link |
123 | | addCssClass: "add-row", // CSS class applied to the add link |
124 | | deleteCssClass: "delete-row", // CSS class applied to the delete link |
125 | | emptyCssClass: "empty-row", // CSS class applied to the empty row |
126 | | formCssClass: "dynamic-form", // CSS class applied to each form in a formset |
127 | | added: null, // Function called each time a new form is added |
128 | | removed: null // Function called each time a form is deleted |
129 | | } |
| 18 | $.fn.formset = function(opts) { |
| 19 | var options = $.extend({}, $.fn.formset.defaults, opts); |
| 20 | var updateElementIndex = function(el, prefix, ndx) { |
| 21 | var id_regex = new RegExp("(" + prefix + "-\\d+)"); |
| 22 | var replacement = prefix + "-" + ndx; |
| 23 | if ($(el).attr("for")) { |
| 24 | $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); |
| 25 | } |
| 26 | if (el.id) { |
| 27 | el.id = el.id.replace(id_regex, replacement); |
| 28 | } |
| 29 | if (el.name) { |
| 30 | el.name = el.name.replace(id_regex, replacement); |
| 31 | } |
| 32 | }; |
| 33 | var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); |
| 34 | var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); |
| 35 | // only show the add button if we are allowed to add more items |
| 36 | var showAddButton = ((maxForms.val() == 0) || ((maxForms.val()-totalForms.val()) > 0)); |
| 37 | $(this).each(function(i) { |
| 38 | $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); |
| 39 | }); |
| 40 | if ($(this).length && showAddButton) { |
| 41 | var addButton; |
| 42 | if ($(this).attr("tagName") == "TR") { |
| 43 | // If forms are laid out as table rows, insert the |
| 44 | // "add" button in a new table row: |
| 45 | var numCols = this.eq(0).children().length; |
| 46 | $(this).parent().append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>"); |
| 47 | addButton = $(this).parent().find("tr:last a"); |
| 48 | } else { |
| 49 | // Otherwise, insert it immediately after the last form: |
| 50 | $(this).filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>"); |
| 51 | addButton = $(this).filter(":last").next().find("a"); |
| 52 | } |
| 53 | addButton.click(function() { |
| 54 | var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); |
| 55 | var nextIndex = parseInt(totalForms.val()) + 1; |
| 56 | var template = $("#" + options.prefix + "-empty"); |
| 57 | var row = template.clone(true).get(0); |
| 58 | $(row).removeClass(options.emptyCssClass).removeAttr("id").insertBefore($(template)); |
| 59 | $(row).html($(row).html().replace(/__prefix__/g, nextIndex)); |
| 60 | $(row).addClass(options.formCssClass).attr("id", options.prefix + nextIndex); |
| 61 | if ($(row).is("TR")) { |
| 62 | // If the forms are laid out in table rows, insert |
| 63 | // the remove button into the last table cell: |
| 64 | $(row).children(":last").append('<div><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></div>"); |
| 65 | } else if ($(row).is("UL") || $(row).is("OL")) { |
| 66 | // If they're laid out as an ordered/unordered list, |
| 67 | // insert an <li> after the last list item: |
| 68 | $(row).append('<li><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></li>"); |
| 69 | } else { |
| 70 | // Otherwise, just insert the remove button as the |
| 71 | // last child element of the form's container: |
| 72 | $(row).children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>"); |
| 73 | } |
| 74 | $(row).find("input,select,textarea,label,a").each(function() { |
| 75 | updateElementIndex(this, options.prefix, totalForms.val()); |
| 76 | }); |
| 77 | // Update number of total forms |
| 78 | $(totalForms).val(nextIndex); |
| 79 | // Hide add button in case we've hit the max, except we want to add infinitely |
| 80 | if ((maxForms.val() != 0) && (maxForms.val() <= totalForms.val())) { |
| 81 | addButton.parent().hide(); |
| 82 | } |
| 83 | // The delete button of each row triggers a bunch of other things |
| 84 | $(row).find("a." + options.deleteCssClass).click(function() { |
| 85 | // Remove the parent form containing this button: |
| 86 | var row = $(this).parents("." + options.formCssClass); |
| 87 | row.remove(); |
| 88 | // If a post-delete callback was provided, call it with the deleted form: |
| 89 | if (options.removed) { |
| 90 | options.removed(row); |
| 91 | } |
| 92 | // Update the TOTAL_FORMS form count. |
| 93 | var forms = $("." + options.formCssClass); |
| 94 | $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); |
| 95 | // Show add button again once we drop below max |
| 96 | if ((maxForms.val() == 0) || (maxForms.val() >= forms.length)) { |
| 97 | addButton.parent().show(); |
| 98 | } |
| 99 | // Also, update names and ids for all remaining form controls |
| 100 | // so they remain in sequence: |
| 101 | for (var i=0, formCount=forms.length; i<formCount; i++) |
| 102 | { |
| 103 | $(forms.get(i)).find("input,select,textarea,label,a").each(function() { |
| 104 | updateElementIndex(this, options.prefix, i); |
| 105 | }); |
| 106 | } |
| 107 | return false; |
| 108 | }); |
| 109 | // If a post-add callback was supplied, call it with the added form: |
| 110 | if (options.added) { |
| 111 | options.added($(row)); |
| 112 | } |
| 113 | return false; |
| 114 | }); |
| 115 | } |
| 116 | return this; |
| 117 | } |
| 118 | /* Setup plugin defaults */ |
| 119 | $.fn.formset.defaults = { |
| 120 | prefix: "form", // The form prefix for your django formset |
| 121 | addText: "add another", // Text for the add link |
| 122 | deleteText: "remove", // Text for the delete link |
| 123 | addCssClass: "add-row", // CSS class applied to the add link |
| 124 | deleteCssClass: "delete-row", // CSS class applied to the delete link |
| 125 | emptyCssClass: "empty-row", // CSS class applied to the empty row |
| 126 | formCssClass: "dynamic-form", // CSS class applied to each form in a formset |
| 127 | added: null, // Function called each time a new form is added |
| 128 | removed: null // Function called each time a form is deleted |
| 129 | } |