polymorphic_inlines.js
338 lines
| 15.2 KiB
| application/javascript
|
JavascriptLexer
r338 | /*global DateTimeShortcuts, SelectFilter*/ | |||
// This is a slightly adapted version of Django's inlines.js | ||||
// Forked for polymorphic by Diederik van der Boor | ||||
/** | ||||
* Django admin inlines | ||||
* | ||||
* Based on jQuery Formset 1.1 | ||||
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) | ||||
* @requires jQuery 1.2.6 or later | ||||
* | ||||
* Copyright (c) 2009, Stanislaus Madueke | ||||
* All rights reserved. | ||||
* | ||||
* Spiced up with Code from Zain Memon's GSoC project 2009 | ||||
* and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. | ||||
* | ||||
* Licensed under the New BSD License | ||||
* See: http://www.opensource.org/licenses/bsd-license.php | ||||
*/ | ||||
(function($) { | ||||
'use strict'; | ||||
$.fn.polymorphicFormset = function(opts) { | ||||
var options = $.extend({}, $.fn.polymorphicFormset.defaults, opts); | ||||
var $this = $(this); | ||||
var $parent = $this.parent(); | ||||
var updateElementIndex = function(el, prefix, ndx) { | ||||
var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); | ||||
var replacement = prefix + "-" + ndx; | ||||
if ($(el).prop("for")) { | ||||
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); | ||||
} | ||||
if (el.id) { | ||||
el.id = el.id.replace(id_regex, replacement); | ||||
} | ||||
if (el.name) { | ||||
el.name = el.name.replace(id_regex, replacement); | ||||
} | ||||
}; | ||||
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); | ||||
var nextIndex = parseInt(totalForms.val(), 10); | ||||
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); | ||||
// only show the add button if we are allowed to add more items, | ||||
// note that max_num = None translates to a blank string. | ||||
var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0; | ||||
$this.each(function(i) { | ||||
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass); | ||||
}); | ||||
if ($this.length && showAddButton) { | ||||
var addContainer; | ||||
var menuButton; | ||||
var addButtons; | ||||
if(options.childTypes == null) { | ||||
throw Error("The polymorphic fieldset options.childTypes is not defined!"); | ||||
} | ||||
// For Polymorphic inlines, the add button opens a menu. | ||||
var menu = '<div class="polymorphic-type-menu" style="display: none;"><ul>'; | ||||
for (var i = 0; i < options.childTypes.length; i++) { | ||||
var obj = options.childTypes[i]; | ||||
menu += '<li><a href="#" data-type="' + obj.type + '">' + obj.name + '</a></li>'; | ||||
} | ||||
menu += '</ul></div>'; | ||||
if ($this.prop("tagName") === "TR") { | ||||
// If forms are laid out as table rows, insert the | ||||
// "add" button in a new table row: | ||||
var numCols = this.eq(-1).children().length; | ||||
$parent.append('<tr class="' + options.addCssClass + ' polymorphic-add-choice"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a>" + menu + "</tr>"); | ||||
addContainer = $parent.find("tr:last > td"); | ||||
menuButton = addContainer.children('a'); | ||||
addButtons = addContainer.find("li a"); | ||||
} else { | ||||
// Otherwise, insert it immediately after the last form: | ||||
$this.filter(":last").after('<div class="' + options.addCssClass + ' polymorphic-add-choice"><a href="#">' + options.addText + "</a>" + menu + "</div>"); | ||||
addContainer = $this.filter(":last").next(); | ||||
menuButton = addContainer.children('a'); | ||||
addButtons = addContainer.find("li a"); | ||||
} | ||||
menuButton.click(function(event) { | ||||
event.preventDefault(); | ||||
event.stopPropagation(); // for menu hide | ||||
var $menu = $(event.target).next('.polymorphic-type-menu'); | ||||
if(! $menu.is(':visible')) { | ||||
var hideMenu = function() { | ||||
$menu.slideUp(50); | ||||
$(document).unbind('click', hideMenu); | ||||
}; | ||||
$(document).click(hideMenu); | ||||
} | ||||
$menu.slideToggle(50); | ||||
}); | ||||
addButtons.click(function(event) { | ||||
event.preventDefault(); | ||||
var polymorphicType = $(event.target).attr('data-type'); // Select polymorphic type. | ||||
var template = $("#" + polymorphicType + "-empty"); | ||||
var row = template.clone(true); | ||||
row.removeClass(options.emptyCssClass) | ||||
.addClass(options.formCssClass) | ||||
.attr("id", options.prefix + "-" + nextIndex); | ||||
if (row.is("tr")) { | ||||
// If the forms are laid out in table rows, insert | ||||
// the remove button into the last table cell: | ||||
row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>"); | ||||
} else if (row.is("ul") || row.is("ol")) { | ||||
// If they're laid out as an ordered/unordered list, | ||||
// insert an <li> after the last list item: | ||||
row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>"); | ||||
} else { | ||||
// Otherwise, just insert the remove button as the | ||||
// last child element of the form's container: | ||||
row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>"); | ||||
} | ||||
row.find("*").each(function() { | ||||
updateElementIndex(this, options.prefix, totalForms.val()); | ||||
}); | ||||
// Insert the new form when it has been fully edited | ||||
row.insertBefore($(template)); | ||||
// Update number of total forms | ||||
$(totalForms).val(parseInt(totalForms.val(), 10) + 1); | ||||
nextIndex += 1; | ||||
// Hide add button in case we've hit the max, except we want to add infinitely | ||||
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) { | ||||
addContainer.hide(); | ||||
} | ||||
// The delete button of each row triggers a bunch of other things | ||||
row.find("a." + options.deleteCssClass).click(function(e1) { | ||||
e1.preventDefault(); | ||||
// Remove the parent form containing this button: | ||||
row.remove(); | ||||
nextIndex -= 1; | ||||
// If a post-delete callback was provided, call it with the deleted form: | ||||
if (options.removed) { | ||||
options.removed(row); | ||||
} | ||||
$(document).trigger('formset:removed', [row, options.prefix]); | ||||
// Update the TOTAL_FORMS form count. | ||||
var forms = $("." + options.formCssClass); | ||||
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); | ||||
// Show add button again once we drop below max | ||||
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) { | ||||
addContainer.show(); | ||||
} | ||||
// Also, update names and ids for all remaining form controls | ||||
// so they remain in sequence: | ||||
var i, formCount; | ||||
var updateElementCallback = function() { | ||||
updateElementIndex(this, options.prefix, i); | ||||
}; | ||||
for (i = 0, formCount = forms.length; i < formCount; i++) { | ||||
updateElementIndex($(forms).get(i), options.prefix, i); | ||||
$(forms.get(i)).find("*").each(updateElementCallback); | ||||
} | ||||
}); | ||||
// If a post-add callback was supplied, call it with the added form: | ||||
if (options.added) { | ||||
options.added(row); | ||||
} | ||||
$(document).trigger('formset:added', [row, options.prefix]); | ||||
}); | ||||
} | ||||
return this; | ||||
}; | ||||
/* Setup plugin defaults */ | ||||
$.fn.polymorphicFormset.defaults = { | ||||
prefix: "form", // The form prefix for your django formset | ||||
addText: "add another", // Text for the add link | ||||
childTypes: null, // defined by the client. | ||||
deleteText: "remove", // Text for the delete link | ||||
addCssClass: "add-row", // CSS class applied to the add link | ||||
deleteCssClass: "delete-row", // CSS class applied to the delete link | ||||
emptyCssClass: "empty-row", // CSS class applied to the empty row | ||||
formCssClass: "dynamic-form", // CSS class applied to each form in a formset | ||||
added: null, // Function called each time a new form is added | ||||
removed: null, // Function called each time a form is deleted | ||||
addButton: null // Existing add button to use | ||||
}; | ||||
// Tabular inlines --------------------------------------------------------- | ||||
$.fn.tabularPolymorphicFormset = function(options) { | ||||
var $rows = $(this); | ||||
var alternatingRows = function(row) { | ||||
$($rows.selector).not(".add-row").removeClass("row1 row2") | ||||
.filter(":even").addClass("row1").end() | ||||
.filter(":odd").addClass("row2"); | ||||
}; | ||||
var reinitDateTimeShortCuts = function() { | ||||
// Reinitialize the calendar and clock widgets by force | ||||
if (typeof DateTimeShortcuts !== "undefined") { | ||||
$(".datetimeshortcuts").remove(); | ||||
DateTimeShortcuts.init(); | ||||
} | ||||
}; | ||||
var updateSelectFilter = function() { | ||||
// If any SelectFilter widgets are a part of the new form, | ||||
// instantiate a new SelectFilter instance for it. | ||||
if (typeof SelectFilter !== 'undefined') { | ||||
$('.selectfilter').each(function(index, value) { | ||||
var namearr = value.name.split('-'); | ||||
SelectFilter.init(value.id, namearr[namearr.length - 1], false); | ||||
}); | ||||
$('.selectfilterstacked').each(function(index, value) { | ||||
var namearr = value.name.split('-'); | ||||
SelectFilter.init(value.id, namearr[namearr.length - 1], true); | ||||
}); | ||||
} | ||||
}; | ||||
var initPrepopulatedFields = function(row) { | ||||
row.find('.prepopulated_field').each(function() { | ||||
var field = $(this), | ||||
input = field.find('input, select, textarea'), | ||||
dependency_list = input.data('dependency_list') || [], | ||||
dependencies = []; | ||||
$.each(dependency_list, function(i, field_name) { | ||||
dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); | ||||
}); | ||||
if (dependencies.length) { | ||||
input.prepopulate(dependencies, input.attr('maxlength')); | ||||
} | ||||
}); | ||||
}; | ||||
$rows.polymorphicFormset({ | ||||
prefix: options.prefix, | ||||
addText: options.addText, | ||||
childTypes: options.childTypes, | ||||
formCssClass: "dynamic-" + options.prefix, | ||||
deleteCssClass: "inline-deletelink", | ||||
deleteText: options.deleteText, | ||||
emptyCssClass: "empty-form", | ||||
removed: alternatingRows, | ||||
added: function(row) { | ||||
initPrepopulatedFields(row); | ||||
reinitDateTimeShortCuts(); | ||||
updateSelectFilter(); | ||||
alternatingRows(row); | ||||
}, | ||||
addButton: options.addButton | ||||
}); | ||||
return $rows; | ||||
}; | ||||
// Stacked inlines --------------------------------------------------------- | ||||
$.fn.stackedPolymorphicFormset = function(options) { | ||||
var $rows = $(this); | ||||
var updateInlineLabel = function(row) { | ||||
$($rows.selector).find(".inline_label").each(function(i) { | ||||
var count = i + 1; | ||||
$(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); | ||||
}); | ||||
}; | ||||
var reinitDateTimeShortCuts = function() { | ||||
// Reinitialize the calendar and clock widgets by force, yuck. | ||||
if (typeof DateTimeShortcuts !== "undefined") { | ||||
$(".datetimeshortcuts").remove(); | ||||
DateTimeShortcuts.init(); | ||||
} | ||||
}; | ||||
var updateSelectFilter = function() { | ||||
// If any SelectFilter widgets were added, instantiate a new instance. | ||||
if (typeof SelectFilter !== "undefined") { | ||||
$(".selectfilter").each(function(index, value) { | ||||
var namearr = value.name.split('-'); | ||||
SelectFilter.init(value.id, namearr[namearr.length - 1], false); | ||||
}); | ||||
$(".selectfilterstacked").each(function(index, value) { | ||||
var namearr = value.name.split('-'); | ||||
SelectFilter.init(value.id, namearr[namearr.length - 1], true); | ||||
}); | ||||
} | ||||
}; | ||||
var initPrepopulatedFields = function(row) { | ||||
row.find('.prepopulated_field').each(function() { | ||||
var field = $(this), | ||||
input = field.find('input, select, textarea'), | ||||
dependency_list = input.data('dependency_list') || [], | ||||
dependencies = []; | ||||
$.each(dependency_list, function(i, field_name) { | ||||
dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id')); | ||||
}); | ||||
if (dependencies.length) { | ||||
input.prepopulate(dependencies, input.attr('maxlength')); | ||||
} | ||||
}); | ||||
}; | ||||
$rows.polymorphicFormset({ | ||||
prefix: options.prefix, | ||||
addText: options.addText, | ||||
childTypes: options.childTypes, | ||||
formCssClass: "dynamic-" + options.prefix, | ||||
deleteCssClass: "inline-deletelink", | ||||
deleteText: options.deleteText, | ||||
emptyCssClass: "empty-form", | ||||
removed: updateInlineLabel, | ||||
added: function(row) { | ||||
initPrepopulatedFields(row); | ||||
reinitDateTimeShortCuts(); | ||||
updateSelectFilter(); | ||||
updateInlineLabel(row); | ||||
}, | ||||
addButton: options.addButton | ||||
}); | ||||
return $rows; | ||||
}; | ||||
$(document).ready(function() { | ||||
$(".js-inline-polymorphic-admin-formset").each(function() { | ||||
var data = $(this).data(), | ||||
inlineOptions = data.inlineFormset; | ||||
switch(data.inlineType) { | ||||
case "stacked": | ||||
$(inlineOptions.name + "-group .inline-related").stackedPolymorphicFormset(inlineOptions.options); | ||||
break; | ||||
case "tabular": | ||||
$(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularPolymorphicFormset(inlineOptions.options); | ||||
break; | ||||
} | ||||
}); | ||||
}); | ||||
})(django.jQuery); | ||||