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

347
nested_admin/dist/nested_admin.css vendored Normal file
View File

@@ -0,0 +1,347 @@
/*!***********************************************************************************************************************************************************************************************************************!*\
!*** css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].use[2]!./node_modules/sass-loader/dist/cjs.js!./nested_admin/static/nested_admin/src/nested_admin.scss ***!
\***********************************************************************************************************************************************************************************************************************/
/* stylelint-disable no-descending-specificity */
.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 .djn-group-nested .djn-items .inline-related {
margin-bottom: 5px;
border: 1px solid #a7a7a7; }
#grp-content .djn-group-nested .djn-items .inline-related.djn-item-dragging {
border: 0; }
.djn-group-nested .djn-items .inline-related:first-child {
margin-top: 0; }
.djn-group-nested .djn-items .inline-related.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.djn-no-drag:first-child + .djn-item.ui-sortable-helper,
.djn-item.djn-no-drag:first-child + .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-content .grp-tabular .grp-table .grp-tbody .grp-td {
vertical-align: top;
overflow: visible; }
#grp-content .grp-tabular .grp-table .grp-tbody .grp-tr > td.original:first-child {
width: 0;
padding: 0;
border: 0;
background: #eee; }
#grp-content .grp-tabular .grp-table .grp-tbody .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; }
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,
.djn-empty-form * {
display: none !important; }
#content.colM .inline-group .tabular .ui-sortable-placeholder tr.has_original td {
padding: 1px; }
#content.colM .inline-group.djn-group ul.tools {
height: 0; }
#content.colM .djn-item.module {
margin-bottom: 0; }
#content.colM tr.djn-has-inlines td {
border-bottom: 1px solid #fff; }
#content.colM td.original {
width: 0;
padding: 2px 0 0 0; }
#content.colM td.original.is-sortable {
position: relative;
width: 15px; }
#content.colM 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;
cursor: -webkit-grab;
cursor: grab;
cursor: -moz- -webkit-grab;
cursor: -moz- grab;
cursor: -webkit- -webkit-grab;
cursor: -webkit- grab; }
#content.colM td.original.is-sortable .djn-drag-handler:active {
cursor: -webkit-grabbing;
cursor: grabbing;
cursor: -moz- -webkit-grabbing;
cursor: -moz- grabbing;
cursor: -webkit- -webkit-grabbing;
cursor: -webkit- grabbing; }
#content.colM td.original.is-sortable p + .djn-drag-handler {
top: 20px; }
#content.colM td.original.is-sortable p {
top: 0;
left: 19px;
white-space: nowrap; }
#content.colM fieldset.has-inlines > .djn-form-row-last {
border-bottom: 0; }
.polymorphic-add-choice .grp-tools {
overflow: visible; }
.polymorphic-add-choice .grp-tools li {
float: none; }
.polymorphic-add-choice .grp-tools li:first-child,
.polymorphic-add-choice .grp-tools li:last-child {
padding: 4px 8px; }
.polymorphic-add-choice .grp-tools a {
width: auto;
height: auto; }
.polymorphic-add-choice .grp-tools > li > a {
min-width: 24px;
min-height: 24px; }
.polymorphic-add-choice .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: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
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; }
#grp-content .grp-group > .grp-items > .grp-module > .grp-tabular::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; }
/*# sourceMappingURL=nested_admin.css.map*/

File diff suppressed because one or more lines are too long

7129
nested_admin/dist/nested_admin.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
nested_admin/dist/nested_admin.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
nested_admin/dist/nested_admin.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
/*!
* jQuery UI Sortable @VERSION
* http://jqueryui.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/sortable/
*
* Depends:
* jquery.ui.core.js
* jquery.ui.mouse.js
* jquery.ui.widget.js
*/

File diff suppressed because one or more lines are too long

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;
}