FossilRepo

fossilrepo / assets / admin / js / inlines.89b3c627c5dc.js
Blame History Raw 360 lines
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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button