All files for deployment ready and tested - further development in this repo.

This commit is contained in:
2025-09-22 10:42:52 +02:00
parent 57b738e9ce
commit 12c3181ad2
394 changed files with 89982 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

View File

@@ -0,0 +1,20 @@
import $ from "jquery";
/**
* Converts a grp.jQuery instance to a django.jQuery instance.
*/
function django$($sel) {
if (typeof window.grp === "undefined") {
return $($sel);
}
if (window.grp.jQuery.fn.init === $.fn.init) {
return $($sel);
}
const $djangoSel = $($sel);
if ($sel.prevObject) {
$djangoSel.prevObject = django$($sel.prevObject);
}
return $djangoSel;
}
export default django$;

View File

@@ -0,0 +1,23 @@
import $ from "jquery";
/**
* For grappelli 2.14, converts a django.jQuery instance to a grp.jQuery
* instance. Otherwise (if grappelli is not present, or for grappelli <= 2.13,
* where the grappelli jQuery instance is the same as django's), returns the
* object that was passed in, unchanged.
*/
function grp$($sel) {
if (typeof window.grp === "undefined") {
return $($sel);
}
if (window.grp.jQuery.fn.init === $.fn.init) {
return $($sel);
}
const $grpSel = window.grp.jQuery($sel);
if ($sel.prevObject) {
$grpSel.prevObject = grp$($sel.prevObject);
}
return $grpSel;
}
export default grp$;

View File

@@ -0,0 +1,54 @@
import $ from "jquery";
import * as grappelli from "grappelli";
import DJNesting from "./utils";
import DjangoFormset from "./jquery.djangoformset";
DJNesting.DjangoFormset = DjangoFormset;
$(document).ready(function () {
// Remove the border on any empty fieldsets
$("fieldset.grp-module, fieldset.module")
.filter(function (i, element) {
return element.childNodes.length == 0;
})
.css("border-width", "0");
// Set predelete class on any form elements with the DELETE input checked.
// These can occur on forms rendered after a validation error.
$('input[name$="-DELETE"]:checked')
.not('[name*="__prefix__"]')
.closest(".djn-inline-form")
.addClass("grp-predelete");
$(document).on(
"djnesting:initialized djnesting:mutate",
function onMutate(e, $inline) {
var $items = $inline.find(
"> .djn-items, > .tabular > .module > .djn-items"
);
var $rows = $items.children(".djn-tbody");
$rows.removeClass("row1 row2");
$rows.each(function (i, row) {
var n = 1 + (i % 2);
$(row).addClass("row" + n);
});
}
);
// Register the nested formset on top level djnesting-stacked elements.
// It will handle recursing down the nested inlines.
$(".djn-group-root").each(function (i, rootGroup) {
$(rootGroup).djangoFormset();
});
$("form").on("submit.djnesting", function (e) {
$(".djn-group").each(function () {
DJNesting.updatePositions($(this).djangoFormsetPrefix());
$(document).trigger("djnesting:mutate", [
$(this).djangoFormset().$inline,
]);
});
});
});
export default DJNesting;

View File

