OIDC Integration: Supports Authentik officially, other solutions currently untested. Social login not implemented

This commit is contained in:
boris committed 2026-03-30 13:38:18 +01:00
1 parent dfea7e869b
commit 68bbb53976
28 files changed
+2365 -751

No files matched your search

@@ -79,9 +79,7 @@
// ".view-related, .change-related, .delete-related"
// );
const siblings = $this
.closest(".related-widget-wrapper")
.find(".view-related, .change-related, .delete-related");
const siblings = $("body").find('#' + $this.closest(".related-widget-wrapper").data("id")).find(".view-related, .change-related, .delete-related");
if (!siblings.length) {
return;
+320 -460
View File
@@ -15,485 +15,345 @@
* Licensed under the New BSD License
* See: https://opensource.org/licenses/bsd-license.php
*/
"use strict";
'use strict';
{
const $ = django.jQuery;
$.fn.formset = function (opts) {
const options = $.extend({}, $.fn.formset.defaults, opts);
const $this = $(this);
const $parent = $this.parent();
const updateElementIndex = function (el, prefix, ndx) {
const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
const replacement = prefix + "-" + ndx;
if ($(el).prop("for")) {
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
// !CHANGED from original
// el.name = el.name.replace(id_regex, replacement);
el.setAttribute("name", el.name.replace(id_regex, replacement));
}
};
const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop(
"autocomplete",
"off"
);
let nextIndex = parseInt(totalForms.val(), 10);
const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop(
"autocomplete",
"off"
);
const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop(
"autocomplete",
"off"
);
let addButton;
const $ = django.jQuery;
$.fn.formset = function(opts) {
const options = $.extend({}, $.fn.formset.defaults, opts);
const $this = $(this);
const $parent = $this.parent();
const updateElementIndex = function(el, prefix, ndx) {
const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
const replacement = prefix + "-" + ndx;
if ($(el).prop("for")) {
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
};
const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
let nextIndex = parseInt(totalForms.val(), 10);
const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off");
let addButton;
/**
* The "Add another MyModel" button below the inline forms.
*/
const addInlineAddButton = function () {
if (addButton === null) {
if ($this.prop("tagName") === "TR") {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
const numCols = $this.eq(-1).children().length;
$parent.append(
'<tr class="' +
options.addCssClass +
'"><td colspan="' +
numCols +
'"><a href="#">' +
options.addText +
"</a></tr>"
);
addButton = $parent.find("tr:last a");
/**
* The "Add another MyModel" button below the inline forms.
*/
const addInlineAddButton = function() {
if (addButton === null) {
if ($this.prop("tagName") === "TR") {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
const numCols = $this.eq(-1).children().length;
$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></tr>");
addButton = $parent.find("tr:last a");
} else {
// Otherwise, insert it immediately after the last form:
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></div>");
addButton = $this.filter(":last").next().find("a");
}
}
addButton.on('click', addInlineClickHandler);
};
const addInlineClickHandler = function(e) {
e.preventDefault();
const template = $("#" + options.prefix + "-empty");
const row = template.clone(true);
row.removeClass(options.emptyCssClass)
.addClass(options.formCssClass)
.attr("id", options.prefix + "-" + nextIndex);
addInlineDeleteButton(row);
row.find("*").each(function() {
updateElementIndex(this, options.prefix, totalForms.val());
});
// Insert the new form when it has been fully edited.
row.insertBefore($(template));
// Update number of total forms.
$(totalForms).val(parseInt(totalForms.val(), 10) + 1);
nextIndex += 1;
// Hide the add button if there's a limit and it's been reached.
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
addButton.parent().hide();
}
// Show the remove buttons if there are more than min_num.
toggleDeleteButtonVisibility(row.closest('.inline-group'));
// Pass the new form to the post-add callback, if provided.
if (options.added) {
options.added(row);
}
row.get(0).dispatchEvent(new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: options.prefix
}
}));
};
/**
* The "X" button that is part of every unsaved inline.
* (When saved, it is replaced with a "Delete" checkbox.)
*/
const addInlineDeleteButton = function(row) {
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(":last").append('<div><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.children(":first").append('<span><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
}
// Add delete handler for each row.
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));
};
const inlineDeleteHandler = function(e1) {
e1.preventDefault();
const deleteButton = $(e1.target);
const row = deleteButton.closest('.' + options.formCssClass);
const inlineGroup = row.closest('.inline-group');
// Remove the parent form containing this button,
// and also remove the relevant row with non-field errors:
const prevRow = row.prev();
if (prevRow.length && prevRow.hasClass('row-form-errors')) {
prevRow.remove();
}
row.remove();
nextIndex -= 1;
// Pass the deleted form to the post-delete callback, if provided.
if (options.removed) {
options.removed(row);
}
document.dispatchEvent(new CustomEvent("formset:removed", {
detail: {
formsetName: options.prefix
}
}));
// Update the TOTAL_FORMS form count.
const forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once below maximum number.
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
addButton.parent().show();
}
// Hide the remove buttons if at min_num.
toggleDeleteButtonVisibility(inlineGroup);
// Also, update names and ids for all remaining form controls so
// they remain in sequence:
let i, formCount;
const updateElementCallback = function() {
updateElementIndex(this, options.prefix, i);
};
for (i = 0, formCount = forms.length; i < formCount; i++) {
updateElementIndex($(forms).get(i), options.prefix, i);
$(forms.get(i)).find("*").each(updateElementCallback);
}
};
const toggleDeleteButtonVisibility = function(inlineGroup) {
if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) {
inlineGroup.find('.inline-deletelink').hide();
} else {
inlineGroup.find('.inline-deletelink').show();
}
};
$this.each(function(i) {
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
});
// Create the delete buttons for all unsaved inlines:
$this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() {
addInlineDeleteButton($(this));
});
toggleDeleteButtonVisibility($this);
// Create the add button, initially hidden.
addButton = options.addButton;
addInlineAddButton();
// Show the add button if allowed to add more items.
// Note that max_num = None translates to a blank string.
const showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
if ($this.length && showAddButton) {
addButton.parent().show();
} else {
// Otherwise, insert it immediately after the last form:
$this
.filter(":last")
.after(
'<div class="' +
options.addCssClass +
'"><a href="#">' +
options.addText +
"</a></div>"
);
addButton = $this.filter(":last").next().find("a");
addButton.parent().hide();
}
}
addButton.on("click", addInlineClickHandler);
return this;
};
const addInlineClickHandler = function (e) {
e.preventDefault();
const template = $("#" + options.prefix + "-empty");
const row = template.clone(true);
row
.removeClass(options.emptyCssClass)
.addClass(options.formCssClass)
.attr("id", options.prefix + "-" + nextIndex);
addInlineDeleteButton(row);
row.find("*").each(function () {
updateElementIndex(this, options.prefix, totalForms.val());
});
// Insert the new form when it has been fully edited.
// !CHANGED from original
if ($(template).parent().is("tbody")) {
row
.wrap('<tbody class="template"></tbody>')
.parent()
.insertBefore($(template).parent());
} else {
row.insertBefore($(template));
}
// Update number of total forms.
$(totalForms).val(parseInt(totalForms.val(), 10) + 1);
nextIndex += 1;
// Hide the add button if there's a limit and it's been reached.
if (maxForms.val() !== "" && maxForms.val() - totalForms.val() <= 0) {
addButton.parent().hide();
}
// Show the remove buttons if there are more than min_num.
toggleDeleteButtonVisibility(row.closest(".inline-group"));
// Pass the new form to the post-add callback, if provided.
if (options.added) {
options.added(row);
}
row.get(0).dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: options.prefix,
},
})
);
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: "form", // The form prefix for your django formset
addText: "add another", // Text for the add link
deleteText: "remove", // Text for the delete link
addCssClass: "add-row", // CSS class applied to the add link
deleteCssClass: "delete-row", // CSS class applied to the delete link
emptyCssClass: "empty-row", // CSS class applied to the empty row
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
addButton: null // Existing add button to use
};
/**
* The "X" button that is part of every unsaved inline.
* (When saved, it is replaced with a "Delete" checkbox.)
*/
const addInlineDeleteButton = function (row) {
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row
.children(":last")
.append(
'<div><a class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></div>"
);
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append(
'<li><a class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></li>"
);
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row
.children(":first")
.append(
'<span><a class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></span>"
);
}
// Add delete handler for each row.
row
.find("a." + options.deleteCssClass)
.on("click", inlineDeleteHandler.bind(this));
};
const inlineDeleteHandler = function (e1) {
e1.preventDefault();
const deleteButton = $(e1.target);
const row = deleteButton.closest("." + options.formCssClass);
const inlineGroup = row.closest(".inline-group");
// Remove the parent form containing this button,
// and also remove the relevant row with non-field errors:
const prevRow = row.prev();
if (prevRow.length && prevRow.hasClass("row-form-errors")) {
prevRow.remove();
}
// Tabular inlines ---------------------------------------------------------
$.fn.tabularFormset = function(selector, options) {
const $rows = $(this);
// !CHANGED from original
if (deleteButton.parent().parent().parent().parent().is("tbody")) {
row.parent().remove();
} else {
row.remove();
}
nextIndex -= 1;
// Pass the deleted form to the post-delete callback, if provided.
if (options.removed) {
options.removed(row);
}
document.dispatchEvent(
new CustomEvent("formset:removed", {
detail: {
formsetName: options.prefix,
},
})
);
// Update the TOTAL_FORMS form count.
const forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once below maximum number.
if (maxForms.val() === "" || maxForms.val() - forms.length > 0) {
addButton.parent().show();
}
// Hide the remove buttons if at min_num.
toggleDeleteButtonVisibility(inlineGroup);
// Also, update names and ids for all remaining form controls so
// they remain in sequence:
let i, formCount;
const updateElementCallback = function () {
updateElementIndex(this, options.prefix, i);
};
for (i = 0, formCount = forms.length; i < formCount; i++) {
updateElementIndex($(forms).get(i), options.prefix, i);
$(forms.get(i)).find("*").each(updateElementCallback);
}
};
const reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const toggleDeleteButtonVisibility = function (inlineGroup) {
if (minForms.val() !== "" && minForms.val() - totalForms.val() >= 0) {
inlineGroup.find(".inline-deletelink").hide();
} else {
inlineGroup.find(".inline-deletelink").show();
}
};
const updateSelectFilter = function() {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter !== 'undefined') {
$('.selectfilter').each(function(index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$('.selectfilterstacked').each(function(index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
// !CHANGED from original. Business logic for tabular inlines is different.
if ($this.parent().is("tbody")) {
$this
.parent()
.parent()
.find("tr.form-row")
.each(function (i) {
$(this)
.not("." + options.emptyCssClass)
.addClass(options.formCssClass);
const initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
const field = $(this),
input = field.find('input, select, textarea'),
dependency_list = input.data('dependency_list') || [],
dependencies = [];
$.each(dependency_list, function(i, field_name) {
dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
added: function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
},
addButton: options.addButton
});
} else {
$this.each(function (i) {
$(this)
.not("." + options.emptyCssClass)
.addClass(options.formCssClass);
});
}
// Create the delete buttons for all unsaved inlines:
// !CHANGED from original, added parent() and used find() instead of filter()
$this
.parent()
.parent()
.find(
"." +
options.formCssClass +
":not(.has_original):not(." +
options.emptyCssClass +
")"
)
.each(function () {
addInlineDeleteButton($(this));
});
toggleDeleteButtonVisibility($this);
// Create the add button, initially hidden.
addButton = options.addButton;
addInlineAddButton();
// Show the add button if allowed to add more items.
// Note that max_num = None translates to a blank string.
const showAddButton =
maxForms.val() === "" || maxForms.val() - totalForms.val() > 0;
if ($this.length && showAddButton) {
addButton.parent().show();
} else {
addButton.parent().hide();
}
return this;
};
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: "form", // The form prefix for your django formset
addText: "add another", // Text for the add link
deleteText: "remove", // Text for the delete link
addCssClass: "add-row", // CSS class applied to the add link
deleteCssClass: "delete-row", // CSS class applied to the delete link
emptyCssClass: "empty-row", // CSS class applied to the empty row
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
addButton: null, // Existing add button to use
};
// Tabular inlines ---------------------------------------------------------
$.fn.tabularFormset = function (selector, options, callback = null) {
const $rows = $(this);
const reinitDateTimeShortCuts = function () {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
return $rows;
};
const updateSelectFilter = function () {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
// Stacked inlines ---------------------------------------------------------
$.fn.stackedFormset = function(selector, options) {
const $rows = $(this);
const updateInlineLabel = function(row) {
$(selector).find(".inline_label").each(function(i) {
const count = i + 1;
$(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
});
};
const reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const updateSelectFilter = function() {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function(index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$(".selectfilterstacked").each(function(index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
const initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
const field = $(this),
input = field.find('input, select, textarea'),
dependency_list = input.data('dependency_list') || [],
dependencies = [];
$.each(dependency_list, function(i, field_name) {
// Dependency in a fieldset.
let field_element = row.find('.form-row .field-' + field_name);
// Dependency without a fieldset.
if (!field_element.length) {
field_element = row.find('.form-row.field-' + field_name);
}
dependencies.push('#' + field_element.find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
},
addButton: options.addButton
});
$(".selectfilterstacked").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
return $rows;
};
const initPrepopulatedFields = function (row) {
row.find(".prepopulated_field").each(function () {
const field = $(this),
input = field.find("input, select, textarea"),
dependency_list = input.data("dependency_list") || [],
dependencies = [];
$.each(dependency_list, function (i, field_name) {
dependencies.push(
"#" +
row
.find(".field-" + field_name)
.find("input, select, textarea")
.attr("id")
);
$(document).ready(function() {
$(".js-inline-admin-formset").each(function() {
const data = $(this).data(),
inlineOptions = data.inlineFormset;
let selector;
switch(data.inlineType) {
case "stacked":
selector = inlineOptions.name + "-group .inline-related";
$(selector).stackedFormset(selector, inlineOptions.options);
break;
case "tabular":
selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row";
$(selector).tabularFormset(selector, inlineOptions.options);
break;
}
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr("maxlength"));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
added: function (row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
},
addButton: options.addButton,
});
if (typeof callback === "function") {
callback();
}
return $rows;
};
// Stacked inlines ---------------------------------------------------------
$.fn.stackedFormset = function (selector, options, callback = null) {
const $rows = $(this);
const updateInlineLabel = function (row) {
$(selector)
.find(".inline_label")
.each(function (i) {
const count = i + 1;
$(this).html(
$(this)
.html()
.replace(/(#\d+)/g, "#" + count)
);
});
};
const reinitDateTimeShortCuts = function () {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const updateSelectFilter = function () {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$(".selectfilterstacked").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
const initPrepopulatedFields = function (row) {
row.find(".prepopulated_field").each(function () {
const field = $(this),
input = field.find("input, select, textarea"),
dependency_list = input.data("dependency_list") || [],
dependencies = [];
$.each(dependency_list, function (i, field_name) {
// Dependency in a fieldset.
let field_element = row.find(".form-row .field-" + field_name);
// Dependency without a fieldset.
if (!field_element.length) {
field_element = row.find(".form-row.field-" + field_name);
}
dependencies.push(
"#" + field_element.find("input, select, textarea").attr("id")
);
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr("maxlength"));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: function (row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
},
addButton: options.addButton,
});
if (typeof callback === "function") {
callback();
}
return $rows;
};
$(window).on("htmx:afterSettle", function (event) {
if (event.target.classList.contains("js-inline-admin-formset")) {
initInlines($(event.target), function () {
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
$(event.target).find(".admin-autocomplete").djangoAdminSelect2();
});
}
});
$(document).ready(function () {
$(".js-inline-admin-formset").each(function () {
initInlines(this);
});
});
function initInlines(el, callback = null) {
const data = $(el).data(),
inlineOptions = data.inlineFormset;
let selector;
switch (data.inlineType) {
case "stacked":
selector = inlineOptions.name + "-group .inline-related";
$(selector).stackedFormset(selector, inlineOptions.options, callback);
break;
case "tabular":
selector =
inlineOptions.name +
"-group .tabular.inline-related tbody:last > tr.form-row";
$(selector).tabularFormset(selector, inlineOptions.options, callback);
break;
}
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-text-shadow-color:initial;--tw-text-shadow-alpha:100%;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Inter",sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-600:oklch(66.6% .179 58.318);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-100:oklch(96.2% .044 156.743);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--leading-normal:1.5;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-xs:4px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary-50:var(--color-primary-50);--color-primary-100:var(--color-primary-100);--color-primary-200:var(--color-primary-200);--color-primary-300:var(--color-primary-300);--color-primary-400:var(--color-primary-400);--color-primary-500:var(--color-primary-500);--color-primary-600:var(--color-primary-600);--color-primary-700:var(--color-primary-700);--color-primary-800:var(--color-primary-800);--color-primary-900:var(--color-primary-900);--color-primary-950:var(--color-primary-950);--color-base-50:var(--color-base-50);--color-base-100:var(--color-base-10Line truncated
/*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-text-shadow-color:initial;--tw-text-shadow-alpha:100%;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Inter", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--leading-normal:1.5;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--animate-spin:Line truncated
@@ -1 +1,11 @@
(()=>{function le(o,t){var e=Object.keys(o);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(o);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(o,i).enumerable})),e.push.apply(e,n)}return e}function K(o){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?arguments[t]:{};t%2?le(Object(e),!0).forEach(function(n){Fe(o,n,e[n])}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(e)):le(Object(e)).forEach(function(n){Object.defineProperty(o,n,Object.getOwnPropertyDescriptor(e,n))})}return o}function Pt(o){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Pt=function(t){return typeof t}:Pt=function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Pt(o)}function Fe(o,t,e){return t in o?Object.defineProperty(o,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):o[t]=e,o}function U(){return U=Object.assign||function(o){for(var t=1;t<arguments.length;t++){var e=arguments[t];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(o[n]=e[n])}return o},U.apply(this,arguments)}function Re(o,t){if(o==null)return{};var e={},n=Object.keys(o),i,r;for(r=0;r<n.length;r++)i=n[r],!(t.indexOf(i)>=0)&&(e[i]=o[i]);return e}function ke(o,t){if(o==null)return{};var e=Re(o,t),n,i;if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(o);for(i=0;i<r.length;i++)n=r[i],!(t.indexOf(n)>=0)&&Object.prototype.propertyIsEnumerable.call(o,n)&&(e[n]=o[n])}return e}var He="1.15.2";function $(o){if(typeof window<"u"&&window.navigator)return!!navigator.userAgent.match(o)}var q=$(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),Ot=$(/Edge/i),se=$(/firefox/i),Et=$(/safari/i)&&!$(/chrome/i)&&!$(/android/i),me=$(/iP(ad|od|hone)/i),ve=$(/chrome/i)&&$(/android/i),be={capture:!1,passive:!1};function y(o,t,e){o.addEventListener(t,e,!q&&be)}function b(o,t,e){o.removeEventListener(t,e,!q&&be)}function Xt(o,t){if(t){if(t[0]===">"&&(t=t.substring(1)),o)try{if(o.matches)return o.matches(t);if(o.msMatchesSelector)return o.msMatchesSelector(t);if(o.webkitMatchesSelector)return o.webkitMatchesSelector(t)}catch{return!1}return!1}}function Xe(o){return o.host&&o!==document&&o.host.nodeType?o.host:o.parentNode}function G(o,t,e,n){if(o){e=e||document;do{if(t!=null&&(t[0]===">"?o.parentNode===e&&Xt(o,t):Xt(o,t))||n&&o===e)return o;if(o===e)break}while(o=Xe(o))}return null}var ue=/\s+/g;function F(o,t,e){if(o&&t)if(o.classList)o.classList[e?"add":"remove"](t);else{var n=(" "+o.className+" ").replace(ue," ").replace(" "+t+" "," ");o.className=(n+(e?" "+t:"")).replace(ue," ")}}function h(o,t,e){var n=o&&o.style;if(n){if(e===void 0)return document.defaultView&&document.defaultView.getComputedStyle?e=document.defaultView.getComputedStyle(o,""):o.currentStyle&&(e=o.currentStyle),t===void 0?e:e[t];!(t in n)&&t.indexOf("webkit")===-1&&(t="-webkit-"+t),n[t]=e+(typeof e=="string"?"":"px")}}function ct(o,t){var e="";if(typeof o=="string")e=o;else do{var n=h(o,"transform");n&&n!=="none"&&(e=n+" "+e)}while(!t&&(o=o.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(e)}function ye(o,t,e){if(o){var n=o.getElementsByTagName(t),i=0,r=n.length;if(e)for(;i<r;i++)e(n[i],i);return n}return[]}function W(){var o=document.scrollingElement;return o||document.documentElement}function O(o,t,e,n,i){if(!(!o.getBoundingClientRect&&o!==window)){var r,a,l,s,u,d,c;if(o!==window&&o.parentNode&&o!==W()?(r=o.getBoundingClientRect(),a=r.top,l=r.left,s=r.bottom,u=r.right,d=r.height,c=r.width):(a=0,l=0,s=window.innerHeight,u=window.innerWidth,d=window.innerHeight,c=window.innerWidth),(t||e)&&o!==window&&(i=i||o.parentNode,!q))do if(i&&i.getBoundingClientRect&&(h(i,"transform")!=="none"||e&&h(i,"position")!=="static")){var m=i.getBoundingClientRect();a-=m.top+parseInt(h(i,"border-top-width")),l-=m.left+parseInt(h(i,"border-left-width")),s=a+r.height,u=l+r.width;break}while(i=i.parentNode);if(n&&o!==window){var w=ct(i||o),v=w&&w.a,E=w&&w.d;w&&(a/=E,l/=v,c/=v,d/=E,s=a+d,u=l+c)}return{top:a,left:l,bottom:s,right:u,width:c,height:d}}}function fe(o,t,e){for(var n=tt(o,!0),i=O(o)[t];n;){var r=O(n)[e],a=void 0;if(e==="top"||e==="left"?a=i>=r:a=i<=r,!a)return n;if(n===W())break;n=tt(n,!1)}return!1}function dt(o,t,e,n){for(var i=0,r=0,a=o.children;r<a.length;){if(a[r].style.display!=="none"&&a[r]!==p.ghost&&(n||a[r]!==p.dragged)&&G(a[r],e.draggable,o,!1)){if(i===t)return a[r];i++}r++}return null}function oe(o,t){for(var e=o.lastElementChild;e&&(e===p.ghost||h(e,"display")==="none"||t&&!Xt(e,t));)e=e.previousElementSibling;return e||null}function H(o,t){var e=0;if(!o||!o.parentNode)return-1;for(;o=o.previousElementSibling;)o.nodeName.toUpperCase()!=="TEMPLATE"&&o!==p.clone&&(!t||Xt(o,t))&&e++;return e}function ce(o){var t=0,e=0,n=W();if(o)do{var i=ct(o),r=i.a,a=i.d;t+=o.scrollLeft*r,e+=o.scrollTop*a}while(o!==n&&(o=o.parentNode));return[t,e]}function Ye(o,t){for(var e inLine truncated
/*! Bundled license information:
sortablejs/modular/sortable.esm.js:
(**!
* Sortable 1.15.2
* @author RubaXa <trash@rubaxa.org>
* @author owenm <owen23355@gmail.com>
* @license MIT
*)
*/
+354 -23
View File
@@ -9,11 +9,49 @@ window.addEventListener("load", (e) => {
warnWithoutSaving();
inlines();
tabNavigation();
scrollSidebarNav();
});
/*************************************************************
* Switch theme
*************************************************************/
function theme(defaultTheme = "auto") {
return {
adminTheme: Alpine.$persist(defaultTheme).as('adminTheme'),
switchTheme(theme) {
this.adminTheme = theme;
},
themeBindings: {
['x-bind:class']() {
if (this.adminTheme === 'dark' || (this.adminTheme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
return 'dark';
}
if (this.adminTheme === 'light') {
return 'light';
}
return '';
},
['x-on:keydown.window'](event) {
if ((event.metaKey || event.ctrlKey) && event.key === "e") {
event.preventDefault();
if (this.adminTheme == "light") {
this.adminTheme = "dark";
} else {
this.adminTheme = "light";
}
}
}
}
}
}
/*************************************************************
* Scroll sidebar to active item
*************************************************************/
@@ -110,18 +148,36 @@ function tabNavigation() {
/*************************************************************
* Alpine.sort.js callback after sorting
*************************************************************/
const sortRecords = (e) => {
const orderingField = e.from.dataset.orderingField;
function sortRecords(e) {
e.from
.querySelectorAll(`.form-group.original`)
.forEach(function (row, index) {
input = row.querySelector(
`input[name$=-${e.from.dataset.orderingField}]`
);
input.value = index;
});
const weightInputs = Array.from(
e.from.querySelectorAll(
`.has_original input[name$=-${orderingField}], td.field-${orderingField} input[name$=-${orderingField}]`
e.from
.querySelectorAll(
`td.field-${e.from.dataset.orderingField} input[name$=-${e.from.dataset.orderingField}]`
)
);
.forEach(function (input, index) {
input.value = index;
});
}
weightInputs.forEach((input, index) => {
input.value = index;
});
/*************************************************************
* Alpine.sort.js callback before moving records
*************************************************************/
const moveRecords = (e) => {
if (
(e.related.classList.contains("template") ||
e.related.classList.contains("form-actions")) &&
e.willInsertAfter
) {
return false;
}
};
/*************************************************************
@@ -434,7 +490,7 @@ const watchClassChanges = (selector, callback) => {
/*************************************************************
* Calendar & clock
*************************************************************/
const dateTimeShortcutsOverlay = () => {
function dateTimeShortcutsOverlay() {
const observer = new MutationObserver((mutations) => {
for (const mutationRecord of mutations) {
const display = mutationRecord.target.style.display;
@@ -448,27 +504,32 @@ const dateTimeShortcutsOverlay = () => {
}
});
const targets = document.querySelectorAll(".calendarbox, .clockbox");
const findCalendars = () => {
for (const target of document.querySelectorAll(".calendarbox, .clockbox")) {
observer.observe(target, {
attributes: true,
attributeFilter: ["style"],
});
}
};
for (const target of targets) {
observer.observe(target, {
attributes: true,
attributeFilter: ["style"],
});
}
new MutationObserver(findCalendars).observe(document.body, {
childList: true,
subtree: true,
});
};
/*************************************************************
* File upload path
*************************************************************/
const fileInputUpdatePath = () => {
const checkInputChanged = () => {
function fileInputUpdatePath() {
function checkInputChanged() {
for (const input of document.querySelectorAll("input[type=file]")) {
if (input.hasChangeListener) {
continue;
}
input.addEventListener("change", (e) => {
input.addEventListener("change", function (e) {
const parts = e.target.value.split("\\");
const placeholder =
input.parentNode.parentNode.parentNode.querySelector(
@@ -479,9 +540,9 @@ const fileInputUpdatePath = () => {
input.hasChangeListener = true;
}
};
}
new MutationObserver(() => {
new MutationObserver(function () {
checkInputChanged();
}).observe(document.body, {
childList: true,
@@ -489,7 +550,7 @@ const fileInputUpdatePath = () => {
});
checkInputChanged();
};
}
/*************************************************************
* Chart
@@ -719,6 +780,276 @@ const renderCharts = () => {
});
};
/*************************************************************
* Inlines
*************************************************************/
function inlines() {
/**
* Replacing: https://github.com/django/django/blob/main/django/contrib/admin/static/admin/js/inlines.js
*
* Expected HTML structure:
*
* <div class="formset-wrapper">
* ... management form ...
* <div class="formset">
* <div class="form-group">
* <div class="form-row">inputs</div>
* <div class="form-nested">nested inlines if exists</div>
* </div>
*
* ... another form group ...
*
* <div class="form-actions">
* <div class="add-row">Add record button</div>
* </div>
* </div>
* </div>
*/
document.querySelectorAll(".add-row").forEach(function (el) {
el.addEventListener("click", addInlineTemplateHandler);
});
document.querySelectorAll(".delete-template").forEach(function (el) {
el.addEventListener("click", deleteInlineTemplateHandler);
});
}
function deleteInlineTemplateHandler(e) {
const formset = e.target.closest(".formset");
const options = JSON.parse(formset.dataset.inlineFormset).options;
const formsetWrapper = e.target.closest(".formset-wrapper");
const minNumForms = formsetWrapper.querySelector(
"input[type=hidden][name$='MIN_NUM_FORMS']"
);
const totalForms = formsetWrapper.querySelector(
"input[type=hidden][name$='TOTAL_FORMS']"
);
const maxNumForms = formsetWrapper.querySelector(
"input[type=hidden][name$='MAX_NUM_FORMS']"
);
const rowToDelete = e.target.closest(".form-group");
if (parseInt(totalForms.value) == parseInt(minNumForms.value)) {
return;
}
let reindex = false;
formset
.querySelectorAll(":scope > .form-group:not(.empty-form)")
.forEach((row, index) => {
if (rowToDelete.isSameNode(row)) {
reindex = true;
}
if (!rowToDelete.isSameNode(row) && reindex) {
const content = row.querySelector(":scope > .form-row");
content.innerHTML = content.innerHTML.replaceAll(
`${options.prefix}-${index}`,
`${options.prefix}-${index - 1}`
);
// Reindex nested formset
const nestedFormset = row.querySelector(":scope .form-nested");
if (nestedFormset) {
nestedFormset.innerHTML = nestedFormset.innerHTML.replaceAll(
`${options.prefix}-${index}`,
`${options.prefix}-${index - 1}`
);
}
}
});
const rowToDeleteParent = rowToDelete.parentElement;
rowToDelete.remove();
updateStackedCounter(rowToDeleteParent);
// Update total forms count
totalForms.value = parseInt(
formset.querySelectorAll(":scope > .form-group:not(.empty-form)").length,
10
);
// Apply delete event
formset.querySelectorAll(".delete-template").forEach(function (el) {
el.removeEventListener("click", deleteInlineTemplateHandler);
el.addEventListener("click", deleteInlineTemplateHandler);
});
// Show add button
if (parseInt(totalForms.value, 10) < parseInt(maxNumForms.value, 10)) {
formset.querySelector(".add-row").classList.remove("hidden");
}
applyLastClass(formset);
checkFormsetEmpty(formset);
document.dispatchEvent(
new CustomEvent("formset:removed", {
detail: {
formsetName: options.prefix,
},
})
);
}
function addInlineTemplateHandler(e) {
e.preventDefault();
const formset = e.target.closest(".formset");
const formsetWrapper = e.target.closest(".formset-wrapper");
const totalForms = formsetWrapper.querySelector(
"input[type=hidden][name$='TOTAL_FORMS']"
);
const maxNumForms = formsetWrapper.querySelector(
"input[type=hidden][name$='MAX_NUM_FORMS']"
);
const template = formset.querySelector(":scope >.empty-form");
const index = formset.querySelectorAll(":scope > .form-group").length - 1;
const options = JSON.parse(formset.dataset.inlineFormset).options;
const row = template.cloneNode(true);
row.classList.remove("empty-form");
if (parseInt(totalForms.value, 10) >= parseInt(maxNumForms.value, 10)) {
return;
}
// Insert new row BEFORE template row which is always last in formset
formset.insertBefore(row, template);
applyLastClass(formset);
checkFormsetEmpty(formset);
// Stacked inlines are having a counter in title
updateStackedCounter(row.parentElement);
// Replace __prefix__ in RECENTLY ADDED row
const formRow = row.querySelector(":scope > .form-row");
formRow.innerHTML = formRow.innerHTML.replaceAll(/__prefix__/g, index);
// Update hidden TOTAL_FORMS input value
totalForms.value = parseInt(totalForms.value, 10) + 1;
// Hide add row button if it is not possible to add more rows
if (totalForms.value === maxNumForms.value) {
e.target.parentElement.classList.add("hidden");
}
// Call special `formsetGroup:added` event to reinitialize inlines
row.dispatchEvent(
new CustomEvent("formsetGroup:added", {
bubbles: true,
detail: {
formsetName: options.prefix,
row: row,
index: index,
},
})
);
}
document.addEventListener("formsetGroup:added", function (e) {
if (!e.detail.row) {
return;
}
e.detail.row.innerHTML = e.detail.row.innerHTML.replaceAll(
`${e.detail.formsetName}-__prefix__`,
`${e.detail.formsetName}-${e.detail.index}`
);
if (typeof DateTimeShortcuts !== "undefined") {
document.querySelectorAll(".datetimeshortcuts").forEach(function (el) {
el.remove();
});
DateTimeShortcuts.init();
dateTimeShortcutsOverlay();
}
e.detail.row.querySelector(":scope > .form-row").dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: { formsetName: e.detail.formsetName },
})
);
e.detail.row
.querySelectorAll(".nested-formset > .form-group:not(.empty-form)")
.forEach(function (formGroup) {
formGroup.querySelector(".form-row").dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: { formsetName: e.detail.formsetName },
})
);
});
e.detail.row.querySelectorAll(".add-row").forEach(function (el) {
el.addEventListener("click", addInlineTemplateHandler);
});
e.detail.row.querySelectorAll(".delete-template").forEach(function (el) {
el.addEventListener("click", deleteInlineTemplateHandler);
});
});
function checkFormsetEmpty(formset) {
const formGroups = formset.querySelectorAll(
":scope > .form-group:not(.empty-form)"
);
if (formGroups.length === 0) {
formset.classList.add("empty");
} else {
formset.classList.remove("empty");
}
}
function applyLastClass(formset) {
formset.querySelectorAll(":scope > .form-group").forEach(function (row) {
if (row.classList.contains("last")) {
row.classList.remove("last");
}
if (
row.nextElementSibling.classList.contains("empty-form") ||
row.nextElementSibling.classList.contains("form-actions")
) {
row.classList.add("last");
}
});
}
function updateStackedCounter(rowsParent) {
rowsParent
.querySelectorAll(":scope > .form-group:not(.empty-form) > .form-row")
.forEach(function (row, index) {
const counter = row.querySelector(".stacked-counter");
if (counter) {
counter.textContent = index + 1;
}
});
}
/*************************************************************
* Reinitialize inlines after pagination AJAX request
*************************************************************/
document.addEventListener("htmx:afterSettle", function (e) {
e.target.querySelectorAll(".form-row").forEach(function (formRow) {
formRow.dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: JSON.parse(e.target.dataset.inlineFormset).options
.prefix,
},
})
);
});
});
function getCurrentTab() {
const fragment = window.location.hash?.replace('#', '');