|
1
|
/*global DateTimeShortcuts, SelectFilter*/ |
|
2
|
/** |
|
3
|
* Django admin inlines |
|
4
|
* |
|
5
|
* Based on jQuery Formset 1.1 |
|
6
|
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) |
|
7
|
* @requires jQuery 1.2.6 or later |
|
8
|
* |
|
9
|
* Copyright (c) 2009, Stanislaus Madueke |
|
10
|
* All rights reserved. |
|
11
|
* |
|
12
|
* Spiced up with Code from Zain Memon's GSoC project 2009 |
|
13
|
* and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. |
|
14
|
* |
|
15
|
* Licensed under the New BSD License |
|
16
|
* See: https://opensource.org/licenses/bsd-license.php |
|
17
|
*/ |
|
18
|
'use strict'; |
|
19
|
{ |
|
20
|
const $ = django.jQuery; |
|
21
|
$.fn.formset = function(opts) { |
|
22
|
const options = $.extend({}, $.fn.formset.defaults, opts); |
|
23
|
const $this = $(this); |
|
24
|
const $parent = $this.parent(); |
|
25
|
const updateElementIndex = function(el, prefix, ndx) { |
|
26
|
const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); |
|
27
|
const replacement = prefix + "-" + ndx; |
|
28
|
if ($(el).prop("for")) { |
|
29
|
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); |
|
30
|
} |
|
31
|
if (el.id) { |
|
32
|
el.id = el.id.replace(id_regex, replacement); |
|
33
|
} |
|
34
|
if (el.name) { |
|
35
|
el.name = el.name.replace(id_regex, replacement); |
|
36
|
} |
|
37
|
}; |
|
38
|
const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); |
|
39
|
let nextIndex = parseInt(totalForms.val(), 10); |
|
40
|
const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); |
|
41
|
const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off"); |
|
42
|
let addButton; |
|
43
|
|
|
44
|
/** |
|
45
|
* The "Add another MyModel" button below the inline forms. |
|
46
|
*/ |
|
47
|
const addInlineAddButton = function() { |
|
48
|
if (addButton === null) { |
|
49
|
if ($this.prop("tagName") === "TR") { |
|
50
|
// If forms are laid out as table rows, insert the |
|
51
|
// "add" button in a new table row: |
|
52
|
const numCols = $this.eq(-1).children().length; |
|
53
|
$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></tr>"); |
|
54
|
addButton = $parent.find("tr:last a"); |
|
55
|
} else { |
|
56
|
// Otherwise, insert it immediately after the last form: |
|
57
|
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></div>"); |
|
58
|
addButton = $this.filter(":last").next().find("a"); |
|
59
|
} |
|
60
|
} |
|
61
|
addButton.on('click', addInlineClickHandler); |
|
62
|
}; |
|
63
|
|
|
64
|
const addInlineClickHandler = function(e) { |
|
65
|
e.preventDefault(); |
|
66
|
const template = $("#" + options.prefix + "-empty"); |
|
67
|
const row = template.clone(true); |
|
68
|
row.removeClass(options.emptyCssClass) |
|
69
|
.addClass(options.formCssClass) |
|
70
|
.attr("id", options.prefix + "-" + nextIndex); |
|
71
|
addInlineDeleteButton(row); |
|
72
|
row.find("*").each(function() { |
|
73
|
updateElementIndex(this, options.prefix, totalForms.val()); |
|
74
|
}); |
|
75
|
// Insert the new form when it has been fully edited. |
|
76
|
row.insertBefore($(template)); |
|
77
|
// Update number of total forms. |
|
78
|
$(totalForms).val(parseInt(totalForms.val(), 10) + 1); |
|
79
|
nextIndex += 1; |
|
80
|
// Hide the add button if there's a limit and it's been reached. |
|
81
|
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) { |
|
82
|
addButton.parent().hide(); |
|
83
|
} |
|
84
|
// Show the remove buttons if there are more than min_num. |
|
85
|
toggleDeleteButtonVisibility(row.closest('.inline-group')); |
|
86
|
|
|
87
|
// Pass the new form to the post-add callback, if provided. |
|
88
|
if (options.added) { |
|
89
|
options.added(row); |
|
90
|
} |
|
91
|
row.get(0).dispatchEvent(new CustomEvent("formset:added", { |
|
92
|
bubbles: true, |
|
93
|
detail: { |
|
94
|
formsetName: options.prefix |
|
95
|
} |
|
96
|
})); |
|
97
|
}; |
|
98
|
|
|
99
|
/** |
|
100
|
* The "X" button that is part of every unsaved inline. |
|
101
|
* (When saved, it is replaced with a "Delete" checkbox.) |
|
102
|
*/ |
|
103
|
const addInlineDeleteButton = function(row) { |
|
104
|
if (row.is("tr")) { |
|
105
|
// If the forms are laid out in table rows, insert |
|
106
|
// the remove button into the last table cell: |
|
107
|
row.children(":last").append('<div><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>"); |
|
108
|
} else if (row.is("ul") || row.is("ol")) { |
|
109
|
// If they're laid out as an ordered/unordered list, |
|
110
|
// insert an <li> after the last list item: |
|
111
|
row.append('<li><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>"); |
|
112
|
} else { |
|
113
|
// Otherwise, just insert the remove button as the |
|
114
|
// last child element of the form's container: |
|
115
|
row.children(":first").append('<span><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>"); |
|
116
|
} |
|
117
|
// Add delete handler for each row. |
|
118
|
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this)); |
|
119
|
}; |
|
120
|
|
|
121
|
const inlineDeleteHandler = function(e1) { |
|
122
|
e1.preventDefault(); |
|
123
|
const deleteButton = $(e1.target); |
|
124
|
const row = deleteButton.closest('.' + options.formCssClass); |
|
125
|
const inlineGroup = row.closest('.inline-group'); |
|
126
|
// Remove the parent form containing this button, |
|
127
|
// and also remove the relevant row with non-field errors: |
|
128
|
const prevRow = row.prev(); |
|
129
|
if (prevRow.length && prevRow.hasClass('row-form-errors')) { |
|
130
|
prevRow.remove(); |
|
131
|
} |
|
132
|
row.remove(); |
|
133
|
nextIndex -= 1; |
|
134
|
// Pass the deleted form to the post-delete callback, if provided. |
|
135
|
if (options.removed) { |
|
136
|
options.removed(row); |
|
137
|
} |
|
138
|
document.dispatchEvent(new CustomEvent("formset:removed", { |
|
139
|
detail: { |
|
140
|
formsetName: options.prefix |
|
141
|
} |
|
142
|
})); |
|
143
|
// Update the TOTAL_FORMS form count. |
|
144
|
const forms = $("." + options.formCssClass); |
|
145
|
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); |
|
146
|
// Show add button again once below maximum number. |
|
147
|
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) { |
|
148
|
addButton.parent().show(); |
|
149
|
} |
|
150
|
// Hide the remove buttons if at min_num. |
|
151
|
toggleDeleteButtonVisibility(inlineGroup); |
|
152
|
// Also, update names and ids for all remaining form controls so |
|
153
|
// they remain in sequence: |
|
154
|
let i, formCount; |
|
155
|
const updateElementCallback = function() { |
|
156
|
updateElementIndex(this, options.prefix, i); |
|
157
|
}; |
|
158
|
for (i = 0, formCount = forms.length; i < formCount; i++) { |
|
159
|
updateElementIndex($(forms).get(i), options.prefix, i); |
|
160
|
$(forms.get(i)).find("*").each(updateElementCallback); |
|
161
|
} |
|
162
|
}; |
|
163
|
|
|
164
|
const toggleDeleteButtonVisibility = function(inlineGroup) { |
|
165
|
if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) { |
|
166
|
inlineGroup.find('.inline-deletelink').hide(); |
|
167
|
} else { |
|
168
|
inlineGroup.find('.inline-deletelink').show(); |
|
169
|
} |
|
170
|
}; |
|
171
|
|
|
172
|
$this.each(function(i) { |
|
173
|
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass); |
|
174
|
}); |
|
175
|
|
|
176
|
// Create the delete buttons for all unsaved inlines: |
|
177
|
$this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() { |
|
178
|
addInlineDeleteButton($(this)); |
|
179
|
}); |
|
180
|
toggleDeleteButtonVisibility($this); |
|
181
|
|
|
182
|
// Create the add button, initially hidden. |
|
183
|
addButton = options.addButton; |
|
184
|
addInlineAddButton(); |
|
185
|
|
|
186
|
// Show the add button if allowed to add more items. |
|
187
|
// Note that max_num = None translates to a blank string. |
|
188
|
const showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0; |
|
189
|
if ($this.length && showAddButton) { |
|
190
|
addButton.parent().show(); |
|
191
|
} else { |
|
192
|
addButton.parent().hide(); |
|
193
|
} |
|
194
|
|
|
195
|
return this; |
|
196
|
}; |
|
197
|
|
|
198
|
/* Setup plugin defaults */ |
|
199
|
$.fn.formset.defaults = { |
|
200
|
prefix: "form", // The form prefix for your django formset |
|
201
|
addText: "add another", // Text for the add link |
|
202
|
deleteText: "remove", // Text for the delete link |
|
203
|
addCssClass: "add-row", // CSS class applied to the add link |
|
204
|
deleteCssClass: "delete-row", // CSS class applied to the delete link |
|
205
|
emptyCssClass: "empty-row", // CSS class applied to the empty row |
|
206
|
formCssClass: "dynamic-form", // CSS class applied to each form in a formset |
|
207
|
added: null, // Function called each time a new form is added |
|
208
|
removed: null, // Function called each time a form is deleted |
|
209
|
addButton: null // Existing add button to use |
|
210
|
}; |
|
211
|
|
|
212
|
|
|
213
|
// Tabular inlines --------------------------------------------------------- |
|
214
|
$.fn.tabularFormset = function(selector, options) { |
|
215
|
const $rows = $(this); |
|
216
|
|
|
217
|
const reinitDateTimeShortCuts = function() { |
|
218
|
// Reinitialize the calendar and clock widgets by force |
|
219
|
if (typeof DateTimeShortcuts !== "undefined") { |
|
220
|
$(".datetimeshortcuts").remove(); |
|
221
|
DateTimeShortcuts.init(); |
|
222
|
} |
|
223
|
}; |
|
224
|
|
|
225
|
const updateSelectFilter = function() { |
|
226
|
// If any SelectFilter widgets are a part of the new form, |
|
227
|
// instantiate a new SelectFilter instance for it. |
|
228
|
if (typeof SelectFilter !== 'undefined') { |
|
229
|
$('.selectfilter').each(function(index, value) { |
|
230
|
SelectFilter.init(value.id, this.dataset.fieldName, false); |
|
231
|
}); |
|
232
|
$('.selectfilterstacked').each(function(index, value) { |
|
233
|
SelectFilter.init(value.id, this.dataset.fieldName, true); |
|
234
|
}); |
|
235
|
} |
|
236
|
}; |
|
237
|
|
|
238
|
const initPrepopulatedFields = function(row) { |
|
239
|
row.find('.prepopulated_field').each(function() { |
|
240
|
const field = $(this), |
|
241
|
input = field.find('input, select, textarea'), |
|
242
|
dependency_list = input.data('dependency_list') || [], |
|
243
|
dependencies = []; |
|
244
|
$.each(dependency_list, function(i, field_name) { |
|
245
|
dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); |
|
246
|
}); |
|
247
|
if (dependencies.length) { |
|
248
|
input.prepopulate(dependencies, input.attr('maxlength')); |
|
249
|
} |
|
250
|
}); |
|
251
|
}; |
|
252
|
|
|
253
|
$rows.formset({ |
|
254
|
prefix: options.prefix, |
|
255
|
addText: options.addText, |
|
256
|
formCssClass: "dynamic-" + options.prefix, |
|
257
|
deleteCssClass: "inline-deletelink", |
|
258
|
deleteText: options.deleteText, |
|
259
|
emptyCssClass: "empty-form", |
|
260
|
added: function(row) { |
|
261
|
initPrepopulatedFields(row); |
|
262
|
reinitDateTimeShortCuts(); |
|
263
|
updateSelectFilter(); |
|
264
|
}, |
|
265
|
addButton: options.addButton |
|
266
|
}); |
|
267
|
|
|
268
|
return $rows; |
|
269
|
}; |
|
270
|
|
|
271
|
// Stacked inlines --------------------------------------------------------- |
|
272
|
$.fn.stackedFormset = function(selector, options) { |
|
273
|
const $rows = $(this); |
|
274
|
const updateInlineLabel = function(row) { |
|
275
|
$(selector).find(".inline_label").each(function(i) { |
|
276
|
const count = i + 1; |
|
277
|
$(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); |
|
278
|
}); |
|
279
|
}; |
|
280
|
|
|
281
|
const reinitDateTimeShortCuts = function() { |
|
282
|
// Reinitialize the calendar and clock widgets by force, yuck. |
|
283
|
if (typeof DateTimeShortcuts !== "undefined") { |
|
284
|
$(".datetimeshortcuts").remove(); |
|
285
|
DateTimeShortcuts.init(); |
|
286
|
} |
|
287
|
}; |
|
288
|
|
|
289
|
const updateSelectFilter = function() { |
|
290
|
// If any SelectFilter widgets were added, instantiate a new instance. |
|
291
|
if (typeof SelectFilter !== "undefined") { |
|
292
|
$(".selectfilter").each(function(index, value) { |
|
293
|
SelectFilter.init(value.id, this.dataset.fieldName, false); |
|
294
|
}); |
|
295
|
$(".selectfilterstacked").each(function(index, value) { |
|
296
|
SelectFilter.init(value.id, this.dataset.fieldName, true); |
|
297
|
}); |
|
298
|
} |
|
299
|
}; |
|
300
|
|
|
301
|
const initPrepopulatedFields = function(row) { |
|
302
|
row.find('.prepopulated_field').each(function() { |
|
303
|
const field = $(this), |
|
304
|
input = field.find('input, select, textarea'), |
|
305
|
dependency_list = input.data('dependency_list') || [], |
|
306
|
dependencies = []; |
|
307
|
$.each(dependency_list, function(i, field_name) { |
|
308
|
// Dependency in a fieldset. |
|
309
|
let field_element = row.find('.form-row .field-' + field_name); |
|
310
|
// Dependency without a fieldset. |
|
311
|
if (!field_element.length) { |
|
312
|
field_element = row.find('.form-row.field-' + field_name); |
|
313
|
} |
|
314
|
dependencies.push('#' + field_element.find('input, select, textarea').attr('id')); |
|
315
|
}); |
|
316
|
if (dependencies.length) { |
|
317
|
input.prepopulate(dependencies, input.attr('maxlength')); |
|
318
|
} |
|
319
|
}); |
|
320
|
}; |
|
321
|
|
|
322
|
$rows.formset({ |
|
323
|
prefix: options.prefix, |
|
324
|
addText: options.addText, |
|
325
|
formCssClass: "dynamic-" + options.prefix, |
|
326
|
deleteCssClass: "inline-deletelink", |
|
327
|
deleteText: options.deleteText, |
|
328
|
emptyCssClass: "empty-form", |
|
329
|
removed: updateInlineLabel, |
|
330
|
added: function(row) { |
|
331
|
initPrepopulatedFields(row); |
|
332
|
reinitDateTimeShortCuts(); |
|
333
|
updateSelectFilter(); |
|
334
|
updateInlineLabel(row); |
|
335
|
}, |
|
336
|
addButton: options.addButton |
|
337
|
}); |
|
338
|
|
|
339
|
return $rows; |
|
340
|
}; |
|
341
|
|
|
342
|
$(document).ready(function() { |
|
343
|
$(".js-inline-admin-formset").each(function() { |
|
344
|
const data = $(this).data(), |
|
345
|
inlineOptions = data.inlineFormset; |
|
346
|
let selector; |
|
347
|
switch(data.inlineType) { |
|
348
|
case "stacked": |
|
349
|
selector = inlineOptions.name + "-group .inline-related"; |
|
350
|
$(selector).stackedFormset(selector, inlineOptions.options); |
|
351
|
break; |
|
352
|
case "tabular": |
|
353
|
selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row"; |
|
354
|
$(selector).tabularFormset(selector, inlineOptions.options); |
|
355
|
break; |
|
356
|
} |
|
357
|
}); |
|
358
|
}); |
|
359
|
} |
|
360
|
|