@@ -0,0 +1,712 @@
import $ from "jquery";
import regexQuote from "./regexquote";
import DJNesting from "./utils";
import * as grappelli from "grappelli";
import grp from "grp";
import grp$ from "./grp$";
import django$ from "./django$";
var pluginName = "djangoFormset";
class DjangoFormset {
constructor(inline) {
this.opts = {
emptyClass: "empty-form grp-empty-form djn-empty-form",
predeleteClass: "grp-predelete",
};
this.$inline = $(inline);
this.prefix = this.$inline.djangoFormsetPrefix();
this._$totalForms = this.$inline.find(
"#id_" + this.prefix + "-TOTAL_FORMS"
);
this._$totalForms.attr("autocomplete", "off");
this._$template = $("#" + this.prefix + "-empty");
var inlineModelClassName = this.$inline.djnData("inlineModel");
const nestingLevel = this.$inline.djnData("nestingLevel");
const handlerSelector = `.djn-model-${inlineModelClassName}.djn-level-${nestingLevel}`;
this.opts = $.extend({}, this.opts, {
childTypes: this.$inline.data("inlineFormset").options.childTypes,
formsetFkModel: this.$inline.djnData("formsetFkModel"),
addButtonSelector: ".djn-add-handler" + handlerSelector,
removeButtonSelector: ".djn-remove-handler" + handlerSelector,
deleteButtonSelector: ".djn-delete-handler" + handlerSelector,
formClass:
"dynamic-form grp-dynamic-form djn-dynamic-form-" +
inlineModelClassName,
formClassSelector: ".djn-dynamic-form-" + inlineModelClassName,
});
DJNesting.initRelatedFields(this.prefix, this.$inline.djnData());
DJNesting.initAutocompleteFields(this.prefix, this.$inline.djnData());
if (this.opts.childTypes) {
this._setupPolymorphic();
}
this._bindEvents();
this._initializeForms();
this.$inline
.find('.djn-items:not([id*="-empty"])')
.trigger("djnesting:init");
// initialize nested formsets
this.$inline
.find(
'.djn-group[id$="-group"][id^="' +
this.prefix +
'"][data-inline-formset]:not([id*="-empty"])'
)
.each(function () {
$(this)[pluginName]();
});
if (this.$inline.is(".djn-group-root")) {
DJNesting.createSortable(this.$inline);
}
$(document).trigger("djnesting:initialized", [this.$inline, this]);
}
_setupPolymorphic() {
if (!this.opts.childTypes) {
throw Error(
"The polymorphic fieldset options.childTypes is not defined!"
);
}
let menu = '<div class="polymorphic-type-menu" style="display: none"><ul>';
this.opts.childTypes.forEach((c) => {
menu += `<li><a href="#" data-type="${c.type}">${c.name}</a></li>`;
});
menu += "</ul></div>";
const $addButton = this.$inline.find(this.opts.addButtonSelector);
const $menu = $(menu);
$addButton.after($menu);
}
_initializeForms() {
var totalForms = this.mgmtVal("TOTAL_FORMS");
var maxForms = this.mgmtVal("MAX_NUM_FORMS");
if (maxForms <= totalForms) {
this.$inline
.find(this.opts.addButtonSelector)
.parents(".djn-add-item")
.hide();
}
for (var i = 0; i < totalForms; i++) {
this._initializeForm("#" + this.prefix + "-" + i);
}
}
_initializeForm(form) {
var $form = $(form);
var formPrefix = $form.djangoFormPrefix();
$form.addClass(this.opts.formClass);
if ($form.hasClass("has_original")) {
$("#id_" + formPrefix + "DELETE:checked").toggleClass(
this.opts.predeleteClass
);
}
var minForms = this.mgmtVal("MIN_NUM_FORMS");
var totalForms = this.mgmtVal("TOTAL_FORMS");
var self = this;
var hideRemoveButton = totalForms <= minForms;
this.$inline.djangoFormsetForms().each(function () {
var showHideMethod = hideRemoveButton ? "hide" : "show";
$(this).find(self.opts.removeButtonSelector)[showHideMethod]();
});
}
_bindEvents($el) {
var self = this;
if (typeof $el == "undefined") {
$el = this.$inline;
}
const $addButton = $el.find(this.opts.addButtonSelector);
$addButton.off("click.djnesting").on("click.djnesting", function (e) {
e.preventDefault();
e.stopPropagation();
const $menu = $(this).next(".polymorphic-type-menu");
if (!$menu.length) {
self.add();
} else {
if (!$menu.is(":visible")) {
function hideMenu() {
$menu.hide();
$(document).off("click", hideMenu);
}
$(document).on("click", hideMenu);
}
$menu.show();
}
});
const $menuButtons = $addButton.parent().find("> .polymorphic-type-menu a");
$menuButtons.off("click.djnesting").on("click.djnesting", function (e) {
e.preventDefault();
e.stopPropagation();
const polymorphicType = $(this).attr("data-type");
self.add(null, polymorphicType);
const $menu = $(e.target).closest(".polymorphic-type-menu");
if ($menu.is(":visible")) {
$menu.hide();
}
});
$el
.find(this.opts.removeButtonSelector)
.filter(function () {
return !$(this).closest(".djn-empty-form").length;
})
.off("click.djnesting")
.on("click.djnesting", function (e) {
e.preventDefault();
e.stopPropagation();
var $form = $(this).closest(self.opts.formClassSelector);
self.remove($form);
});
var deleteClickHandler = function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var $form = $(this).closest(self.opts.formClassSelector);
var $deleteInput = $("#id_" + $form.djangoFormPrefix() + "DELETE");
if (!$deleteInput.is(":checked")) {
self["delete"]($form);
} else {
self.undelete($form);
}
};
var $deleteButton = $el
.find(this.opts.deleteButtonSelector)
.filter(function () {
return !$(this).closest(".djn-empty-form").length;
});
$deleteButton
.off("click.djnesting")
.on("click.djnesting", deleteClickHandler);
$deleteButton
.find('[id$="-DELETE"]')
.on("mousedown.djnesting", deleteClickHandler);
}
remove(form) {
var $form = $(form);
var totalForms = this.mgmtVal("TOTAL_FORMS");
var minForms = this.mgmtVal("MIN_NUM_FORMS");
var maxForms = this.mgmtVal("MAX_NUM_FORMS");
var index = $form.djangoFormIndex();
var isInitial = $form.data("isInitial");
// Clearing out the form HTML itself using DOM APIs is much faster
// than using jQuery to remove the element. Using jQuery is so
// slow that it hangs the page.
$form[0].innerHTML = "";
$form.remove();
totalForms -= 1;
this.mgmtVal("TOTAL_FORMS", totalForms);
if (maxForms - totalForms >= 0) {
this.$inline
.find(this.opts.addButtonSelector)
.parent(".djn-add-item,li")
.show();
}
this._fillGap(index, isInitial);
var self = this;
var hideRemoveButton = totalForms <= minForms;
this.$inline.djangoFormsetForms().each(function () {
var showHideMethod = hideRemoveButton ? "hide" : "show";
$(this).find(self.opts.removeButtonSelector)[showHideMethod]();
});
DJNesting.updatePositions(this.prefix);
$(document).trigger("djnesting:mutate", [this.$inline]);
// Also fire using the events that were added in Django 1.9
$(document).trigger("formset:removed", [$form, this.prefix]);
document.dispatchEvent(
new CustomEvent("formset:removed", {
detail: {
formsetName: this.prefix,
},
})
);
}
delete(form) {
var self = this,
$form = $(form),
formPrefix = $form.djangoFormPrefix(),
$deleteInput = $("#id_" + formPrefix + "DELETE");
if ($form.hasClass(this.opts.predeleteClass)) {
return;
}
if (!$form.data("isInitial")) {
return;
}
$deleteInput.attr("checked", "checked");
if ($deleteInput.length) {
$deleteInput[0].checked = true;
}
$form.addClass(this.opts.predeleteClass);
$form.find(".djn-group").each(function () {
var $childInline = $(this);
var childFormset = $childInline.djangoFormset();
$childInline.djangoFormsetForms().each(function () {
if ($(this).hasClass(self.opts.predeleteClass)) {
$(this).data("alreadyDeleted", true);
} else {
childFormset.delete(this);
}
});
});
$form.find(".cropduster-form").each(function () {
var formPrefix = $(this).djangoFormsetPrefix() + "-0-";
var $deleteInput = $("#id_" + formPrefix + "DELETE");
$deleteInput.attr("checked", "checked");
if ($deleteInput.length) {
$deleteInput[0].checked = true;
}
});
DJNesting.updatePositions(this.prefix);
$(document).trigger("djnesting:mutate", [this.$inline]);
$(document).trigger("formset:deleted", [$form, this.prefix]);
}
undelete(form) {
var $form = $(form),
formPrefix = $form.djangoFormPrefix(),
$deleteInput = $("#id_" + formPrefix + "DELETE");
if ($form.parent().closest("." + this.opts.predeleteClass).length) {
return;
}
if ($form.hasClass("has_original")) {
$deleteInput.removeAttr("checked");
if ($deleteInput.length) {
$deleteInput[0].checked = false;
}
$form.removeClass(this.opts.predeleteClass);
}
$form.data("alreadyDeleted", false);
$form.find(".djn-group").each(function () {
var $childInline = $(this);
var childFormset = $childInline.djangoFormset();
$childInline.djangoFormsetForms().each(function () {
if ($(this).data("alreadyDeleted")) {
$(this).data("alreadyDeleted", false);
} else {
childFormset.undelete(this);
}
});
});
$form.find(".cropduster-form").each(function () {
var formPrefix = $(this).djangoFormsetPrefix() + "-0-";
var $deleteInput = $("#id_" + formPrefix + "DELETE");
$deleteInput.removeAttr("checked");
if ($deleteInput.length) {
$deleteInput[0].checked = false;
}
});
DJNesting.updatePositions(this.prefix);
$(document).trigger("djnesting:mutate", [this.$inline]);
$(document).trigger("formset:undeleted", [$form, this.prefix]);
}
add(spliceIndex, ctype) {
var self = this;
const $template = ctype
? $(`#${this.prefix}-empty-${ctype}`)
: this._$template;
var $form = $template.clone(true);
// For django-grappelli >= 2.14, where the grp.jQuery instance is not
// the same as django.jQuery, we must copy any prepopulated_field
// dependency data from grp.jQuery to the cloned nodes.
grp$($template)
.find(":data(dependency_ids)")
.each(function () {
const id = $(this).attr("id");
const $el = $form.find(`#${id}`);
grp$($el).data($.extend({}, $el.data(), grp$(this).data()));
});
var index = this.mgmtVal("TOTAL_FORMS");
var maxForms = this.mgmtVal("MAX_NUM_FORMS");
var isNested = this.$inline.hasClass("djn-group-nested");
$(document).trigger("djnesting:beforeadded", [this.$inline, $form]);
$form.removeClass(this.opts.emptyClass);
$form.addClass("djn-item");
$form.attr("id", $form.attr("id").replace(/\-empty.*?$/, "-" + index));
if (isNested) {
$form.append(DJNesting.createContainerElement());
}
DJNesting.updateFormAttributes(
$form,
new RegExp(
'([#_]id_|[\\#]|^id_|"|^)' +
regexQuote(this.prefix) +
"\\-(?:__prefix__|empty)\\-",
"g"
),
"$1" + this.prefix + "-" + index + "-"
);
let $firstTemplate = this._$template;
if (this.opts.childTypes) {
$firstTemplate = $template
.closest(".djn-group")
.find(
'> .djn-items > [id*="-empty"], > .djn-fieldset > .djn-items > [id*="-empty"]'
)
.eq(0);
}
if (this.opts.childTypes) {
const compatibleParents = this.$inline.djnData("compatibleParents") || {};
$form.find("> .djn-group").each((i, el) => {
const fkModel = $(el).djnData("formsetFkModel");
const compatModels = compatibleParents[ctype] || [];
const $el = $(el);
const parentModel = $el.djnData("inlineParentModel");
const isPolymorphic = !!$el.data("inlineFormset").options.childTypes;
const formPrefix = $el.data("inlineFormset").options.prefix;
if (
parentModel !== ctype ||
(isPolymorphic &&
fkModel !== ctype &&
compatModels.indexOf(fkModel) === -1)
) {
$el.find('input[id$="_FORMS"]').each((i, input) => {
input.value = 0;
input.setAttribute("value", "0");
el.parentNode.appendChild(input);
});
el.parentNode.removeChild(el);
}
});
}
$form.insertBefore($firstTemplate);
this.mgmtVal("TOTAL_FORMS", index + 1);
if (maxForms - (index + 1) <= 0) {
this.$inline
.find(this.opts.addButtonSelector)
.parent(".djn-add-item,li")
.hide();
}
DJNesting.updatePositions(this.prefix);
if ($.isNumeric(spliceIndex)) {
this.spliceInto($form, spliceIndex, true);
} else {
$(document).trigger("djnesting:mutate", [this.$inline]);
}
if (grappelli) {
grappelli.reinitDateTimeFields(grp$($form));
}
DJNesting.DjangoInlines.initPrepopulatedFields(django$($form));
DJNesting.DjangoInlines.reinitDateTimeShortCuts();
DJNesting.DjangoInlines.updateSelectFilter($form);
DJNesting.initRelatedFields(this.prefix);
DJNesting.initAutocompleteFields(this.prefix);
if (grp && grp.jQuery.fn.grp_collapsible) {
var addBackMethod = grp.jQuery.fn.addBack ? "addBack" : "andSelf";
grp$($form)
.find('.grp-collapse:not([id$="-empty"]):not([id*="-empty-"])')
[addBackMethod]()
.grp_collapsible({
toggle_handler_slctr: ".grp-collapse-handler:first",
closed_css: "closed grp-closed",
open_css: "open grp-open",
on_toggle: function () {
$(document).trigger("djnesting:toggle", [self.$inline]);
},
});
}
if (typeof $.fn.curated_content_type == "function") {
$form.find(".curated-content-type-select").each(function () {
$(this).curated_content_type();
});
}
this._initializeForm($form);
this._bindEvents($form);
if (ctype) {
const formsetModelClassName = this.$inline.djnData("inlineModel");
const inlineModelClassName = $form.attr("data-inline-model");
const $buttons = $form.find(`.djn-model-${formsetModelClassName}`);
$buttons.addClass(`djn-model-${inlineModelClassName}`);
$form.addClass(`djn-dynamic-form-${inlineModelClassName}`);
}
// find any nested formsets
$form
.find(
'.djn-group[id$="-group"][id^="' +
this.prefix +
'"][data-inline-formset]:not([id*="-empty"])'
)
.each(function () {
$(this)[pluginName]();
});
// Fire an event on the document so other javascript applications
// can be alerted to the newly inserted inline
$(document).trigger("djnesting:added", [this.$inline, $form]);
// Also fire using the events that were added in Django 1.9
$(document).trigger("formset:added", [$form, this.prefix]);
try {
$form.get(0).dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: this.prefix,
},
})
);
} catch (e) {}
return $form;
}
_fillGap(index, isInitial) {
var $initialForm, $newForm;
var formsets = this.$inline.djangoFormsetForms().toArray();
// Sort formsets in index order, so that we get the last indexed form for the swap.
formsets.sort(function (a, b) {
return $(a).djangoFormIndex() - $(b).djangoFormIndex();
});
formsets.forEach(function (form) {
var $form = $(form);
var i = $form.djangoFormIndex();
if (i <= index) {
return;
}
if ($form.data("isInitial")) {
$initialForm = $form;
} else {
$newForm = $form;
}
});
var $form = isInitial ? $initialForm || $newForm : $newForm;
if (!$form) {
return;
}
var oldIndex = $form.djangoFormIndex();
var oldFormPrefixRegex = new RegExp(
"([\\#_]|^)" + regexQuote(this.prefix + "-" + oldIndex) + "(?!\\-\\d)"
);
$form.attr("id", this.prefix + "-" + index);
DJNesting.updateFormAttributes(
$form,
oldFormPrefixRegex,
"$1" + this.prefix + "-" + index
);
// Update prefixes on nested DjangoFormset objects
$form.find(".djn-group").each(function () {
var $childInline = $(this);
var childFormset = $childInline.djangoFormset();
childFormset.prefix = $childInline.djangoFormsetPrefix();
});
$(document).trigger("djnesting:attrchange", [this.$inline, $form]);
if (isInitial && $initialForm && $newForm) {
this._fillGap(oldIndex, false);
}
}
_makeRoomForInsert() {
var initialFormCount = this.mgmtVal("INITIAL_FORMS"),
totalFormCount = this.mgmtVal("TOTAL_FORMS"),
gapIndex = initialFormCount,
$existingForm = $("#" + this.prefix + "-" + gapIndex);
if (!$existingForm.length) {
return;
}
var oldFormPrefixRegex = new RegExp(
"([\\#_]|^)" + regexQuote(this.prefix) + "-" + gapIndex + "(?!\\-\\d)"
);
$existingForm.attr("id", this.prefix + "-" + totalFormCount);
DJNesting.updateFormAttributes(
$existingForm,
oldFormPrefixRegex,
"$1" + this.prefix + "-" + totalFormCount
);
// Update prefixes on nested DjangoFormset objects
$existingForm.find(".djn-group").each(function () {
var $childInline = $(this);
var childFormset = $childInline.djangoFormset();
childFormset.prefix = $childInline.djangoFormsetPrefix();
});
$(document).trigger("djnesting:attrchange", [this.$inline, $existingForm]);
}
/**
* Splice a form into the current formset at new position `index`.
*/
spliceInto($form, index, isNewAddition) {
var initialFormCount = this.mgmtVal("INITIAL_FORMS"),
totalFormCount = this.mgmtVal("TOTAL_FORMS"),
oldFormsetPrefix = $form.djangoFormsetPrefix(),
newFormsetPrefix = this.prefix,
isInitial = $form.data("isInitial"),
newIndex,
$before;
// Make sure the form being spliced is from a different inline
if ($form.djangoFormsetPrefix() == this.prefix) {
var currentPosition = $form.prevAll(
".djn-item:not(.djn-no-drag,.djn-thead)"
).length;
if (currentPosition === index || typeof index == "undefined") {
DJNesting.updatePositions(newFormsetPrefix);
return;
}
$before = this.$inline
.find("> .djn-items, > .tabular > .module > .djn-items")
.find("> .djn-item:not(#" + $form.attr("id") + ")")
.eq(index);
$before.after($form);
} else {
var $oldInline = $("#" + oldFormsetPrefix + "-group");
var $currentFormInline = $form.closest(".djn-group");
if ($currentFormInline.djangoFormsetPrefix() != newFormsetPrefix) {
$before = this.$inline
.find("> .djn-items, > .tabular > .module > .djn-items")
.find("> .djn-item")
.eq(index);
$before.after($form);
}
var oldDjangoFormset = $oldInline.djangoFormset();
oldDjangoFormset.mgmtVal(
"TOTAL_FORMS",
oldDjangoFormset.mgmtVal("TOTAL_FORMS") - 1
);
oldDjangoFormset._fillGap($form.djangoFormIndex(), isInitial);
if (isInitial) {
oldDjangoFormset.mgmtVal(
"INITIAL_FORMS",
oldDjangoFormset.mgmtVal("INITIAL_FORMS") - 1
);
var $parentInline = this.$inline.parent().closest(".djn-group");
if ($parentInline.length) {
var $parentForm = this.$inline.closest(".djn-inline-form");
var parentPkField = ($parentInline.djnData("fieldNames") || {}).pk;
var $parentPk = $parentForm.djangoFormField(parentPkField);
if (!$parentPk.val()) {
$form.data("isInitial", false);
$form.attr("data-is-initial", "false");
isInitial = false;
// Set initial form counts to 0 on nested DjangoFormsets
setTimeout(function () {
$form
.find(
'[name^="' +
$form.djangoFormPrefix() +
'"][name$="-INITIAL_FORMS"]'
)
.val("0")
.trigger("change");
}, 0);
}
}
}
if (isInitial) {
this._makeRoomForInsert();
}
// Replace the ids for the splice form
var oldFormPrefixRegex = new RegExp(
"([\\#_]|^)" + regexQuote($form.attr("id")) + "(?!\\-\\d)"
);
newIndex = isInitial ? initialFormCount : totalFormCount;
$form.attr("id", newFormsetPrefix + "-" + newIndex);
DJNesting.updateFormAttributes(
$form,
oldFormPrefixRegex,
"$1" + newFormsetPrefix + "-" + newIndex
);
// Update prefixes on nested DjangoFormset objects
$form.find(".djn-group").each(function () {
var $childInline = $(this);
var childFormset = $childInline.djangoFormset();
childFormset.prefix = $childInline.djangoFormsetPrefix();
});
$(document).trigger("djnesting:attrchange", [this.$inline, $form]);
if (isInitial) {
this.mgmtVal("INITIAL_FORMS", initialFormCount + 1);
}
this.mgmtVal("TOTAL_FORMS", totalFormCount + 1);
DJNesting.updatePositions(oldFormsetPrefix);
$(document).trigger("djnesting:mutate", [$oldInline]);
}
DJNesting.updatePositions(newFormsetPrefix);
if (!isNewAddition) {
$(document).trigger("djnesting:mutate", [this.$inline]);
}
}
mgmtVal(name, newValue) {
var $field = this.$inline.find("#id_" + this.prefix + "-" + name);
if (typeof newValue == "undefined") {
return parseInt($field.val(), 10);
} else {
return parseInt($field.val(newValue).trigger("change").val(), 10);
}
}
}
$.fn[pluginName] = function () {
var options, fn, args;
var $el = this.eq(0);
if (
arguments.length === 0 ||
(arguments.length === 1 && $.type(arguments[0]) != "string")
) {
options = arguments[0];
var djangoFormset = $el.data(pluginName);
if (!djangoFormset) {
djangoFormset = new DjangoFormset($el, options);
$el.data(pluginName, djangoFormset);
}
return djangoFormset;
}
fn = arguments[0];
args = $.makeArray(arguments).slice(1);
if (fn in DjangoFormset.prototype) {
return $el.data(pluginName)[fn](args);
} else {
throw new Error("Unknown function call " + fn + " for $.fn." + pluginName);
}
};
export default DjangoFormset;

