diff --git a/polymorphic/admin/inlines.py b/polymorphic/admin/inlines.py index 779625f..9fd6218 100644 --- a/polymorphic/admin/inlines.py +++ b/polymorphic/admin/inlines.py @@ -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': ( diff --git a/polymorphic/static/polymorphic/css/polymorphic_inlines.css b/polymorphic/static/polymorphic/css/polymorphic_inlines.css index daae436..eb6d86c 100644 --- a/polymorphic/static/polymorphic/css/polymorphic_inlines.css +++ b/polymorphic/static/polymorphic/css/polymorphic_inlines.css @@ -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; } diff --git a/polymorphic/static/polymorphic/js/jquery.django-inlines.js b/polymorphic/static/polymorphic/js/jquery.django-inlines.js deleted file mode 100644 index 6da2419..0000000 --- a/polymorphic/static/polymorphic/js/jquery.django-inlines.js +++ /dev/null @@ -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 = ''; - $addButton = $('
' + this.options.addText + "" + menu + "
"); - 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 = $('
' + this.options.addText + "
"); - 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); - $('').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- - 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 $("
").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); diff --git a/polymorphic/static/polymorphic/js/polymorphic_inlines.js b/polymorphic/static/polymorphic/js/polymorphic_inlines.js index 4e9bb77..b321993 100644 --- a/polymorphic/static/polymorphic/js/polymorphic_inlines.js +++ b/polymorphic/static/polymorphic/js/polymorphic_inlines.js @@ -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('' + options.addText + ""); - addButton = $parent.find("tr:last a"); - } else { - // Otherwise, insert it immediately after the last form: - $this.filter(":last").after('"); - addButton = $this.filter(":last").next().find("a"); - } + var addContainer; + var menuButton; + var addButtons; + + // For Polymorphic inlines, the add button opens a menu. + var menu = ''; + + 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('' + options.addText + "" + menu + ""); + 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('"); + 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; } }); diff --git a/polymorphic/templates/admin/polymorphic/edit_inline/stacked.html b/polymorphic/templates/admin/polymorphic/edit_inline/stacked.html index 592981e..8dc4d3b 100644 --- a/polymorphic/templates/admin/polymorphic/edit_inline/stacked.html +++ b/polymorphic/templates/admin/polymorphic/edit_inline/stacked.html @@ -1,6 +1,6 @@ {% load i18n admin_urls static %} -
@@ -12,8 +12,7 @@ {% for inline_admin_form in inline_admin_formset %}