FossilRepo

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

Keyboard Shortcuts

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