View File

@@ -0,0 +1,234 @@
import $ from "jquery";
var prefixCache = {};
$.fn.djnData = function (name) {
var inlineFormsetData = $(this).data("inlineFormset") || {},
nestedOptions = inlineFormsetData.nestedOptions || {};
if (!name) {
return nestedOptions;
} else {
return nestedOptions[name];
}
};
$.fn.djangoPrefixIndex = function () {
var $this = this.length > 1 ? this.first() : this;
var id = $this.attr("id"),
name = $this.attr("name"),
forattr = $this.attr("for"),
prefix,
$form,
$group,
groupId,
cacheKey,
match,
index;
if (
(match = prefixCache[id]) ||
(match = prefixCache[name]) ||
(match = prefixCache[forattr])
) {
return match;
}
if (id && !prefix) {
prefix = (id.match(/^(.*)\-group$/) || [null, null])[1];
}
if (id && !prefix && $this.is(".djn-item") && id.match(/\d+$/)) {
[cacheKey, prefix, index] = id.match(/(.*?)\-(\d+)$/) || [null, null, null];
}
if (!prefix) {
$form = $this.closest(".djn-inline-form");
if ($form.length) {
[cacheKey, prefix, index] = $form.attr("id").match(/(.*?)\-(\d+)$/) || [
null,
null,
null,
];
} else {
$group = $this.closest(".djn-group");
if (!$group.length) {
return null;
}
groupId = $group.attr("id") || "";
prefix = (groupId.match(/^(.*)\-group$/) || [null, null])[1];
}
} else {
if (prefix.substr(0, 3) == "id_") {
prefix = prefix.substr(3);
}
if (!document.getElementById(prefix + "-group")) {
return null;
}
}
if (cacheKey) {
prefixCache[cacheKey] = [prefix, index];
}
return [prefix, index];
};
$.fn.djangoFormPrefix = function () {
var prefixIndex = this.djangoPrefixIndex();
if (!prefixIndex || !prefixIndex[1]) {
return null;
}
return prefixIndex[0] + "-" + prefixIndex[1] + "-";
};
$.fn.djangoFormIndex = function () {
var prefixIndex = this.djangoPrefixIndex();
return !prefixIndex || !prefixIndex[1] ? null : parseInt(prefixIndex[1], 10);
};
$.fn.djangoFormsetPrefix = function () {
var prefixIndex = this.djangoPrefixIndex();
return !prefixIndex ? null : prefixIndex[0];
};
var filterDjangoFormsetForms = function (form, $group, formsetPrefix) {
var formId = form.getAttribute("id"),
formIndex = formId.substr(formsetPrefix.length + 1);
// Check if form id matches /{prefix}-\d+/
if (formId.indexOf(formsetPrefix) !== 0) {
return false;
}
return !formIndex.match(/\D/);
};
// Selects all initial forms within the same formset as the
// element the method is being called on.
$.fn.djangoFormsetForms = function () {
var forms = [];
this.each(function () {
var $this = $(this),
formsetPrefix = $this.djangoFormsetPrefix(),
$group = formsetPrefix ? $("#" + formsetPrefix + "-group") : null,
$forms;
if (!formsetPrefix || !$group.length) return;
$forms = $group.find(".djn-inline-form").filter(function () {
return filterDjangoFormsetForms(this, $group, formsetPrefix);
});
var sortedForms = $forms.toArray().sort(function (a, b) {
return $(a).djangoFormIndex() - $(b).djangoFormIndex;
});
Array.prototype.push.apply(forms, sortedForms);
});
return this.pushStack(forms);
};
if (typeof $.djangoFormField != "function") {
$.djangoFormField = function (fieldName, prefix, index) {
var $empty = $([]),
matches;
if ((matches = prefix.match(/^(.+)\-(\d+)\-$/))) {
prefix = matches[1];
index = matches[2];
}
index = parseInt(index, 10);
if (isNaN(index)) {
return $empty;
}
var namePrefix = prefix + "-" + index + "-";
if (fieldName == "*") {
return $('*[name^="' + namePrefix + '"]').filter(function () {
var fieldPart = $(this).attr("name").substring(namePrefix.length);
return fieldPart.indexOf("-") === -1;
});
}
var $field = $("#id_" + namePrefix + fieldName);
if (!$field.length && (fieldName == "pk" || fieldName == "position")) {
var $group = $("#" + prefix + "-group"),
fieldNameData = $group.djnData("fieldNames") || {};
fieldName = fieldNameData[fieldName];
if (!fieldName) {
return $empty;
}
$field = $("#id_" + namePrefix + fieldName);
}
return $field;
};
}
if (typeof $.fn.djangoFormField != "function") {
/**
* Given a django model's field name, and the forms index in the
* formset, returns the field's input element, or an empty jQuery
* object on failure.
*
* @param String fieldName - 'pk', 'position', or the field's
* name in django (e.g. 'content_type',
* 'url', etc.)
* @return jQuery object containing the field's input element, or
* an empty jQuery object on failure
*/
$.fn.djangoFormField = function (fieldName, index) {
var prefixAndIndex = this.djangoPrefixIndex();
var $empty = $([]);
if (!prefixAndIndex) {
return $empty;
}
var prefix = prefixAndIndex[0];
if (typeof index == "undefined") {
index = prefixAndIndex[1];
if (typeof index == "undefined") {
return $empty;
}
}
return $.djangoFormField(fieldName, prefix, index);
};
}
if (typeof $.fn.filterDjangoField != "function") {
var djRegexCache = {};
$.fn.filterDjangoField = function (prefix, fieldName, index) {
var $field, fieldNameData;
if (typeof index != "undefined") {
if (typeof index == "string") {
index = parseInt(index, 10);
}
if (typeof index == "number" && !isNaN(index)) {
var fieldId = "id_" + prefix + "-" + index + "-" + fieldName;
$field = $("#" + fieldId);
}
} else {
if (typeof djRegexCache[prefix] != "object") {
djRegexCache[prefix] = {};
}
if (typeof djRegexCache[prefix][fieldName] == "undefined") {
djRegexCache[prefix][fieldName] = new RegExp(
"^" + prefix + "-\\d+-" + fieldName + "$"
);
}
$field = this.find('input[name$="' + fieldName + '"]').filter(
function () {
return this.getAttribute("name").match(
djRegexCache[prefix][fieldName]
);
}
);
}
if (!$field.length && (fieldName == "pk" || fieldName == "position")) {
fieldNameData = $("#" + prefix + "-group").djnData("fieldNames") || {};
if (
typeof fieldNameData[fieldName] &&
fieldNameData[fieldName] != fieldName
) {
$field = $(this).filterDjangoField(
prefix,
fieldNameData[fieldName],
index
);
}
}
return $field;
};
}

