Update polymorphic inline script.

Now based on standard Django script.
This makes upgrading it to newer Django versions much easier.
Changes for polymorphic are included in this commit
This commit is contained in:
Diederik van der Boor
2016-08-10 11:45:48 +02:00
parent 249fc0088b
commit 60db4f63b9
5 changed files with 75 additions and 477 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ class PolymorphicInlineModelAdmin(InlineModelAdmin):
#: This can be redefined for subclasses.
polymorphic_media = Media(
js=(
'polymorphic/js/jquery.django-inlines.js',
'polymorphic/js/polymorphic_inlines.js',
),
css={
'all': (
@@ -1,12 +1,12 @@
.add-row-choice {
.polymorphic-add-choice {
position: relative;
}
.add-row-choice a:focus {
.polymorphic-add-choice a:focus {
text-decoration: none;
}
.add-row .inline-type-choice {
.polymorphic-type-menu {
position: absolute;
top: 2.2em;
left: 0.5em;
@@ -16,12 +16,12 @@
background-color: #fff;
}
.add-row .inline-type-choice ul {
.polymorphic-type-menu ul {
padding: 2px;
margin: 0;
}
.add-row .inline-type-choice li {
.polymorphic-type-menu li {
list-style: none inside none;
padding: 4px 8px;
}
@@ -1,440 +0,0 @@
/**
* jQuery plugin for Django inlines
*
* (c) 2011-2016 Diederik van der Boor, Apache 2 Licensed.
*/
(function($){
function DjangoInline(group, options) {
options = $.extend({}, $.fn.djangoInline.defaults, options);
this.group = group;
this.$group = $(group);
this.options = options;
options.prefix = options.prefix || this.$group.attr('id').replace(/-group$/, '');
if( options.formTemplate ) {
this.$form_template = $(options.formTemplate);
} else {
this.$form_template = this.$group.find(this.options.emptyFormSelector); // the extra item to construct new instances.
}
// Create the add button if requested (null/undefined means auto select)
if(options.showAddButton !== false) {
var dominfo = this._getManagementForm();
if (dominfo.max_forms == null || dominfo.max_forms.value === '' || (dominfo.max_forms.value - dominfo.total_forms.value) > 0) {
this.createAddButton();
}
}
}
DjangoInline.prototype = {
/**
* Create the add button
*/
createAddButton: function() {
var $addButton;
var myself = this;
if (this.options.childTypes) {
// Polymorphic inlines!
// The add button opens a menu.
var menu = '<div class="inline-type-choice" style="display: none;"><ul>';
for (var i = 0; i < this.options.childTypes.length; i++) {
var obj = this.options.childTypes[i];
menu += '<li><a href="#" data-type="' + obj.type + '">' + obj.name + '</a></li>';
}
menu += '</ul></div>';
$addButton = $('<div class="' + this.options.addCssClass + ' add-row-choice"><a href="#">' + this.options.addText + "</a>" + menu + "</div>");
this.$group.append($addButton);
$addButton.children('a').click($.proxy(this._onMenuToggle, this));
$addButton.find('li a').click(function(event){ myself._onMenuItemClick(event); });
}
else {
// Normal inlines
$addButton = $('<div class="' + this.options.addCssClass + '"><a href="#">' + this.options.addText + "</a></div>");
this.$group.append($addButton);
$addButton.find('a').click(function(event) { event.preventDefault(); myself.addForm() });
}
},
_onMenuToggle: function(event) {
event.preventDefault();
event.stopPropagation();
var $menu = $(event.target).next('.inline-type-choice');
if(! $menu.is(':visible')) {
function hideMenu() {
$menu.slideUp();
$(document).unbind('click', hideMenu);
}
$(document).click(hideMenu);
}
$menu.slideToggle();
},
_onMenuItemClick: function(event) {
event.preventDefault();
var type = $(event.target).attr('data-type');
var empty_form_selector = this.options.emptyFormSelector + "[data-inline-type=" + type + "]";
this.addForm(empty_form_selector);
},
/**
* The main action, add a new row.
* Allow to select a different form template (for polymorphic inlines)
*/
addForm: function(emptyFormSelector) {
var $form_template;
if(emptyFormSelector) {
$form_template = this.$group.find(emptyFormSelector);
if($form_template.length === 0) {
throw new Error("Form template '" + emptyFormSelector + "' not found")
}
}
else {
if(! this.$form_template || this.$form_template.length === 0) {
throw new Error("No empty form available. Define the 'form_template' setting or add an '.empty-form' element in the '" + this.options.prefix + "' formset group!");
}
$form_template = this.$form_template;
}
// The Django admin/media/js/inlines.js API is not public, or easy to use.
// Recoded the inline model dynamics.
var management_form = this._getManagementForm();
if(! management_form.total_forms) {
throw new Error("Missing '#" + this._getGroupFieldIdPrefix() + "-TOTAL_FORMS' field. Make sure the management form included!");
}
// When a inline is presented in a complex table,
// the newFormTarget can be very useful to direct the output.
var container;
if(this.options.newFormTarget == null) {
container = $form_template.parent();
}
else if($.isFunction(this.options.newFormTarget)) {
container = this.options.newFormTarget.apply(this.group);
}
else {
container = this.$group.find(this.options.newFormTarget);
}
if(container === null || container.length === 0) {
throw new Error("No container found via custom 'newFormTarget' function!");
}
// Clone the item.
var new_index = management_form.total_forms.value;
var item_id = this._getFormId(new_index);
var newhtml = _getOuterHtml($form_template).replace(/__prefix__/g, new_index);
var newitem = $(newhtml).removeClass("empty-form").attr("id", item_id);
// Add it
container.append(newitem);
var formset_item = $("#" + item_id);
if( formset_item.length === 0 ) {
throw new Error("New FormSet item not found: #" + item_id);
}
formset_item.data('djangoInlineIndex', new_index);
if(this.options.onAdd) {
this.options.onAdd.call(this.group, formset_item, new_index, this.options);
}
// Update administration
management_form.total_forms.value++;
return formset_item;
},
getFormAt: function(index) {
return $('#' + this._getFormId(index));
},
_getFormId: function(index) {
// The form container is expected by the numbered as #prefix-NR
return this.options.itemIdTemplate.replace('{prefix}', this.options.prefix).replace('{index}', index);
},
_getGroupFieldIdPrefix: function() {
// typically: #id_modelname
return this.options.autoId.replace('{prefix}', this.options.prefix);
},
/**
* Get the management form data.
*/
_getManagementForm: function() {
var group_id_prefix = this._getGroupFieldIdPrefix();
return {
// management form item
total_forms: $("#" + group_id_prefix + "-TOTAL_FORMS")[0],
max_forms: $("#" + group_id_prefix + "-MAX_NUM_FORMS")[0],
group_id_prefix: group_id_prefix
}
},
_getItemData: function(child_node) {
var formset_item = $(child_node).closest(this.options.itemsSelector);
if( formset_item.length === 0 ) {
return null;
}
// Split the ID, using the id_template pattern.
// note that ^...$ is important, as a '-' char can occur multiple times with generic inlines (inlinetype-id / app-model-ctfield-ctfkfield-id)
var id = formset_item.attr("id");
var cap = (new RegExp('^' + this.options.itemIdTemplate.replace('{prefix}', '(.+?)').replace('{index}', '(\\d+)') + '$')).exec(id);
return {
formset_item: formset_item,
prefix: cap[1],
index: parseInt(cap[2], 0) // or parseInt(formset_item.data('djangoInlineIndex'))
};
},
/**
* Get the meta-data of a single form.
*/
_getItemForm: function(child_node) {
var dominfo = this._getItemData(child_node);
if( dominfo === null ) {
return null;
}
var field_id_prefix = this._getGroupFieldIdPrefix() + "-" + dominfo.index;
return $.extend({}, dominfo, {
// Export settings data
field_id_prefix: field_id_prefix,
field_name_prefix: dominfo.prefix + '-' + dominfo.index,
// Item fields
pk_field: $('#' + field_id_prefix + '-' + this.options.pkFieldName),
delete_checkbox: $("#" + field_id_prefix + "-DELETE")
});
},
/**
* Remove a row
*/
removeForm: function(child_node)
{
// Get dom info
var management_form = this._getManagementForm();
var itemform = this._getItemForm(child_node);
if( itemform === null ) {
throw new Error("No form found for the selector '" + child_node.selector + "'!");
}
var total_count = parseInt(management_form.total_forms.value, 0);
var has_pk_field = itemform.pk_field.length != 0;
if(this.options.onBeforeRemove) {
this.options.onBeforeRemove.call(this.group, itemform.formset_item, this.options);
}
// In case there is a delete checkbox, save it.
if( itemform.delete_checkbox.length )
{
if(has_pk_field)
itemform.pk_field.insertAfter(management_form.total_forms);
itemform.delete_checkbox.attr('checked', true).insertAfter(management_form.total_forms).hide();
}
else if( has_pk_field && itemform.pk_field[0].value )
{
// Construct a delete checkbox on the fly.
itemform.pk_field.insertAfter(management_form.total_forms);
$('<input type="hidden" id="' + itemform.field_id_prefix + '-DELETE" name="' + itemform.field_name_prefix + '-DELETE" value="on">').insertAfter(itemform.total_forms);
}
else
{
// Newly added item, renumber in reverse order
for( var i = itemform.index + 1; i < total_count; i++ )
{
this._renumberItem(this.getFormAt(i), i - 1);
}
management_form.total_forms.value--;
}
// And remove item
itemform.formset_item.remove();
if(this.options.onRemove) {
this.options.onRemove.call(this.group, itemform.formset_item, this.options);
}
return itemform.formset_item;
},
// Based on django/contrib/admin/media/js/inlines.js
_renumberItem: function($formset_item, new_index)
{
var id_regex = new RegExp("(" + this._getFormId('(\\d+|__prefix__)') + ")");
var replacement = this._getFormId(new_index);
$formset_item.data('djangoInlineIndex', new_index);
// Loop through the nodes.
// Getting them all at once turns out to be more efficient, then looping per level.
var nodes = $formset_item.add( $formset_item.find("*") );
for( var i = 0; i < nodes.length; i++ )
{
var node = nodes[i];
var $node = $(node);
var for_attr = $node.attr('for');
if( for_attr && for_attr.match(id_regex) ) {
$node.attr("for", for_attr.replace(id_regex, replacement));
}
if( node.id && node.id.match(id_regex) ) {
node.id = node.id.replace(id_regex, replacement);
}
if( node.name && node.name.match(id_regex) ) {
node.name = node.name.replace(id_regex, replacement);
}
}
},
// Extra query methods for external callers:
getFormIndex: function(child_node) {
var dominfo = this._getItemData(child_node);
return dominfo ? dominfo.index : null;
},
getForms: function() {
// typically: .inline-related:not(.empty-form)
return this.$group.children(this.options.itemsSelector + ":not(" + this.options.emptyFormSelector + ")");
},
getEmptyForm: function() {
// typically: #modelname-group > .empty-form
return this.$form_template;
},
getFieldIdPrefix: function(item_index) {
if(! $.isNumeric(item_index)) {
var dominfo = this._getItemData(item_index);
if(dominfo === null) {
throw new Error("Unexpected element in getFieldIdPrefix, needs to be item_index, or DOM child node.");
}
item_index = dominfo.index;
}
// typically: #id_modelname-NN
return this._getGroupFieldIdPrefix() + "-" + item_index;
},
getFieldsAt: function(index) {
var $form = this.getFormAt(index);
return this.getFields($form);
},
getFields: function(child_node) {
// Return all fields in a simple lookup object, with the prefix stripped.
var dominfo = this._getItemData(child_node);
if(dominfo === null) {
return null;
}
var fields = {};
var $inputs = dominfo.formset_item.find(':input');
var name_prefix = this.prefix + "-" + dominfo.index;
for(var i = 0; i < $inputs.length; i++) {
var name = $inputs[i].name;
if(name.substring(0, name_prefix.length) == name_prefix) {
var suffix = name.substring(name_prefix.length + 1); // prefix-<name>
fields[suffix] = $inputs[i];
}
}
return fields;
},
removeFormAt: function(index) {
return this.removeForm(this.getFormAt(index));
}
};
function _getOuterHtml($node)
{
if( $node.length )
{
if( $node[0].outerHTML ) {
return $node[0].outerHTML;
} else {
return $("<div>").append($node.clone()).html();
}
}
return null;
}
// jQuery plugin definition
// Separated from the main code, as demonstrated by Twitter bootstrap.
$.fn.djangoInline = function(option) {
var args = Array.prototype.splice.call(arguments, 1);
var call_method = (typeof option == 'string');
var plugin_result = (call_method ? undefined : this);
this.filter('.inline-group').each(function() {
var $this = $(this);
var data = $this.data('djangoInline');
if (! data) {
var options = typeof option == 'object' ? option : {};
$this.data('djangoInline', (data = new DjangoInline(this, options)));
}
if (typeof option == 'string') {
plugin_result = data[option].apply(data, args);
}
});
return plugin_result;
};
$.fn.djangoInline.defaults = {
pkFieldName: 'id', // can be `tablename_ptr` for inherited models.
autoId: 'id_{prefix}', // the auto id format used in Django.
prefix: null, // typically the model name in lower case.
newFormTarget: null, // Define where the row should be added; a CSS selector or function.
itemIdTemplate: '{prefix}-{index}', // Format of the ID attribute.
itemsSelector: '.inline-related', // CSS class that each item has
emptyFormSelector: '.empty-form', // CSS class that
formTemplate: null, // Complete HTML of the new form
childTypes: null, // Extra for django-polymorphic, allow a choice between empty-forms.
showAddButton: true,
addText: "add another", // Text for the add link
deleteText: "remove", // Text for the delete link
addCssClass: "add-row" // CSS class applied to the add link
};
// Also expose inner object
$.fn.djangoInline.Constructor = DjangoInline;
// Auto enable inlines
$.fn.ready(function(){
$('.js-jquery-django-inlines').each(function(){
var $this = $(this);
var data = $this.data();
var inlineOptions = data.inlineFormset;
$this.djangoInline(inlineOptions.options)
});
})
})(window.django ? window.django.jQuery : jQuery);
@@ -1,4 +1,8 @@
/*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
*
@@ -17,8 +21,8 @@
*/
(function($) {
'use strict';
$.fn.formset = function(opts) {
var options = $.extend({}, $.fn.formset.defaults, opts);
$.fn.polymorphicFormset = function(opts) {
var options = $.extend({}, $.fn.polymorphicFormset.defaults, opts);
var $this = $(this);
var $parent = $this.parent();
var updateElementIndex = function(el, prefix, ndx) {
@@ -44,23 +48,55 @@
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
});
if ($this.length && showAddButton) {
var addButton = options.addButton;
if (addButton === null) {
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 + '"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a></tr>");
addButton = $parent.find("tr:last a");
} else {
// Otherwise, insert it immediately after the last form:
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="#">' + options.addText + "</a></div>");
addButton = $this.filter(":last").next().find("a");
}
var addContainer;
var menuButton;
var addButtons;
// 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>';
}
addButton.click(function(e) {
e.preventDefault();
var template = $("#" + options.prefix + "-empty");
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)
@@ -88,7 +124,7 @@
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) {
addButton.parent().hide();
addButtons.parent().hide();
}
// The delete button of each row triggers a bunch of other things
row.find("a." + options.deleteCssClass).click(function(e1) {
@@ -106,7 +142,7 @@
$("#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) {
addButton.parent().show();
addButtons.parent().show();
}
// Also, update names and ids for all remaining form controls
// so they remain in sequence:
@@ -130,9 +166,10 @@
};
/* Setup plugin defaults */
$.fn.formset.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
@@ -145,7 +182,7 @@
// Tabular inlines ---------------------------------------------------------
$.fn.tabularFormset = function(options) {
$.fn.tabularPolymorphicFormset = function(options) {
var $rows = $(this);
var alternatingRows = function(row) {
$($rows.selector).not(".add-row").removeClass("row1 row2")
@@ -191,9 +228,10 @@
});
};
$rows.formset({
$rows.polymorphicFormset({
prefix: options.prefix,
addText: options.addText,
childTypes: options.childTypes,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
@@ -212,7 +250,7 @@
};
// Stacked inlines ---------------------------------------------------------
$.fn.stackedFormset = function(options) {
$.fn.stackedPolymorphicFormset = function(options) {
var $rows = $(this);
var updateInlineLabel = function(row) {
$($rows.selector).find(".inline_label").each(function(i) {
@@ -258,9 +296,10 @@
});
};
$rows.formset({
$rows.polymorphicFormset({
prefix: options.prefix,
addText: options.addText,
childTypes: options.childTypes,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
@@ -279,15 +318,15 @@
};
$(document).ready(function() {
$(".js-inline-admin-formset").each(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").stackedFormset(inlineOptions.options);
$(inlineOptions.name + "-group .inline-related").stackedPolymorphicFormset(inlineOptions.options);
break;
case "tabular":
$(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularFormset(inlineOptions.options);
$(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularPolymorphicFormset(inlineOptions.options);
break;
}
});
@@ -1,6 +1,6 @@
{% load i18n admin_urls static %}
<div class="js-jquery-django-inlines inline-group"
<div class="js-inline-polymorphic-admin-formset inline-group"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
@@ -12,8 +12,7 @@
{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related inline-{{ inline_admin_form.model_admin.opts.model_name }}{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if not inline_admin_form.original.pk %} empty-form {% endif %}{% if forloop.last %} last-related{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{% if inline_admin_form.original.pk %}{{ forloop.counter0 }}{% else %}empty{% endif %}"
data-inline-type="{{ inline_admin_form.model_admin.opts.model_name }}">
id="{% if inline_admin_form.original.pk %}{{ inline_admin_formset.formset.prefix }}-{{ forloop.counter0 }}{% else %}{{ inline_admin_form.model_admin.opts.model_name }}-empty{% endif %}">
<h3><b>{{ inline_admin_form.model_admin.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>