View File

@@ -0,0 +1 @@
export default window.django.jQuery;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,794 @@
import $ from "jquery";
import "./jquery.ui.djnsortable";
/*
* jQuery UI Nested Sortable
* v 1.3.4 / 28 apr 2011
* http://mjsarfatti.com/sandbox/nestedSortable
*
* Depends:
* jquery.ui.sortable.js 1.8+
*
* License CC BY-SA 3.0
* Copyright 2010-2011, Manuele J Sarfatti
*/
if (typeof $.fn.nearest != "function") {
/**
* Returns the descendant(s) matching a given selector which are the
* shortest distance from the search context element (in otherwords,
* $.fn.closest(), in reverse).
*/
$.fn.nearest = function (selector) {
var nearest = [],
node = this,
distance = 10000;
node.find(selector).each(function () {
var d = $(this).parentsUntil(node).length;
if (d < distance) {
distance = d;
nearest = [this];
} else if (d == distance) {
nearest.push(this);
}
});
return this.pushStack(nearest, "nearest", [selector]);
};
}
var counter = 0;
var expando = "djn" + ("" + Math.random()).replace(/\D/g, "");
var createChildNestedSortable = function (parent, childContainer) {
// Don't continue if the new element is the same as the old
if (parent && parent.element && parent.element[0] == childContainer) {
return;
}
var $childContainer = $(childContainer),
options = $.extend({}, parent.options);
options.connectWith = [parent.element];
if ($childContainer.data(parent.widgetName)) {
return;
}
var widgetConstructor = $childContainer[parent.widgetName];
widgetConstructor.call($childContainer, options);
var newInstance = $childContainer.data(parent.widgetName);
for (var i = 0; i < parent.options.connectWith.length; i++) {
var $otherContainer = parent.options.connectWith[i];
newInstance.addToConnectWith($otherContainer);
var otherInstance = $otherContainer.data(parent.widgetName);
if (otherInstance) {
otherInstance.addToConnectWith($childContainer);
}
}
parent.addToConnectWith($childContainer);
return newInstance;
};
$.widget("ui.nestedSortable", $.ui.djnsortable, {
options: {
tabSize: 20,
disableNesting: "ui-nestedSortable-no-nesting",
errorClass: "ui-nestedSortable-error",
nestedContainerSelector: ":not(*)",
// Whether to clear empty list item and container elements
doNotClear: false,
/**
* Create a list container element if the draggable was dragged
* to the top or bottom of the elements at its level.
*
* @param DOMElement parent - The element relative to which the
* new element will be inserted.
* @return DOMElement - The new element.
*/
createContainerElement: function (parent) {
return $(document.createElement("ol"));
},
// Selector which matches all container elements in the nestedSortable
containerElementSelector: "ol",
// Selector which matches all list items (draggables) in the nestedSortable
listItemSelector: "li",
// Selector which, when applied to a container, returns its child list items
items: "> li",
maxLevels: 0,
revertOnError: 1,
protectRoot: false,
rootID: null,
rtl: false,
// if true, you can not move nodes to different levels of nesting
fixedNestingDepth: false,
// show the error div or just not show a drop area
showErrorDiv: true,
// if true only allows you to rearrange within its parent container
keepInParent: false,
isAllowed: function (item, parent) {
return true;
},
canConnectWith: function (container1, container2, instance) {
var model1 = container1.data("inlineModel");
var model2 = container2.data("inlineModel");
if (model1 !== model2) {
return false;
}
var instance2 = container2.data(instance.widgetName);
if (!instance.options.fixedNestingDepth) {
if (!instance2 || !instance2.options.fixedNestingDepth) {
return true;
}
}
var container1Level = instance._getLevel(container1);
var container2Level = instance._getLevel(container2);
return container1Level === container2Level;
},
},
_createWidget: function (options, element) {
var $element = $(element || this.defaultElement || this),
dataOptions = $element.data("djnsortableOptions");
element = $element[0];
if (dataOptions) {
options = $.extend({}, options, dataOptions);
}
return $.ui.djnsortable.prototype._createWidget.call(
this,
options,
element
);
},
_create: function () {
if (this.element.data("uiNestedSortable")) {
this.element.data(
"nestedSortable",
this.element.data("uiNestedSortable")
);
}
if (this.element.data("ui-nestedSortable")) {
this.element.data(
"nestedSortable",
this.element.data("ui-nestedSortable")
);
}
this.element.data("djnsortable", this.element.data("nestedSortable"));
if (this.element.data("uiNestedSortable")) {
this.element.data("uiSortable", this.element.data("nestedSortable"));
}
// if (!this.element.is(this.options.containerElementSelector)) {
// throw new Error('nestedSortable: Please check that the ' +
// 'containerElementSelector option matches ' +
// 'the element passed to the constructor.');
// }
$.ui.djnsortable.prototype._create.apply(this, arguments);
this._connectWithMap = {};
var self = this,
o = this.options,
$document = $(document);
var originalConnectWith = o.connectWith;
if (!originalConnectWith || typeof originalConnectWith == "string") {
this.options.connectWith = [];
if (typeof originalConnectWith == "string") {
var connected = this._connectWith();
for (var i = 0; i < connected.length; i++) {
this.addToConnectWith($(connected[i]));
}
}
// HACK!! FIX!! (django-specific logic)
$document.on(
"djnesting:init.nestedSortable",
o.containerElementSelector,
function (event) {
createChildNestedSortable(self, this);
}
);
this.element
.find(o.containerElementSelector + ":not(.subarticle-wrapper)")
.each(function (i, el) {
if (
$(el)
.closest("[data-inline-formset]")
.attr("id")
.indexOf("-empty") > -1
) {
return;
}
createChildNestedSortable(self, el);
});
}
$document.trigger("nestedSortable:created", [this]);
$document.on(
"nestedSortable:created.nestedSortable",
function (e, instance) {
instance.addToConnectWith(self.element);
self.addToConnectWith(instance.element);
}
);
},
addToConnectWith: function (element) {
var self = this,
$element = typeof element.selector != "undefined" ? element : $(element),
uniqueId;
if ($element.length > 1) {
$element.each(function (i, el) {
self.addToConnectWith($(el));
});
return;
}
uniqueId = element[0][expando];
if (typeof uniqueId == "undefined") {
uniqueId = element[0][expando] = ++counter;
}
if (typeof this.options.connectWith == "string") {
return;
}
if (this._connectWithMap[uniqueId]) {
return;
}
this.options.connectWith.push(element);
this._connectWithMap[uniqueId] = 1;
},
_destroy: function () {
this.element.removeData("nestedSortable").unbind(".nestedSortable");
$(document).unbind(".nestedSortable");
return $.ui.djnsortable.prototype.destroy.apply(this, arguments);
},
/**
* Override this method to add extra conditions on an item before it's
* rearranged.
*/
_intersectsWithPointer: function _intersectsWithPointer(item) {
var itemElement = item.item[0],
o = this.options,
intersection = $.ui.djnsortable.prototype._intersectsWithPointer.apply(
this,
arguments
);
this.lastItemElement = null;
if (!intersection) {
return intersection;
}
// Only put the placeholder inside the current Container, skip all
// items from other containers. This works because when moving
// an item from one container to another the
// currentContainer is switched before the placeholder is moved.
//
// Without this moving items in "sub-sortables" can cause the placeholder to jitter
// between the outer and inner container.
if (item.instance !== this.currentContainer) {
return false;
}
var $itemElement = $(itemElement);
if (
o.fixedNestingDepth &&
this._getLevel(this.currentItem) === 1 + this._getLevel($itemElement)
) {
$itemElement = (function () {
var containerSel = o.containerElementSelector;
var $childItems = $itemElement.find(".djn-item");
if ($childItems.length != 1) {
return $itemElement;
}
if (!$childItems.is(".djn-no-drag,.djn-thead")) {
return $itemElement;
}
var itemElementClosestContainer = $itemElement.closest(containerSel);
if (!itemElementClosestContainer.length) {
return $itemElement;
}
// Make sure the item is only one level deeper
if (
itemElementClosestContainer[0] !=
$childItems.closest(containerSel).closest(containerSel)[0]
) {
return $itemElement;
}
return $($childItems[0]);
})();
itemElement = $itemElement[0];
}
if (
itemElement != this.currentItem[0] && //cannot intersect with itself
this.placeholder[intersection == 1 ? "next" : "prev"]()[0] !=
itemElement && //no useless actions that have been done before
!$.contains(this.placeholder[0], itemElement) && //no action if the item moved is the parent of the item checked
(this.options.type == "semi-dynamic"
? !$.contains(this.element[0], itemElement)
: true) &&
(!o.keepInParent ||
itemElement.parentNode == this.placeholder[0].parentNode) && //only rearrange items within the same container
(!o.fixedNestingDepth ||
this._getLevel(this.currentItem) === this._getLevel($itemElement)) && //maintain the nesting level of node
(o.showErrorDiv ||
o.isAllowed.call(
this,
this.currentItem[0],
itemElement.parentNode,
this.placeholder
))
) {
this.lastItemElement = itemElement;
return intersection;
} else {
return false;
}
},
// This method is called after items have been iterated through.
// Overriding this is cleaner than copying and pasting _mouseDrag()
// and inserting logic in the middle.
_contactContainers: function _contactContainers(event) {
if (this.lastItemElement) {
this._clearEmpty(this.lastItemElement);
}
var o = this.options,
_parentItem = this.placeholder.closest(o.listItemSelector),
parentItem =
_parentItem.length && _parentItem.closest(".ui-sortable").length
? _parentItem
: null,
level = this._getLevel(this.placeholder),
childLevels = this._getChildLevels(this.helper);
var placeholderClassName = this.placeholder.attr("class");
var phClassSearch = " " + placeholderClassName + " ";
// If the current level class isn't already set
if (
phClassSearch.indexOf(" ui-sortable-nested-level-" + level + " ") == -1
) {
var phOrigClassName;
// Check if another level class is set
var phOrigClassNameEndPos =
phClassSearch.indexOf(" ui-sortable-nested-level-") - 1;
if (phOrigClassNameEndPos > -1) {
phOrigClassName = placeholderClassName.substring(
0,
phOrigClassNameEndPos
);
} else {
phOrigClassName = placeholderClassName;
}
// Add new level to class
this.placeholder.attr(
"class",
phOrigClassName + " ui-sortable-nested-level-" + level
);
}
// To find the previous sibling in the list, keep backtracking until we hit a valid list item.
var previousItem = this.placeholder[0].previousSibling
? $(this.placeholder[0].previousSibling)
: null;
if (previousItem != null) {
while (
!previousItem.is(this.options.listItemSelector) ||
previousItem[0] == this.currentItem[0] ||
previousItem[0] == this.helper[0]
) {
if (previousItem[0].previousSibling) {
previousItem = $(previousItem[0].previousSibling);
} else {
previousItem = null;
break;
}
}
}
// To find the next sibling in the list, keep stepping forward until we hit a valid list item.
var nextItem = this.placeholder[0].nextSibling
? $(this.placeholder[0].nextSibling)
: null;
if (nextItem != null) {
while (
!nextItem.is(this.options.listItemSelector) ||
nextItem[0] == this.currentItem[0] ||
nextItem[0] == this.helper[0]
) {
if (nextItem[0].nextSibling) {
nextItem = $(nextItem[0].nextSibling);
} else {
nextItem = null;
break;
}
}
}
this.beyondMaxLevels = 0;
// We will change this to the instance of the nested container if
// appropriate, so that the appropriate context is applied to the
// super _contactContainers prototype method
var containerInstance = this;
this.refreshPositions();
// If the item is moved to the left, send it to its parent's level unless there are siblings below it.
if (
!o.fixedNestingDepth &&
parentItem != null &&
nextItem == null &&
((o.rtl &&
this.positionAbs.left + this.helper.outerWidth() >
parentItem.offset().left + parentItem.outerWidth()) ||
(!o.rtl && this.positionAbs.left < parentItem.offset().left))
) {
parentItem.after(this.placeholder[0]);
containerInstance =
parentItem.closest(o.containerElementSelector).data(this.widgetName) ||
containerInstance;
this._clearEmpty(parentItem[0]);
this.refreshPositions();
this._trigger("change", event, this._uiHash());
}
// If the item is below a sibling and is moved to the right, make it a child of that sibling.
else if (
!o.fixedNestingDepth &&
previousItem != null &&
!previousItem.is(".djn-no-drag,.djn-thead") &&
((o.rtl &&
this.positionAbs.left + this.helper.outerWidth() <
previousItem.offset().left + previousItem.outerWidth() - o.tabSize) ||
(!o.rtl &&
this.positionAbs.left > previousItem.offset().left + o.tabSize))
) {
this._isAllowed(previousItem, level, level + childLevels);
if (this.beyondMaxLevels > 0) {
return $.ui.djnsortable.prototype._contactContainers.apply(
this,
arguments
);
}
var $previousItemChildContainer;
$previousItemChildContainer = previousItem
.nearest(o.containerElementSelector)
.first();
if (
!$previousItemChildContainer.length &&
!previousItem.closest(o.nestedContainerSelector).length
) {
$previousItemChildContainer = this.options.createContainerElement(
previousItem[0]
);
previousItem.append($previousItemChildContainer);
}
if ($previousItemChildContainer.length) {
$previousItemChildContainer.append(this.placeholder);
containerInstance = $previousItemChildContainer.data(this.widgetName);
if (!containerInstance) {
containerInstance = createChildNestedSortable(
this,
$previousItemChildContainer[0]
);
}
this.refreshPositions();
}
this._trigger("change", event, this._uiHash());
} else {
this._isAllowed(parentItem, level, level + childLevels);
}
$.ui.djnsortable.prototype._contactContainers.call(this, event);
},
_rearrange: function _rearrange(event, item, a, hardRefresh) {
// Cache the rearranged element for the call to _clear()
var o = this.options;
if (item && typeof item == "object" && item.item) {
this.lastRearrangedElement = item.item[0];
}
if (
item &&
typeof item == "object" &&
item.item &&
this.placeholder.closest(o.nestedContainerSelector).length
) {
// This means we have been dropped into a nested container down a level
// from the parent.
var placeholderParentItem = this.placeholder.closest(o.listItemSelector);
var comparisonElement =
this.direction == "down"
? placeholderParentItem.next(o.nestedContainerSelector)
: placeholderParentItem;
if (comparisonElement.length && comparisonElement[0] == item.item[0]) {
//Various things done here to improve the performance:
// 1. we create a setTimeout, that calls refreshPositions
// 2. on the instance, we have a counter variable, that get's higher after every append
// 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
// 4. this lets only the last addition to the timeout stack through
this.counter = this.counter ? ++this.counter : 1;
var counter = this.counter;
this._delay(function () {
if (counter == this.counter) this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove
});
// The super method will pop the container out of its nested container,
// which we don't want.
return;
}
}
$.ui.djnsortable.prototype._rearrange.apply(this, arguments);
},
_convertPositionTo: function (d, pos) {
// Cache the top offset before rearrangement
this.previousTopOffset = this.placeholder.offset().top;
return $.ui.djnsortable.prototype._convertPositionTo.apply(this, arguments);
},
_clear: function () {
$.ui.djnsortable.prototype._clear.apply(this, arguments);
// If lastRearrangedElement exists and is still attached to the document
// (i.e., hasn't been removed)
if (
typeof this.lastRearrangedElement == "object" &&
this.lastRearrangedElement.ownerDocument
) {
this._clearEmpty(this.lastRearrangedElement);
}
},
_mouseStop: function _mouseStop(event, noPropagation) {
// If the item is in a position not allowed, send it back
if (this.beyondMaxLevels) {
this.placeholder.removeClass(this.options.errorClass);
if (this.domPosition.prev) {
$(this.domPosition.prev).after(this.placeholder);
} else {
$(this.domPosition.parent).prepend(this.placeholder);
}
this._trigger("revert", event, this._uiHash());
}
// Clean last empty container/list item
for (var i = this.items.length - 1; i >= 0; i--) {
var item = this.items[i].item[0];
this._clearEmpty(item);
}
$.ui.djnsortable.prototype._mouseStop.apply(this, arguments);
},
toArray: function (o) {
o = $.extend(true, {}, this.options, o || {});
var sDepth = o.startDepthCount || 0,
ret = [],
left = 2;
ret.push({
item_id: o.rootID,
parent_id: "none",
depth: sDepth,
left: "1",
right: ($(o.listItemSelector, this.element).length + 1) * 2,
});
var _recursiveArray = function (item, depth, left) {
var right = left + 1,
id,
pid;
var $childItems = $(item)
.children(o.containerElementSelector)
.find(o.items);
if ($childItems.length > 0) {
depth++;
$childItems.each(function () {
right = _recursiveArray($(this), depth, right);
});
depth--;
}
id = $(item)
.attr(o.attribute || "id")
.match(o.expression || /(.+)[-=_](.+)/);
if (depth === sDepth + 1) {
pid = o.rootID;
} else {
var parentItem = $(item)
.parent(o.containerElementSelector)
.parent(o.items)
.attr(o.attribute || "id")
.match(o.expression || /(.+)[-=_](.+)/);
pid = parentItem[2];
}
if (id) {
ret.push({
item_id: id[2],
parent_id: pid,
depth: depth,
left: left,
right: right,
});
}
left = right + 1;
return left;
};
$(this.element)
.children(o.listItemSelector)
.each(function () {
left = _recursiveArray(this, sDepth + 1, left);
});
ret = ret.sort(function (a, b) {
return a.left - b.left;
});
return ret;
},
_clearEmpty: function (item) {
if (this.options.doNotClear) {
return;
}
var $item = $(item);
var childContainers = $item.nearest(this.options.containerElementSelector);
childContainers.each(function (i, childContainer) {
var $childContainer = $(childContainer);
if (!$childContainer.children().length) {
var instance = $childContainer.data(this.widgetName);
if (typeof instance == "object" && instance.destroy) {
instance.destroy();
}
$childContainer.remove();
}
});
if (!$item.children().length) {
$item.remove();
}
},
_getLevel: function (item) {
var level = 1,
o = this.options,
list;
if (o.containerElementSelector) {
list = item.closest(o.containerElementSelector);
while (list && list.length > 0 && !list.parent().is(".djn-group-root")) {
// if (!list.is(o.nestedContainerSelector)) {
level++;
// }
list = list.parent().closest(o.containerElementSelector);
}
}
return level;
},
_getChildLevels: function (parent, depth) {
var self = this,
o = this.options,
result = 0;
depth = depth || 0;
$(parent)
.nearest(o.containerElementSelector)
.first()
.find(o.items)
.each(function (index, child) {
if ($(child).is(".djn-no-drag,.djn-thead")) {
return;
}
result = Math.max(self._getChildLevels(child, depth + 1), result);
});
return depth ? result + 1 : result;
},
_isAllowed: function _isAllowed(parentItem, level, levels) {
var o = this.options,
isRoot = $(this.domPosition.parent).hasClass("ui-sortable")
? true
: false;
// this takes into account the maxLevels set to the recipient list
// var maxLevels = this.placeholder.closest('.ui-sortable').nestedSortable('option', 'maxLevels');
var maxLevels = o.maxLevels;
// Is the root protected?
// Are we trying to nest under a no-nest?
// Are we nesting too deep?
if (
parentItem &&
typeof parentItem == "object" &&
typeof parentItem.selector == "undefined"
) {
parentItem = $(parentItem);
}
if (
!o.isAllowed.call(this, this.currentItem, parentItem, this.placeholder) ||
(parentItem && parentItem.hasClass(o.disableNesting)) ||
(o.protectRoot &&
((parentItem == null && !isRoot) || (isRoot && level > 1)))
) {
this.placeholder.addClass(o.errorClass);
if (maxLevels < levels && maxLevels != 0) {
this.beyondMaxLevels = levels - maxLevels;
} else {
this.beyondMaxLevels = 1;
}
} else {
if (maxLevels < levels && maxLevels != 0) {
this.placeholder.addClass(o.errorClass);
this.beyondMaxLevels = levels - maxLevels;
} else {
this.placeholder.removeClass(o.errorClass);
this.beyondMaxLevels = 0;
}
}
},
_connectWith: function _connectWith() {
var origConnectWith = $.ui.djnsortable.prototype._connectWith.apply(
this,
arguments
),
connectWith = [];
var self = this;
for (var i = 0; i < origConnectWith.length; i++) {
var $elements = $(origConnectWith[i]);
$elements.each(function (j, el) {
if (el == self.element[0]) {
return;
}
if (!self.options.canConnectWith(self.element, $(el), self)) {
return;
}
connectWith.push(el);
});
}
return connectWith;
},
_removeCurrentsFromItems: function () {
var list = this.currentItem.find(":data(sortable-item)");
for (var i = 0; i < this.items.length; i++) {
for (var j = 0; j < list.length; j++) {
if (list[j] == this.items[i].item[0]) {
this.items.splice(i, 1);
if (i >= this.items.length) {
break;
}
}
}
}
},
createContainerElement: function (parent) {
if (!parent.childNodes) {
throw new Error(
"Invalid element 'parent' passed to " + "createContainerElement."
);
}
var newContainer = this.options.createContainerElement.apply(
this,
arguments
);
parent.appendChild(newContainer[0]);
return $(newContainer);
},
});
$.ui.nestedSortable.prototype.options = $.extend(
{},
$.ui.djnsortable.prototype.options,
$.ui.nestedSortable.prototype.options
);

View File

@@ -0,0 +1,5 @@
function regexQuote(str) {
return (str + "").replace(/([\.\?\*\+\^\$\[\]\\\(\)\{\}\|\-])/g, "\\$1");
}
export default regexQuote;

View File

@@ -0,0 +1,256 @@
import $ from "jquery";
import regexQuote from "./regexquote";
import "./jquery.ui.nestedsortable";
function updatePositions(prefix) {
var position = 0, // the value of the position formfield
count = 1, // The count displayed in stacked inline headers
$group = $("#" + prefix + "-group"),
groupData = $group.djnData(),
fieldNames = groupData.fieldNames,
// The field name on the fieldset which is a ForeignKey to the parent model
groupFkName = groupData.formsetFkName,
parentPkVal,
[, parentPrefix, index] =
prefix.match(/^(.*)\-(\d+)-[^\-]+(?:\-\d+)?$/) || [],
sortableOptions = groupData.sortableOptions,
sortableExcludes = (sortableOptions || {}).sortableExcludes || [];
sortableExcludes.push(groupFkName);
if (parentPrefix) {
var $parentGroup = $("#" + parentPrefix + "-group");
var parentFieldNames = $parentGroup.djnData("fieldNames");
var parentPkFieldName = parentFieldNames.pk;
var parentPkField = $parentGroup.filterDjangoField(
parentPrefix,
parentPkFieldName,
index
);
parentPkVal = parentPkField.val();
}
if (groupFkName && typeof parentPkVal != "undefined") {
$group
.filterDjangoField(prefix, groupFkName)
.val(parentPkVal)
.trigger("change");
}
$group.find(".djn-inline-form").each(function () {
if (!this.id || this.id.substr(-6) == "-empty") {
return true; // Same as continue
}
var regex = new RegExp("^(?:id_)?" + regexQuote(prefix) + "\\-\\d+$");
if (!this.id.match(regex)) {
return true;
}
// Cache jQuery object
var $this = $(this),
[formPrefix, index] = $this.djangoPrefixIndex() || [null, null],
namePrefix = formPrefix + "-" + index + "-";
if (!formPrefix) {
return;
}
// Set header position for stacked inlines in Django 1.9+
var $inlineLabel = $this.find("> h3 > .inline_label");
if ($inlineLabel.length) {
$inlineLabel.html($inlineLabel.html().replace(/(#\d+)/g, "#" + count));
}
count++;
var $fields = $this.djangoFormField("*"),
$positionField,
setPosition = false;
// position is being updated if
// a) the field has a value
// b) if the field is not exluded with sortable_excludes (e.g. with default values)
$fields.each(function () {
var $field = $(this);
if (!$field.is(":input[type!=radio][type!=checkbox],input:checked")) {
return;
}
var hasValue =
$field.val() ||
($field.attr("type") == "file" && $field.siblings("a").length),
fieldName = $field.attr("name").substring(namePrefix.length);
if (fieldName == fieldNames.position) {
$positionField = $field;
}
if (hasValue && $.inArray(fieldName, sortableExcludes) === -1) {
setPosition = true;
}
});
if (!setPosition || !$positionField) {
return;
}
$positionField.val(position).trigger("change");
position++;
});
}
function createSortable($group) {
const isPolymorphic = $group.is(".djn-is-polymorphic");
return $group
.find(
"> .djn-items, > .djn-fieldset > .djn-items, > .tabular > .module > .djn-items"
)
.nestedSortable({
handle: [
"> h3.djn-drag-handler",
"> .djn-tools .drag-handler",
"> .djn-td > .djn-tools .djn-drag-handler",
"> .djn-tr > .is-sortable > .djn-drag-handler",
"> .djn-tr > .grp-tools-container .djn-drag-handler",
].join(", "),
/**
* items: The selector for ONLY the items underneath a given
* container at that level. Not to be confused with
* listItemSelector, which is the selector for all list
* items in the nestedSortable
*/
items: "> .djn-item",
forcePlaceholderSize: true,
placeholder: {
element: function ($currentItem) {
var el = $(document.createElement($currentItem[0].nodeName))
.addClass($currentItem[0].className + " ui-sortable-placeholder")
.removeClass("ui-sortable-helper")[0];
if ($currentItem.is(".djn-tbody")) {
var $originalTr = $currentItem.children(".djn-tr").eq(0);
var trTagName = $originalTr.prop("tagName").toLowerCase();
var $tr = $(`<${trTagName}></${trTagName}>`);
$tr.addClass($originalTr.attr("class"));
var $originalTd = $originalTr.children(".djn-td").eq(0);
var tdTagName = $originalTd.prop("tagName").toLowerCase();
var numColumns = 0;
$originalTr.children(".djn-td").each(function (i, td) {
numColumns += parseInt($(td).attr("colspan"), 10) || 1;
});
$tr.append(
$(
`<${tdTagName} colspan="${numColumns}" class="djn-td grp-td"></${tdTagName}>`
)
);
el.appendChild($tr[0]);
}
return el;
},
update: function (instance, $placeholder) {
var $currItem = instance.currentItem;
if (!$currItem) {
return;
}
var opts = instance.options;
// 1. If a className is set as 'placeholder option, we
// don't force sizes - the class is responsible for that
// 2. The option 'forcePlaceholderSize can be enabled to
// force it even if a class name is specified
if (opts.className && !opts.forcePlaceholderSize) return;
if ($placeholder.is(".djn-tbody")) {
// debugger;
$placeholder = $placeholder
.children(".djn-tr")
.eq(0)
.children(".djn-td")
.eq(0);
}
// If the element doesn't have a actual height by itself
// (without styles coming from a stylesheet), it receives
// the inline height from the dragged item
if (!$placeholder.height()) {
var innerHeight = $currItem.innerHeight(),
paddingTop = parseInt($currItem.css("paddingTop") || 0, 10),
paddingBottom = parseInt($currItem.css("paddingBottom") || 0, 10);
$placeholder.height(innerHeight - paddingTop - paddingBottom);
}
if (!$placeholder.width()) {
var innerWidth = $currItem.innerWidth(),
paddingLeft = parseInt($currItem.css("paddingLeft") || 0, 10),
paddingRight = parseInt($currItem.css("paddingRight") || 0, 10);
$placeholder.width(innerWidth - paddingLeft - paddingRight);
}
},
},
helper: "clone",
opacity: 0.6,
maxLevels: 0,
connectWith: ".djn-items",
tolerance: "intersection",
// Don't allow dragging beneath an inline that is marked for deletion
isAllowed: function (currentItem, parentItem) {
if (parentItem && parentItem.hasClass("predelete")) {
return false;
}
const $parentGroup = parentItem.closest(".djn-group");
const parentModel = $parentGroup.data("inlineModel");
const childModels = $parentGroup.djnData("childModels");
const currentModel = currentItem.data("inlineModel");
const isPolymorphicChild =
childModels && childModels.indexOf(currentModel) !== -1;
if (currentModel !== parentModel && !isPolymorphicChild) {
return false;
}
return true;
},
// fixedNestingDepth: not a standard ui.sortable parameter.
// Prevents dragging items up or down levels
fixedNestingDepth: true,
// The selector for ALL list containers in the nested sortable.
containerElementSelector: ".djn-items",
// The selector for ALL list items in the nested sortable.
listItemSelector: ".djn-item",
start: function (event, ui) {
ui.item.addClass("djn-item-dragging");
ui.item.show();
},
stop: function (event, ui) {
ui.item.removeClass("djn-item-dragging");
},
/**
* Triggered when a sortable is dropped into a container
*/
receive: function (event, ui) {
var $inline = $(this).closest(".djn-group");
$inline.djangoFormset().spliceInto(ui.item);
updatePositions(ui.item.djangoFormsetPrefix());
},
update: function (event, ui) {
// Ensure that <div class='djn-item djn-no-drag'/>
// is the first child of the djn-items. If there
// is another <div class='djn-item'/> before the
// .do-not-drag element then the drag-and-drop placeholder
// margins don't work correctly.
var $nextItem = ui.item.nextAll(".djn-item").first();
if ($nextItem.is(".djn-no-drag,.djn-thead")) {
var nextItem = $nextItem[0];
var parent = nextItem.parentNode;
parent.insertBefore(nextItem, parent.firstChild);
}
var groupId = $(event.target).closest(".djn-group").attr("id"),
$form = ui.item,
$parentGroup = $form.closest("#" + groupId);
if ($form.data("updateOperation") == "removed") {
$form.removeAttr("data-update-operation");
} else if (!$parentGroup.length) {
$form.attr("data-update-operation", "removed");
}
updatePositions($form.djangoFormsetPrefix());
$(document).trigger("djnesting:mutate", [
$("#" + $form.djangoFormsetPrefix() + "-group"),
]);
},
});
}
export { updatePositions, createSortable };

View File

@@ -0,0 +1,406 @@
/* globals SelectFilter, DateTimeShortcuts */
import $ from "jquery";
import "./jquery.djnutils";
import { createSortable, updatePositions } from "./sortable";
import regexQuote from "./regexquote";
import django$ from "./django$";
import grp$ from "./grp$";
var DJNesting = typeof window.DJNesting != "undefined" ? window.DJNesting : {};
DJNesting.regexQuote = regexQuote;
DJNesting.createSortable = createSortable;
DJNesting.updatePositions = updatePositions;
/**
* Update attributes based on a regular expression
*/
DJNesting.updateFormAttributes = function ($elem, search, replace, selector) {
if (!selector) {
selector = [
":input",
"span",
"table",
"iframe",
"label",
"a",
"ul",
"p",
"img",
".djn-group",
".djn-inline-form",
".cropduster-form",
".dal-forward-conf",
].join(",");
}
var addBackMethod = $.fn.addBack ? "addBack" : "andSelf";
$elem
.find(selector)
[addBackMethod]()
.each(function () {
var $node = $(this),
attrs = [
"id",
"name",
"for",
"href",
"class",
"onclick",
"data-inline-formset",
];
$.each(attrs, function (i, attrName) {
var attrVal = $node.attr(attrName);
if (attrVal) {
$node.attr(attrName, attrVal.replace(search, replace));
if (attrName === "data-inline-formset") {
$node.data("inlineFormset", JSON.parse($node.attr(attrName)));
}
}
});
});
// update prepopulate ids for function initPrepopulatedFields
$elem.find(".prepopulated_field").each(function () {
var $node = grp$(this);
if (typeof $node.prepopulate !== "function") {
$node = django$(this);
}
var dependencyIds = $.makeArray($node.data("dependency_ids") || []);
$node.data(
"dependency_ids",
$.map(dependencyIds, function (id) {
return id.replace(search, replace);
})
);
});
};
DJNesting.createContainerElement = function () {
return;
};
// Slight tweaks to the grappelli functions of the same name
// (initRelatedFields and initAutocompleteFields).
//
// The most notable tweak is the call to $.fn.grp_related_generic() (a
// jQuery method provided by django-curated) and the use of
// DJNesting.LOOKUP_URLS to determine the ajax lookup urls.
//
// We abstract this out using form prefix because the way grappelli does it
// (adding javascript at the bottom of each formset) doesn't really scale
// with nested formsets.
// The second parameter (groupData) is optional, and only exists to prevent
// redundant calls to jQuery() and jQuery.fn.data() in the calling context
DJNesting.initRelatedFields = function (prefix, groupData) {
if (
typeof DJNesting.LOOKUP_URLS != "object" ||
!DJNesting.LOOKUP_URLS.related
) {
return;
}
var lookupUrls = DJNesting.LOOKUP_URLS;
var $inline = $("#" + prefix + "-group");
if (!groupData) {
groupData = $inline.djnData();
}
var lookupFields = groupData.lookupRelated;
$inline.djangoFormsetForms().each(function (i, form) {
$.each(lookupFields.fk || [], function (i, fk) {
$(form)
.djangoFormField(fk)
.each(function () {
grp$(this).grp_related_fk({
lookup_url: lookupUrls.related,
});
});
});
$.each(lookupFields.m2m || [], function (i, m2m) {
$(form)
.djangoFormField(m2m)
.each(function () {
grp$(this).grp_related_m2m({ lookup_url: lookupUrls.m2m });
});
});
$.each(lookupFields.generic || [], function () {
var [contentType, objectId] = this;
$(form)
.djangoFormField(objectId)
.each(function () {
var $this = $(this);
var index = $this.djangoFormIndex();
if ($this.hasClass("grp-has-related-lookup")) {
$this.parent().find("a.related-lookup").remove();
$this.parent().find(".grp-placeholder-related-generic").remove();
}
grp$($this).grp_related_generic({
content_type: `#id_${prefix}-${index}-${contentType}`,
object_id: `#id_${prefix}-${index}-${objectId}`,
lookup_url: lookupUrls.related,
});
});
});
});
};
DJNesting.initAutocompleteFields = function (prefix, groupData) {
if (
typeof DJNesting.LOOKUP_URLS != "object" ||
!DJNesting.LOOKUP_URLS.related
) {
return;
}
var lookupUrls = DJNesting.LOOKUP_URLS;
var $inline = $("#" + prefix + "-group");
if (!groupData) {
groupData = $inline.djnData();
}
var lookupFields = groupData.lookupAutocomplete;
$inline.djangoFormsetForms().each(function (i, form) {
$.each(lookupFields.fk || [], function (i, fk) {
$(form)
.djangoFormField(fk)
.each(function () {
var $this = $(this),
id = $this.attr("id");
// An autocomplete widget has already been initialized, return
if ($("#" + id + "-autocomplete").length) {
return;
}
grp$($this).grp_autocomplete_fk({
lookup_url: lookupUrls.related,
autocomplete_lookup_url: lookupUrls.autocomplete,
});
});
});
$.each(lookupFields.m2m || [], function (i, m2m) {
$(form)
.djangoFormField(m2m)
.each(function () {
var $this = $(this),
id = $this.attr("id");
// An autocomplete widget has already been initialized, return
if ($("#" + id + "-autocomplete").length) {
return;
}
grp$($this).grp_autocomplete_m2m({
lookup_url: lookupUrls.m2m,
autocomplete_lookup_url: lookupUrls.autocomplete,
});
});
});
$.each(lookupFields.generic || [], function () {
var [contentType, objectId] = this;
$(form)
.djangoFormField(objectId)
.each(function () {
var $this = $(this);
var index = $this.djangoFormIndex();
// An autocomplete widget has already been initialized, return
if ($("#" + $this.attr("id") + "-autocomplete").length) {
return;
}
grp$($this).grp_autocomplete_generic({
content_type: `#id_${prefix}-${index}-${contentType}`,
object_id: `#id_${prefix}-${index}-${objectId}`,
lookup_url: lookupUrls.related,
autocomplete_lookup_url: lookupUrls.m2m,
});
});
});
});
};
function getLevelPrefix(id) {
return id
.replace(/^\#?id_/, "")
.split(/-(?:empty|__prefix__|\d+)-/g)
.slice(0, -1)
.join("-");
}
// I very much regret that these are basically copy-pasted from django's
// inlines.js, but they're hidden in closure scope so I don't have much choice.
DJNesting.DjangoInlines = {
initPrepopulatedFields: function (row) {
const formPrefix = row.djangoFormPrefix();
if (!formPrefix) return;
const fields = $("#django-admin-prepopulated-fields-constants").data(
"prepopulatedFields"
);
const fieldNames = new Set();
const fieldDependencies = {};
if (Array.isArray(fields)) {
fields.forEach(
({ id, name, dependency_list, maxLength, allowUnicode }) => {
fieldNames.add(name);
const levelPrefix = getLevelPrefix(id);
if (typeof fieldDependencies[levelPrefix] !== "object") {
fieldDependencies[levelPrefix] = {};
}
fieldDependencies[levelPrefix][name] = {
dependency_list,
maxLength,
allowUnicode,
};
}
);
fieldNames.forEach((name) => {
row
.find(`.form-row .field-${name}, .form-row.field-${name}`)
.each(function () {
const $el = $(this);
const prefix = $el.djangoFormPrefix();
if (!prefix) return;
const levelPrefix = getLevelPrefix(prefix);
const dep = (fieldDependencies[levelPrefix] || {})[name];
if (dep) {
$el.addClass("prepopulated_field");
const $field = $el.is(":input") ? $el : $el.find(":input");
$field.data("dependency_list", dep.dependency_list);
$field.data("maxLength", dep.maxLength);
$field.data("allowUnicode", dep.allowUnicode);
}
});
});
}
if (formPrefix.match(/__prefix__/)) return;
row.find(".prepopulated_field").each(function () {
var field = $(this),
input = field.is(":input") ? field : field.find(":input"),
$input = grp$(input),
inputFormPrefix = input.djangoFormPrefix(),
dependencyList = $input.data("dependency_list") || [],
dependencies = [];
if (!inputFormPrefix || inputFormPrefix.match(/__prefix__/)) return;
if (!dependencyList.length || !$input.prepopulate) {
$input = django$(input);
dependencyList = $input.data("dependency_list") || [];
}
$.each(dependencyList, function (i, fieldName) {
dependencies.push("#id_" + inputFormPrefix + fieldName);
});
if (dependencies.length) {
$input.prepopulate(
dependencies,
$input.data("maxLength") || $input.attr("maxlength"),
$input.data("allowUnicode")
);
}
});
},
reinitDateTimeShortCuts: function () {
// Reinitialize the calendar and clock widgets by force
if (typeof window.DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
},
updateSelectFilter: function ($form) {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof window.SelectFilter !== "undefined") {
$form.find(".selectfilter").each(function (index, value) {
var namearr = value.name.split("-");
SelectFilter.init(value.id, namearr[namearr.length - 1], false);
});
$form.find(".selectfilterstacked").each(function (index, value) {
var namearr = value.name.split("-");
SelectFilter.init(value.id, namearr[namearr.length - 1], true);
});
}
},
};
function patchSelectFilter() {
window.SelectFilter.init = (function (oldFn) {
return function init(field_id, field_name, is_stacked) {
if (field_id.match(/\-empty\-/)) {
return;
} else {
oldFn.apply(this, arguments);
}
};
})(window.SelectFilter.init);
}
if (typeof window.SelectFilter !== "undefined") {
patchSelectFilter();
} else {
setTimeout(function () {
if (typeof window.SelectFilter !== "undefined") {
patchSelectFilter();
}
}, 12);
}
const djangoFuncs = ["prepopulate", "djangoAdminSelect2"];
djangoFuncs.forEach((funcName) => {
(function patchDjangoFunction(callCount) {
if (callCount > 2) {
return;
}
if (typeof $.fn[funcName] === "undefined") {
return setTimeout(() => patchDjangoFunction(++callCount), 12);
}
$.fn[funcName] = (function (oldFn) {
return function django_fn_patch() {
return oldFn.apply(
this.filter(
':not([id*="-empty-"]):not([id$="-empty"]):not([id*="__prefix__"])'
),
arguments
);
};
})($.fn[funcName]);
})(0);
});
const grpFuncs = [
"grp_autocomplete_fk",
"grp_autocomplete_generic",
"grp_autocomplete_m2m",
"grp_collapsible",
"grp_collapsible_group",
"grp_inline",
"grp_related_fk",
"grp_related_generic",
"grp_related_m2m",
"grp_timepicker",
"datepicker",
"prepopulate",
"djangoAdminSelect2",
];
grpFuncs.forEach((funcName) => {
(function patchGrpFunction(callCount) {
if (callCount > 2) {
return;
}
if (
typeof window.grp === "undefined" ||
typeof window.grp.jQuery.fn[funcName] === "undefined"
) {
return setTimeout(() => patchGrpFunction(++callCount), 12);
}
window.grp.jQuery.fn[funcName] = (function (oldFn) {
return function grp_fn_patch() {
return oldFn.apply(
this.filter(
':not([id*="-empty-"]):not([id$="-empty"]):not([id*="__prefix__"])'
),
arguments
);
};
})(window.grp.jQuery.fn[funcName]);
})(0);
});
export default DJNesting;

View File

@@ -0,0 +1,390 @@
/* stylelint-disable no-descending-specificity */
@mixin cursor($val) {
cursor: $val;
cursor: -moz-$val;
cursor: -webkit-$val;
}
.djn-group .djn-group-nested {
float: none;
width: auto;
margin: 0 10px;
background: transparent;
}
.djn-group-nested.grp-stacked h2.djn-collapse-handler,
.djn-group-nested.grp-stacked > .grp-tools {
display: none;
}
.djn-group-nested {
border-color: transparent;
}
.grp-tools span.delete {
cursor: auto !important;
}
.djn-group-nested .djn-items .inline-related {
border: 1px solid transparent;
border-radius: 4px;
#grp-content & {
margin-bottom: 5px;
border: 1px solid #a7a7a7;
&.djn-item-dragging {
border: 0;
}
}
&:first-child {
margin-top: 0;
}
&.last-related {
margin-bottom: 0;
}
}
.djn-group-nested div.items .module:first-child {
margin-top: 0 !important;
}
.nested-placeholder,
.djn-group .ui-sortable-placeholder {
margin-bottom: 5px;
background: #9f9f9f !important;
}
.djn-group .ui-nestedsortable-error,
.djn-group .ui-nestedSortable-error {
background: #9f6464 !important;
}
.ui-sortable .grp-module.ui-sortable-placeholder.ui-nestedSortable-error {
background-color: #9f6464 !important;
}
.djn-items {
position: relative;
min-height: 0;
overflow: visible;
}
.djn-item {
overflow: visible;
}
.djn-item.djn-no-drag:first-child {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: -1;
height: 19px;
& + .djn-item.ui-sortable-helper,
& + .djn-item-dragging {
margin-top: 0;
}
}
.djn-item-dragging {
height: 0;
padding: 0;
margin: 0;
overflow: hidden;
border: 0;
}
.djn-tbody.djn-item-dragging {
display: none !important;
}
.djn-tbody.ui-sortable-placeholder td {
background: #fbfad0;
}
.djn-collapse-handler-verbose-name {
display: inline;
}
#grp-content .grp-tabular .grp-table .grp-tbody {
.grp-th,
.grp-td {
vertical-align: top;
overflow: visible;
}
.grp-tr > td.original:first-child {
width: 0;
padding: 0;
border: 0;
background: #eee;
}
.grp-tr.djn-has-inlines .grp-td {
border-bottom: 0 !important;
}
}
#grp-content .grp-tabular .grp-table .grp-thead .grp-th {
border-radius: 0;
border-top: 0;
border-bottom: 0;
line-height: 16px;
color: #aaa;
font-weight: bold;
}
#grp-content table.djn-table thead > tr > th {
font-size: 11px;
line-height: inherit;
}
#grp-content .grp-tabular .grp-table.djn-table .grp-thead > .grp-tr > .grp-th {
padding-top: 1px;
padding-bottom: 1px;
}
#grp-content
.grp-tabular
.grp-table.djn-table
.grp-thead
> .grp-tr
> .grp-th:last-of-type {
border-right: 0;
}
#grp-content
.grp-tabular
.grp-table.djn-table
.grp-tbody
> .grp-tr
> .grp-td:first-of-type {
border-left: 1px solid #d4d4d4 !important;
}
table.djn-table.grp-table td div.grp-readonly,
table.djn-table.grp-table th div.grp-readonly {
margin: 0 !important;
}
.grp-tabular.djn-tabular td.grp-td ul.errorlist {
margin: 0 !important;
}
table.djn-table.grp-table td div.grp-readonly:empty,
table.djn-table.grp-table th div.grp-readonly:empty {
margin-bottom: -5px !important;
}
table.djn-table.grp-table td > input[type="checkbox"],
table.djn-table.grp-table td > input[type="radio"],
table.djn-table.grp-table th > input[type="checkbox"],
table.djn-table.grp-table th > input[type="radio"] {
margin: 3px 0.5ex !important;
margin: revert !important;
}
table.djn-table.grp-table td > textarea,
table.djn-table.grp-table th > textarea {
margin: 0 !important;
}
// Grappelli is the absolute worst with !important
table.djn-table.grp-table td > input[type="text"],
table.djn-table.grp-table td > input[type="password"],
table.djn-table.grp-table td > input[type="url"],
table.djn-table.grp-table td > input[type="email"],
table.djn-table.grp-table td > input[type="number"],
table.djn-table.grp-table td > input[type="button"],
table.djn-table.grp-table td > select,
table.djn-table.grp-table td p input[type="text"],
table.djn-table.grp-table td p input[type="url"],
table.djn-table.grp-table td p input[type="email"],
table.djn-table.grp-table td p input[type="number"],
table.djn-table.grp-table td p > input[type="button"],
table.djn-table.grp-table th > input[type="text"],
table.djn-table.grp-table th > input[type="password"],
table.djn-table.grp-table th > input[type="url"],
table.djn-table.grp-table th > input[type="email"],
table.djn-table.grp-table th > input[type="number"],
table.djn-table.grp-table th > input[type="button"],
table.djn-table.grp-table th > select,
table.djn-table.grp-table th p input[type="text"],
table.djn-table.grp-table th p input[type="url"],
table.djn-table.grp-table th p input[type="email"],
table.djn-table.grp-table th p input[type="number"],
table.djn-table.grp-table th p > input[type="button"] {
vertical-align: middle;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.djn-empty-form {
&,
& * {
display: none !important;
}
}
// Django (sans grappelli) specific styles
#content.colM {
.inline-group .tabular .ui-sortable-placeholder tr.has_original td {
padding: 1px;
}
.inline-group.djn-group ul.tools {
height: 0;
}
.djn-item.module {
margin-bottom: 0;
}
tr.djn-has-inlines td {
border-bottom: 1px solid #fff;
}
td.original {
width: 0;
padding: 2px 0 0 0;
}
td.original.is-sortable {
position: relative;
width: 15px;
}
td.original.is-sortable .djn-drag-handler {
position: absolute;
top: 4px;
left: 0;
display: block;
width: 10px;
height: 20px;
margin: 5px;
cursor: move;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAyCAYAAABcfPsmAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAE5JREFUeNrs0zEKACAMBEEDST6X9+Z92lpYHNgI7naBK9KMdfcch6rK9lvdeWYOJXXnESEN1d2HH9J9hhSkEFKQQkhBClKQghSkPNYSYADFZiuygfao+AAAAABJRU5ErkJggg==")
no-repeat top left;
background-size: 10px 25px;
@include cursor(grab);
}
// (Optional) Apply a "closed-hand" cursor during drag operation.
td.original.is-sortable .djn-drag-handler:active {
@include cursor(grabbing);
}
td.original.is-sortable p + .djn-drag-handler {
top: 20px;
}
td.original.is-sortable p {
top: 0;
left: 19px;
white-space: nowrap;
}
fieldset.has-inlines > .djn-form-row-last {
border-bottom: 0;
}
}
// polymorphic
.polymorphic-add-choice {
.grp-tools {
overflow: visible;
}
.grp-tools li {
float: none;
}
.grp-tools li:first-child,
.grp-tools li:last-child {
padding: 4px 8px;
}
.grp-tools a {
width: auto;
height: auto;
}
.grp-tools > li > a {
min-width: 24px;
min-height: 24px;
}
.grp-tools .polymorphic-type-menu {
right: 0.5em;
left: auto;
}
}
.grp-tools.grp-related-widget-tools a.add-another {
top: 0;
margin: 0;
}
.grp-td > .grp-related-widget-wrapper .grp-related-widget-tools {
overflow: visible;
display: flex;
}
.select2-container + .grp-tools.grp-related-widget-tools {
position: relative;
right: 0;
}
#grp-content .grp-group > .grp-items > .grp-module > .grp-tabular {
background: #fff;
border: 2px solid #ccc;
margin-bottom: 5px;
&::after {
content: "";
display: block;
clear: both;
}
}
table.grp-table.djn-table td.djn-td > input[type="text"],
table.grp-table.djn-table td.djn-td > input[type="password"],
table.grp-table.djn-table td.djn-td > input[type="url"],
table.grp-table.djn-table td.djn-td > input[type="email"],
table.grp-table.djn-table td.djn-td > input[type="number"],
table.grp-table.djn-table td.djn-td > input[type="button"],
table.grp-table.djn-table td.djn-td > select,
table.grp-table.djn-table td.djn-td p input[type="text"],
table.grp-table.djn-table td.djn-td p input[type="url"],
table.grp-table.djn-table td.djn-td p input[type="email"],
table.grp-table.djn-table td.djn-td p input[type="number"],
table.grp-table.djn-table td.djn-td p > input[type="button"],
table.grp-table.djn-table td.djn-td div.grp-related-widget-wrapper,
table.grp-table.djn-table th.djn-th > input[type="text"],
table.grp-table.djn-table th.djn-th > input[type="password"],
table.grp-table.djn-table th.djn-th > input[type="url"],
table.grp-table.djn-table th.djn-th > input[type="email"],
table.grp-table.djn-table th.djn-th > input[type="number"],
table.grp-table.djn-table th.djn-th > input[type="button"],
table.grp-table.djn-table th.djn-th > select,
table.grp-table.djn-table th.djn-th p input[type="text"],
table.grp-table.djn-table th.djn-th p input[type="url"],
table.grp-table.djn-table th.djn-th p input[type="email"],
table.grp-table.djn-table th.djn-th p input[type="number"],
table.grp-table.djn-table th.djn-th p > input[type="button"],
table.grp-table.djn-table th.djn-th div.grp-related-widget-wrapper {
vertical-align: baseline;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
table.grp-table.djn-table td.djn-td a.fb_show,
table.grp-table.djn-table td.djn-td a.related-lookup,
table.grp-table.djn-table td.djn-td .ui-datepicker-trigger,
table.grp-table.djn-table td.djn-td .ui-timepicker-trigger,
table.grp-table.djn-table th.djn-th a.fb_show,
table.grp-table.djn-table th.djn-th a.related-lookup,
table.grp-table.djn-table th.djn-th .ui-datepicker-trigger,
table.grp-table.djn-table th.djn-th .ui-timepicker-trigger {
margin: 0 0 0 -25px !important;
}
table.grp-table.djn-table td.djn-td .grp-autocomplete-wrapper-m2m,
table.grp-table.djn-table td.djn-td .grp-autocomplete-wrapper-fk,
table.grp-table.djn-table th.djn-th .grp-autocomplete-wrapper-m2m,
table.grp-table.djn-table th.djn-th .grp-autocomplete-wrapper-fk {
margin: 0 !important;
}
table.grp-table.djn-table td.djn-td > input[type="file"],
table.grp-table.djn-table td.djn-td > input[type="checkbox"],
table.grp-table.djn-table td.djn-td > input[type="radio"],
table.grp-table.djn-table td.djn-td > select,
table.grp-table.djn-table td.djn-td p input[type="text"],
table.grp-table.djn-table th.djn-th > input[type="file"],
table.grp-table.djn-table th.djn-th > input[type="checkbox"],
table.grp-table.djn-table th.djn-th > input[type="radio"],
table.grp-table.djn-table th.djn-th > select,
table.grp-table.djn-table th.djn-th p input[type="text"] {
margin-top: 0 !important;
margin-bottom: 0 !important;
}